読者です 読者をやめる 読者になる 読者になる

ejectで学ぶ Dynamic module機能

emacs

Dynamic module機能は 2015年 12月 12日現在開発中の機能です(だいぶ落ち着いてはきていますが). なので以下の内容は最新版では正しくない可能性があります. 動かない場合等はソースコード及び modules以下に含まれるテストコードを確認してみてください.

リポジトリ

https://github.com/syohex/emacs-eject

リビジョンは 20151212 とします.

ヘッダファイル

関連するヘッダファイルは emacs-module.hだけです. Cファイルではこのヘッダのインクルードが必須です. その他は実装するものに応じて適宜 includeしてください.

emacs-module.hに定義される型で重要になるのが, emacs_envemacs_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言語の関数の登録, パッケージの登録を行います.

関数の登録

関数の登録の流れは以下のとおりです.

  1. C言語の関数を関数オブジェクト(emacs_value型)に変換
  2. 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の引数は以下の通りです.

  1. emacs_env*型変数
  2. 引数の最少数
  3. 引数の最大数
  4. C言語関数
  5. docstring
  6. C言語関数に渡す引数

C言語関数のシグネチャ

make_functionに渡せる C言語関数のシグネチャは以下のとおりです.

emacs_value func(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data);

引数はそれぞれ以下のとおりです.

  1. emacs_env*型変数
  2. 引数の個数
  3. 引数
  4. 関数登録時に設定した引数

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");
}

おわりに

理解を深めるためにもっとコードを書いていこうと思います.