Emacsのダイナミックモジュールを作る時の勘所

しむどん 2025-02-19

Emacsは古くから使われているテキストエディタであり、様々な歴史を経て主流となったGNU Emacsは今も根強い人気がある。単にEmacsという場合、大抵はGNU Emacsの事を表している(以降、Emacsという時はGNU Emacsの事とする)。Emacsにはダイナミックモジュールという機構がある。これにより他の言語で実装されたライブラリをEmacsから利用しやすくなる。今回はこのダイナミックモジュールを作る時の勘所について考える。

ダイナミックモジュールを大雑把に把握する

2016年頃リリースされた Emacs 25 から、Emacsにはダイナミックモジュールが組み込まれた。この仕組みを使うと、Emacs LispでインタプリタにLispの loadrequire を使って、 ddlso のような動的リンク可能な共有ライブラリを読み込む事ができる。

空のダイナミックモジュールを作る

ここでは空のダイナミックモジュールを作る。正しい作り方は公式ドキュメントで解説されている。ここではその内容をかいつまんでいく。

環境構築

ダイナミックモジュールの基本的な作り方は、CやC++で実装し、Cコンパイラでコンパイルする。ここではCで実装しgccでコンパイルする。

ダイナミックモジュールを作るにはコンパイラ(今回は gcc を使用)と、ダイナミックモジュール用のEmacsのヘッダーファイル emacs-module.h が必要となる。

今回はmacOS上でビルドするため、gccはHomebrewでインストールする。

brew install gcc

ヘッダーファイル emacs-module.h はEmacsに同梱されている。これは環境によって配置されている場所が異なるため、いくつか例を示す。

macOSで /Applications 配下にインストールするようなアプリケーションバンドルの場合、 /Applications/Emacs.app/Contents/Resources/include/emacs-module.h にある。 /usr/local 配下にEmacsをインストールしている場合、 /usr/local/include 配下を探してみて欲しい。 Homebrew でAppleシリコン系のmacにEmacsをインストールしている場合、 /opt/homebrew/include/opt/homebrew/opt/emacs/include 配下にあるかもしれない。

僕の環境の場合、Emacs自体を自分でビルドして使っているため、ソースツリーがローカルにあり、その中にヘッダファイル emacs-module.h もあった。Emacsのソースコードをリポジトリからダウンロードし、 ./cofigure を行う事でテンプレートファイルから生成される仕組みとなっている。

ダイナミックモジュールのコードを書く

それでは空のダイナミックモジュールをCで書いていく。必要な事は実はかなり少ない。「ヘッダーファイルをインクルードする」、「plugin_is_GPL_compatible変数をグローバルに宣言する」、「emacs_module_init関数を定義する」、この3つだ。

  • ヘッダーファイル emacs-module.h をインクルードする ダイナミックモジュールで使われる emacs_runtime や、後ほど使う emacs_value といった型の宣言がある。そのためヘッダーファイル emacs-module.h は必須となる。
  • plugin_is_GPL_compatible変数をグローバルに宣言する EmacsにリンクするコードはGNU GPLv3互換のライセンスで公開しなけばならない。それをチェックするための機構としてplugin_is_GPL_compatible変数が宣言されているかどうかを使っている。この変数が宣言されているという事は、そのコードはGNU GPLv3互換だと見なして扱われる。そのためコードを求められたら、それを受け取れる状態にする必要がある。
  • emacs_module_init関数を定義する この関数はダイナミックモジュールとしてEmacsにロードされる時に呼び出される。通常はこの関数に初期化処理としてさまざまな事を行うが、今回は空のモジュールであるため何もしない。
#include <emacs-module.h>

int plugin_is_GPL_compatible;

int emacs_module_init (struct emacs_runtime *ert) {
    return 0;
}
mylibempty.c

このファイルをコンパイルして共有ライブラリを作成する。

EMACS_INCLUDE_DIR="emacs-module.hのあるディレクトリへのパス"

gcc --shared \
    -I${EMACS_INCLUDE_DIR} \
    -o mylibempty.dylib \
    mylibempty.c

処理が正常に終了すると mylibempty.dylib という名前で、共有ライブラリが生成される。拡張子が .dylib なのは、環境がmacOSだからであって、Linuxの場合は .so 、Windowsの場合は .dll にすると良いだろう。

loadを使って共有ライブラリをむりやりロードする

共有ライブラリができたら、それをロードしてみよう。Emacsにロードする。ライブラリをロードする時はLispであれ、共有ライブラリであれ、通常は require を使だろう。しかし、これはまだ初期化処理で何もしていないから使えない。

他の方法として、 load を使ってファイルパスを指定して直接読み込むという原始的な方法があるため、ここではそれを使う。

(load "共有ライブラリのファイルへのパス")

空のダイナミックモジュールのため何も起きないが、このLispが正常にロードを完了すると t を返す。

ダイナミックモジュールではEmacsは気軽に死ぬ

