状态:生成管线已实施;错误模型与整数宽度未决(见 §7) 适用范围:
core/src/capi/、tools/codegen/核实基准:2026-08-25,28 个 capi 头中 27 个、27 个实现中 26 个为生成产物
本规范回答:C ABI 长什么样、由谁产出、C++ 类型怎么过桥。 句柄的所有权与失效语义是独立一篇:handle-ownership.md。
src/capi/ 里唯一手写的是 string_utils_c.h / string_utils_c.cpp。其余全部
带横幅:
// AUTO-GENERATED. DO NOT EDIT.
// Any manual changes WILL BE LOST when this file is regenerated.
改 C ABI 的正确路径永远是:
# 1. 改 core/src/ 下的 C++ 头
# 2. 需要新增模块时,把头文件加进 tools/codegen/shared/src/lib.rs 的 API_HEADERS
# 3. 在 workspace 根目录
./codegen想让 C ABI 长成某个样子,就去改 C++ 头或改生成器,不要改产物。
./codegen check 是只读校验,产物过期时非零退出(CI 用)。
一个模块要进 C ABI,必须显式加进 API_HEADERS(当前 34 条)。这是刻意的:绑定仓库
里生成文件与手写文件同目录,隐式纳入会静默覆盖别人手写的封装。
| C++ | C | 形态 |
|---|---|---|
身份对象(shared_ptr<Window> 等) |
native_window_t = uint64_t |
句柄,见 §3 |
值对象(Point/Size/Rectangle/Color) |
同名 native_*_t struct |
按值 |
XxxId(IdAllocator::IdType) |
native_xxx_id_t = unsigned int |
按值 |
std::string |
char* |
调用方所有,见 §4 |
std::vector<shared_ptr<T>> |
native_x_list_t |
见 §5 |
bool / 整数 / 浮点 |
stdbool.h / stdint.h 对应类型 |
按值 |
| 监听器 id | native_listener_id_t = uint64_t |
common_c.h |
分界线就是 object-model.md 的那条:身份对象走句柄,值对象走 struct。 新类型过不了桥,先回去确认它的归属,而不是在生成器里开特例。
common_c.h 承载跨模块共享的定义(FFI_PLUGIN_EXPORT 导出宏、
native_listener_id_t、NATIVE_INVALID_LISTENER_ID),它本身也是生成的。
typedef uint64_t native_<类型>_t;——不透明整数,不是指针。编码为
[世代 32 位 | 槽位 32 位],解析时校验槽位存在、世代匹配、类型 tag 匹配。
失效句柄上的任何操作安全失败,不解引用悬垂内存。
完整规则(谁负责释放、回调参数的例外、世代失效语义)见 handle-ownership.md。
实际落地的不是「统一改名 _release」,而是按语义分成三个函数:
| 函数 | 语义 |
|---|---|
native_<x>_free(handle) |
释放调用方持有的那一份引用。对无效或已释放的句柄调用是安全的。 |
native_<x>_list_free(list*) |
释放数组,并释放其中每一个句柄。 |
native_<x>_list_release(list*) |
只释放数组;其中的句柄交给调用方接管。 |
_free 与 _list_release 的区别不是历史包袱,是「要不要连带释放元素」的真实分叉。
绑定层从列表里取走句柄自行管理时用 _list_release,一次性用完时用 _list_free。
这一条取代 handle-ownership.md §2.5 提出的「全部改名
_release」——那个方案没有区分数组与元素两级所有权,实施时被上表替换。
返回 char* 的 getter,所有权归调用方,用 free_c_str() 释放
(string_utils_c.h,仓库里唯一手写的 capi 模块)。
传入方向用 const char*,库内立即拷贝,不留引用。
回调参数里的字符串只在回调期间有效,回调返回即失效——需要留存就自行拷贝。
typedef struct {
native_display_t* displays;
long count;
} native_display_list_t;
count目前是long——Windows 上 32 位、其余平台 64 位,同一个 ABI 宽度不一致。 待收敛为固定宽度整数(§7)。新写生成器代码时不要沿用long。
事件通过 add_listener / remove_listener 函数对暴露,注册返回
native_listener_id_t,失败返回 NATIVE_INVALID_LISTENER_ID(即 0)。
每个领域生成一个事件类型枚举(NATIVE_DISPLAY_EVENT_TYPE_ADDED 等)加一个事件
struct,C++ 侧的 dynamic_cast 层级在 C 侧摊平成 tag + 联合字段。
回调签名统一带 void* user_data 尾参。回调里拿到的句柄和字符串都不需要也不应该
释放(handle-ownership.md §2.6 的例外条)。
凡是接收回调的地方——函数参数、add_listener、struct 的回调字段——都在
user_data 之后再带一个 native_release_user_data_t(common_c.h,可为
NULL)。什么时候释放由 core 决定,绑定不推测:
- 恰好一次。 每次调用都会释放它收到的
user_data,包括调用失败、句柄无效、 回调为NULL的情况。注册失败时绑定不需要自己清理。 - 在 core 最后一次可能调用该回调之后。 监听器被移除、回调被替换或清空、注册
结束、持有者被销毁时,core 都会释放。实现方式是生成的胶水代码在函数开头创建
nativeapi::capi::UserData(core/src/capi/user_data.h),并让它随std::function被捕获;最后一份拷贝析构时释放。所以 C++ 侧持有回调时必须保证 调用期间回调不会析构(参见Shortcut::Invoke的做法)。 - 在主线程上,异步执行。 释放永远投递到主线程(
RunOnMainThread),不会在让回调 离开的那次调用内部执行:绑定的释放函数可能运行任意代码(比如 Rust 闭包的析构), 不能在 core 某个对象的锁里跑。没有主线程派发机制的平台(Android / OHOS)改为就地 执行。进程退出、投递失败时直接放弃释放,因为绑定的运行时可能已经不在了。 - 主线程不一定是绑定自己的线程(Dart 的 isolate、
deno desktop下的 JS 线程), 需要切换线程的绑定在自己的释放函数里切换。
struct 里的回调字段在 core 读取这个 struct 时接管(生成的 to_cpp_* 转换)。一个
从未传给 core 的 struct,它的 user_data 由调用方自己负责。
写生成器或改 ABI 前先看这几条,避免把问题复制到下游:
| 问题 | 说明 | 方向 |
|---|---|---|
| 没有统一错误模型 | 无 native_get_last_error 之类的通道;无效句柄静默返回默认值,调用方分不清「成功返回默认值」与「句柄已失效」。C++ 一侧的现行做法见 api-style.md §4 |
在 IR 层统一:状态码 + out 参数,或 thread-local last error |
| 整数宽度不可移植 | native_*_id_t 是 unsigned int 而非 uint32_t;list 的 count 是 long、get_size 返回 unsigned long——Windows LLP64 下 32 位、其余平台 64 位,而 Dart / C# / Rust 的 FFI 各自硬编码宽度 |
统一映射为 <stdint.h> 定宽类型 |
| 空串与缺失折叠 | to_c_str 对空字符串返回 nullptr,optional<string> 的「未设置」与 "" 在 ABI 上不可区分,get_title 无法往返 |
空串返回合法的 "" 分配,nullptr 只表示无值 |
| 回调 typedef 生成质量 | 同一个 std::function<void()> 生成三个名字;set_will_show_hook 的回调参数名漏成 arg0、类型退化为 unsigned int |
相同签名共享 typedef;IR 保留参数名与语义类型 |
| 内部 API 泄漏进 ABI | native_shortcut_create_with_id_*、native_shortcut_manager_emit_shortcut_activated、native_window_manager_handle_will_show/hide 与 call_original_*、native_display_create() |
给 IR 加 internal / exclude 标注;C++ 一侧的预防见 api-style.md §3.3 |
| 重载的 C 命名 | register_with_accelerator_and_callback、get_with_accelerator 这类机械后缀可读性差 |
在 C++ 层拆名(api-style.md §1.6),生成名自然变好 |
void* 包装构造被导出 |
native_window_create_with_native_window 等接管原生对象,所有权语义在 C 文档里缺失 |
补所有权说明,或随 internal 标注摘除 |
| 导出宏 | FFI_PLUGIN_EXPORT 在每个头重复定义;#if _WIN32 应为 #ifdef;没有 dllimport 分支 |
收敛到统一 export 头 |
| 生成的文档模板 | 所有 get_native_object 的注释都是 display 的(「NSScreen*, HMONITOR…」被复用到 window / tray / image) |
修生成器模板 |
| 枚举魔数 | NATIVE_DISPLAY_ORIENTATION_LANDSCAPE = 90 继承自 C++ |
修 C++ 枚举(api-style.md §3.1) |
- 没有手工编辑带 AUTO-GENERATED 横幅的文件。
- 新模块已加入
API_HEADERS,并跑过./codegen。 - 新类型的归属明确(身份对象 → 句柄,值对象 → struct)。
- 身份对象已在
IdTypeTag注册表登记(句柄表的类型校验依赖它)。 - 返回字符串的函数已在文档里写明由
free_c_str()释放。 - 返回列表的函数已说明该配
_list_free还是_list_release。 - 新增的回调持有方式能保证:调用期间回调不析构,放手时析构(
user_data才会被释放,§6.1)。 - 改动经
./codegen sync传播到三个绑定(见 workspaceAGENTS.md)。