淺談 emacs25 的 dynamic modules 功能

emacs 預計在 emacs 25 加入 dynamic modules 的功能,透過這個功能我們可以使用 C/C++ 等語言將你的 emacs-lisp 函式變成改寫成如同 builtin 的模組,來提升 emacs-lisp 執行速度或是讓 emacs-lisp 可以與外部函式庫互動。

在本篇文章中,我將稍微講解自己測試 dynamic modules 的心得。

dynamic modules 功能處於測試階段,由於此功能可能在未來有進行不同的更動,因此預設編譯時這選項 (--with-modules) 是關閉的。

編譯 emacs

目前 dynamic modules 功能已經併入到 master branch, 因此只要下載 master branch 即可測試。

當然,你的系統必須滿足編譯 emacs 的條件,具體需要安裝的函式庫就不在本文贅述。

我們首先透過 git 去下載目前最新的版本的 emacs 原始碼並進行編譯,編譯 emacs 時記得打開 --with-modules 這個選項,這樣才能使用 dynamic modules 功能。

git clone http://git.sv.gnu.org/r/emacs.git
cd emacs
./autogen.sh
./configure --with-modules --prefix=${INSTALL_PATH} # ex: --prefix=/usr
make && make install

編譯好支援 dynamic modules 功能的 emacs 後,讓我們來撰寫第一個最簡單的模組吧,不過為了方便學習,先從 emacs-lisp 版本來寫起。

Hello World (elisp 版本)

在 emacs-lisp 中,除了使用 defun 以外,我們也可以使用 fset 搭配匿名函式來完成我們要使用的 function。

我們建立一個名為 hello-elisp.el 的模組,這個模組中我們宣告一個名為 hello 的函式,當執行他的時候就回傳 Hello Emacs 這個字串。

;; A simple function define with `fset' following code work the same as
;;
;; (defun hello () "Hello Emacs")
;;

(fset 'hello '(lambda () "Hello Emacs"))

;; we need to provide this feature to make emacs can use following method to
;; load it
;;  (require 'hello-elisp)

(provide 'hello-elisp)
;; hello-elisp.el ends here

在這個檔案的最後加上 provide 可以讓 emacs 透過 require 的功能來找到他,假設你這份 hello-elisp.el 存放在 ~/test 資料夾下,則我們可以在 emacs 中這樣讀取他

;; Add ~/test to load-path let emacs can find it
(add-to-list 'load-path "~/test")

;; load the hello-elisp module
;; you can also use (load "~/test/hello-elisp.el") but not recommand
(require 'hello-elisp)

;; call our hello function and a string should return
(hello)   ; => Hello Emacs

當我們執行到 (hello) 時,就會看到這個函式回傳 Hello Emacs 字串出來,於是 emacs-lisp 版本的 hello 函式就完成了 !

現在讓我們將他變成 C 語言版本的模組。

Hello World (c 版本)

我們要怎樣建立我們的 C 語言版本的模組呢? 其實在你的 emacs 原始碼 modules 資料夾下有 modhelp.py 這個檔案,他會幫忙產生出可以使用的模組樣板,不過本文為了講解方便就直接用手刻吧 ~

Makefile

我們首先先弄 Makefile 來紀錄編譯這個模組的方法,在這邊 EMACS_ROOT 指的是 emacs 原始碼的資料夾, EMACS 則是有打開 --with-modules 選項後編譯出來的 emacs 執行檔。

EMACS_ROOT ?=  ../..
EMACS ?= emacs

CC      = gcc
LD      = gcc
CPPFLAGS = -I$(EMACS_ROOT)/src
CFLAGS = -std=gnu99 -ggdb3 -O2 -Wall -fPIC $(CPPFLAGS)

.PHONY : clean test

all: test

hello-core.so: hello-core.o
  $(LD) -L . -shared $(LDFLAGS) -o $@ $^

hello-core.o: hello-core.c
  $(CC) $(CFLAGS) -c -o $@ $^

clean:
  -rm -f hello-core.o hello-core.so

test: hello-core.so
  $(EMACS) -Q -batch -L . -l test/test.el -f ert-run-tests-batch-and-exit

為了確認我們編譯出來的 .so 模組是否有正確執行,我們另外弄一個 test.el 來執行 ert 測試,因此先來完成這個測試程式。

test.el

在 test.el 中我們載入了 ert 模組以及我們即將要編譯出來的 hello-core.so 模組,注意到載入 dynamic module 模組的方式和載入一般 .el 的函式庫是一樣的。

於是我們就可以寫個簡單的測試來確認等等要寫的 C 語言版本的 hello-c 函式是否會真的回傳 "Hello Emacs" 字串回來。

;;; test.el --- hello test

(require 'ert)
(require 'hello-core)

(ert-deftest test-hello ()
  "hello-c should return \"Hello Emacs\" string."
  (should (string= "Hello Emacs" (hello-c))))

;;; test.el ends here

hello-core.c

接下來就到了我們的重頭戲, hello-core.c 這個模組上,我們首先添加可能會需要的標頭檔

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <emacs-module.h>       /* in emacs source code */

接下來我們要定義一個特別的變數,這和寫 gcc plugin 是一樣的,就是要告訴 emacs 說這個 .so 檔是 GPL 相容 的,如果你不添加這個符號進去,那你的 emacs 就不會載入你寫的這個模組。

// `plugin_is_GPL_compatible' indicates that its code is released under the GPL
// or compatible license; Emacs will refuse to load modules that don't export
// such a symbol.
int plugin_is_GPL_compatible;

