C++ 构建一个插件式计算器工具系统

1. 整体功能概述

代码实现的是一个 C++ 插件式数学计算器工具系统,是一个可以被宿主程序(如 AI Agent、命令行工具等)动态加载的插件。核心能力有三:

  • 字符串数学表达式解析:支持 3 + sin(1)(2^3)^2sqrt(9) * log(10) 这类复合表达式的实时求值。
  • 独立数学工具:提供加、减、乘、除、幂、开方、阶乘等 8 个基础工具,参数通过 JSON 传入。
  • 标准插件接口:通过 PluginAPI 导出,宿主程序可以动态发现工具列表、发送请求、获取结果,整个通信协议基于 JSON。

用一句话概括:

「基于递归解析的表达式计算引擎 + JSON 驱动的插件工具系统」


2. 插件的五层架构

整个插件可以清晰地划分为以下五层,每层职责独立、边界清楚:

层级 名称 主要内容
L0 插件接口导出层 CreatePlugin / DestroyPlugin,供宿主动态加载,遵循 C ABI
L1 插件生命周期管理层 GetName / GetVersion / Initialize / Shutdown / GetToolCount / GetTool
L2 请求分发层 HandleRequestImpl:解析 JSON 请求 → 按 toolName 路由 → 返回统一 JSON 响应
L3 工具注册表(Metadata) PluginTool methods[]:工具名、描述、JSON Schema 参数定义
L4 计算核心层 ExpressionParser(递归下降)+ factorial() 等独立数学函数

最底层是真正的「数学能力」,最顶层是给宿主程序看的「插件入口」。中间的 HandleRequestImpl 起到总调度作用,是整个插件最核心的函数。


3. 核心:递归下降表达式解析器

ExpressionParser 是这套插件最有技术含量的部分,它用递归下降法(Recursive Descent Parsing)实现了一个完整的数学表达式解析器,无需第三方库。

运算优先级分层

解析器将语法按运算优先级分为四层,形成调用链:

parseExpression     // 最低优先级:+ 和 -
    └── parseTerm       // 次低:* / %
        └── parsePower      // 幂运算:^ (右结合)
            └── parseFactor     // 最高:数字、括号、函数、正负号
                ├── parseFunction   // 识别 sin(x) 等
                └── parseNumber     // 读取 123、45.6 等

右结合的幂运算

幂运算 ^ 在数学上是右结合的,即 (2{32} = 2{(32)} = 512),而不是 ((23)2 = 64)。代码通过在 parsePower 中递归调用自身来实现这一点:

static double parsePower(const std::string& expr, size_t& pos) {
    double base = parseFactor(expr, pos);
    if (pos < expr.length() && expr[pos] == '^') {
        pos++;
        double exponent = parsePower(expr, pos); // 递归 → 右结合
        return std::pow(base, exponent);
    }
    return base;
}

支持的内置函数

parseFunction 识别函数名后,映射到标准库函数:

函数名 对应 std 函数 说明
sin / cos / tan std::sin / cos / tan 三角函数(弧度制)
sqrt std::sqrt 平方根
abs std::abs 绝对值
log std::log 自然对数 (\ln)
exp std::exp (e^x)
floor / ceil / round 同名 取整函数

4. 工具注册表——Metadata 定义

插件使用静态数组 methods[] 定义工具清单,每个条目包含三个字段:工具名、描述、JSON Schema。这是整个「接口契约」的来源,宿主程序通过 GetToolImpl 枚举所有工具,再按 Schema 构造请求。

static PluginTool methods[] = {
    {
        "calculator",
        "Evaluates a mathematical expression",
        "{\"type\":\"object\","
        "\"properties\":{\"expression\":{\"type\":\"string\"}},"
        "\"required\":[\"expression\"]}"
    },
    {
        "add",
        "Adds two numbers",
        "{\"type\":\"object\","
        "\"properties\":{\"a\":{\"type\":\"number\"},\"b\":{\"type\":\"number\"}},"
        "\"required\":[\"a\",\"b\"]}"
    },
    // ... 其余工具
};

这种「先声明接口再实现功能」的设计,让整个系统具备良好的自描述性,非常适合被 AI Agent 或自动化框架动态发现和调用。


5. HandleRequest 的完整流程

HandleRequestImpl 是插件的总调度中心,负责完整的「请求 → 处理 → 响应」闭环。

执行步骤:

  1. 初始化响应结构content: []isError: false
  2. 解析请求json::parse(req) 解析外部传入的 JSON 字符串
  3. 读取路由信息:取出 params.name(工具名)和 params.arguments(参数对象)
  4. 按 toolName 分发:进入对应 if / else if 分支,调用计算逻辑
  5. 成功:构造 {"type":"text","text":"3+5 = 8"},推入 content 数组
  6. 异常catch 捕获,置 isError: true,填入错误信息
  7. 序列化返回response.dump() → 分配 char* → 返回给宿主

成功与失败的统一返回格式

不管成功还是失败,返回格式始终一致,宿主程序只需检查 isError 字段即可:

// 成功
{
  "content": [{ "type": "text", "text": "3 + 5 = 8" }],
  "isError": false
}

// 失败
{
  "content": [{ "type": "text", "text": "Error: Division by zero" }],
  "isError": true
}

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

因为插件接口采用 C ABI(应用二进制接口),使用 extern "C" 导出,确保兼容 C 语言或其他非 C++ 宿主程序。std::string 是 C++ 特有类型,跨模块传递可能引发二进制不兼容问题,因此选择 char*


6. 总结

这份代码展示了一个设计合理、职责清晰的 C++ 插件工程实践。它将数学计算能力封装成 JSON 驱动的工具接口,让宿主程序(如 AI Agent)可以像调用 API 一样使用它。

「先声明接口,再接收请求,处理参数并执行功能,最后把结果按 JSON 协议返回。」

一个 tool 插件的最小闭环,不外乎这四步:
定义 Metadata → 解析参数 → 执行逻辑 → 返回 JSON
掌握这个骨架,就可以把任何计算能力或业务逻辑封装为可被动态调用的工具插件。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