OpenTUI迁移逻辑到Zig,优化Node FFI性能
OpenTUI下一步是什么?这是一篇技术文章。
过去几个月,OpenTUI在稳定性上有了很大提升,还增加了实时音频流等有趣但非必要的功能,以及将滚动缓冲区与实时TUI混合渲染的实用功能(称为footer模式)。总体而言,功能集支持构建大型复杂应用。React和Solid使其非常简便。但仍有大量工作要做。
我们设定了三个主要里程碑:
- 将大部分目前位于TypeScript的行为逻辑迁移到原生Zig核心
- Node兼容性
- 极致优化文本渲染等原语
渲染树机制目前仅能从TypeScript使用。可以将其想象为DOM,但像场景图一样可控。渲染树中的元素称为renderables。它们可以暴露render方法来自行绘制。所有renderable都派生自BaseRenderable。Renderable和渲染树将成为原生原语。可用于任何语言绑定的构建块。将TypeScript绑定减薄到极薄的一层,所有行为逻辑位于原生二进制中。
将逻辑下移不仅仅是把TypeScript类迁移到Zig。TypeScript目前拥有树、脏状态传播、布局读取、剔除和渲染顺序。如果它仍然需要遍历每个节点并为每个步骤调用原生代码,那么我们将保留大部分复杂性并增加FFI开销。整个处理过程及其状态需要一起移动。
最近我们通过将yoga-layout构建到原生二进制中朝着这个目标迈出了一大步。它通过FFI暴露了官方yoga-layout TypeScript包的一部分API。仅限OpenTUI实际使用的API面。由原始yoga-layout包的测试套件覆盖。这已经带来了约2.5倍的中位速度提升,在狭窄场景下可达30倍。yoga-layout集成的好处不仅限于速度提升。内置的文本和编辑器测量现在可以在布局期间完全在原生代码中进行,而无需回调JavaScript。
上个月我做了一个实验,让GPT 5.6将yoga-layout从C++移植到Zig,效果非常好。但目前维护起来负担太大,所以暂时搁置。以后可能会再考虑。
@simonklee 正在不懈地致力于Node兼容性,并且已经有一个完整的Node版OpenCode在运行。Node在v26.4.0中增加了FFI支持,感谢Node社区的帮助,特别是@matteocollina 和@p_insogna。Node和Bun之间的行为和接口似乎相似,但存在一些重大差异。为了充分利用Node FFI实现的最佳性能,其使用必须遵循一些规则。
Node有三种调用原生函数的方式:通用C++/libffi路径、SharedBuffer路径和V8 Fast API。
通用路径在Node的C++层转换每个参数,然后通过libffi调用函数。它很灵活,但对于频繁调用的函数来说是最慢的选择。
SharedBuffer路径是中间方案。JavaScript将标量值和BigInt指针写入每个函数的小缓冲区,减少了一些转换工作。但实际的native调用仍然通过libffi。用作指针的类型化数组无法打包到该缓冲区中,从而回退到通用路径。
我们真正想要的是V8 Fast API。Node为精确的函数签名生成一个小型机器码蹦床,使优化的JavaScript可以直接调用原生函数,而无需通过通用转换器或libffi。这仅适用于JavaScript到原生函数的调用。从原生代码到JavaScript的回调仍然使用libffi闭包。
要进入此路径非常严格。签名最多可以有8个参数,并且必须完全适合CPU寄存器。x86-64 Unix系统有6个通用寄存器(GP)和8个浮点寄存器(FP)用于参数。ARM64...
过去几个月,OpenTUI在稳定性上有了很大提升,还增加了实时音频流等有趣但非必要的功能,以及将滚动缓冲区与实时TUI混合渲染的实用功能(称为footer模式)。总体而言,功能集支持构建大型复杂应用。React和Solid使其非常简便。但仍有大量工作要做。
我们设定了三个主要里程碑:
- 将大部分目前位于TypeScript的行为逻辑迁移到原生Zig核心
- Node兼容性
- 极致优化文本渲染等原语
渲染树机制目前仅能从TypeScript使用。可以将其想象为DOM,但像场景图一样可控。渲染树中的元素称为renderables。它们可以暴露render方法来自行绘制。所有renderable都派生自BaseRenderable。Renderable和渲染树将成为原生原语。可用于任何语言绑定的构建块。将TypeScript绑定减薄到极薄的一层,所有行为逻辑位于原生二进制中。
将逻辑下移不仅仅是把TypeScript类迁移到Zig。TypeScript目前拥有树、脏状态传播、布局读取、剔除和渲染顺序。如果它仍然需要遍历每个节点并为每个步骤调用原生代码,那么我们将保留大部分复杂性并增加FFI开销。整个处理过程及其状态需要一起移动。
最近我们通过将yoga-layout构建到原生二进制中朝着这个目标迈出了一大步。它通过FFI暴露了官方yoga-layout TypeScript包的一部分API。仅限OpenTUI实际使用的API面。由原始yoga-layout包的测试套件覆盖。这已经带来了约2.5倍的中位速度提升,在狭窄场景下可达30倍。yoga-layout集成的好处不仅限于速度提升。内置的文本和编辑器测量现在可以在布局期间完全在原生代码中进行,而无需回调JavaScript。
上个月我做了一个实验,让GPT 5.6将yoga-layout从C++移植到Zig,效果非常好。但目前维护起来负担太大,所以暂时搁置。以后可能会再考虑。
@simonklee 正在不懈地致力于Node兼容性,并且已经有一个完整的Node版OpenCode在运行。Node在v26.4.0中增加了FFI支持,感谢Node社区的帮助,特别是@matteocollina 和@p_insogna。Node和Bun之间的行为和接口似乎相似,但存在一些重大差异。为了充分利用Node FFI实现的最佳性能,其使用必须遵循一些规则。
Node有三种调用原生函数的方式:通用C++/libffi路径、SharedBuffer路径和V8 Fast API。
通用路径在Node的C++层转换每个参数,然后通过libffi调用函数。它很灵活,但对于频繁调用的函数来说是最慢的选择。
SharedBuffer路径是中间方案。JavaScript将标量值和BigInt指针写入每个函数的小缓冲区,减少了一些转换工作。但实际的native调用仍然通过libffi。用作指针的类型化数组无法打包到该缓冲区中,从而回退到通用路径。
我们真正想要的是V8 Fast API。Node为精确的函数签名生成一个小型机器码蹦床,使优化的JavaScript可以直接调用原生函数,而无需通过通用转换器或libffi。这仅适用于JavaScript到原生函数的调用。从原生代码到JavaScript的回调仍然使用libffi闭包。
要进入此路径非常严格。签名最多可以有8个参数,并且必须完全适合CPU寄存器。x86-64 Unix系统有6个通用寄存器(GP)和8个浮点寄存器(FP)用于参数。ARM64...