Dynamic module機能は 2015年 12月 12日現在開発中の機能です(だいぶ落ち着いてはきていますが). なので以下の内容は最新版では正しくない可能性があります. 動かない場合等はソースコード及び modules以下に含まれるテストコードを確認してみてください.
リポジトリ
https://github.com/syohex/emacs-eject
リビジョンは 20151212 とします.
ヘッダファイル
関連するヘッダファイルは emacs-module.hだけです. Cファイルではこのヘッダのインクルードが必須です. その他は実装するものに応じて適宜 includeしてください.
型
emacs-module.hに定義される型で重要になるのが, emacs_envと emacs_valueです.
emacs_env
Version 25ではその実体は struct emacs_env_25
です. フィールドはほとんど関数ポインタで, Emacs Lisp側から渡された値を Cの型に変換したり, その逆を行うときに必要となります. データのやりとり, 返還等でわからなくなったらとにかくこの構造体のフィールドにそれらしいものがないか確認すると良いでしょう.
C言語側で Emacs Lispから渡された整数値を受け取るときは以下のようになります.
// envは emacs_env*型, eint_valは emacs_value型 intmax_t cval = env->extract_integer(env, eint_val);
逆に C言語側から Emacs Lisp側に整数値を返したい場合は以下のようになります.
// envは emacs_env*型, cint_valは int型. emacs_value eval = env->make_integer(env, (intmax_t)cint_val);
このように C <=> Emacs Lisp間での値の変換はすべて emacs_env型を使って変換します.
emacs_value
Emacs Lispでの値を表現する型です. Emacs Lisp側から渡ってきた値はすべて emacs_value
型です. また C言語から Emacs Lisp側に値を返すときは emacs_value
型に変換する必要があります.
コードを見ていく
ライセンスの宣言
https://github.com/syohex/emacs-eject/blob/20151212/eject.c#L28
コードが GPL互換のライセンスでライセンスされていないとリンクできません. なのでそのことを示すためのシンボル plugin_is_GPL_compatible
を宣言します.
追記
GPLでなく, GPL互換のライセンスでした. 失礼しました.
エントリポイント
https://github.com/syohex/emacs-eject/blob/20151212/eject.c#L55-L56
各モジュールでエントリポイントなる関数は emacs_module_init
です. ここで C言語の関数の登録, パッケージの登録を行います.
関数の登録
関数の登録の流れは以下のとおりです.
- C言語の関数を関数オブジェクト(
emacs_value
型)に変換 fset
関数を呼び出し, Emacs Lisp側で利用する関数名と1.
で作成した関数オブジェクトを関連付ける
それでは順を追って見ていきましょう.
https://github.com/syohex/emacs-eject/blob/20151212/eject.c#L60
emacs_value ejectfn = env->make_function(env, 0, 1, Feject, "Control CD tray", NULL);
make_function
の引数は以下の通りです.
make_functionに渡せる C言語関数のシグネチャは以下のとおりです.
emacs_value func(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data);
引数はそれぞれ以下のとおりです.
emacs_env*
型変数- 引数の個数
- 引数
- 関数登録時に設定した引数
https://github.com/syohex/emacs-eject/blob/20151212/eject.c#L62-L65
次に作成した関数を登録します. 登録には fset
関数を使います. ここで C言語から Emacs Lisp側の関数を呼び出す必要があります. C言語側から Emacs Lisp側の関数を呼び出す方法はひとつだけで, シンボルを internし, 引数を設定, それらを使い funcall
を呼び出します.
// (fset 'eject ejectfn)と等価 emacs_value Qfset = env->intern(env, "fset"); emacs_value Qeject = env->intern(env, "eject"); emacs_value fset_args[] = { Qeject, ejectfn }; env->funcall(env, Qfset, 2, fset_args);
これで関数の登録ができました. (eject)
と呼び出せば, C言語の関数を呼び出すことができます.
ロードできるようにする
これで関数は呼び出せる準備は整いましたが, ロードできないので provide
を呼び出し, require
でロードできるようにします. これも fset
と同じ流れで provide
呼び出します.
https://github.com/syohex/emacs-eject/blob/20151212/eject.c#L67-L69
// (provide 'eject) と等価 emacs_value Qprovide = env->intern(env, "provide"); emacs_value provide_args[] = { Qeject }; // 先ほど作成した 'eject env->funcall(env, Qprovide, 1, provide_args);
これで require
を使ってライブラリをロードできるようになりました.
関数の登録, provideは定形なので, Emacs本体の modules/以下にあるサンプルコードではDEFUN
マクロや provide
関数が定義されています. (それも面倒なので本体側に何か用意して欲しいところだが...)
eject本体
本体は以下の通りで, 戻り値の部分を除き普通の Cです. 戻り値は emacs_value
である必要があるので, emacs_env
の適切な関数フィールドを使い変換します. ここでは成功か失敗かだけわかればいいので, シンボルを返しています.
static emacs_value Feject(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) { int fd = open("/dev/cdrom", O_RDONLY|O_NONBLOCK); if (fd < 0) { perror("open /dev/cdrom"); return env->intern(env, "nil"); } int cmd = CDROMEJECT; if (!env->is_not_nil(env, args[0])) { if (ioctl(fd, CDROM_DRIVE_STATUS, 0) == CDS_TRAY_OPEN) cmd = CDROMCLOSETRAY; else cmd = CDROMEJECT; } if (ioctl(fd, cmd, 0) < 0) { perror("ioctl"); return env->intern(env, "nil"); } return env->intern(env, "t"); }
おわりに
理解を深めるためにもっとコードを書いていこうと思います.