為了可以載入模組,我們需要一個進入點來讓 emacs 知道這個模組的相關資訊,並透過 fset 宣告了名為 hello-c 的函式,其原型會透過後面定義的 Fcall_hello 函式來實現,而在這個進入點中我們也提供了這個模組的名稱,這樣我們就可以在 emacs-lisp 中透過 require 來組入這個模組。

/* Module init function.  */
int
emacs_module_init(struct emacs_runtime *ert)
{
        emacs_env *env = ert->get_environment(ert);

        // Bind NAME to FUN.
        // (fset 'hello-c '(lambda () "Hello Emacs"))
        emacs_value Qfset = env->intern(env, "fset");
        emacs_value Qsym = env->intern(env, "hello-c");
        emacs_value Qfn = env->make_function(env, 0, 0, Fcall_hello, "return hello string", NULL);
        emacs_value fset_args[] = { Qsym, Qfn };

        env->funcall(env, Qfset, 2, fset_args);

        // Provide FEATURE to Emacs.
        // (provide 'hello-core)
        emacs_value Qfeat = env->intern(env, "hello-core");
        emacs_value Qprovide = env->intern(env, "provide");
        emacs_value provide_args[] = { Qfeat };

        env->funcall(env, Qprovide, 1, provide_args);

        return 0;
}

最後就是實現我們的 hello 函式的方法,我們透過 Fcall_hello 作為中間層來和 hello 函式溝通,並回傳 emacs-lisp 的字串類型回去給 emacs-vm。

const char *
hello()
{
        return "Hello Emacs";
}

static emacs_value
Fcall_hello(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data)
{
        const char *str = hello();
        return env->make_string(env, str, strlen(str));
}

驗證你寫的模組

都完成後,你就可以使用 make test 去執行測試,一切正常的話就會像這個樣子。

Running 1 tests (2016-01-03 13:10:24+0800)
   passed  1/1  test-hello

當然,你也可以啟動你的 emacs 並且將這個 .so 加入到你的 load-path 中,並執行他

