第 40 章 / 共 51 章
卷五 · 扩展、运维与实战
嵌套 TCC 与插件系统:脚本里再开编译器
xsCreateTCC 给脚本一台预置好一切的编译器——动态规则、表达式求值、第三方插件,都在"运行期编译原生代码"这条路上。
本章导读
站位:ch27 管理路由的"升级形态"——当管理面开始编译代码,它就成了插件平台。func_test 有嵌套 TCC 与 reload 并发的真实用例(4 线程 × 12 请求),本章以它为验收标准。
40.1 引入
有一类需求无法用"写死的代码"满足:风控规则每周变、报表公式由用户定义、第三方想给你的平台写扩展。传统答案是把一门解释器嵌进来(Lua/JS)——代价是双运行时与边界开销。XServer 的答案顺理成章:宿主用 TCC 编译脚本,脚本也能用 TCC 编译代码——同一种语言、同一套符号、零边界。xsCreateTCC 预置了 include 路径与全部宿主符号,嵌套代码生下来就能调用你的 API。
40.2 插件编译五步
表达式插件(完整可运行模式)
/* 需求:用户给一段 C 表达式,服务端求值 */
typedef int (*ExprFn)(int);
static bool CompileExpr(const char* sExpr, ExprFn* ppFn)
{
char sSource[1024];
TCCState* pTcc;
snprintf(sSource, sizeof(sSource),
"int eval(int x) { return %s; }", sExpr);
/* 1. 创建(预置 include/符号) */
pTcc = xsCreateTCC();
if ( pTcc == NULL ) return false;
/* 2. 编译字符串 */
if ( tcc_compile_string(pTcc, sSource) < 0 ) {
xsDestroyTCC(pTcc);
return false; /* 语法错——回 400 */
}
/* 3. 重定位到内存 */
if ( tcc_relocate(pTcc, TCC_RELOCATE_AUTO) < 0 ) {
xsDestroyTCC(pTcc);
return false;
}
/* 4. 取符号(须先声明函数指针类型——签名约定在你手里)*/
*ppFn = (ExprFn)tcc_get_symbol(pTcc, "eval");
/* 5. pTcc 由调用方持有:符号使用期间不得销毁 */
return *ppFn != NULL;
}
/* 用毕:xsDestroyTCC(pTcc) —— 之后 ppFn 立即失效 */
刚才发生了什么:编译五步(创建→编译→重定位→取符号→销毁)的关键纪律是符号的生命周期绑在 TCCState 上——产物是 TCCState 拥有的内存,Destroy 即失效。所以插件缓存的正确结构是"缓存 TCCState",不是"缓存函数指针"。
40.3 错误回调与符号注入
- 接管错误输出:
tcc_set_error_func把编译诊断引到你的收集器(默认打 stderr)——用户提交的表达式写错时,把诊断返回给他是产品质量的一部分 - 符号注入:
tcc_add_symbol让插件调用你定义的宿主函数——暴露一个plugin_api_*前缀的小函数集,就是你的插件 SDK - 权限默认全开:xsCreateTCC 注入的是全套宿主符号——需要收窄时自行封装(只 add_symbol 白名单集),这是你设计插件沙箱边界的原料(完整讨论见 ch44)
- VFS 可用:插件代码同样 include VFS 内的头;
tcc_vfs_mount_memory能在运行期注入"虚拟文件"——宿主装载脚本用的就是这招
40.4 与热重载并发
嵌套编译器与 reload 并发时的安全问题是:插件符号解析到的宿主函数,会不会在换代时被回收?func_test 的用例给出了答案与验收——4 线程并发跑嵌套编译 × 12 请求,同时反复 reload,全程零失败。机制上:
- 插件的 TCCState 是你的代的状态——挂进注册表、随代销毁,就自动安全(ServiceUnit 里 DestroyTCC)
- 旧代插件解析的符号指向旧代宿主函数——旧代 lease 保住它们到插件一起退役
- 新代重新编译插件(或按需惰性重编)——与路由表重建同构
40.5 契约
- 生命周期线:函数指针活在 TCCState 里;缓存的单位是 TCCState;Destroy 后指针即死。
- 代际线:插件状态随代管理(Init 建/Unit 销)——与 reload 并发即安全。
- 签名线:取符号后的函数指针类型由你声明——配套文档写清约定,错了就是未定义行为。
常见错误
缓存了函数指针,销毁了编译器
- 症状
- 插件缓存命中时跳转到已释放内存。
- 原因
- 产物内存归 TCCState 所有。缓存键值都应是"表达式 → TCCState",生命周期与缓存一致。
用错误的函数指针签名调用
- 症状
- 参数错位、栈损坏、随机崩溃。
- 原因
- tcc_get_symbol 只给地址不验签名——类型约定全靠你。插件 SDK 里固定一个签名模板(如 int fn(int)),文档写死。
编程练习
基础表达式服务
实现 POST /expr {code:"x*2+1", x:5} → 11。
验收:正确求值;语法错误返回 400 + 诊断信息。
进阶编译缓存
加"表达式 → TCCState"缓存(LRU 上限 100),统计命中。
验收:同表达式二次调用不再编译;缓存满时最久未用的被销毁。
挑战并发 + reload 风暴
复刻 func_test 用例:4 线程并发求值 × 12 请求,期间换代 5 次。
验收:零失败;旧代插件的 Unit 里能看到 DestroyTCC 被调。
小结
| 要点 | 结论 |
|---|---|
| 入口 | xsCreateTCC:预置 include + 全套宿主符号 |
| 五步 | 创建→编译→重定位→取符号→(用毕)销毁 |
| 缓存单位 | TCCState(不是函数指针) |
| 并发安全 | 插件状态随代管理;func_test 有并发验收用例 |
| 边界 | 权限默认全开;收窄靠自封装符号白名单(见 ch44) |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · memory — 插件缓存与销毁路径的内存核对