Emacs下,compile的具体机制是怎样的?

整理重构自己的配置文件的时候,看到了不少曾经抄来的代码,其中有一些eval相关的内容,看了manual也不甚理解,不知道论坛里有没有和我一样有些困惑的朋友,一起讨论一下。

  1. emacs将*.el编译为elceln是由什么变量决定的?又是在什么时机编译的?

  2. 为什么编译出的elc文件一般与el文件放置在同一目录下,但eln文件又会默认单独放置在eln-cache下呢?

  3. 为什么说elpa,melpa上提供的是经过了预编译的package?straight.el的readme中就这样说:

    package.el downloads pre-built packages from central servers using a special (undocumented?) HTTP protocol, while straight.el clones Git (or other) repositories and builds packages locally.

    但melpa上提供的也仅仅是el文件而已,并非elc文件

  4. init.elearly-init.el等配置文件为什么不会被编译为elc文件?

  5. eval-and-compileeval-when-compile等函数的作用是什么?被他们包裹的语句块一样是要执行的,那这样做的意义是什么?比如centaur emacs的init-package.el就是这样写的:

    (eval-and-compile
      (setq use-package-always-ensure t)
      (setq use-package-always-defer t)
      (setq use-package-expand-minimally t)
      (setq use-package-enable-imenu-support t))
    
    (eval-when-compile
      (require 'use-package))
    

主要因为 elc 和 el 一样是可以跨平台的,eln 不行。

它的用词有点问题,其实是想说 package.el 提供的是打包好的 tarball,straight 是 repo 啥样就啥样。

可以编译,不推荐而已。

1 个赞

emacs 是不会主动把 el 编译成 elc 的。但是有 elc 的话就会自动编译成 eln

主要区别再会不会出现在编译出来的 elc 文件里。

  1. emacs将*.el编译为elceln是由什么变量决定的?又是在什么时机编译的?

印象里面 emacs 如果只是 load-file 的话不会自动编译 elc, package.el 安装的时候倒是会调用 byte-compile 去编译一遍: source.

如果开了 native-compilation 的话会有个 JIT 编译的机制, 入口在 defalias 里面, 在关联函数定义和函数名的时候会自动触发的样子. 这里 是当时 Andrea 加这个功能的 commit.

eval-when-compile 是只在 bytecompile 的时候执行, eval-and-compile 在 bytecompile 和 load 的时候都执行. 所以

(eval-and-compile
  (setq use-package-always-ensure t))

的作用实际上类似于

(eval-when-compile
  (setq use-package-always-ensure t))
(setq use-package-always-ensure t)

eval-when-compile 可以用于定义 macro, 比如下面代码里面 sum3 这个 macro 用到的 -sum 函数是 dash 包提供的, 所以需要在 bytecompile 的时候就引入, 不然编译的时候就该报错找不到这函数了.

(eval-when-compile
  (require 'dash))
(defmacro sum3 (a b c)
    (-sum `(,a ,b ,c)))

谢谢解释,不过我还是有些没太理解,比如这一段

emacs -Q下,我直接对这一段eval-last-sexp,会直接报错,找不到dash这个包。在这个过程中,我并没有“编译”这个操作啊,require 'dash的这部分怎么就被执行了呢?

因为这两个唯一区别就在会不会出现编译出来的 elc 里,eval-when 不会出现在 elc 里,eval-and 会。所谓编译时执行只是照本宣科的理解,实际上不编译也会执行。它们和 progn 的区别就是编译的时候会执行。

(defvar foo)
(byte-compile '(eval-when-compile
                 (setq foo 12)))
12

foo
12

(byte-compile '(progn
                 (setq foo 19)))
(byte-code ... [foo 19] 2)

foo
12

(byte-compile '(eval-and-compile
                 (setq foo 17)))
(byte-code ... [foo 17] 2)

foo
17

思考题: Emacs 的 byte compiler 看到文件 toplevel 里有个常量会做什么?为什么我要说

eval-when 不会出现在 elc 里,eval-and 会。

比如:

;; -*- lexical-binding:t -*-
12
"foo"

(defun foo () 1)

有没有更准确的说法?

最后送题主一句话

see documentation first, read code second, flounder last.

谢谢,收获良多,我问题的根源可能在于对byte-compile这事儿有些望文生义了,确实需要静下心沉下气好好读一下manual和documentation。

想当然了, 看了下确实是只在 bytecompile 过程有区别 (emacs 用了点 magic 去特殊处理这俩 macro), load 或者 eval 的时候这两个函数行为应该是一样的, 基本等价于 progn.

阅读了emacs manual的macro与byte-compile相关章节,有一些关于使用eval-when-compile动机的理解,不知道理解的得对不对.

manual中说:

To avoid loading the macro definition files when someone runs the compiled program, write eval-when-compile around the require calls (see Evaluation During Compilation). For more details, See Macros and Byte Compilation.

是否可以这样理解,在编写一段elisp code时,如果需要使用别处定义的macro,那就需要require相应的包,这样在compile时,首先会根据macro的定义展开相应的代码,然后再进行编译。然而,如果直接require的话,在执行已经byte-compiled代码时,macro已经被展开了,再require相应的包就是多余的了,所以在一开始就应该把require放到eval-when-compile中。

换言之,所谓的eval-when-compile,对应的意思包括了两层:

  1. evaluate when compiling,即在byte-compile这个.el文件时执行
  2. do not evaluate after compiled,即对于已经byte-compiled的.elc文件,不要执行(加载macro的定义以供展开)

这样的理解是否正确?

EDIT:那同时,eval-and-compile的意义在哪里呢,被其包裹的代码块,既在compile时执行,也在byte-compile后执行,存在于.elc文件中,那与progn何异?

progn 不会在编译时执行。

说白了还是代码写少了,想像不出应用的場景。

(defvar var)
(eval (byte-compile '(progn
                 (progn
                   (defmacro foo (a)
                     (message "get %d." a)
                     `(setq var ,a)))
                 (foo 1)))) ;; error: undefined function foo

(eval (byte-compile '(progn
                 (eval-and-compile
                   (defmacro foo (a)
                     (message "get %d." a)
                     `(setq var ,a)))
                 (foo 1))))

如果代码只有一个文件,后面的代码要用同一个文件里定义的 macro,就要 eval-and-compile

順帶一提 Common Lisp 的 defmacro 相当於自帶 eval-and-compile,和 Emacs Lisp 不同。

CLHS: Macro DEFMACRO (lispworks.com)

If a defmacro form appears as a top level form, the compiler must store the macro definition at compile time, so that occurrences of the macro later on in the file can be expanded correctly. Users must ensure that the body of the macro can be evaluated at compile time if it is referenced within the file being compiled .

3 个赞

你这个

(progn
  (progn
    (defmacro test (a)
      (message "get %d." a)
      `(setq var ,a)))
  (test 1))

放文件里面是可以编译的,也不会提示 undefined macro.

生成

;ELC\0\0\0
;;; Compiled
;;; in Emacs version 28.1
;;; with all optimizations.



(defalias 'test '(macro . #[(a) "\301\302\"\210\303\304E\207" [a message "get %d." setq var] 3]))
(byte-code "\301\211\207" [var 1] 2)

直接运行你给的代码里面的 byte-compile 生成的代码与放在文件里面的不同。在实践中,一个文件里面定义的 macro 在同一个文件里面用的也不需要 eval-and-compile

需要用 eval-and-compile 的是这种情况:某个 macro 展开时用到了一个 helper function,你又要在 helper function 和 macro 所在文件的同一个文件用这个 macro,这种情况下 helper function 要加 eval-and-compile

(defun test-helper (a)
  (message "get %d." a)
  `(progn (setq var ,a)))

(defmacro test (a)
  (test-helper a))

;; Error: Symbol's function definition is void: test-helper
(test 1)
1 个赞

才发现用 eval-when-compile 定义会在编译时加載,用 progn 不会。用 CCL 测了下 compile-fileeval-whendefmacro 的行为和 Emacs 是差不多的。

应该是“在编译期间用”吧。

像这样在「同一个文件用」就不会有问题:

(defun test-1 ()
  (test 1))

eval-[when, and]-compile 最迷惑的是它俩的实现竟然是一样的,文档也写得有点绕口。

什么时候用哪个,我的简单判断是,取决于什么时候求值:

\ 编译期 运行期
progn :x: :white_check_mark:
eval-when-compile :white_check_mark: :x:
eval-and-compile :white_check_mark: :white_check_mark:

我们写的代码大部分都是运行时求值的,例如:

(defvar foo 1)
(defvar bar 2)
(defvar qux (+ foo bar))

这段代码编译成 .elc 之后是:

;ELC
;;; Compiled
;;; in Emacs version 29.0.50
;;; with all optimizations.



(defvar foo 1)
(defvar bar 2)
(defvar qux (+ foo bar))

编译后的代码依然保持书写时的样子,运行时进行 (+ foo bar) 求值。

如果想把最后一个变量求值后编译到 .elc 应该怎么写?

首先尝试:

(defvar foo 1)
(defvar bar 2)
(defvar qux (eval-when-compile
              (+ foo bar)))

编译出错:

In toplevel form:
test-compiletGIvpj:1:16: Warning: global/dynamic var ‘foo’ lacks a prefix
test-compiletGIvpj:1:31: Warning: global/dynamic var ‘bar’ lacks a prefix
test-compiletGIvpj:1:51: Error: Symbol’s value as variable is void: foo

看来前两个变量也要先求值:

(eval-and-compile
  (defvar foo 1)
  (defvar bar 2))
(defvar qux (eval-when-compile
              (+ foo bar)))

编译得到:

;ELC
;;; Compiled
;;; in Emacs version 29.0.50
;;; with all optimizations.



(defvar foo 1)
(defvar bar 2)
(defvar qux 3)

如果前两个变量也用 eval-when-compile 会是什么结果?

(eval-when-compile
  (defvar foo 1)
  (defvar bar 2))
(defvar qux (eval-when-compile
              (+ foo bar)))

编译得到:

;ELC
;;; Compiled
;;; in Emacs version 29.0.50
;;; with all optimizations.



(defvar qux 3)

如果全部改成 eval-and-compile 又会是什么结果?

(eval-and-compile
  (defvar foo 1)
  (defvar bar 2))
(defvar qux (eval-and-compile
              (+ foo bar)))

编译得到:

;ELC
;;; Compiled
;;; in Emacs version 29.0.50
;;; with all optimizations.



(defvar foo 1)
(defvar bar 2)
(defvar qux (+ foo bar))

看起来又回到了最初的样子,不同的是这些变量在编译期是可以使用的。所以 eval-and-compile 虽然求值了,但不会把求值结果编译到 .elc 文件。

2 个赞