上一章:宏html
Emacs 從版本 25 開始支持動態模塊。所謂動態模塊,即 C 語言編寫的共享庫 1 。Emacs 的動態模塊,就是 Elisp 的動態模塊。所以,假若 Elisp 語言編寫的程序某些環節存在性能瓶頸,可借 C 語言之力予以緩解。對於其餘編程語言,只要可以調用 C 程序庫,皆能用於編寫 Emacs 的動態模塊。本章僅講述如何使用 C 語言完成此事,所用的 C 編譯器爲 gcc。node
Hello world 程序老是可以幫助咱們忽略大量的細節,而掌握一個程序的基本相貌,這一經驗對於如何編寫 Emacs 動態模塊依然適用。編程
我建議使用 Emacs 可是並不由止使用其餘文本編輯器建立 C 程序源文件 foo.c,在其中鄭重其事地寫下segmentfault
#include <emacs-module.h> int plugin_is_GPL_compatible;
使用 C 語言爲 Emacs 編寫的任何一個動態模塊皆以上述代碼做爲開頭。編程語言
接下來應該寫 main
函數了。每一個 C 程序皆以 main
函數做爲程序的入口和出口。可是,Emacs 動態模塊的入口和出口不是 main
,而是編輯器
int emacs_module_init (struct emacs_runtime *ert) { return 0; }
跟 C 程序的 main
函數類似,返回 0 表示成功,返回其餘整型數值意味着失敗。函數
還記得 C 程序的 Hello world 嗎?性能
#include <stdio.h> int main(void) { printf("Hello world!\n"); return 0; }
Emacs 的動態模塊在以上述的代碼爲基礎,也能寫出相似的 Hello world 程序。下面給出 foo.c 的所有內容:學習
#include <stdio.h> #include <emacs-module.h> int plugin_is_GPL_compatible; int emacs_module_init (struct emacs_runtime *ert) { printf("Hello world!\n"); return 0; }
執行如下命令操作系統
$ gcc -I /usr/include/emacs-27 -fPIC -shared foo.c -o foo.so
即可將 foo.c 編譯爲共享庫 foo.so。注意,上述命令裏,/usr/include/emacs-27
是我機器上的 Linux 系統裏 emacs-module.h
文件所在路徑,不一樣的 Emacs 版本或不一樣的操做系統,須要因地制宜。
將 foo.so 放到系統變量 EMACSLOADPATH
定義的目錄或 Elisp 的 load-path
列表裏定義的目錄裏。完成上述工做,即可在 Elisp 程序裏載入 foo.so,例如建立 Elisp 程序 foo.el,令其內容爲
(load "foo" nil t)
而後執行
$ emacs -Q --script foo.el
可獲得如下輸出:
Hello world!
這就是 Emacs 動態模塊的 Hello world。成功加載這個模塊後,內心不由有些小激動呢。
如今考慮用 C 寫一個能夠計算宇宙的終極答案的函數,而後在 Elisp 裏調用。這樣的函數稱爲模塊函數。
在動態模塊的 C 代碼裏,可在 Elisp 程序調用的 C 函數,其格式必須像下面這樣
emacs_value func (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) { }
上述代碼僅僅是一個空殼函數,由於如今我還不知道 emacs_value
這個類型的返回值該如何構造。因爲宇宙的終極答案是 42,通過認真閱讀 Elisp 手冊,我找到了一個辦法。emacs_env
裏有一個函數 make_integer
,用它能夠構造 emacs_value
類型的實例,例如
emacs_value foo_answer (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) { return env->make_integer(env, 42); }
在還沒有搞清楚 env
,nargs
,args
以及 data
等參數的含義的狀況下,我已經寫出 foo_answer
。學習的過程,要學會臨時放棄一些東西。接下來要考慮的問題是,如何讓 foo_answer
這個 C 函數變成 Elisp 體制內的函數。
Elisp 手冊裏提供了示例代碼,我針對 foo_answer
對其略做修改並置入 emacs_module_init
函數裏,以下
int emacs_module_init (struct emacs_runtime *ert) { emacs_env *env = ert->get_environment(ert); emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL); emacs_value symbol = env->intern(env, "foo-anwser"); emacs_value args[] = {symbol, func}; env->funcall(env, env->intern(env, "defalias"), 2, args); return 0; }
爲了方便代碼複製,在本地機器上算出宇宙的終極答案,在此我不厭其煩,給出 foo.c 所有的代碼:
#include <emacs-module.h> int plugin_is_GPL_compatible; emacs_value foo_answer (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) { return env->make_integer(env, 42); } int emacs_module_init (struct emacs_runtime *ert) { emacs_env *env = ert->get_environment(ert); emacs_value symbol = env->intern(env, "foo-anwser"); emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL); emacs_value args[] = {symbol, func}; env->funcall(env, env->intern(env, "defalias"), 2, args); return 0; }
從新編譯 foo.c,並將編譯所得 foo.so 放到它應該在的目錄裏。而後,在 Elisp 程序 foo.el 裏,載入 foo.so,並調用 foo-anwser
函數,即
(load "newbie" nil t) (load "foo" nil t) (print! (foo-anwser))
執行 foo.el 程序,
$ emacs -Q --script foo.el
可輸出
42
上一節的示例代碼,大多數都是莫名其妙的。儘管如此,大體上它們的舉動無非是將一個 Elisp 裏一個體制內的符號 foo-anwser
綁定到模塊函數 foo_anwser
,而真正完成此事的代碼是
env->funcall(env, env->intern(env, "defalias"), 2, args);
首先看 env
,它是怎麼來的?來自 Emacs 運行時 emacs_runtime
,即
emacs_env *env = ert->get_environment(ert);
emacs_runtime
怎麼來的呢?是 Emacs 傳給 emacs_module_init
函數的。問題追溯至此,即可以結束了。身處城鄉結合部,就沒必要再問城市是怎麼來的了。
能夠再問的是,env->intern(env, "defalias")
是什麼意思?是讓 Elisp 解釋器派遣一個符號 defalias
過來。若是 Elisp 解釋器所維護的符號表裏有沒有這個符號,若是沒有就建立一個,而後以 emacs_value
的形式封裝這個符號,將其做爲 env->intern
的返回值。簡而言之,env->intern
返回一個符號。
因爲 env->intern(env, "defalias")
是 env->funcall
的參數,那麼後者拿到前者返回的符號,要作什麼呢?若是前者返回的符號綁定了一個 Elisp 函數,那麼 env->funcall
即可以經過這個符號調用它綁定的函數。那麼,Elisp 符號 defalias
綁定的是一個 Elisp 函數嗎?是的。Elisp 函數 defalias
能夠用於定義一個函數,相似於 defun
,兩者的區別是,defalias
是函數,而 defun
其實是宏。env->funcall
能夠調用函數,但不能夠調用宏。
env->funcall
要調用 defalias
函數,就須要給它傳遞兩個參數,一個是符號,一個是函數的定義,如下代碼即是爲 defalias
函數準備參數:
emacs_value symbol = env->intern(env, "foo-anwser"); emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL); emacs_value args[] = {symbol, func};
基於上述解釋,
env->funcall(env, env->intern(env, "defalias"), 2, args);
的含義就基本上清晰了。env->funcall
調用了 Elisp 函數 defalias
,將 symbol
和 func
這兩個參數傳遞給了 defalias
,由 defalias
在 Elisp 環境裏,亦即上述代碼裏幾乎無處不在的 env
,將一個符號 foo-anwser
綁定了一個函數 func
。
func
是怎麼來的呢?它其實是一個匿名函數,是 env->make_function
的返回值。這不奇怪,Elisp 語言能夠將函數像數據同樣傳來傳去。env->make_function
建立並返回的,其實是一個匿名函數。
匿名函數也叫 Lambda 表達式。在 Elisp 語言裏,幾乎全部的函數本質上都是匿名函數,它們之因此有名字,是由於有符號綁定了它們。defalias
的用處就是將一個符號綁定到一個 Lambda 表達式。例如
(defalias 'foo (lambda () (print! "Hello world!")))
defalias
將符號 foo
綁定了 Lambda 表達式
(lambda () (print! "Hello world!"))
這個 Lambda 表達式就是一個函數,可在終端裏輸出 Hello world!
。
若是使用 Elisp 函數 funcall
,能夠調用 defalias
,將 foo
綁定到上述的 Lambda 表達式,例如
(funcall 'defalias 'foo (lambda () (print! "Hello world")))
在 Emacs 的動態模塊裏,用 env->make_function
建立並返回的匿名函數,其定義就是符合格式要求的 C 函數。所以,上述 Elisp 代碼徹底能夠用 Emacs 動態模塊的代碼予以模擬,即
#include <stdio.h> #include <emacs-module.h> int plugin_is_GPL_compatible; emacs_value lambda_func (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) { printf("Hello world\n"); return env->make_integer(env, 0); } int emacs_module_init (struct emacs_runtime *ert) { emacs_env *env = ert->get_environment(ert); emacs_value symbol = env->intern(env, "foo"); emacs_value lambda = env->make_function(env, 0, 0, lambda_func, "", NULL); emacs_value args[] = {symbol, lambda}; env->funcall(env, env->intern(env, "defalias"), 2, args); return 0; }
在 emacs_module_init
函數裏,一旦從 emacs_runtime
裏得到 emacs_env
,即
emacs_env *env = ert->get_environment(ert);
便至關於在 C 程序裏獲得了一個 Emacs 的所有功能,同時這也意味着,Emacs 能夠從 C 程序裏獲得它想要的東西。因此,前文中我用了一個隱喻「城鄉結合部」形容 emacs_env
,它的做用就是溝通 Emacs 和 C 程序,經過它,C 程序裏的數據和函數能夠傳送到 Elisp 的世界裏,反過來,Elisp 世界裏的的一切也能夠經過它傳送到 C 程序的世界裏。
在計算宇宙終極答案的 C 代碼裏,已經見識了使用 env->make_integer
函數將 C 程序裏的數據 42 封裝爲 Elisp 世界裏的整型數,即
env->make_integer(env, 42);
反過來,使用 env->extract_integer
函數能夠從 Elisp 世界裏的整型數裏取出 C 程序須要的數據,例如
emacs_value foo = env->make_integer(env, 42); int bar = env->extract_inter(env, foo);
浮點類型和字符串類型的實例也能經過 emacs_env
包含的函數在兩個世界裏來回轉換,具體方法,可查閱 Elisp 手冊 2 。
Elisp 程序可以經過動態模塊調用一個可以計算宇宙終極答案的 C 函數,這意味着……這個教程可能須要結束了。