展示HN:Coi – 一种编译为WASM的语言,超越React/Vue
我通常用 C++ 开发网页游戏,但使用 Emscripten 总让我觉得有些过于复杂,因为我并不需要完整的 POSIX 模拟或庞大的标准库,只是为了在画布上渲染一些东西并处理基本的用户界面。
我想解决的主要问题是 JS/WASM 的互操作瓶颈。与其为每个调用使用标准的粘合代码,我将所有内容迁移到一个共享内存架构,使用命令和事件缓冲区。
它的工作原理是,我将所有的指令批量处理在 WASM 中,然后只向 JS 发送一个“刷新”信号。JS 端随后一次性直接从共享内存中读取所有内容。这种方式效率更高,我进行了一次基准测试,在画布上渲染 10,000 个矩形,结果差异显著:Emscripten 的帧率大约为 40 FPS,而我的设置达到了 100 FPS。
但是在 C++ 中编写 DOM 逻辑是非常痛苦的,所以我构建了 Coi。它是一种基于组件的语言,在编译时静态分析变化,以实现 O(1) 的反应性。与传统框架不同,它没有虚拟 DOM 的开销;编译器将状态变化直接映射到命令缓冲区中的特定句柄。
我最近对比了 Coi 和 React、Vue 在一个 1,000 行表格上的性能:在行创建、行更新和元素交换方面,Coi 表现最佳,因为它完全避免了“差异”步骤,并最小化了桥接跨越。它的包大小也是三者中最小的。
这个架构最酷的地方之一是标准库的工作方式。如果我想支持一个新的浏览器 API(比如 Web Audio 或新的 Canvas 功能),我只需将定义添加到我的 WebCC 模式文件中。当我重新编译 Coi 编译器时,该语言会自动获得一个新的标准库函数来访问该 API,完全不需要手动封装。
我对这个项目的进展感到非常自豪。它结合了自定义 WASM 堆栈的性能和一种让我觉得写起来很舒服的语法(至少对我来说是这样 :P)。此外,由于中间步骤是 C++,我还在考虑让它在服务器端工作,这样就可以在整个堆栈中共享组件。
示例(Coi 代码):
```coi
component Counter(string label, mut int& value) {
def add(int i) : void {
value += i;
}
style {
.counter {
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 8px 16px;
cursor: pointer;
}
}
view {
<div class="counter">
<span>{label}: {value}</span>
<button onclick={add(1)}>+</button>
<button onclick={add(-1)}>-</button>
</div>
}
}
component App {
mut int score = 0;
style {
.app {
padding: 24px;
font-family: system-ui;
}
h1 {
color: #1a73e8;
}
.win {
color: #34a853;
font-weight: bold;
}
}
view {
<div class="app">
<h1>Score: {score}</h1>
<Counter label="Player" &value={score} />
<if score >= 10>
<p class="win">You win!</p>
</if>
</div>
}
}
app {
root = App;
title = "My Counter App";
description = "A simple counter built with Coi";
lang = "en";
}
```
在线演示: [https://io-eric.github.io/coi](https://io-eric.github.io/coi)
Coi(语言): [https://github.com/io-eric/coi](https://github.com/io-eric/coi)
WebCC: [https://github.com/io-eric/webcc](https://github.com/io-eric/webcc)
我很想听听你的想法。虽然它仍然远未完成,但作为一个副项目,我对此感到非常兴奋 :)
查看原文
I usually build web games in C++, but using Emscripten always felt like overkill for what I was doing. I don't need full POSIX emulation or a massive standard library just to render some stuff to a canvas and handle basic UI.<p>The main thing I wanted to solve was the JS/WASM interop bottleneck. Instead of using the standard glue code for every call, I moved everything to a Shared Memory architecture using Command and Event buffers.<p>The way it works is that I batch all the instructions in WASM and then just send a single "flush" signal to JS. The JS side then reads everything directly out of Shared Memory in one go. It’s way more efficient, I ran a benchmark rendering 10k rectangles on a canvas and the difference was huge: Emscripten hit around 40 FPS, while my setup hit 100 FPS.<p>But writing DOM logic in C++ is painful, so I built Coi. It’s a component-based language that statically analyzes changes at compile-time to enable O(1) reactivity. Unlike traditional frameworks, there is no Virtual DOM overhead; the compiler maps state changes directly to specific handles in the command buffer.<p>I recently benchmarked this against React and Vue on a 1,000-row table: Coi came out on top for row creation, row updating and element swapping because it avoids the "diffing" step entirely and minimizes bridge crossings. Its bundle size was also the smallest of the three.<p>One of the coolest things about the architecture is how the standard library works. If I want to support a new browser API (like Web Audio or a new Canvas feature), I just add the definition to my WebCC schema file. When I recompile the Coi compiler, the language automatically gains a new standard library function to access that API. There is zero manual wrapping involved.<p>I'm really proud of how it's coming along. It combines the performance of a custom WASM stack with a syntax that actually feels good to write (for me atleast :P). Plus, since the intermediate step is C++, I’m looking into making it work on the server side too, which would allow for sharing components across the whole stack.<p>Example (Coi Code):<p>component Counter(string label, mut int& value) {<p><pre><code> def add(int i) : void {
value += i;
}
style {
.counter {
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 8px 16px;
cursor: pointer;
}
}
view {
<div class="counter">
<span>{label}: {value}</span>
<button onclick={add(1)}>+</button>
<button onclick={add(-1)}>-</button>
</div>
}</code></pre>
}<p>component App {
mut int score = 0;<p><pre><code> style {
.app {
padding: 24px;
font-family: system-ui;
}
h1 {
color: #1a73e8;
}
.win {
color: #34a853;
font-weight: bold;
}
}
view {
<div class="app">
<h1>Score: {score}</h1>
<Counter label="Player" &value={score} />
<if score >= 10>
<p class="win">You win!</p>
</if>
</div>
}</code></pre>
}<p>app {
root = App;
title = "My Counter App";
description = "A simple counter built with Coi";
lang = "en";
}<p>Live Demo: <a href="https://io-eric.github.io/coi" rel="nofollow">https://io-eric.github.io/coi</a><p>Coi (The Language): <a href="https://github.com/io-eric/coi" rel="nofollow">https://github.com/io-eric/coi</a><p>WebCC: <a href="https://github.com/io-eric/webcc" rel="nofollow">https://github.com/io-eric/webcc</a><p>I'd love to hear what you think. It's still far from finished, but as a side project I'm really excited about :)