ダイナミックモジュールなんてマニアックな手段を知りたいと考えているのだろうから、諸兄諸姉はEmacsについてある程度知識があるハッカーなのだろう。当然コードはEmacsで書くだろうし、シェル等の操作もEmacs上で行っている事だろう。

その今使用しているEmacsで不適切な実装のダイナミックモジュールをロードしたり、その関数を呼び出したりすると、使用しているEmacsがOSによって強制終了させられる事がある。

ダイナミックモジュールはC等で実装する事が多いため、領域外参照などのメモリ保護の違反を犯してしまいやすい。そうするとOSからの例外処理により、Emacsのプロセスごと終了させられてしまう。これでは開発を円滑に進めにくい。

これを回避する方法はいくつかあるけれど、僕が好んで使っている方法は、サブプロセスとしてEmacsをバッチモードで起動し、そこでダイナミックモジュールを読み込むというものだ。たとえOSにEmacsを強制終了させられたとしても、サブプロセスとして起動したEmacsを強制終了させられただけだから、親プロセスのEmacsには影響はない。

load を使って読み込みだけを確認する例を示す。

emacs -nw \
     --batch \
     --eval '(load "共有ライブラリのフィアルへのパス")'

-nwNo Window--batch はEmacsでインタラクティブな操作を行わないためのオプションの詰め合せセット、 --eval は実行するLispのコードを文字列として渡す事ができる。

--eval を使わず --script を使い、実行するLispを test.el などのファイルに記述し、そのLispを実行するといった事もできる。

emacs -nw \
      --batch \
      --script test.el

これらを使う事で、今使用しているEmacsに影響を与えず、ダイナミックモジュールを円滑に確認できる。

ちゃんとした最小のダイナミックモジュールを作る

空のダイナミックモジュールを作って、作成の流れを確認した。次はEmacsのライブラリとしてちゃんとしている最小のライブラリを作る。これにより require でこのライブラリをロードできるようになる。

#include <emacs-module.h>

int plugin_is_GPL_compatible;
int emacs_version;

int emacs_module_init (struct emacs_runtime *ert) {
  /* verify compatibitiy */
  if (ert->size < sizeof (*ert)) {
    return 1;
  }
  emacs_env *env = ert->get_environment(ert);

  if (env->size >= sizeof (struct emacs_env_26)) {
    emacs_version = 26;
  } else if (env->size >= sizeof (struct emacs_env_25)) {
    emacs_version = 25;
  } else {
    return 2;
  }

  /* packaging */
  emacs_value QSym_provide = env->intern(env, "provide");
  emacs_value QSym_feat = env->intern(env, "mylibmini");
  emacs_value args_feat[] = { QSym_feat };
  env->funcall(env, QSym_provide, 1, args_feat);
  return 0;
}
mylibmini.c

このコードでは emacs_module_init の記述が結構増えた。この関数の前半部分はEmacsやダイナミックモジュールのAPIに関する互換性のチェックを行っている。

  /* verify compatibitiy */
  if (ert->size < sizeof (*ert)) {
    return 1;
  }
  emacs_env *env = ert->get_environment(ert);

  if (env->size >= sizeof (struct emacs_env_26)) {
    emacs_version = 26;
  } else if (env->size >= sizeof (struct emacs_env_25)) {
    emacs_version = 25;
  } else {
    return 2;
  }

後半部分はパッケージとしての宣言を行っている。

  emacs_value QSym_provide = env->intern(env, "provide");
  emacs_value QSym_feat = env->intern(env, "mylibmini");
  emacs_value args_feat[] = { QSym_feat };
  env->funcall(env, QSym_provide, 1, args_feat);

このコードはEmacs Lispの次のコードと同じ意味となる。

(provide 'mylibmini)

それではこのコードを先程の要領でビルドしよう。今回は mylibmini.dylib という名前で出力する事にする。

gcc --shared \
    -I${EMACS_INCLUDE_DIR} \
    -o mylibmini.dylib \
    mylibmini.c

生成された共有ライブラリをロードする。今回は provide で宣言を行っているため、共有ライブラリのあるディレクトリへのパスが load-path に追加されていれば、 require を使ってロードできる。

実行するLispのスクリプトファイルを作る。

(add-to-list 'load-path (expand-file-name "."))
(require 'mylibmini)

test_mini.el

emacs -nw \
     --batch \
     --script test_mini.el

正常にロードできると、エラーが発生せず終了する。

まとめ

今回はEmacsのダイナミックモジュールを作りながら、開発時の勘所について考えた。世の中にはCやC++で実装されている素晴しいツールがたくさんあるので、今後はそれらをEmacsに気軽に飲み込んでいきたい。ただし、EMacsはどちらかというと、ダイナミックモジュールを使うよりも、サブプロセスで処理する方が好まれるだろうし向いていると思うので、使い所を見極めながら活用していきたい。