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);
}

设计点:

  1. 命令带上插件命名空间:hi@hello 存表键,防止两个插件同注册 hi 相互覆盖(真实编辑器都这么做);
  2. enable 前清空登记区:防上次插件残留的命令串入本次;
  3. disable 只清命令/钩子,不删目录信息:list() 仍可显示"已禁用";
  4. @ 分隔符是预留的"这个命令来自哪个插件"的可逆元数据。

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)

  1. 完成第 4 节"未尽稿":补齐收集登记与 hooks 的完整版(对照完整正确版);
  2. 给 notify 加"钩子内出错时把 bridge 的 last_error() 记录到日志"(教程 26 期 Log 知识);
  3. 实现 :plug Ex 命令:plug list / plug load name / plug unload name 三个子命令接到 Editor;
  4. 挑战:插件"依赖"支持——manifest 里 depends = other,enable 自动递归 enable。

下一节 27-为插件暴露EditorAPI.md:让 Python 真正够到编辑器内核——Buffer、光标、事件,一条不落。

posted @ 2026-09-08 13:51  IcarusLee  阅读(19)  评论(0)    收藏  举报