Skip to content

Latest commit

 

History

History
203 lines (154 loc) · 12.8 KB

c-abi.md

File metadata and controls

203 lines (154 loc) · 12.8 KB

C ABI 规范:生成管线与类型映射

状态:生成管线已实施;错误模型与整数宽度未决(见 §7) 适用范围:core/src/capi/、tools/codegen/ 核实基准:2026-08-25,28 个 capi 头中 27 个、27 个实现中 26 个为生成产物

本规范回答:C ABI 长什么样、由谁产出、C++ 类型怎么过桥。 句柄的所有权与失效语义是独立一篇:handle-ownership.md。

1. 第一条:不要手写 capi/

src/capi/ 的公开 ABI 声明和实现由生成器产出。手写支持层包括 string_utils_c.{h,cpp}、私有 C++ user_data.h 和 event_delivery.h; 它们只管理资源所有权,不手写某个业务模块的 ABI。生成物全部带横幅:

// 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 条)。这是刻意的:绑定仓库 里生成文件与手写文件同目录,隐式纳入会静默覆盖别人手写的封装。

2. 类型映射

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::optional<std::string> char* NULL 表示缺失;非 NULL 空字符串表示存在的空内容
std::vector<Enum> native_enum_list_t values 与 long count;复制后释放
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),它本身也是生成的。

枚举传入时逐值映射;未声明的数值保留后交给 C++ 判断,不能偷偷换成首项。 否则 bool setter 无法拒绝非法参数,甚至会以「成功」返回并重置属性。 例如 Window::SetCornerPreference 拒绝未知值,C ABI 也必须返回 false, 保留上一次成功的偏好。传出方向与语言绑定遇到未知值的默认回退另行处理。

3. 句柄

typedef uint64_t native_<类型>_t;——不透明整数,不是指针。编码为 [世代 32 位 | 槽位 32 位],解析时校验槽位存在、世代匹配、类型 tag 匹配。 失效句柄上的任何操作安全失败,不解引用悬垂内存。

完整规则(谁负责释放、回调参数的例外、世代失效语义)见 handle-ownership.md。

3.1 释放函数的三种形态

实际落地的不是「统一改名 _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」——那个方案没有区分数组与元素两级所有权,实施时被上表替换。

4. 字符串

返回 char* 的 getter,所有权归调用方,用 free_c_str() 释放 (string_utils_c.h,仓库里唯一手写的 capi 模块)。

传入方向用 const char*,库内立即拷贝,不留引用。

回调参数里的字符串只在回调期间有效,回调返回即失效——需要留存就自行拷贝。

5. 列表

typedef struct {
  native_display_t* displays;
  long count;
} native_display_list_t;

count 目前是 long——Windows 上 32 位、其余平台 64 位,同一个 ABI 宽度不一致。 待收敛为固定宽度整数(§7)。新写生成器代码时不要沿用 long。

6. 事件与回调

事件通过 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 的例外条)。

6.1 user_data 的释放

凡是接收回调的地方——函数参数、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 由调用方自己负责。

6.2 异步事件交付

同步 add_listener 的签名和回调期间的借用规则保持不变。每个 emitter 另外生成 add_listener_async,回调接收 (event*, native_event_delivery_t, user_data)。 交付句柄使用同一世代句柄表和独立的类型 tag,不是可以随意释放的裸指针。

  • core 保留事件 struct、其中的字符串 / 借用句柄以及 user_data,直到消费端调用 native_event_delivery_complete(delivery, accept);每次交付必须确认一次。
  • 退订立即使 native_event_delivery_is_active 为 false,但排队的交付仍持有资源。 消费端调用用户代码前检查 active;失效的交付仍需 complete(false)。正在运行的 回调可以完成,最后一次交付确认之后才会释放 user_data。
  • 事件若携带共享 EventRequest,每个异步监听器在原生派发期间取得独立的隐式 延后投票;回调成功完成才同意,异常 / 丢弃则否决。不可取消的请求不取得投票, 因此系统强制结束不会等待异步观察者。此规则由 IR 中的字段类型决定。
  • complete 在锁外释放负载和回答投票。重复、失效或类型混淆句柄安全返回 false; 一份交付的并发回答只生效一次。

Dart 的事件订阅使用 NativeCallable.listener,在注册 isolate 消费交付,回调 可返回 FutureOr<void>。JS 使用 Node-API thread-safe function 投递到 JS 线程, 回调可返回 Promise<void>。借用对象到 Future / Promise 完成之前仍有效;异常先 取消交付,再交给运行时的错误处理。JS 环境清理会取消全部未决交付,包括未消费的 队列和永不结束的 Promise;原生排队失败也释放交付。普通方法参数里的同步 callback 不因事件订阅迁移而改变,其余同步 C 消费端继续使用旧接口。

6.3 普通回调的复杂负载

携带可选字符串、对象、列表或 struct 的普通回调增加 native_event_delivery_t 参数,位置在 user_data 前。struct / 列表通过 const T* 交付;字符串、列表及其中 对象句柄在确认前有效。消费者必须调用一次 native_event_delivery_complete,重复确认 返回 false。该 lease 同时保活回调的 user_data,复用现有交付的撤销与释放机制。 简单标量回调的 ABI 不变。

六种高级绑定自动复制内容并确认交付。图片结果通过 native_handle_retain 创建独立 所有权槽位;该函数对 0 或过期句柄返回 0,保留原有类型与基类解析规则。调用方可以 在回调返回、确认完成或剪贴板随后变化后继续使用读取结果。

7. 已知未决

写生成器或改 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)

8. 检查单

  • 没有手工编辑带 AUTO-GENERATED 横幅的文件。
  • 新模块已加入 API_HEADERS,并跑过 ./codegen。
  • 新类型的归属明确(身份对象 → 句柄,值对象 → struct)。
  • 身份对象已在 IdTypeTag 注册表登记(句柄表的类型校验依赖它)。
  • 返回字符串的函数已在文档里写明由 free_c_str() 释放。
  • 返回列表的函数已说明该配 _list_free 还是 _list_release。
  • 新增的回调持有方式能保证:调用期间回调不析构,放手时析构(user_data 才会被释放,§6.1)。
  • 改动经 ./codegen sync 传播到三个绑定(见 workspace AGENTS.md)。