C++ 绑定¶
C++ 直接对着 ABI 头文件写。中间没有 SDK 层,因为这一侧没什么可做的:sdk/abi.h 本身就是 C、
本身就是契约,而且所有其他绑定都以它为准。
这是一笔交换。Rust 给你 Result、RAII,每个槽位都有检查过的封装;C++ 给你没有任何遮挡的契约,
SDK 本来会替你做的四件事归你自己做:
| Rust | C++ | |
|---|---|---|
| 入口点 | 宏生成 | 自己写 pier_main |
| 能力检查 | 每个封装里都有 | 每个调用点自己比对 struct_size |
| 字符串 | &str 转换 |
自己构造 PierStr |
| 错误 | Result<T> |
bool 返回值加出参 |
模组大部分工作在引擎侧、ABI 只占一小块,或者它得待在一个已经是 C++ 的代码库里——选 C++。 ABI 就是模组的主要内容——选 Rust。
构建一个需要什么¶
一个头文件,不链库,不需要构建系统:
| Text Only | |
|---|---|
仓库里的 examples/hello-pier-cpp 就是这个模组,旁边有四个构建文件——MSVC、clang-cl、
CMake、xmake——产出同一个 DLL。用你项目已经在用的那个,源码不因此改变。
编译器必须产出 MSVC ABI 的 x64 二进制。Windows 上就是 cl 或 clang-cl。
GNU 驱动的 clang++ 和 MinGW 的 g++ 编得过、链得上,然后加载不了——这是个很慢才发现的错误。
最小的模组¶
里面有三处不是可选的。
PIER_MAIN_EXPORT,不是裸的 extern "C"。 给符号起名不等于把它导出。
DLL 不主动要求就什么都不导出,光声明能干净编过,加载时才被拒,报
"does not export pier_main"。
那四个头部标量。 宿主读 struct_size 才知道能碰你这张表的多少,这正是你对它的表做的事情的镜像。
留零就等于告诉宿主这张表长度为零。
mod_flags 写明目标。 服务端模组填 0,给客户端宿主用的填 PIER_FLAG_CLIENT。
宿主拿第 0 位和自己的比,不一致就在加载时拒绝,这样一个为某个目标构建的 DLL
会直接说出来,而不是在另一个目标上一个槽位一个槽位地失败。
log 调用前不做能力检查,而且只有 log 这样。它是固定头部里四个槽位之一,任何宿主都有。
头部之后的一切都要做下面这个检查。
调用前先检查能力¶
PierApi 只增不减。比你编译时用的头文件旧的宿主,那张表更短、提前结束,
所以读一个超出末尾的槽位读的是宿主从没写过的内存。
槽位不可用有两种原因,都属正常:
- 超出
struct_size—— 宿主比这个能力还早。 - 为 NULL —— 宿主够新,但构建时没编进填这个槽位的包。
pier-dimensions和pier-lane是可裁的,客户端构建填的是另一组。 NULL 不是要上报的 bug,是一个要学会没有它也能过的能力。
tools/pier-probe 是一个把这两种原因逐槽位报出来的模组。
字符串¶
PierStr 是 {指针, 长度},不保证以 NUL 结尾。传进你回调的字符串归调用方所有,
只在那一次调用内有效:想留就自己拷贝。
| C++ | |
|---|---|
模组通过 sink 回调在当前调用帧内把字符串交出去。所有权在两个方向上都不跨越边界; 这个 ABI 里没有任何一个调用会返回一个「对方必须负责释放」的指针。
错误¶
会失败的槽位返回 bool,值放进出参。false 表示宿主没有答案,
而且它不区分「没找到」和「这个引擎版本不暴露该属性」。两者都是结果、不是错误,宿主也都不打日志。
false 和「结果是 0」是两个不同的答案。把它们合并正是契约 §5.2 要防的那个错误。
异常¶
宿主调用的每个函数都是 C 函数:pier_main、生命周期回调,以及所有事件、命令、任务、服务和 sink 回调。
异常绝不能从其中任何一个里抛出去。 栈展开进入宿主的帧是未定义行为;
宿主用 MSVC 的 /EHsc 编译,前提就是 C 函数从不抛异常。
宿主会在 pier_main 和生命周期回调外面尽量接住异常并按拒绝处理,但那只是最后一道栏杆,不是契约。
凡是会分配内存、解析数据、或调用可能抛异常的库的回调,都要在最外层自己包一个 try:
| C++ | |
|---|---|
改成把回调标为 noexcept 不是一回事:异常到达它时会调用 std::terminate,
挂掉的是整个服务器,而不是一个请求。
卡死¶
回调跑在宿主的线程上,通常是服务器主线程。它不返回,服务器就不再 tick。
Pier 的看门狗在 watchdog.warn_ms 后点名是哪个模组、卡在哪个入口,
在 watchdog.hang_ms 后结束进程,好让守护程序重启服务器,见配置。
它没法只停掉你的模组:没有安全的办法把一个线程从持有锁的代码里拽出来。
会阻塞的工作——网络、磁盘、长时间计算——放到你自己的线程上,结果通过 schedule 交回主线程。