本章导读
类型(113)与对象(114)之后,动态调用电墨登场:脚本要调 C 函数、C 要回调脚本函数、绑定层要在两者之间传参——runtime_call 提供统一的调用模型。三件套:签名(xrtfunctionsig——参数名/类型/返回类型的借用声明;命名参数参与签名身份);调用帧(xrtcallframe——Self/位置参数/关键字参数/上下文的借用组装;FrameValidate 全量校验:重复关键字/必需参数/未知关键字/varargs 标志);callable(xrtCallableCreate(签名, 入口, 环境, 环境析构)——创建后不可变、引用计数原子;环境由 callable 持有)。结果(xrtcallresult——前四个内联零分配、Set/Push 引用语义、Take 转移)。Invoke 的六步原子序列(复制帧→验证→临时结果→失败清理→数量检查→一次替换)保证失败调用不破坏调用方原结果。关键边界:只提供类型明确的动态入口——不通用调用任意 C ABI(无 libffi 依赖)。
116.1引入
"动态调用"的朴素做法是 void* fn(void** args) ——丢失全部类型信息,每个绑定手写转型,错误在运行深处爆炸。第二条歧路是 libffi 式通用调用——平台 ABI 的深渊(cdecl/stdcall/fastcall、结构体返回约定……契约文档明说"C 无法在没有准确原型或 libffi 的前提下安全通用调用任意函数")。xruntime 的第三条路:类型明确的动态入口 xrtcallproc——编译器、绑定生成器或 FFI 模块为每个原始函数生成一个适配器(适配器有准确原型、调原始函数零 UB),动态层只见适配器。签名在动态层流通:参数名/类型/返回类型都是声明——入口拿到的是"已验证的帧 + 有效签名",转型在适配器里静态完成。
命名参数的设计值得注意:KeywordNames/KeywordValues 是借用的名称+值数组——不是字典!"调用方不需要为了传递 kwargs 构造字典"——热路径零分配的又一次贯彻(对比脚本语言的常规做法:kwargs 先建 Map 再传)。
116.2概念
签名与调用帧
参数读取三入口:FrameArgument/FrameKeyword(原始传参视图)、FrameParameter(按有效签名形参下标完成位置/关键字选择——普通入口首选;可选参数未提供返回 NULL 不设错)。
结果:内联与引用语义
xrtcallresult 拥有其中的 xvalue 引用:前四个结果内联(零结果数组分配);第五个起轻量指针数组(不建动态 Value 容器)。操作族:Set/Push(增引用)、SetTake/PushTake(转移并清空来源槽)、Clear(释放值保留容量——热路径复用)、Unit(全释放)、Move(整体转移)。只允许替换现有下标或追加末尾——不允许稀疏写入(旧版为填空洞构造多个 null 的教训——返回数量始终明确)。
callable 与 Invoke 六步
入口可并发可重入——环境是否并发安全由入口实现负责(契约划界)。callable 类型(xrtTypeCallable()):拥有一个强引用的稳定 C ABI 槽——Copy/Clone 增引用替换、Move 转移清空、Drop 释放——callable 作为值在容器/字段流通(第 115 章对象值同款模式)。
原始 ABI 边界
旧版 callable 保存 cdecl/stdcall/fastcall 指针与动态入口的混合——本模块只提供一种类型明确的动态入口。绑定生成器生成 xrtcallproc 适配器调原始函数:这条边界避免未定义行为,也不把平台 ABI 成本施加给基础调用层(裁剪与可移植的双赢)。
调用层的分层位置
116.3示例
第一个完整程序:callable 创建与调用
下面的程序来自 examples/runtime/call——签名、帧、调用的完整闭环:
#include <stdio.h> #include <xruntime.h> /* 把两个整数参数相加并移交唯一的返回值引用。 */ static bool add( ptr pEnvironment, const xrtcallframe* pFrame, xrtcallresult* pResult ) { int64 iLeft; int64 iRight; xvalue* pValue; (void)pEnvironment; if ( !xrtValueGetInt(xrtCallFrameParameter(pFrame, 0u), &iLeft) || !xrtValueGetInt(xrtCallFrameParameter(pFrame, 1u), &iRight) ) { return false; } pValue = xrtValueInt(iLeft + iRight); if ( pValue == NULL ) { return false; } if ( !xrtCallResultPushTake(pResult, &pValue) ) { xrtValueRelease(pValue); return false; } return true; } int main(void) { xrtparamdesc Params[2] = { { XRT_STR_INIT("left"), xrtTypeInt64(), XRT_PARAM_DEFAULT, 0u }, { XRT_STR_INIT("right"), xrtTypeInt64(), XRT_PARAM_DEFAULT, 0u } }; const xrttype* ReturnType = xrtTypeInt64(); xrtfunctionsig Signature = { .Name = XRT_STR_INIT("add"), .ParamCount = 2u, .Params = Params, .ReturnCount = 1u, .ReturnTypes = &ReturnType }; xvalue* Arguments[2] = { xrtValueInt(7), xrtValueInt(5) }; xrtcallframe Frame = { .ArgumentCount = 2u, .Arguments = Arguments }; xrtcallresult Result = XRT_CALL_RESULT_INIT; xrtcallable* pCallable = xrtCallableCreate( &Signature, add, NULL, NULL); int64 iSum; int iResult = 0; if ( (pCallable == NULL) || !xrtCallableInvoke(pCallable, &Frame, &Result) || !xrtValueGetInt(xrtCallResultGet(&Result, 0u), &iSum) ) { iResult = 1; } else { printf("type=%.*s\n", (int)xrtTypeCallable()->Name.Size, xrtTypeCallable()->Name.Data); printf("7 + 5 = %lld\n", (long long)iSum); } xrtCallResultUnit(&Result); xrtCallableUnref(pCallable); xrtValueRelease(Arguments[1]); xrtValueRelease(Arguments[0]); return iResult; }
刚才发生了什么。 ① 创建侧:签名(两 int64 参数、int64 返回——借用声明)+ 加法入口 + 环境组合出 callable——xrtTypeCallable()->Name 打印 callable:callable 本身是个有类型的值(第 114 章类型谱系里的 CALLABLE 类)。② 调用侧:帧组装两个位置参数 → xrtCallableInvoke(pCallable, &Frame, &Result) → xrtCallResultGet(&Result, 0) 取首结果 → xrtValueGetInt 解引用——四步从动态结果回到静态 int64。③ 7 + 5 = 12 的背后是完整六步序列:帧验证过、临时结果原子替换——任何一步失败你的 Result 保持原样(下一个调用可以放心复用)。Unit 收尾 + Unref callable——引用纪律与第 115 章对象一致。
第二个完整程序:值侧 callable 装箱
第二个程序来自 examples/runtime/value_callable——callable 进 Value 世界:
#include <stdio.h> #include <xruntime.h> /* 返回一个整数。 */ static bool answer( ptr pEnvironment, const xrtcallframe* pFrame, xrtcallresult* pResult ) { xvalue* pValue = xrtValueInt((int64)(intptr_t)pEnvironment); (void)pFrame; if ( pValue == NULL ) { return false; } if ( !xrtCallResultPushTake(pResult, &pValue) ) { xrtValueRelease(pValue); return false; } return true; } int main(void) { xrtcallable* pCallable = xrtCallableCreate( NULL, answer, (ptr)(intptr_t)42, NULL); xvalue* pFunction = xrtValueCallableTake(&pCallable); xrtcallresult Result = XRT_CALL_RESULT_INIT; int64 iValue; int iResult = 0; if ( (pFunction == NULL) || !xrtValueInvoke(pFunction, NULL, &Result) || !xrtValueGetInt(xrtCallResultGet(&Result, 0u), &iValue) ) { iResult = 1; } else { printf("answer=%lld\n", (long long)iValue); } xrtCallResultUnit(&Result); xrtValueRelease(pFunction); return iResult; }
刚才发生了什么。 ① runtime_value_callable 是独立裁剪层:callable 强引用装进 xvalue(第 31 章动态值的运行时扩展)——脚本层的"函数是一等值"由此落地:Value 里的 callable 可以存进 dict、传给函数、从函数返回。② 基础 runtime_call 不强制启用 Value 装箱——不需要一等的场景零装箱成本(第 114 章裁剪哲学的逐层贯彻)。③ 配套示例族:value_future(Future 装箱)、value_object(对象装箱)、value_type(类型值装箱)——运行时对象↔动态值的完整桥面。
116.4契约
- 签名借用:签名/类型/参数/返回数组声明方持有、生命覆盖 callable;命名参数参与签名身份且唯一;空名仅位置。
- 帧借用:Self/参数/关键字数组/上下文全部借用——调用期间有效。
- 校验全量:完整性/空值/重复关键字/必需/重复传参/未知关键字/标志组合——验证过的帧才进入口。
- 参数读取:FrameParameter 按签名下标选位置/关键字(首选);可选未提供=NULL 不设错;关键字不存在=NULL 不设错。
- 结果语义:前四内联;Set/Push 增引用、Take 族转移清源;Clear 保容量;只替换或追加——无稀疏写入。
- callable 不可变:创建后签名/入口/环境不变;引用计数原子;环境恰一次析构。
- Invoke 原子:六步序列;失败释放部分结果+包装原因链、调用方结果无损;成功一次替换。
- 入口纪律:可并发可重入;环境并发安全归入口实现。
- ABI 边界:仅一种类型明确入口;原始函数经适配器接入——无通用调用无 UB。
- 装箱独立:Value callable 是独立裁剪——基础层零装箱成本。
常见错误
症状:callable 调用时读到垃圾签名——创建时的参数/返回数组已随栈帧消亡。
原因:签名是借用——生命周期必须覆盖 callable 全程(callable 不复制它)。签名放静态存储或与 callable 同生命的堆。
xrtcallable* make(void) { xrtfunctionsig Sig = { ...栈上... }; return xrtCallableCreate(&Sig, entry, NULL, NULL); /* 返回即悬空 */ }
static const xrttype* const kArgs[] = { ... }; /* 静态 */ static const xrtfunctionsig kSig = { ... }; xrtcallable* C = xrtCallableCreate(&kSig, entry, NULL, NULL);
症状:失败路径读 Result——拿到的是上一次成功调用的旧值(原子性保护了它,但它不是本次结果)。
原因:六步序列保证"失败不破坏原结果"——原结果可能是空(首次)也可能是旧值;失败后 Result 的语义是"未变",必须走错误分支而不是读结果。
if ( !xrtCallableInvoke(C, &Frame, &Result) ) { use(xrtCallResultGet(&Result, 0)); /* 旧值/空——不是本次的 */ }
if ( !xrtCallableInvoke(C, &Frame, &Result) ) { report(xrtErrorMessage(xrtGetError())); /* 错误分支:只看错误 */ } else { use(xrtCallResultGet(&Result, 0)); /* 成功才有本次结果 */ }
症状:被拒绝——结果只允许替换现有或追加末尾。
原因:稀疏写入的语义空洞(中间下标是什么?null?)是旧版设计坑——新契约"返回数量始终明确",要占位就显式 Push 空。
xrtCallResultSet(&Result, 2u, Value); /* 越界稀疏:拒绝 */
/* 依次追加——数量与顺序都明确 */ xrtCallResultPush(&Result, V0); xrtCallResultPush(&Result, V1); xrtCallResultPush(&Result, V2);
编程练习
同一入口:位置参数调用 + 关键字参数调用(名称数组传参)——FrameParameter 双轨取参。验收标准:两轨解出同参;未知关键字被 Validate 拒绝。
三参签名(一必需两可选):入口对未提供参数取默认——FrameParameter 的 NULL 语义。验收标准:三种调用形态(全给/给一个/只给必需)行为正确;重复传参(位置+关键字同名)被拒。
用 dict(第 32 章)存 name→callable 的注册表;实现 call(name, 帧字符串) 的迷你解释器:查表→帧→Invoke→结果打印。加一个 C 原函数(如 max(int64,int64))经适配器注册。验收标准:三函数注册可调;未知名清晰报错;并发调用同一 callable 零错(入口无状态)。
小结
| 知识点 | 速查 |
|---|---|
| 三件套 | 签名(借用声明)/帧(借用组装)/callable(不可变+原子引用) |
| 命名参数 | 名称+值借用数组——非字典;命名参与签名身份且唯一;空名仅位置 |
| 校验 | Validate 全量:重复关键字/必需/未知/varargs 组合 |
| 参数读取 | FrameParameter 按签名下标(首选);可选未给=NULL 不设错 |
| 结果 | 前四内联;Set/Push 引用、Take 转移、Clear 保容量;只替换/追加 |
| Invoke 六步 | 复制帧→验证→临时结果→失败清理→数量检查→一次替换 |
| 原子性 | 失败不破坏调用方原结果;错误包 xrt.call 原因链 |
| 入口 | 可并发可重入;环境并发归实现 |
| ABI 边界 | 仅一种类型明确入口;原始函数经适配器——无 UB 无平台成本 |
| 装箱 | Value callable 独立裁剪——一等函数可选 |
📖 本章涉及的 API 参考手册:xruntime-runtime_call(21 个条目)xruntime-runtime_type(60 个条目)
点击查看完整签名、参数约束、返回值与错误说明