文档 / C ABI

C ABI 总览

crates/ysu-capi 把 YSU 包成一层 C 接口。C++ 外壳通过它调内核,别的语言想用 YSU 也是走这条路。

这一层不是「自动生成的绑定」,是手写的。26 个 extern "C" 函数,一个手写头文件,两边由测试盯着参数个数是否一致。

边界上有什么

只有一个东西:像素。 建实例、喂 HTML、设视口、渲染一帧、拿到一块 RGBA 图。外壳从头到尾不知道 wgpu 是什么,也不需要知道。

GPU 设备、渲染目标、字形图集、着色器、管线的生命周期全在 Rust 里。C 侧拿到的是一个不透明指针 YsuEngine *,里面有什么它看不见。

第二样东西是字符串。 页面标题、链接地址、外部样式表的 URL 列表、内置页面的 HTML、地址解析结果——这些都从内核以 char * 的形式交出去,用完由调用方释放。

就这么两样。没有回调表,没有虚函数表,没有跨语言的对象模型。

为什么这么切

如果渲染改成交换链或者共享纹理,外壳就得知道 wgpu 的设备、表面、重建时机、释放顺序。这些都是「必须知道才能用对」的知识,泄漏到 C++ 里就是永久的负担——每加一个平台后端就要重来一遍。

现在的代价是每次渲染有一趟 GPU 到 CPU 的回读,多一份拷贝。换来的是 26 个函数、一个不透明指针、两种数据形状。

一帧的生命周期

YsuEngine *engine = ysu_engine_new(1280, 800, 1.0);
ysu_engine_load_html(engine, html, "https://example.com/");
const uint8_t *pixels = ysu_engine_render(engine, 0.0, 0.0);
// pixels 由引擎持有,下一次 ysu_engine_render 之前有效
ysu_engine_free(engine);

ysu_engine_render 做四件事:把显示列表翻成 draw call、画进离屏纹理、读回 CPU 内存、返回首地址。它不是每帧都调用——滚动、加载完、改尺寸、改缩放的时候调用一次。

拿到的像素缓冲不由调用方释放。它是引擎内部一块 Vec<u8> 的指针,下一次渲染会原地覆盖。想留着就自己拷一份。

26 个函数的分类

类别函数
版本ysu_capi_versionysu_engine_version
错误与内存ysu_last_errorysu_string_free
实例ysu_engine_newysu_engine_free
加载ysu_engine_load_htmlysu_engine_load_file
视口ysu_engine_set_viewportysu_engine_set_zoom
查询ysu_engine_document_heightysu_engine_titleysu_engine_link_at
渲染ysu_engine_renderysu_last_render_ok
图像ysu_image_widthysu_image_heightysu_image_stride
外部样式表ysu_engine_stylesheet_linksysu_engine_set_linked_stylesheet
网络ysu_fetch
内置页面ysu_page_forysu_page_loadingysu_page_error
地址ysu_resolve_addressysu_resolve_link

逐个说明见函数参考

错误处理

没有错误码。三套约定并存:

  • 返回指针的函数:失败返回空指针,原因用 ysu_last_error() 取。取一次就清空。
  • 返回 bool 的函数:失败返回 false。目前只有 ysu_engine_load_file
  • ysu_resolve_address:用出参 kind 写回 YSU_TARGET_INTERNAL / YSU_TARGET_FILE / YSU_TARGET_REMOTE

ysu_last_error 是全局的,不是每个引擎一份。多线程同时用多个引擎时,错误信息互相覆盖。

三套尺寸的约定

这是最容易搞错的一处,写在这里:

参数单位
ysu_engine_new 的 width / height逻辑像素
ysu_engine_new 的 scale设备像素比
ysu_engine_set_viewport 的三个参数同上
ysu_engine_render 的 scroll_x / scroll_y布局坐标(CSS 像素)
ysu_engine_render 返回的像素物理像素
ysu_image_width / height / stride物理像素
ysu_engine_document_height逻辑像素
ysu_engine_link_at 的 x / y布局坐标

两条推论:

物理尺寸当视口传进去是错的。页面会按物理宽度排版,比该有的宽,文字在屏幕上偏小四分之一(在 2 倍屏上)。

滚动量用屏幕像素传也是错的。外壳按屏幕像素记滚动,交给内核之前要除以缩放倍数。

头部文件

ui/include/ysu_capi.hpp。扩展名是 .hpp 只因为它主要给 C++ 引,但内容完全是 C 的——所有函数都在 extern "C" 里,从 C 里引也成立。

它是手写的,与 crates/ysu-capi/src/lib.rs 一一对应。改签名要同时改两边,并把 YSU_CAPI_VERSION 加一。

例子

crates/ysu-capi/examples/ 下三个例子直接以 Rust 调这些 ABI 函数——因为 ysu-capi 同时产出 rlib,Rust 代码可以像调普通函数一样调 extern "C" 函数。它们走的是完全相同的二进制接口,只是不经过 C 编译器。

完整的一段 C 代码见完整示例