C++ 实现插件式 sleep 工具:

本文以一个极简但完整的 C++ 插件示例为切入点,带你搞清楚插件框架的核心设计模式、JSON Schema 的参数约束方式,以及跨模块 ABI 的若干实际陷阱。


背景:为什么需要插件式工具?

在很多宿主程序(Host)中——无论是 AI 推理引擎、脚本运行时还是自动化平台——都有一类需求:

在运行时动态扩展功能,而无需重新编译宿主。

插件(Plugin)机制就是解决这个问题的经典方案。宿主定义好接口规范,插件按规范实现并编译成动态库(.dll / .so),宿主在运行时加载、调用。

sleep-tools 就是这样一个最小可运行示例:它只做一件事——让线程睡一觉,然后告诉宿主"睡了多久"。麻雀虽小,五脏俱全。


整体结构一览

sleep-tools 插件
│
├── 元信息层       GetNameImpl / GetVersionImpl / GetTypeImpl
├── 生命周期层     InitializeImpl / ShutdownImpl
├── 工具描述层     methods[] + JSON Schema
├── 请求处理层     HandleRequestImpl(核心)
└── 导出层         CreatePlugin / DestroyPlugin

宿主通过 CreatePlugin() 拿到一张"函数表"(PluginAPI*),之后所有交互都走这张表完成。


逐层拆解

一、工具元信息:methods[]

methods[] 数组向宿主声明"这个插件能做什么":

字段 内容
工具名 sleep
描述 暂停执行指定毫秒数
参数 Schema JSON Schema Draft-07

JSON Schema 的含义

{
  "type": "object",
  "properties": {
    "milliseconds": { "type": "number", "minimum": 0 }
  },
  "required": ["milliseconds"],
  "additionalProperties": false
}

这段 Schema 规定:

  • 请求体必须是对象
  • 必须包含 milliseconds 字段,且为非负数
  • 不允许传入多余字段

宿主拿到这段 Schema,就能在调用前做参数校验,而不必深入插件实现。


二、核心函数:HandleRequestImpl

这是整个插件最重要的部分,分四步走:

第一步:解析 JSON

auto request = json::parse(req);

宿主传入的 req 是一段 JSON 字符串,结构大致如下:

{
  "params": {
    "arguments": {
      "milliseconds": 1000
    }
  }
}

第二步:提取参数

auto milliseconds = request["params"]["arguments"]["milliseconds"].get<int>();

沿着 params → arguments → milliseconds 的路径取值,并转成 int

第三步:执行休眠

std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));

调用标准库 <thread> 中的 sleep_for,阻塞当前线程指定毫秒数。例如传入 1000 就暂停 1 秒,传入 5000 就暂停 5 秒。

第四步:构造并返回响应

响应 JSON 的最终结构:

{
  "content": [
    {
      "type": "text",
      "text": "Waited for 1000 milliseconds"
    }
  ],
  "isError": false
}

三、为什么返回 char* 而不是 std::string

这是跨模块 ABI 的经典问题。

插件编译成动态库后,宿主和插件可能使用不同版本的 C++ 标准库,std::string 的内存布局、引用计数实现等细节在不同版本间并不一致。直接跨 DLL / .so 边界传递 std::string 非常危险

因此这里做了一个"转义":

std::string result = response.dump();        // C++ 侧操作
char* buffer = new char[result.length() + 1]; // 分配 C 风格缓冲区
strcpy(buffer, result.c_str());              // 复制内容
return buffer;                               // 返回裸指针

C 风格的 char* 没有运行时依赖,任何语言、任何编译器都能安全处理。


四、extern "C" 与名字改编

extern "C" PLUGIN_API PluginAPI* CreatePlugin();
extern "C" PLUGIN_API void       DestroyPlugin(PluginAPI*);

C++ 编译器会对函数名做"名字改编"(Name Mangling),给同名函数加上参数类型编码,以支持函数重载。例如 CreatePlugin 在编译后可能变成 _Z12CreatePluginv(GCC 风格)或 ?CreatePlugin@@YAPAUPluginAPI@@XZ(MSVC 风格)。

加上 extern "C" 后,编译器按 C 的规则导出符号,函数名保持原样,宿主才能通过固定的字符串名称找到它。


Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