27-为插件暴露EditorAPI

27 · 为插件暴露 Editor API(Python API for Plugins)

本节目标(Goals):给插件提供"够得着编辑器内核"的 Python API——缓冲区读写、光标移动、set/insert 文本、消息提示,全部以字符串进出。
前置知识(Prerequisites):教程 25(桥)、26(PluginManager)、09(Cursor)、08(Buffer)。


1. 三层翻译,各司其职

插件从"Python 世界"够到"编辑器内核",要穿过三层:

Python 调用                     C++ 世界
vicarus_get_text()   ──▶  _vicarus_c.get_text ──▶ std::function (Editor 注册)
                        (C 函数蹦床)            (真实操作 buffer_)
  • 桥(PythonBridge):提供 install_callable(name, std::function),把一个 C++ 函数挂成 Python 模块 _vicarus_c 里的可调用属性;
  • API 层(PythonAPI):把这些函数包装成 vicarus_<name> 的 Python 全局函数(插件友好的平易名字);
  • Editor(组合根):用 lambda 把真实操作(replace_all、光标移动……)注册进来。

关键词:string 进、string 出。插件看到的永远是字符串——这是整条链路的铁律。


2. 给桥加 install_callable(蹦床)

python_bridge.hpp 增加类型与声明:

    // Python 可调的后端(参数与返回值均为字符串)
    using CCallable = std::function<std::string(const std::vector<std::string>&)>;
    bool install_callable(const std::string& name, CCallable fn);

