ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

在浏览器里跑真正的 Rio 终端内核:librio-wasm 的架构、构建与 JS ABI 实战

2026/9/28 2:20:41 拓冰建站 浏览量
在浏览器里跑真正的 Rio 终端内核:librio-wasm 的架构、构建与 JS ABI 实战 开发工具CLI跨平台【免费下载链接】rioA hardware-accelerated GPU terminal emulator focusing to run in desktops and browsers.项目地址https://gitcode.com/gh_mirrors/ri/rio点击查看免费下载librio-wasm 是 Rio 终端内核的 WebAssembly 封装它把剥离了 PTY 功能的librio编译到wasm32-unknown-unknown再通过 wasm-bindgen 暴露成浏览器可调用的 JavaScript ABI是开源 npm 包 rioterm 背后的 JS 终端引擎。本文围绕 librio-wasm/README.md 展开结合仓库源码讲解它的传输模型、RioTerm完整 API 面、渲染状态拉取协议与构建流程读者读完后能独立完成一次 wasm 构建、理解feed/output双向数据通路并照着 API 清单把一个可用的终端页面串起来。librio-wasm 是什么从 librio 到 JS ABIRio 的项目仓库里终端内核被提取成了可嵌入的 librio crate它把 PTY伪终端、VT 状态机和渲染状态拉取 API 打包在一起并通过 C ABIlibrio/src/capi.rs提供给 Swift/C 宿主。librio-wasm 则是这条嵌入链路的另一端libriowithout itsptyfeature, compiled to wasm32-unknown-unknown and exposed through wasm-bindgen. This is the JS ABI behind the rioterm npm package, the same waylibrios C ABI backs the Swift/C embedders.也就是说librio的 C ABI 服务于 Swift/C 嵌入者而librio-wasm的 JS ABI 服务于 Web 嵌入者两者共享同一套终端内核。从依赖关系可以印证这一点librio-wasm/Cargo.toml[dependencies] librio { path ../librio, default-features false } wasm-bindgen 0.2.106 js-sys 0.3.83关键在default-features falselibrio 的默认 feature 是pty graphics见 librio/Cargo.toml而 wasm 构建显式关掉了pty从而走宿主自持传输的非 PTY 路径。一个值得注意的实现细节整个 crate 以#![cfg(target_arch wasm32)]开头librio-wasm/src/lib.rs意味着它在原生目标上是一个空 crate。原因正如源码注释所写JS 委托是刻意单线程的RefCell JS 回调这会被原生SurfaceDelegate要求的Send Sync约束正确拒绝原生嵌入者应该直接用librio。librio 侧也用条件编译放宽了线程约束——在 wasm 目标上MaybeSendSync是一个空 traitlibrio/src/lib.rs因为 wasm 只有单线程且回调持有!Send的 JS 函数。浏览器里没有 PTYhost-owned transport 模型README 用一句话点破了 Web 终端与桌面终端最本质的差异There is no PTY in a browser, so the host owns the transport: child output goes in throughfeed, and bytes the terminal wants delivered to the child (key encodings, mouse reports, DA responses) come back out through theoutputcallback.桌面端librio默认开启ptyfeatureSurface会 spawn 一个 shell并起一个 IO 线程通过teletypewriter/corcovado泵数据。而在浏览器里既没有 PTY 也没有子进程所以角色对调——宿主页面里的 JS 代码拥有传输层下行子进程 → 终端通过RioTerm.feed(bytes)把字节喂进终端feed内部调用Surface::inject_output走持久化的 VT 解析器librio/src/lib.rs。注意这里的语义是把字节注入终端显示而非写入 PTY 输入所以回放历史滚动缓冲是安全的——shell 永远看不到这些字节。上行终端 → 子进程终端想把字节交给子进程按键编码、鼠标上报、DA 响应等时会走SurfaceDelegate::output回调。在非 pty 构建下Listener收到RioEvent::PtyWrite后不再发往 PTY channel而是直接调用delegate.outputlibrio/src/lib.rs。README 给出了典型的接线方式把这两条通路接到一个 WebSocket 就是一个真实 shell接到页内解释器就是一个演示终端。上游 Web 仓库rioterm会锁定某个 rio 修订版本并在 CI 中执行这套构建librio-wasm 本身不会发布到 crates.iolibrio-wasm/Cargo.toml 中publish false。构建流程与产物README 给出的构建命令很精简两行即完成从 Rust 源码到浏览器可用 JS 包的全过程cargo build -p librio-wasm --release --target wasm32-unknown-unknown wasm-bindgen --target web --out-dir pkg \ target/wasm32-unknown-unknown/release/librio_wasm.wasm要点拆解第一步用 Cargo 把librio-wasm编译成wasm32-unknown-unknown目标的 release 产物。crate 的 lib 类型是[cdylib, rlib]librio-wasm/Cargo.tomlcdylib保证产出可被 wasm-bindgen 消费的.wasm文件rlib则允许它作为普通 Rust 库被测试和复用。第二步用wasm-bindgen --target web生成 ES module 风格的 JS 胶水层和类型声明输出到pkg目录。产物文件名来自 crate 名的下划线形式librio_wasm.wasm。前置条件是把wasm32-unknown-unknown目标加入 rustuprustup target add wasm32-unknown-unknown并安装wasm-bindgen-cli。另外如果使用 wasm-pack 构建仓库已经预置了 release profile 的 wasm-opt 参数librio-wasm/Cargo.toml[package.metadata.wasm-pack.profile.release] wasm-opt [-O, --enable-bulk-memory, --enable-nontrapping-float-to-int, --enable-sign-ext]注释说明了原因rustc 默认会发出 bulk-memory / sign-ext 指令wasm-opt 必须显式接受它们否则优化阶段会报错。仓库根目录的 Makefile 还提供了另一条面向演示前端的路径run-wasm先cargo build -p rioterm --target wasm32-unknown-unknown --lib再cargo run -p rioterm-wasm对应 frontends/wasm 里基于 winit softbuffer 的浏览器演示它是另一个独立目标与 librio-wasm 的产物形态不同适合对比理解引擎封装与完整前端两种 wasm 形态的区别。RioTerm一个终端表面 一份渲染状态wasm-bindgen 暴露的核心类型是RioTermlibrio-wasm/src/lib.rs其结构体注释说得很直白One terminal surface plus its pulled render state. The JS Terminal class in the rioterm package owns exactly one of these.——即一个RioTerm对应一个终端表面Surface和与之绑定的渲染状态RenderState。构造函数#[wasm_bindgen(constructor)]librio-wasm/src/lib.rsnew RioTerm(cols: number, rows: number, pixelWidth: number, pixelHeight: number, scrollback: number): RioTermcols/rows网格的列数与行数底层会做max(2)兜底防止退化为 0 尺寸pixel_width/pixel_height宿主像素尺寸。它不是可有可无的装饰——终端用pixel_width / cols推导单元格度量GridSize见 librio/src/lib.rs而 kitty 图像放置协议必须把像素映射到单元格像素为 0 会静默丢弃所有放置scrollback滚动缓冲历史行容量SurfaceDesc的默认值是 10000 行librio/src/lib.rs。构造函数返回ResultRioTerm, JsError创建 Surface 失败时会抛 JS 异常。创建过程在内部走Engine::new(delegate)→engine.create_surface(desc)两步librio/src/lib.rswasm 侧把引擎只负责铸造 surface id、每个 RioTerm 恰好持有一个 surface这一关系写进了注释。键盘常量的双 ABI 一致性librio-wasm 在顶层导出一整套KEY_*常量librio-wasm/src/lib.rs从KEY_CHAR 0到KEY_SUPER_RIGHT 25外加KEY_ACTION_PRESS/REPEAT/RELEASE。源码注释点明了设计意图Key tags, matching librios C ABI (RIO_KEY_*) so the two stay one vocabulary across Swift, C, and JS embedders.对照 librio/src/capi.rs 里的RIO_KEY_*常量可以看到数值一一对应。这意味着无论宿主是 Swift、C 还是 JS按键事件的标签词表都是同一份减少了跨语言移植时的映射错误。wasm 侧的key()方法负责把(action, tag, codepoint, ...)还原成librio::Key枚举再交给Surface::key。事件委托队列 flush保证回调安全重入wasm 宿主通过 8 个on_*方法注册事件回调librio-wasm/src/lib.rs回调参数触发时机on_outputUint8Array终端想把字节交给子进程按键编码、鼠标上报、DA 响应on_wakeup无终端画面被破坏、需要重绘驱动 rAF 调度on_title(title, subtitle|null)OSC 标题/副标题变化on_bell无终端请求响铃on_cursor_blink无光标闪烁状态变化on_progress(state, value)OSC 9;4 进度报告ConEmu 编号0 移除、1 设置、2 错误、3 不确定、4 暂停value 为 0-100on_clipboard(kind, text)剪贴板写入请求0 剪贴板、1 选区on_close无终端请求关闭表面这套机制背后是一个精心的并发设计。所有委托事件先被压进JsDelegate的RefCellVecEvent队列librio-wasm/src/lib.rs然后由flush()在每个入口点返回后统一排空并派发给 JS 回调librio-wasm/src/lib.rs。两个关键性质绝不在持有终端锁时执行 JS事件只是入队JS 回调只在flush阶段运行此时终端锁已释放。因此 JS 回调可以安全地回调进这个对象比如在on_output里调用feed向终端回写数据不会死锁。回调抛错不卡死排空flush对每个回调的调用结果做Result检查抛异常的回调被捕获丢弃let _ err;保证一个坏回调不会卡住后续事件的分发。wakeup事件还做了合并去重librio-wasm/src/lib.rs队列末尾已有Wakeup就不再追加因为一帧一个 wakeup足够驱动 rAF 调度器。事件源则来自 librio 的Listener::dispatchlibrio/src/lib.rs它把RioEvent渲染、标题、响铃、光标闪烁、剪贴板、PTY 写、退出等映射为委托调用。输入路径按键、文本、粘贴与滚轮key让终端决定字节长什么样key()是输入的核心入口librio-wasm/src/lib.rs签名key(action: number, tag: number, codepoint: number, functionKey: number, mods: number, consumedMods: number, composing: boolean, text: string | null): boolean宿主把平台按键事件几乎原样交进来然后由librio::key::encode决定最终到达子进程的字节。这个决策依赖终端状态——应用光标模式DECCKM、kitty 键盘标志、modifyOtherKeys——而宿主没有义务跟踪这些状态所以编码逻辑留在内核里而不是前端librio/src/key.rs。Surface::key会构造EncodeContextlibrio/src/lib.rs把app_cursor、kitty flags、modify_other_keys、alt_is_meta全部读出来再编码返回true表示按键产出了字节已通过output回调送达。注意KeyEvent语义librio/src/key.rskey是未 shift 的键shifta的 key 是Char(a)平台产出的A放在text里consumed_mods记录平台为产出text已消耗的修饰键比如北欧布局上 AltGr 已被消费Alt 不应再编码为 meta。send_text / paste普通文本与安全粘贴send_text(text)把原始文本当作合成输入发给子进程librio-wasm/src/lib.rspaste(text)按终端的 bracketed paste 规则处理。当程序开启 mode 2004 时文本被包进ESC[200~ ... ESC[201~标记并剔除 ESC、ETX 和 8-bit CSI——因为负载永远不能提前闭合括号注入按键librio/src/lib.rs 与encode_paste同文件 L403-L410未开启时则把换行归一化为CR回车键产生的就是它。仓库测试 librio/src/lib.rs 专门验证了a\x1b[201~rm -rf /\x03这类恶意负载会被安全过滤。mode_bits给宿主做输入决策的位图mode_bits(): number返回终端模式位图librio-wasm/src/lib.rs位定义来自 librio/src/lib.rsbit 0鼠标上报MOUSE_MODEbit 1应用光标键APP_CURSORbit 2备用屏幕ALT_SCREENbit 3bracketed paste宿主可以用它做自己负责的那部分输入决策触摸滚动、按键栏无需解析终端内部状态。scroll_wheel程序的优先级先于滚动缓冲scroll_wheel(lines, col, row, mods)完整复刻了终端滚轮的语义librio-wasm/src/lib.rs底层逻辑见 librio/src/lib.rs按顺序三选一程序开启鼠标上报 → 滚轮变成鼠标事件wheel 按钮 64 上 / 65 下SGR 或 X10 编码备用屏幕 alternate scroll → 滚轮变成方向键分页器可滚动应用光标模式决定CSI/SS3否则 → 移动宿主的滚动缓冲视图。按住 Shift 永远强制走滚动缓冲用户明确要求看历史覆盖前两条。返回true表示程序消费了事件。仓库测试 librio/src/lib.rs 用注入的CSI ?1049h/CSI ?1000h序列分别验证了三条路径。渲染状态一帧一快照按行拉取Web 渲染器与原生渲染器共享同一套拉取式渲染状态协议每帧或每次 wakeup 后调用update()拉取一份新鲜快照然后只读地消费它librio-wasm/src/lib.rs。RenderState::update在同一把锁下同时快照网格、解析后的行样式、终端调色板、光标、选区与 kitty 放置librio/src/render_state.rs保证 GPU 发射路径拿到的索引色/命名色和当帧画面严格对应。快照读取 API 一览方法作用lines()/columns()视口尺寸cursor_line()/cursor_col()/cursor_visible()光标位置与可见性CSI ?25l隐藏或滚入历史时为 falsedisplay_offset()/alt_screen()滚动偏移与备用屏幕状态row_dirty(line)/reset_dirty()脏行增量重绘write_cells(out)/write_row(line, out)把整视口/单行写入 u32 缓冲text_row(line)/dump()纯文本视图测试、无障碍树用不做渲染serialize()整缓冲序列化为可回放的 VT 字节流含 SGR 样式与 OSC 8 链接history_lines()滚动缓冲当前行数单元格线格式每格 4 个 u32 字write_cells输出的是紧凑二进制格式而非对象数组CELL_WORDS 4定义每格的字数librio-wasm/src/lib.rs[codepoint | wide 21 | flags, fg, bg, style_flags]word0基础码点低 21 位、宽字符位第 21 位、CELL_HAS_CLUSTER标志1 23表示该格还挂着组合字符簇见 librio-wasm/src/lib.rsword1/word2前景/背景色统一打包成kind 24 | payload颜色种类由COLOR_NAMED0、COLOR_INDEXED1、COLOR_RGB2区分librio-wasm/src/lib.rs。主题活在 JS 侧所以命名色和索引色在 JS 里解析成具体 RGBword3样式标志位StyleFlags。fill_row的实现librio-wasm/src/lib.rs还处理了空格的兜底无内容的格写成空格码点 默认前景/背景 0 标志。返回值为写入的字数缓冲过小则返回 0所以调用方需要按lines * columns * CELL_WORDS预分配。簇文本让 ZWJ 表情和分解重音渲染正确CELL_HAS_CLUSTER标志指向一个更完整的文本模型当单元格挂着附加码点组合字符或 DEC 私有模式 2027 下的字素簇尾部时用cluster_text(line, col)取回基础码点 全部附加码点的完整字符串librio-wasm/src/lib.rs。渲染器画它而不是只画基础字符ZWJ 表情如‍和分解重音如e U0301才能渲染成序列本义的单个字形暂时不读这个标志的渲染器则继续画基础字符只是退化为旧行为。对应的测试在 librio/src/lib.rsmode 2027 开启后注入 ZWJ 表情与分解重音cell_cluster_text(0, 0)返回完整的‍三码点序列。配套的顶层函数cluster_width(codepoints)测量 UTF-32 缓冲里第一个字素簇的[长度, 宽度]librio-wasm/src/lib.rs宽度取 2宽/ 1窄/ 0零宽标记与 mode 2027 下的排版规则完全一致——渲染器可用它给单元格排版文本而无需回放输入。选择、搜索、链接与 kitty 图像选择Selectionselection_begin(viewportLine, col, kind, sideRight)的kind沿用 C ABI 取值0 简单、1 词、2 行、3 块librio-wasm/src/lib.rs内部映射到SelectionKind。side参数决定指针落在单元格的哪一半这直接决定拖动端点单元格是否被选中——仓库测试 librio/src/lib.rs 验证了向右拖回第 0 列必须用Side::Left才能把列 0 包含进来。配套 APIselection_update、selection_clear、selection_text以及视口坐标下的viewport_selection()返回[start_line, start_col, end_line, end_col, is_block]。搜索search(pattern, max)在滚动缓冲 屏幕全范围做正则搜索按从上到下返回扁平四边形数组[start_line, start_col, end_line, end_col, ...]行坐标相对滚动缓冲环顶部librio-wasm/src/lib.rs因此滚动视口后命中坐标依然有效无匹配或非法模式返回空数组。底层实现在 librio/src/lib.rs测试 librio/src/lib.rs 验证了环相对坐标与max上限。链接OSC 8 与纯文本 URL链接是命中测试形状的 API只在指针事件时调用不为每帧付费link_at(line, col)取 OSC 8 超链接 URIlibrio-wasm/src/lib.rslink_run(line, col)悬停行上可下划线的链接区间[start_col, end_col]跨行链接是共享一个 URI 的多段 runurl_at(line, col)正则检测的纯文本 URL覆盖 20 余种 scheme见 librio/src/lib.rs对未换行的逻辑行做检测所以折行 URL 能解析完整结果还会修剪掉行文标点——https://rio.dev,链接时不含逗号右括号只有在其开括号也在 URL 内时才属于 URLlibrio/src/lib.rs。OSC 8 链接解析的验证测试在 librio/src/lib.rs。kitty 图像协议渲染器按索引枚举画面上的 kitty 放置kitty_count()当前画面内放置数量kitty_geometry(index, cellWidth, cellHeight)返回[image_id, z_index, x, y, width, height, src_x, src_y, src_w, src_h]f64 数组见 librio-wasm/src/lib.rs几何已针对视口解析滚出视野时返回空kitty_image_info(image_id)[width, height, stamp]stamp 在像素变化时改变渲染器可按(id, stamp)缓存上传/位图kitty_image_rgba(image_id, out)拷贝 RGBA 像素RGB 源补不透明 alpha返回写入字节数。底层RenderState::update会同时收集直接放置direct overlay与虚拟放置U10EEEE 占位符行段kitten icat --transfer-mode在复用器下产生的形态并按 z-index 升序排序librio/src/render_state.rs宿主可以按背景之下 / 文字之下 / 文字之上的边界切分绘制层。与 librio 的关系一份内核多语言 ABI最后回到 README 的定位librio-wasm 是librio的 JS ABI正如librio的 C ABI 服务 Swift/C 嵌入者。两者共享同一套终端内核rio-vt的 VT 解析、网格、字素与图形协议差异只在外层原生/PTY 构建Surface持有 PTY channel、shell pid、IO 线程write把字节送进 channellibrio/src/lib.rswasm/非 PTY 构建Surface持有delegatewrite把字节交给delegate.output由 JS 端决定送往 WebSocket 还是页内解释器librio/src/lib.rs。这解释了为什么 librio-wasm 的 Cargo.toml 里default-features false也解释了为什么 specs/librio.md 与 librio/README.md 会作为嵌入文档与本文配套阅读。若要在你自己的项目里复刻 rioterm 的接入方式标准路径是wasm-bindgen 生成pkg→new RioTerm(...)创建表面 → 注册 8 个on_*回调 → WebSocket 收字节调feed、把on_output的字节发出去 → 每帧update()后按CELL_WORDS格式画格子。整套 ABI 已经被 rioterm 的 Web 仓库锁版本并在 CI 中构建你可以放心照此模式接入。赞分享开发工具CLI跨平台【免费下载链接】rioA hardware-accelerated GPU terminal emulator focusing to run in desktops and browsers.项目地址https://gitcode.com/gh_mirrors/ri/rio点击查看免费下载相关推荐3步解锁数字时光机GetQzonehistory帮你找回失落的QQ空间记忆3步解锁数字时光机GetQzonehistory帮你找回失落的QQ空间记忆 你是否曾在深夜翻看QQ空间试图找回十年前那个青涩的自己那些深夜写下的心情、和朋开发工具CLI跨平台WrenAI wren-core-wasm 演进与实战在浏览器里跑 DataFusion 语义 SQL 引擎WrenAI wren core wasm 演进与实战在浏览器里跑 DataFusion 语义 SQL 引擎 wren core wasm 是 WrenAI后端人工智能AI Agent数据分析用 tinygrad 把 LLM 跑进浏览器tinychat WebGPU 与 WASM 端构建全指南用 tinygrad 把 LLM 跑进浏览器tinychat WebGPU 与 WASM 端构建全指南 本指南围绕仓库中 examples/tinychat/人工智能深度学习大模型上一篇3.6GB显存也能跑CogVideoX-2b量化推理终极优化指南下一篇vi-gemma-2b-RAG社区与支持开发者资源和贡献指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考