with-emacs: 在独立的 Emacs 进程中运行 elisp 代码


自从有了 orgmode 之后,就经常用它来写一些代码片段,快速验证想法。但是对于 elisp 似乎没有隔离,使得当前的配置常常受到污染。

后来找到 ob-async 这个包,可以用它启动一个异步的 Emacs 进程来执行代码(当然,它不仅仅用于 elisp)。但是用在 elip 上感觉比用在其它地方体验略显糟糕:因为这个异步执行过程很慢。另外还有个缺点是,不能指定运行特定版本的 Emacs。

然后我参考 async.el(把代码进行 base64 编码,然后传递给命令行)写了第一版的 with-emacs (gist:test-with-emacs-v1.el),基本满足了我的需求:

  • 隔离代码
  • 指定版本

第一版用了好一段时间了,但始终有一点点不爽的是,这个版本是返回的是标准输入输出,所以当我即想打印 debug 信息,又想获得正常返回值的时候,就有点冲突了。

所以有了第二版(最上面的链接)。在第二版里,所有 (message ...) 的输出会重新定向到当前主 Emacs 的 *Messages*,同时再把最末尾表达式的结果作为返回值。使用起来更向普通的函数。

除了一般使用之外,例如〈如何确定某个函数是哪一版本的Emacs引入的?〉这样的问题也容易解决了,或者给〈用 Emacs Web Server 写的 Emacs Lisp Docstring Server〉增加获得指定版本的 doc string 的功能。

第二版目前仍然只有一个 `emacs-with` 函数,打算以后扩展成可以指定加载第三方包/配置:
(with-emacs "/path/to/emacs"
  :dir "/path/to/pkg-dir/"
  :package "/path/to/pkg-file"
  (do-something)
  ...)

增加对 org-babel 的支持,这点有待商榷,直接代码块里写 (with-emacs ...) 比增加一个 :with-emacs 头部参数似乎更容易理解。


EDIT: 1. with-emacs 增加了 with-emacs-define-partially-applied 宏; 2. 发布了新的包 ob-with-emacs。(详见 15#楼)

EDIT2: 增加了 with-emacs-server 宏 和 :timeout 参数。(详见 18#楼)

5 个赞

要是有个基于这个得repl就好了

我直接用了 async.el

(other-emacs-eval '(fboundp 'define-advice) "emacs-24.5")
;; => nil

(other-emacs-eval '(fboundp 'define-advice) "emacs-25.1")
;; => t
4 个赞

没想到你这早就有类似的包了 :sweat_smile:

不过还是有点区别。你这个包接口规划比较细致、有针对性,with-emas 则像其它各种 with- 开头的宏一样使用,可以直接在里面 (message ...),也可以用来获得返回值。

也许应该反过来,把这个 repl 作为 with-emacs 的基础。

这个包刚刚也进了 melpa:MELPA

3 个赞

MELPA 和 GNU ELPA 总共有大概 4500 个包,Visual Studio Code 有 12088 个(光主题就有 2000 多个),大家还是可以多多写包,分享出来:

melpa人手不足,包要等好久才过审核。

数量是赶不上了,用户基数相差太悬殊。

感觉melpa的审核过于严格,不仅lint要过,管理员还会提出自己的设计建议。

建议说明管理员不好当,必须去阅读和理解別人的设计,还要有一定的品味,能迅速发现设计当中不酷的地方。

建议当然是出于好意,但它是否必要,有待商榷。

一旦审核通过,就放任包的更新了,这恐怕是个大大的漏洞。

审核确实很严格,这是好事,至少包的质量比以前好了。当然,关于设计方面的建议确实有待商榷。也就是说,开发者必须说服管理员这么做是很cool的,包是很有价值的,而且没有其他问题,这样才能上线。

但是,上线后就随便你改了。以后更新是没有任何审核的,完全靠开发人员的职业素质了。这里漏洞是相当大的。

Emacs Lisp缺乏能用的静态分析工具,如果有,或许可以大大减轻管理员持续审核的压力

有lint啊,条件还挺苛刻。必须没有error和warning才能提交审核。

我说的是带上类型推导和类型标注的静态分析检查。现在不过要byte-compile clean还有一些格式上的检查(trailing whitespace,non lisp-case symbol,etc.)

1. with-emacs 增加了 with-emacs-define-partially-applied

用这个宏来固定部分参数,免去每次都填写长参数的烦恼,例如:

(with-emacs-define-partially-applied
 (t      nil t)
 (24.3   \"/path/to/emacs-24.3\")
 (24.4-t \"/path/to/emacs-24.4\" t))

将生成以下宏定义,每个宏的参数列表都有些不同,因为有些参数已经固化了:

(with-emacs-t      &rest BODY &key PATH)
(with-emacs-24.3   &rest BODY &key LEXICAL)
(with-emacs-24.4-t &rest BODY)

使用时分别等效于:


(with-emacs :lexical t ...)
(with-emacs :path "/path/to/emacs-24.3" ...)
(with-emacs :path "/path/to/emacs-24.4" :lexical t ...)

2. 发布了新的包 ob-with-emacs

虽说可以直接在代码块中用 with-emacs,但由于 with-emacs 没有区分 print 和 message 输出,全部都显示在 *Message* 中了:

#+BEGIN_SRC emacs-lisp :results output
(with-emacs
  (print emacs-version))
#+END_SRC

#+RESULTS:
: print 没有输出

这点与 orgmode 默认行为是不同的。

所以还是要一个 ob-with-emacs,处理一下 print 输出结果。其实也正是之前写 with-emacs 的初衷:用来隔离 orgmode 中的 elisp block,防止污染当前 Emacs。

使用方法:

在 block 头部加上参数 :with-emacs "/path/to/{version}/emacs" (path 可选)

#+BEGIN_SRC emacs-lisp :results output :with-emacs
(print emacs-version)
#+END_SRC

如果定义了 partially applied 函数 (详见 with-emacs-define-partially-applied 文档),

#+BEGIN_SRC emacs-lisp :results output :with-emacs-24.3
(print emacs-version)
#+END_SRC
1 个赞

毕竟人手不足,持续的审查是很不现实的。第一次的审查过了基本说明作者至少是靠谱的,知道自己在干啥,之后应该也可以信任。

对的,基本靠信任。我指的漏洞是,如果后续由其他开发人员维护可能就就会为所欲为。如果后续由随机抽查也会好很多。最好的方式是每次提交版本都有自动化的检查,这样能避免很多问题。

1. 增加 with-emacs-server 宏(有一段时间了,感谢 @xuchunyang 的建议)

如果对运行环境不是很严苛,不必每次都启动一个全新的进程,那么用 server 可以节省不少时间:

(with-emacs-server "foo"
  :ensure t ;; 可选参数,确保 foo 存在,不存在则就地创建
  (1+ 1))

2. 给 with-emacs-server 增加超时退出机制(昨天刚想到)

这样就可以任意使用,不必担心"僵尸"进程了:

(with-emacs-server "foo"
  :ensure t
  :timeout 100 ;; 空闲 100 分钟自动退出
  (1+ 1))

或者设置默认退出时间:

(setq with-emacs-server-timeout 100)
(with-emacs-server "foo"
  :ensure t
  (1+ 1))

但仍然可以继续使用 :timeout 参数来覆盖默认值,比如当临时需要不同的超时时间或禁止超时退出:

(with-emacs-server "foo"
  :ensure t
  :timeout nil
  (1+ 1))

EDIT: 可以考虑把 :ensure t 也设成可默认

2 个赞