(add-to-list 'load-path "~/emacs-hello") ; path contains hello-core.so

;; you can use (load "~/emacs-hello/hello-core.so") directly, but we use
;; require here.
(require 'hello-core)

(hello-c)               ; => "Hello Emacs"

參考 mod-test.c 進行更多的簡化

在上面的 C 語言版本中,我們每次定義一個函式都要這樣一大串其實還蠻累人的

// Bind NAME to FUN.
// (fset 'hello-c '(lambda () "Hello Emacs"))
emacs_value Qfset = env->intern(env, "fset");
emacs_value Qsym = env->intern(env, "hello-c");
emacs_value Qfn = env->make_function(env, 0, 0, Fcall_hello, "return hello string", NULL);
emacs_value fset_args[] = { Qsym, Qfn };

這邊可以參考 emacs 程式碼中的 src/modules/mod-test/mod-test.c ,先加入這樣的實現

// Provide FEATURE to Emacs.
static void
provide (emacs_env *env, const char *feature)
{
    emacs_value Qfeat = env->intern (env, feature);
    emacs_value Qprovide = env->intern (env, "provide");
    emacs_value args[] = { Qfeat };

    env->funcall (env, Qprovide, 1, args);
}

// Bind NAME to FUN.
static void
bind_function (emacs_env *env, const char *name, emacs_value Sfun)
{
    emacs_value Qfset = env->intern (env, "fset");
    emacs_value Qsym = env->intern (env, name);
    emacs_value args[] = { Qsym, Sfun };

    env->funcall (env, Qfset, 2, args);
}

這樣在我們實作 emacs_module_int 的時候,就可以透過 C 語言的巨集簡化函式的宣告

int
emacs_module_init(struct emacs_runtime *ert)
{
    emacs_env *env = ert->get_environment(ert);

#define DEFUN(lsym, csym, amin, amax, doc, data)                        \
    bind_function (env, lsym,                                           \
                   env->make_function (env, amin, amax, csym, doc, data))

    DEFUN ("fib-c", Fcall_fib_c, 1, 1, "Calculate Fibonacci number with recursive function call.", NULL);
    DEFUN ("fib-loop-c", Fcall_fib_loop_c, 1, 1, "Calculate Fibonacci number with loop.", NULL);

#undef DEFUN

    provide(env, "fib-core");
    return 0;
}

使用 C/C++ 寫模組一定比較快?

既然 emacs 終於增加了 dynamic modules 功能,那是不是把大多數的 emacs-lisp 改寫成 c/c++ 模組會比較好?實際上是不一定,我們先看看我用 C 寫的 fibonacci 效能和 emacs-lisp 的比較狀況

emacs-lisp

(defun fib-elisp (n)
  "Fibonacci in recursive function call."
  (if (= 0 n) 0
    (if (= 1 n) 1
      (+ (fib-elisp (- n 1))
         (fib-elisp (- n 2))))))

Elapsed time: 211.466410s

c

static intmax_t fib(intmax_t n) {
        if (0 == n) return 0;
        if (1 == n) return 1;

        return fib(n - 1) + fib (n - 2);
}

Elapsed time: 1.389031s

以遞迴的版本來看,用 C 語言寫的 Fibonacci 數列運算數度是大幅勝過 emacs-lisp,那如果是使用迴圈的版本呢?我們再來比較一次看看

emacs-lisp

(defun fib-loop-elisp (n)
  "Calculate Fibonacci number with loop."
  (let ((a 0)
        (b 1)
        (tmp 0))
    (dotimes (i n 0)
      (setq tmp a)
      (setq a b)
      (setq b (+ tmp b)))
    a))

Elapsed time: 0.002195s

c

static intmax_t fib_loop(intmax_t n) {
        int a = 0, b = 1;
        for (int i = 0; i < n; i++) {
                int tmp = a;
                a = b;
                b = tmp + b;
        }

        return a;
}

Elapsed time: 0.000072s

好像還是用 C 寫的效能比較好?我們來看看一個反例,用 C++ 寫的模仿 s.el 的功能

emacs-lisp

(defun s-trim-left (s)
  "Remove whitespace at the beginning of S."
  (if (string-match "\\`[ \t\n\r]+" s)
      (replace-match "" t t s)
    s))

Elapsed time: 0.000780s

c++

std::string ltrim(const std::string &s) {
    static const std::regex lws{"^[ \t\n\r]+",
        std::regex_constants::extended};
    return std::regex_replace(s, lws, "");
}

Elapsed time: 0.044819s

在這個版本中,為了方便對照因此都是使用 regex 來處理字串,但是 C++ 的版本結果就比 emacs-lisp 慢了許多。雖然我們可以知道效能瓶頸應該是出在 std::regex 身上,但這也同時說明了不是什麼東西都用 C/C++ 重寫一定可以獲得最佳效能。

總結

dynamic module 終於可以在 emacs 中使用,這個消息對我而言其實是蠻開心的,畢竟這代表了我可以透過自己寫的模組去實現更多的用途,而不一定要強制修改 emacs 核心程式碼。

目前我測試用的程式碼已經放到 GitHub 上,有興趣也歡迎來玩玩看,看能不能替 emacs 增加更多有趣的功能。