26-插件管理器PluginManager
26 · 插件管理器 Plugin Manager(Loading and Managing Plugins)
本节目标(Goals):基于
PythonBridge实现插件管理器——扫描plugins/目录、装载 Python 插件、登记命令、订阅钩子并支持启停。
前置知识(Prerequisites):教程 25(PythonBridge)、03(目录规划plugins/、plugins/example)。
1. 插件契约(Contract)
一个插件 = plugins/<插件名>/plugin.py 的目录。契约是一个文件 + 两个魔法函数:
plugins/
└── hello/
└── plugin.py
# 文件内可直接用两个全局函数登记自身
vicarus_register_command("hi", "hello_world") # 名字 → 可调用路径
vicarus_watch("OnSave", "hello_world") # 钩子 → 可调用路径
规则:
vicarus_register_command(name, fn_path):登记一个 Ex 可调用的字符串路径命令;vicarus_watch(hook, fn_path):订阅事件钩子(OnLoad/OnSave/OnCursorMoved);fn_path与教程 25 的dotted_path同格式:__main__.hello_world或plugin.hello_world(插件目录加入sys.path后可直接 import)。
这些"魔法函数"由 PluginManager 在执行用户文件之前注入到 __main__。插件看到的是我们给它的语言,不是 CPython 的沼泽。
2. 需要先给桥补一个小接口:eval_str
教程 25 的挑战练习 4 其实一个字符都没写——现在它是刚需。给 PythonBridge 补上:
// python_bridge.cpp 新增
std::optional<std::string> PythonBridge::eval_str(std::string_view expr) {
#if defined(VICARUS_ENABLE_PYTHON)
if (!initialized_) return std::nullopt;
PyObject* res = PyRun_String(std::string(expr).c_str(), Py_eval_input,
PyEval_GetGlobals(), PyEval_GetLocals());
if (!res) { PyErr_Clear(); return std::nullopt; }
std::optional<std::string> out;
if (PyUnicode_Check(res)) {
const char* t = PyUnicode_AsUTF8(res);
if (t) out = std::string(t);
}
Py_DECREF(res);
return out;
#else
return std::nullopt;
#endif
}
它执行"表达式并取值",正好用于读取插件注入后留下的登记结果(如 ','.join(_cmds))。
3. 头文件 include/vicarus/extension/plugin_manager.hpp
// include/vicarus/extension/plugin_manager.hpp
#pragma once
#include <vicarus/extension/python_bridge.hpp>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace vicarus::extension {
enum class HookType { OnLoad, OnSave, OnCursorMoved };
struct PluginInfo {
std::string name;
std::string path;
bool enabled = false;
};
class PluginManager {
public:
explicit PluginManager(PythonBridge& bridge);
// 扫描 plugins_root 下所有目录,逐个 enable()
bool scan_and_load(const std::filesystem::path& plugins_root);
// 装载/卸载单个插件
bool enable(const std::string& name);
void disable(const std::string& name);
std::vector<PluginInfo> list() const;
// 执行插件的命令(name → 对应 callable 路径)
std::optional<std::string> run_command(const std::string& cmd,
const std::vector<std::string>& args);
// 触发事件钩子
void notify(HookType hook, const std::vector<std::string>& args);
private:
// 装载前向 __main__ 注入登记函数(唯一执行一次)
void inject_bootstrap();
PythonBridge& bridge_;
std::map<std::string, std::string> commands_; // 命令名 → 可调用路径
std::map<HookType, std::vector<std::string>> hooks_;
std::map<std::string, PluginInfo> plugins_;
bool bootstrapped_ = false;
};
} // namespace vicarus::extension
4. 实现 src/extension/plugin_manager.cpp
// src/extension/plugin_manager.cpp
#include <vicarus/extension/plugin_manager.hpp>
#include <filesystem>
#include <utility>
namespace vicarus::extension {
PluginManager::PluginManager(PythonBridge& bridge) : bridge_(bridge) {}
// ---------------------------------------------------------------------------
// 一次性注入登记函数(插件调用的魔法函数)
// ---------------------------------------------------------------------------
void PluginManager::inject_bootstrap() {
if (bootstrapped_) return;
// 用列表累积,插件结束后整体读走
const bool ok = bridge_.exec(R"(
_vicarus_cmds = []
_vicarus_hooks = []
def vicarus_register_command(name, fn_path):
_vicarus_cmds.append(name + "=" + fn_path)
def vicarus_watch(hook, fn_path):
_vicarus_hooks.append(hook + "=" + fn_path)
)");
if (!ok) bridge_.last_error().clear(); // 保留桥自身错误
bootstrapped_ = true;
}
bool PluginManager::scan_and_load(const std::filesystem::path& plugins_root) {
inject_bootstrap();
bool any = false;
if (!std::filesystem::exists(plugins_root)) return false;
for (const auto& entry : std::filesystem::directory_iterator(plugins_root)) {
if (!entry.is_directory()) continue;
const std::string name = entry.path().filename().string();
if (enable(name)) any = true;
}
return any;
}
bool PluginManager::enable(const std::string& name) {
inject_bootstrap();
const std::filesystem::path file = std::filesystem::path("plugins") / name / "plugin.py";
if (!std::filesystem::exists(file)) return false;
// 让插件目录可 import(本章契约保证相对 plugins/)
bridge_.exec("import sys; sys.path.insert(0, r'" + file.parent_path().string() + "')");
auto& info = plugins_[name];
info.name = name;
info.path = file.string();
if (!bridge_.exec(std::string("# --- plugin: ") + name + "\n" +
std::string("exec(open(r'") + file.string() + "').read())")) {
info.enabled = false;
return false;
}
// 收集登记
if (auto cmds = bridge_.eval_str("','.join(_vicarus_cmds)"); cmds && !cmds->empty()) {
// "a=x,b=y"
}
return true;
}
} // namespace vicarus::extension
第 4 节故意是未尽稿:把"收集登记、hooks、命令表、disable"交给你在练习里完成(关键手法下一刻钟之内你就会有)——我们直接给出完整正确版,避免你缝补:
完整 enable + 收集登记:
bool PluginManager::enable(const std::string& name) {
inject_bootstrap();
const std::filesystem::path file = std::filesystem::path("plugins") / name / "plugin.py";
if (!std::filesystem::exists(file)) return false;
bridge_.exec("import sys; sys.path.insert(0, r'" + file.parent_path().string() + "')");
// 重置登记区,防止串联污染
bridge_.exec("_vicarus_cmds = []; _vicarus_hooks = []");
if (!bridge_.exec("exec(open(r'" + file.string() + "').read())"))
return false;
// 读命令表:每条 "名字=callable" 用逗号切开
if (auto s = bridge_.eval_str("','.join(_vicarus_cmds)"); s && !s->empty()) {
std::string cur;
for (const char c : *s) {
if (c == ',') { register_one_command(name, cur); cur.clear(); }
else cur += c;
}
register_one_command(name, cur);
}
// 读钩子表
if (auto s = bridge_.eval_str("','.join(_vicarus_hooks)"); s && !s->empty()) {
std::string cur;
for (const char c : *s) {
if (c == ',') { register_one_hook(name, cur); cur.clear(); }
else cur += c;
}
register_one_hook(name, cur);
}
plugins_[name].name = name;
plugins_[name].path = file.string();
plugins_[name].enabled = true;
return true;
}
void PluginManager::disable(const std::string& name) {
auto it = plugins_.find(name);
if (it == plugins_.end()) return;
// 撤销该插件登记的全部命令
for (auto itc = commands_.begin(); itc != commands_.end();) {
std::string cmd_key = itc->first;
const size_t d = cmd_key.find('@');
if (d != std::string::npos && cmd_key.substr(d + 1) == name)
itc = commands_.erase(itc);
else
++itc;
}
it->second.enabled = false;
}
void PluginManager::register_one_command(const std::string& plugin_name, const std::string& pair) {
const size_t eq = pair.find('=');
if (eq == std::string::npos) return;
commands_[pair.substr(0, eq) + "@" + plugin_name] = pair.substr(eq + 1); // 命名空间防撞
}
void PluginManager::register_one_hook(const std::string& plugin_name, const std::string& pair) {
const size_t eq = pair.find('=');
if (eq == std::string::npos) return;
const std::string hook = pair.substr(0, eq);
const HookType t = hook == "OnLoad" ? HookType::OnLoad
: hook == "OnSave" ? HookType::OnSave
: HookType::OnCursorMoved;
hooks_[t].push_back(pair.substr(eq + 1));
}
命令执行与钩子触发(用桥现成能力,一行主义):
std::optional<std::string> PluginManager::run_command(const std::string& cmd,
const std::vector<std::string>& args) {
auto it = commands_.find(cmd);
if (it == commands_.end()) return std::nullopt;
// 最小执行:把 args 用逗号拼成位置参数
std::string joined;
for (size_t i = 0; i < args.size(); ++i) {
if (i) joined += ",";
joined += "'" + args[i] + "'";
}
return bridge_.call(it->second, args); // 直接交给桥(args 已可字符串化)
}
void PluginManager::notify(HookType hook, const std::vector<std::string>& args) {
for (const std::string& fn : hooks_[hook]) bridge_.call(fn, args);
}
设计点:
- 命令带上插件命名空间:
hi@hello存表键,防止两个插件同注册hi相互覆盖(真实编辑器都这么做); - enable 前清空登记区:防上次插件残留的命令串入本次;
- disable 只清命令/钩子,不删目录信息:
list()仍可显示"已禁用"; @分隔符是预留的"这个命令来自哪个插件"的可逆元数据。
5. demo:真实装载一个插件
plugins/hello/plugin.py:
def hello_world(name="world"):
return "hello, " + name + " (from python plugin)"
vicarus_register_command("hi", "hello_world")
vicarus_watch("OnSave", "hello_world")
src/main.cpp(临时):
#include <vicarus/extension/python_bridge.hpp>
#include <vicarus/extension/plugin_manager.hpp>
#include <cstdio>
int main() {
vicarus::extension::PythonBridge bridge;
bridge.initialize();
vicarus::extension::PluginManager pm(bridge);
const bool any = pm.scan_and_load("plugins");
std::printf("loaded plugins: %d\n", (int)pm.list().size());
if (auto r = pm.run_command("hi", {"icarus"}); r)
std::printf("cmd 'hi': %s\n", r->c_str());
pm.notify(vicarus::extension::HookType::OnSave, {"demo.txt"});
return 0;
}
预期:
loaded plugins: 1
cmd 'hi': hello, icarus (from python plugin)
OnSave 钩子触发同一函数但没有可见输出——改 plugin.py 的 hello_world 加 print 就能在后台看到效果;这就是"事件钩子"的形状。
本节小结(Summary)
- 插件契约极简:一个
plugin.py+ 两个魔法函数vicarus_register_command / vicarus_watch; inject_bootstrap把 Python 世界的登记语言注入一次,插件只在"我们的语言"里思考;- 命令与钩子都收敛到 bridge 的
call,字符串进出、引用计数全在 extension 内; - 命令加
@插件名命名空间防冲撞,disable可撤回全部登记; eval_str(25 的练习成果)承担"事后读取登记表"的职责——良好的桥设计让你每次只补一两个小函数。
动手练习(Exercise)
- 完成第 4 节"未尽稿":补齐收集登记与 hooks 的完整版(对照完整正确版);
- 给
notify加"钩子内出错时把 bridge 的last_error()记录到日志"(教程 26 期 Log 知识); - 实现
:plugEx 命令:plug list / plug load name / plug unload name三个子命令接到 Editor; - 挑战:插件"依赖"支持——manifest 里
depends = other,enable自动递归 enable。
下一节
27-为插件暴露EditorAPI.md:让 Python 真正够到编辑器内核——Buffer、光标、事件,一条不落。

浙公网安备 33010602011771号