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 增加更多有趣的功能。