Neovim 在 iPad 上真实运行
内容提要
Neovim 现已能在 iPad 上原生运行,通过 WebAssembly 编译,并利用 WebView 和 Metal 进行渲染。文章详细介绍了技术挑战,包括 libuv 事件循环的 WASM 适配、共享内存通信、文件系统持久化以及插件限制。应用 NeoVibe 在五天内开发完成,支持真实配置加载和文件保存,但无法运行 LSP 或终端,未来计划配对远程机器。
延伸解读
技术实现的巧妙之处
文章详细介绍了Neovim通过WebAssembly在iPad上原生运行的技术方案。核心是利用Emscripten的pthreads和SharedArrayBuffer实现真正的多线程,并通过自定义的libuv平台层(wasm_stubs.c)模拟事件循环。这种方案绕过了iOS对进程和JIT的限制,使得Neovim能以接近原生的速度运行,为其他大型应用的移植提供了参考。
实际使用中的限制
尽管Neovim能在iPad上运行,但存在明显限制:无法运行LSP、终端或依赖子进程的插件,且不支持LuaJIT的ffi模块。文件系统基于内存,每次应用被系统回收都会丢失,需要依赖原生存储同步。这些限制意味着它更适合轻量编辑,而非完整的开发环境。
对开发者的启示
文章展示了如何通过WebView和WebAssembly在iOS上运行复杂应用,并处理了跨进程通信、文件持久化等挑战。开发者可借鉴其使用WebSocket桥接、基于RPC的文件同步等模式。同时,文章强调了测试和验证的重要性,通过自动化测试确保行为符合预期,这对类似项目有参考价值。
Q&A
Neovim 如何在 iPad 上原生运行?
Neovim 通过编译为 WebAssembly(WASM)在 iPad 上原生运行,利用 WKWebView 的 WebContent 进程执行 WASM,并通过 Metal 进行渲染。应用 NeoVibe 在本地运行 HTTP 服务器,使 WebView 获得跨源隔离,从而支持共享内存和线程。
Neovim 的 WASM 构建为什么使用 Lua 5.1 而不是 LuaJIT?
因为 LuaJIT 在运行时生成机器码,而 WASM 模块无法向自身内存写入新代码,所以 WASM 构建使用 PUC Lua 5.1。这导致 ffi 模块不可用,LuaJIT 特有语法无法解析,热循环性能下降,但 Neovim 自身的 Lua 代码不依赖 ffi,影响有限。
Neovim 的 WASM 构建如何解决 libuv 事件循环的问题?
libuv 上游拒绝支持 Emscripten,因此 Neovim 自带了一个 300 行的 wasm_stubs.c 文件,其中 uv__io_poll 基于共享内存中的原子变量和 futex 实现。它通过一个 generation 计数器和 readable/writable 数组,让 JavaScript 在数据可用时通知 C 侧,从而模拟事件循环。
NeoVibe 如何实现文件持久化?
NeoVibe 将原生存储作为唯一事实来源,MEMFS 只是会话期间的投影。文件通过 RPC 调用 nvim_exec_lua 写入 MEMFS,保存时通过 BufWritePost 自动命令将文件内容读回并发送到原生侧,原生侧进行 SHA-256 校验后原子写入。
NeoVibe 如何处理 iPadOS 杀死后台进程导致的数据丢失风险?
当应用进入后台时,NeoVibe 会执行 silent! wall 保存所有缓冲区,并通过 BufWritePost 通知将字节发送到原生侧。它使用屏障通知确保所有保存完成,然后才认为刷新完成。如果进程在窗口期被杀,最多丢失一次写入,但旧文件完好。
NeoVibe 支持哪些插件?有哪些限制?
NeoVibe 支持纯 Lua 插件,通过将插件目录放入 ~/.local/share/nvim/site/pack/neovibe/opt/ 并启用。但无法运行需要子进程的插件(如 LSP、git 集成)、需要 ffi 的插件,以及无法安装新的 tree-sitter 解析器。
NeoVibe 如何实现配置加载?
NeoVibe 在启动前通过修改上游 worker 的 fork,在调用 _nvim_main 之前从本地服务器获取配置种子,解压到 MEMFS 的 /home/user 目录,并设置 XDG_CONFIG_HOME 等环境变量,使 Neovim 正常发现并加载 init.lua,确保 VimEnter 事件正常触发。
NeoVibe 如何处理文件冲突?
保存时,NeoVibe 会重新读取目标文件并计算 SHA-256,与种子时的摘要比较。如果不匹配,说明文件在外部被修改,会弹出选项:保存副本、替换磁盘文件或保留磁盘文件。