感谢大佬的教程, 才读完, 我这几天练练手。
今天尝试写了一下Rust动态模块, 说一下自己的感受:
优点:
- 动态模块, Rust可以直接通过env访问Emacs的内存数据, 要比RPC的方式效率高一点,特别是传递 obarray 这种比较大的数据
- Rust的执行性能确实好, 甩Elisp不是一点半点
但是缺点更多:
- 开发效率低下, 需要经历 cargo build → rs-module/load → elisp code 的开发过程, 频繁的编译 + emacs-rs-module 的基础设施不完善( rust panic 后 rs-module/load 就废了, 需要重启Emacs), 基本上没有 Elisp 这种随时执行
load-file
就热替换的爽, 当写代码频繁的被编译过程打断以后就没有乐趣了, 更谈不上生产力 - Rust强类型的缺点: Rust的作者肯定是大量借鉴了 Haskell/GHC 的特性, 强调编译器强类型检查, 虽然在内存安全领域确实是亮点, 但是我写过多年Haskell代码, 知道 Rust/Haskell 这类语言最大的缺点就是, 当系统本身很复杂, 就会导致编译器推导出来的表达式类型很复杂, 开发人员会大量时间和编译器做斗争, 保证类型正常可以编译, 如果大部分代码都是自己写的还好, 一旦用到很多库, 第三方作者定义的库有很多奇奇怪怪的类型, 甚至你需要先看库的类型定义弄懂以后才能继续写代码。 写代码最重要的是总体思路、灵感和流畅性, rust/haskell这种语言不适合写应用层代码, 心智被打断的概率太大了
- 类型转换: 动态模块两种差别比较大的语言都会面临类型转换, 类型转换要考虑作用域、引用和内存分配, 一般动态模块开发都需要中间层的帮忙, 就像 haskell 的 c2hs 和 rust 的 emacs-rs-module, 中间层的健壮性和兼容性直接决定了动态模块的开发效率, 目前看, emacs-rs-module 的类型转换还比较麻烦 (比如我从 env.call 调用 obarray 数据到 Rust 里面后, 转换 obarray 并对 obarray rust 进行 loop 操作各种障碍, 也许我的 rust 还不熟练导致的),写Rust动态模块除了第二点的编译器斗争以外, 要随时随地做各种类型转换的代码, 导致代码看起来非常丑
我认为大家喜欢Emacs主要是因为Elisp这个语言的热替换带来的随心所欲hacking的快感, 但是Rust除了性能强悍以外, 真的毫无乐趣, 客观的说, 还不如 C++ 写动态模块再结合 Elisp 呢。
所以, 我通过实践告诉大家 remacs这种的项目没有前途。
Emacs的核心三大缺点: Elisp性能慢、 缺乏真多线程支持、 图形绘制能力。
-
图形绘制能力和语言级别支持多线程其实是一体化的, 必须在语言级别就要支持多线程, 才能保证后台计算和前端代码可以基于线程通讯高效访问, 如果语言级别不支持真并发, 就会导致Emacs这么多年都在用外部工具生成图片, 再发图片回Emacs渲染的hacking way。 在图形绘制每秒30帧的要求下, 外部进程计算再传递图片回Emacs就比直接绘制逻辑代码绘制窗口的性能低很多, 最简单的对比就是 doc-view 基于图片的方式和 EAF pdf-viewer 直接渲染的性能差距。 xwidget的发展瓶颈是, Elisp是单线程,一旦网页JS代码卡住就会导致Emacs卡住, 或者后台图形绘制数据性能要求高, Elisp没法在 xwidget 绘制的时候提供足够的并发计算模型, 所以Emacs的图形增强也许只有EAF这一条路走。
-
撇开图形绘制能力, 单讲Elisp的执行性能和多线程能力, 其实 lsp-bridge 这种 “RPC + Python + Multi-Thread” 方案就是一种非常好的实践, 如果大家认真研究 Neovim 会发现, Neovim 和 lsp-bridge 的技术思路非常像。 通过外部进程和语言可以极大的解决Elisp性能不足和多线程没法支持的问题, 而且还可以借助外部语言的生态库来扩展Emacs的能力 (比如, orjson这个Python库的性能就比 Emacs 29 本身的JSON解析性能高很多、 pygit2就可以直接访问 libgit 的能力而不用Elisp去绑定 libgit 库, mupdf这个库可以直接操作PDF而不需要写Elisp去解析PDF格式等等)。
-
针对第二点, 还有一种方式就是 Rust 这种动态模块, 但是从实践看 Rust 动态模块来扩展 Emacs 插件非常痛苦也缺乏效率。
所以, 我个人认为快速通过第三方语言扩展Emacs有几种路径:
- RPC + Python : 类似 EAF 和 lsp-bridge 的技术, 非常成熟, 特别是 lsp-bridge, 大家可以明显体验到代码补全流畅性的大幅提升
- RPC + JavaScript: 可以采用 RPC 对接 npm 或者 Deno 的方式来扩展Emacs, 享受 V8 的性能和 JavaScript 库生态, 考虑到 npm 的依赖地狱, Deno 也许是一条不错的发展路径
- RPC + Lua: LuaJIT本身性能非常快, Lua的生态也很好, Lua的缺陷是缺乏真多线程支持
- RPC + Clojure: Clojure的语法很像 Lisp, 同时底层基于JVM, 底层库也非常丰富, 缺点是要带一个 JVM 虚拟机, 不过在大内存的时代, LSP和TabNine这些都可以忍受, JVM 反而不是那么扎眼
这是我摸索 EAF/lsp-bridge 过程中的一些想法分享给大家, RPC + 外部高性能脚本语言会是扩大Emacs能力的好方法。 按照实践看, 特别是 lsp-bridge 项目的完成, 充分说明这种方式的可行。
唯一阻碍这种方式发展的是Emacser本身的思维限制, 对面的 Neovim 借助RPC, 已经可以协同Python、 Lua、 JavaScript写插件了, 为什么 Emacs 还要固步自封呢?