emacs 預計在 emacs 25 加入 dynamic modules 的功能,透過這個功能我們可以使用 C/C++ 等語言將你的 emacs-lisp 函式變成改寫成如同 builtin 的模組,來提升 emacs-lisp 執行速度或是讓 emacs-lisp 可以與外部函式庫互動。
在本篇文章中,我將稍微講解自己測試 dynamic modules 的心得。
dynamic modules 功能處於測試階段,由於此功能可能在未來有進行不同的更動,因此預設 編譯時這選項 (--with-modules) 是關閉的。
目前 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 版本來寫起。
在 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 語言版本的模組。
我們要怎樣建立我們的 C 語言版本的模組呢? 其實在你的 emacs 原始碼 modules 資料 夾下有 modhelp.py 這個檔案,他會幫忙產生出可以使用的模組樣板,不過本文為了講解 方便就直接用手刻吧 ~
我們首先先弄 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 中我們載入了 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 這個模組上,我們首先添加可能會需要的標頭檔
#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"在上面的 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;
}既然 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 增加更多有趣的功能。
[1] ejectで學ぶ Dynamic module 機能
[4] https://lists.gnu.org/archive/html/emacs-devel/2015-02/msg00960.html