实现(python_bridge.cpp,#if VICARUS_ENABLE_PYTHON 之内):

namespace {
// 静态表:名字 → 后端。单桥实例下够用(多实例需升级成实例注册表)
std::map<std::string, PythonBridge::CCallable> g_callables;

PyObject* vicarus_tramp(PyObject* self, PyObject* arg) {
    // self 里保存了名字;METH_O 保证 arg 是单个对象(字符串或元组)
    const char* name = PyUnicode_Check(self) ? PyUnicode_AsUTF8(self) : "";
    std::vector<std::string> args;
    if (PyUnicode_Check(arg)) {
        args.emplace_back(PyUnicode_AsUTF8(arg));
    } else if (PyTuple_Check(arg)) {
        for (Py_ssize_t i = 0; i < PyTuple_Size(arg); ++i) {
            PyObject* item = PyTuple_GetItem(arg, i);
            if (!PyUnicode_Check(item)) { args.clear(); break; }
            args.emplace_back(PyUnicode_AsUTF8(item));
        }
    }
    auto it = g_callables.find(name);
    if (it == g_callables.end()) {
        PyErr_SetString(PyExc_AttributeError, "no such vicarus API");
        return nullptr;
    }
    return PyUnicode_FromString(it->second(args).c_str());
}

PyMethodDef vicarus_mdef = {"tramp", vicarus_tramp, METH_O, nullptr};
}  // namespace

bool PythonBridge::install_callable(const std::string& name, CCallable fn) {
    if (!initialized_) return false;
    g_callables[name] = std::move(fn);
    PyObject* mod = PyImport_AddModule("_vicarus_c");        // 常驻模块
    if (!mod) return false;
    PyObject* f = PyCFunction_NewEx(&vicarus_mdef,
                                    PyUnicode_FromString(name.c_str()), nullptr);
    if (!f) return false;
    PyModule_AddObject(mod, name.c_str(), f);   // PyModule_AddObject 窃取引用
    return true;
}

两处引用纪律别漏:

  • PyCFunction_NewEx 的第二个参数(self)是新引用,PyModule_AddObject 会窃取它——所以 PyUnicode_FromString(name) 的结果我们不手动 DÉCR;
  • 蹦床统一 METH_O:把"字符串或元组"都喂给一样的参数解析。

3. API 层 include/vicarus/extension/python_api.hpp

// include/vicarus/extension/python_api.hpp
#pragma once

#include <vicarus/extension/python_bridge.hpp>

#include <functional>
#include <string>
#include <utility>
#include <vector>

namespace vicarus::extension {

// 把 C++ 后端翻译成插件可见的 vicarus_* 全局函数
class PythonAPI {
public:
    explicit PythonAPI(PythonBridge& bridge) : bridge_(bridge) {}

    // 登记一个后端;name 会成为 _vicarus_c.<name> 与 vicarus_<name>
    bool install(std::string name, PythonBridge::CCallable fn);

    // 把所有已登记函数落成 __main__ 里的 vicarus_* 包装(一次 commit)
    bool commit();

private:
    PythonBridge& bridge_;
    std::vector<std::pair<std::string, PythonBridge::CCallable>> table_;
};

}  // namespace vicarus::extension

实现:

// src/extension/python_api.cpp
#include <vicarus/extension/python_api.hpp>

namespace vicarus::extension {

bool PythonAPI::install(std::string name, PythonBridge::CCallable fn) {
    if (!bridge_.install_callable(name, std::move(fn))) return false;
    table_.emplace_back(std::move(name), nullptr);   // 占位保留名字供 commit
    return true;
}

bool PythonAPI::commit() {
    std::string code = "import _vicarus_c\n";
    for (const auto& [name, _] : table_) {
        code += "def vicarus_" + name + "(*a):\n";
        code += "    return _vicarus_c." + name + "(a[0] if len(a) == 1 else tuple(a))\n";
    }
    return bridge_.exec(code);
}

}  // namespace vicarus::extension

commit 用一行 Python 生成一堆 vicarus_xxx 包装:一个参数直接传,多个参数打包成元组。C++ 端只造蹦床,可读名字由 Python 阿司匹林式提供。


4. Editor 端接线:真实操作注册进来

Editor 创建 bridge/api/plugins 的地方(初始化尾部):

// Editor::init 尾部
auto& api = python_api_;                       // PythonAPI python_api_;
api.install("get_text",        [this](const std::vector<std::string>&) {
    return buffers_.active().to_string();
});
api.install("get_line",        [this](const std::vector<std::string>& a) {
    if (a.empty()) return std::string();
    return line_of(buffers_.active(), std::stoi(a[0]));   // 行 → 文本
});
api.install("set_text",        [this](const std::vector<std::string>& a) {
    if (a.empty()) return std::string("err: set_text needs text");
    undo_.begin(buffers_.active(), cursor_.pos());
    buffers_.active().replace_all(a[0]);
    undo_.end(buffers_.active(), cursor_.pos());
    notify_edit();
    return std::string();
});
api.install("insert",          [this](const std::vector<std::string>& a) {
    if (a.empty()) return std::string("err: insert needs text");
    cursor_.insert_text(a[0], buffers_.active());   // 教程 09 的光标插入
    notify_edit();
    return std::string();
});
api.install("get_cursor",      [](const std::vector<std::string>&) {
    return std::string();   // 改由下面 overload 版(真实做写字)
});
api.install("get_cursor",      [this](const std::vector<std::string>&) {
    return std::to_string(cursor_.pos().row) + "," + std::to_string(cursor_.pos().col);
});
api.install("set_cursor",      [this](const std::vector<std::string>& a) {
    if (a.size() < 1) return std::string("err: set_cursor needs 'r,c'");
    const std::string rc = a[0];
    const size_t comma = rc.find(',');
    if (comma == std::string::npos) return std::string("err: bad format");
    cursor_.set({std::stoi(rc.substr(0, comma)), std::stoi(rc.substr(comma + 1))});
    return std::string();
});
api.install("message",         [this](const std::vector<std::string>& a) {
    status_bar_->show_message(a.empty() ? "" : a[0]);
    return std::string();
});
api.commit();                 // 生成 vicarus_* 包装

// 插件装载放在其后,保证插件能用到 vicarus_*
plugin_manager_.scan_and_load("plugins");

注意我连续登记了两个 get_cursor——后一个会覆盖前一个(g_callables 是 map)。这是对"安装顺序即优先级"的确认:同名覆盖最后一次生效。真实的编辑器需要考虑"不能被同名插件覆盖核心 API",教程 30 再加更严的访问控制。


5. demo:插件读写缓冲区

plugins/hello/plugin.py 现在可以伸进编辑器了:

def dump():
    return vicarus_get_text()

def put(line):
    vicarus_set_cursor("0,0")
    vicarus_insert(line + "\n")
    return "inserted ok"

vicarus_register_command("dump", "dump")
vicarus_register_command("put",  "put")

src/main.cpp(临时):加载插件后,用 run_command("put", {"hi from python"}) 写一行,再 run_command("dump") 读全文:

// main 中(桥+manager 初始化后)
pm.run_command("put", {"hello!"});
std::printf("buffer now: [%s]\n",
            pm.run_command("dump", {}).value_or("?").c_str());

预期能看到 hello!\n 出现在缓冲区字符串开头——插件已经真实地改写了编辑器内存。这一条链路的打通,就是 Vicarus 可编程性的地基。


本节小结(Summary)

  • 三层翻译:桥造蹦床、API 层造包装、Editor 供后端;全程字符串进出;
  • PyCFunction_NewEx(METH_O) + 名字存 self:一套蹦床服务所有 API;
  • install_callable 键名即 Python 属性名,同名后装覆盖(暴露优先级问题,教程 30 加固);
  • Editor 用 lambda [] 注册 6 个核心操作:读写缓冲、光标、插入、消息;
  • scan_and_load 排在 api.commit() 之后——插件启动即拥有 vicarus_* 能力。

动手练习(Exercise)

  1. 补 get_line 之前的 line_of 实现(用 buf.line_text(n) 或 open 区间),并在错误时返回 "err: line out of range";
  2. 给 API 加 undo:vicarus_undo() → 调用 undo_.undo(),并讲清它为什么不直接暴露 undo 栈对象;
  3. 实现 vicarus_register_operator(插件注册类似 d 的 Operator,复用教程 12 的 Operator 表);
  4. 挑战:安全检查——把"插件对 set_text 的调用"放进撤销步,但"插件对 message"不进;给 API 加权限标志位 access 枚举。

下一节 28-构建第一个真实插件BuildingAPlugin.md:把"注释切换/自动格式化"写成真插件,端到端跑通。

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