16
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Emacsのダイナミックモジュール

これは Emacs Advent Calendar 2020 20日目の記事です。

2020 年 8 月にリリースされた Emacs 27.1 は、久しぶりのメジャーバージョンアップということもあり様々な新機能が話題になったと思います。そうした華々しい新機能の陰で地味にひっそりとした変更がありました。ダイナミックモジュールがデフォルトで有効になったのです。このおかげで、例えば GNU が配布している Windows 用 Emacs 公式バイナリそのままで、IME パッチを当てなくてもダイナミックモジュールでエミュレーションすることにより、IME パッチ版と同様の使用感を実現できる tr-ime のようなことができるようになりました。

本記事は、(Windows に限定せず)ダイナミックモジュールとはどんなものなのか説明し、そして実際にダイナミックモジュールを作ってみます。

はじめに

Emacs のダイナミックモジュール機構は Emacs 25.1 で導入されました。何ができるのか一言でいうと「Emacs Lisp から呼び出せる関数を C 言語などで実装して追加できる」ということになると思います。Emacs Lisp だけでは実現できなかった、例えば OS の API を直接叩く関数が作れるというわけです。

しかし、Emacs 26.3 までダイナミックモジュールはデフォルト無効になっており、Emacs をビルドする際に明示的に有効にしなければ使えないものでした。そのため、多くのバイナリ配布されている Emacs ではダイナミックモジュールを使うことができませんでした。Emacs を自分でビルドして使うのであれば、ダイナミックモジュールを有効にすることができましたが、その場合は Emacs の C 実装部分にパッチを当てるなどして手を出すこともできるので、わざわざダイナミックモジュールを使いたいと思う人は少なかったかもしれません。逆に自分でビルドしないで使う場合、つまりインストーラを使って Emacs をインストールしたりパッケージシステムから Emacs をインストールするなら、Emacs の C 実装部分に手を出すことはできない上に、(パッケージメンテナ次第ですが大抵は)ダイナミックモジュールも使えませんでした。

それが、Emacs 27.1 では、デフォルトでダイナミックモジュールが有効になりました。これに伴って、GNU が配布している Windows 用 Emacs バイナリをはじめ、各種システムでパッケージ化されている Emacs バイナリでも、ほとんどの場合でダイナミックモジュールが有効で使えるようになりました。そのため、Emacs を自分でビルドしなくても、ダイナミックモジュールが使えるようになったのです。

ダイナミックモジュールとは

ダイナミックモジュールとは一言でいうと「Emacs Lisp から呼び出せる関数を C 言語などで実装して追加できる」と書きました。もう少し詳しく見るために、Emacs Lisp リファレンスマニュアルを見てみます。ayatakesi さんの翻訳による Emacsのダイナミックモジュール を読むと、Linux などの POSIX システムであれば拡張子 .so を持ち、Windows であれば拡張子 .dll を持った共有ライブラリであり、Emacs Lisp から loadrequire によってロードすることができる、ということがわかります。つまり、拡張子 .el.elc の Emacs Lisp ファイルをロードするのと基本的に同じだけど、共有ライブラリなので C 言語などで実装するということになります。

ダイナミックモジュールを作ってみる

ダイナミックモジュールを作る方法は、同じく ayatakesi さんの翻訳で 動的にロードされるモジュールの記述 に書いてあります。基本的には C99 以降に対応した C コンパイラで共有ライブラリを作成することになります。もちろん C 言語の ABI に対応していれば他の言語で作成しても構いません1

では、何もしない空のダイナミックモジュールを作ってみましょう。C 言語で作ります。ソースは以下のようになります。

test-empty.c
#include <emacs-module.h>

int plugin_is_GPL_compatible;

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

このソースから共有ライブラリを作ってみます。Windows (MinGW または Cygwin)であれば、以下のようにすれば共有ライブラリ test-empty.dll が得られます。

$ gcc -shared -o test-empty.dll test-empty.c

Linux などであれば(試していないので間違っていたら申し訳ないのですが)以下のようにすると test-empty.so が得られると思います。

$ gcc -shared -fPIC -o test-empty.so test-empty.c

もし emacs-module.h が見つからない旨のエラーが出た場合は、Emacs のソースから emacs-module.h をもってきてどこかのディレクトリに入れ、-I オプションでそのディレクトリを指定すればよいでしょう。

次に、こうして得られた test-empty.dll ないし test-empty.so を Emacs の load-path が通っているディレクトリにコピーします2。そして Emacs で以下を評価するとロードができます3。もちろん、ロードしても何もしないダイナミックモジュールなので、何も起きません。

(load "test-empty")

さて、この何もしないダイナミックモジュールのソースには、変数と関数が 1 つずつあります。ダイナミックモジュールは、最低限この 2 つが共有ライブラリのシンボルとして外部から(Emacs から)見えるようになっていないと Emacs がロードしてくれません。gcc の場合(デフォルトでは)グローバル変数や関数は共有ライブラリの外から見えるようになりますが、Visual C++ などでは見えるようになりませんので、以下のように __declspec(dllexport) を使って明示的に外部から見えるようにした方がいいかもしれません(この書き方なら Windows の gcc でも大丈夫だし、Linux の gcc でも大丈夫なハズです)。

test-empty2.c
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

#include <emacs-module.h>

DLL_EXPORT int plugin_is_GPL_compatible;

DLL_EXPORT int emacs_module_init (struct emacs_runtime *ert)
{
  return 0;
}

C 言語の変数 plugin_is_GPL_compatible は、ダイナミックモジュールが GPL 互換のライセンスで提供されていることを宣言するためのシンボルです。ご存知の通り Emacs は GPL なソフトウェアです。ダイナミックモジュールは共有ライブラリですから、Emacs がロードすると動的リンクすることになります。GPL 非互換の共有ライブラリを GPL なソフトウェアとリンクしたらライセンス的にマズいので、Emacs は共有ライブラリに plugin_is_GPL_compatible というシンボルがあった場合だけロードするようになっています。

C 言語の関数 emacs_module_init は、ダイナミックモジュールがロードされた際に呼ばれる初期化用の C 関数です。初期化に成功したら 0 を、失敗したら非 0 を返すように実装します。この何もしないダイナミックモジュールでは、何もせずに成功を意味する 0 を返すだけにしています。0 を返す前に、OS の API などを呼ぶようにすれば、ロード時に API を呼ぶことができるようになります。

ダイナミックモジュールから Emacs Lisp 関数を呼ぶ

ダイナミックモジュールは C 言語などで実装するので、OS の API を直接叩くことができます。でも、それだけじゃ困る、Emacs Lisp で実装した関数を呼びたい、ということもあるでしょう。というわけで、ダイナミックモジュールの中から Emacs Lisp 関数を呼んでみます4。さきほどの初期化用 C 関数 emacs_module_init の中から Emacs Lisp 関数の message を呼んでみます。

test-message.c
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

#include <emacs-module.h>
#include <string.h>

DLL_EXPORT int plugin_is_GPL_compatible;

DLL_EXPORT int emacs_module_init (struct emacs_runtime *ert)
{
  if (ert->size < sizeof (*ert))
    return 1;

  emacs_env *env = ert->get_environment (ert);
  if (env->size < sizeof (*env))
    return 2;

  const char *phello = "Hello World";

  emacs_value func = env->intern (env, "message");
  emacs_value str = env->make_string (env, phello, strlen (phello));
  emacs_value args[] = {str};
  env->funcall (env, func, 1, args);

  return 0;
}

ではソースの解説をします。 C 関数 emacs_module_init の最初にある以下の部分、

  if (ert->size < sizeof (*ert))
    return 1;

  emacs_env *env = ert->get_environment (ert);
  if (env->size < sizeof (*env))
    return 2;

は、emacs_env * 型の C 変数 env を得るためのものです。この env はダイナミックモジュール内で Emacs の機能を使うために必要となります。これが正しく得られているか確認するためにダイナミックモジュールのコンパイル時の構造体サイズと、ダイナミックモジュールをロードした時のランタイム時の構造体サイズを比較しています。ランタイム時の方が小さければ(つまり、ロードした Emacs バージョンが古ければ)ダイナミックモジュールが期待する構造体メンバが足りないことになるので、非ゼロを返して初期化エラーとしています。逆にサイズが同じかランタイム時の方が大きければ(つまり、ロードした Emacs バージョンが同じか新しければ)ダイナミックモジュールが期待する構造体メンバは揃っているので、正しい env が得られたことになります。

次に emacs_value という型が出てきますが、これはダイナミックモジュールで Emacs の Lisp オブジェクトを扱うための型になります。Emacs Lisp とやりとりする引数や返り値などが、C 言語ではすべてこの emacs_value 型となります5。そして

  emacs_value func = env->intern (env, "message");

は、env で得られる C 関数の intern を使い message という Emacs のシンボルを作り、emacs_value 型の func という名前の変数に格納しています。次に、

  emacs_value str = env->make_string (env, phello, strlen (phello));

は、env で得られる C 関数の make_string を使い C 言語の文字ポインタ変数 phello に格納されている文字列 Hello World を Emacs の文字列オブジェクトに変換して emacs_value 型の str という名前の変数に格納しています。そして、

  emacs_value args[] = {str};

では、Emacs Lisp 関数を呼ぶときの引数を作っています。引数は emacs_value 型の配列にすればよいのですが、今回は文字列オブジェクト 1 つだけなので、文字列オブジェクト str だけが入った配列 args を作っています。これでようやく関数を呼ぶ準備ができましたので、

  env->funcall (env, func, 1, args);

によって、env で得られる C 関数の funcall を使い Emacs Lisp 関数を呼び出しています6。これは、第 2 引数 func で呼び出したい関数のシンボルを指定、第 3 引数 1 で引数の数を指定、第 4 引数 args で引数の配列を指定しています。今回は funcall で呼び出した関数の返り値は使っていませんが、必要であれば emacs_value 型の返り値を得ることもできます。

このソースをビルドして Emacs でロードすると、 *Messages*Hello World が出力されるのがわかると思います。

ダイナミックモジュール関数を作る

ここまでで、Emacs から C 言語で実装した関数を呼び出すことができました。ですが、初期化関数なので、ロードした時だけしか実行できません。まだ「Emacs Lisp から呼び出せる関数を C 言語などで実装して追加できる」にはなっていません。そこで、Emacs Lisp から呼び出せるダイナミックモジュール関数を作ります。

まず、ダイナミックモジュール関数の中身の処理を記述した C 関数を作ります。これは、以下の引数と返り値を持ったものにする必要があります。

emacs_value module_func (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data)

env は、これまでに述べた Emacs の機能を呼び出すための変数です。nargs は、この関数が呼び出されたときの引数の数、args は引数の配列、data は追加のデータです。返り値は、ダイナミックモジュール関数の返り値を設定します。

ここで、引数がゼロ個の場合は message 関数で Hello World を出力し、引数が 1 個以上ある場合は、1 番目の引数を message 関数で出力する、というダイナミックモジュール関数を作ってみます。返り値は常に t とすると、中身の処理を行う C 関数は以下のようになります。

emacs_value
my_hello (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data)
{
  emacs_value str;

  if (nargs == 0)
    {
      const char *phello = "Hello World";
      str = env->make_string (env, phello, strlen (phello));
    }
  else if (nargs > 0)
    {
      str = args[0];
    }

  emacs_value func_symb = env->intern (env, "message");
  emacs_value func_args[] = {str};
  env->funcall (env, func_symb, 1, func_args);

  return env->intern (env, "t");
}

この C 関数からダイナミックモジュール関数を作るには以下のようにします。

    emacs_value func = env->make_function (env, 0, 1, my_hello,
                                           "My Hello function.", NULL);

env で得られる C 関数の make_function を使います。第 2 引数と第 3 引数で、このダイナミックモジュール関数が取る引数の最小値と最大値を指定します。この場合は、ゼロまたは 1 つの引数を取るという意味になります。第 4 引数で、ダイナミックモジュール関数の元になる C 関数を指定します。ここでは先ほど示した C 関数の my_hello を指定しています。第 5 引数は、このダイナミックモジュール関数の docstring 文字列を指定します。ここでは適当な文字列として My Hello function. を指定しています。第 6 引数は、C 関数を呼び出すときに最後の引数に指定される追加のデータを指定します。ここでは NULL として追加のデータを指定していません。以上でダイナミックモジュール関数が作られて、変数 func にオブジェクトとして格納されます。ただ、このままでは名前がついていないので Emacs Lisp から呼び出すことはできません7

名前を付けるには defalias を使います8

    emacs_value symbol = env->intern (env, "my-hello");
    emacs_value args[] = {symbol, func};
    env->funcall (env, env->intern (env, "defalias"), 2, args);

my-hello という Emacs のシンボルを作り、その作ったシンボルが格納されている symbol と、先ほどのダイナミックモジュール関数が格納されている func の 2 つを引数 args に格納して defalias を呼び出します。すると、Emacs Lisp から my-hello という関数として呼び出すことができるようになります。

そして、普通の Emacs Lisp の .el ファイルの最後でやるのと同じく、最後に providetest-hello フィーチャを登録するようにします。こうすることで、このダイナミックモジュールに対して requirefeaturep などが機能するようになります。

    emacs_value symbol = env->intern (env, "test-hello");
    emacs_value args[] = {symbol};
    env->funcall (env, env->intern (env, "provide"), 1, args);

これらの処理を初期化用 C 関数 emacs_module_init 内に書いておけば、このダイナミックモジュールをロードすると my-hello 関数が使えるようになる、というわけです。では全体のソースを示します。

test-hello.c
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

#include <emacs-module.h>
#include <string.h>

DLL_EXPORT int plugin_is_GPL_compatible;

emacs_value
my_hello (emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data)
{
  emacs_value str;

  if (nargs == 0)
    {
      const char *phello = "Hello World";
      str = env->make_string (env, phello, strlen (phello));
    }
  else if (nargs > 0)
    {
      str = args[0];
    }

  emacs_value func_symb = env->intern (env, "message");
  emacs_value func_args[] = {str};
  env->funcall (env, func_symb, 1, func_args);

  return env->intern (env, "t");
}

DLL_EXPORT int emacs_module_init (struct emacs_runtime *ert)
{
  if (ert->size < sizeof (*ert))
    return 1;

  emacs_env *env = ert->get_environment (ert);
  if (env->size < sizeof (*env))
    return 2;

  {
    emacs_value func = env->make_function (env, 0, 1, my_hello,
                                           "My Hello function.", NULL);
    emacs_value symbol = env->intern (env, "my-hello");
    emacs_value args[] = {symbol, func};
    env->funcall (env, env->intern (env, "defalias"), 2, args);
  }

  {
    emacs_value symbol = env->intern (env, "test-hello");
    emacs_value args[] = {symbol};
    env->funcall (env, env->intern (env, "provide"), 1, args);
  }

  return 0;
}

これをビルドして test-hello.dll ないし test-hello.so を作り、load-path 上にコピーします。そして、Emacs で以下を評価するとロードできます。

(require 'test-hello)

そして、

(my-hello)

を評価すると Hello World が、

(my-hello "foo bar baz")

を評価すると foo bar baz*Messages* に出力されます。

ダイナミックモジュールの実例

ダイナミックモジュールを使った実例として拙作 tr-ime を紹介します。これはダイナミックモジュールを使うことで、IME パッチの当たっていない GNU 公式 Emacs バイナリ(Windows 版)でも、IME パッチ版と同様の操作感を実現することができます。

Windows で Emacs をお使いの場合、IME パッチ無しでも MS-IME のような Windows の IME を使って日本語入力することは一応できます。しかし、

  • IME を on/off してもモードラインのかな漢字変換状態表示が変わらない
  • IME on で使っているとき、 状況に応じて自動的に IME off してくれる機能が無く、 直接入力したいキーが IME に吸われて未確定文字になってしまう
    • ミニバッファでの y/n 入力
    • M-x によるミニバッファでのコマンド名入力
    • など
  • C-\ (toggle-input-method) すると IME ではなくて、 IM による Emacs 独自の日本語入力モードになってしまい、 他の Windows アプリと操作感や変換辞書が異なるため使いにくい

といった問題があります。IME パッチは、これらの問題を解消してくれる「かゆい所に手が届く」ものでしたが、Emacs のバージョンアップの都度、いちいちパッチを当ててビルドするのは煩雑でした。

そこでダイナミックモジュールによって IME パッチの動作をエミュレートすることで、IME パッチ版と同等の操作感を実現したのが tr-ime になります。ダイナミックモジュールなので、Emacs をバージョンアップしても、いちいちパッチを当てたりビルドしたりする必要はありません。大抵の場合は Emacs 本体をバージョンアップするだけで、そのまま動作するものと思います。

MELPA に掲載されていますので、インストールは簡単です9。インストール方法など詳しくは tr-ime をご覧ください。

おわりに

本記事では、 Emacs 27.1 からデフォルトで有効となったダイナミックモジュールについて説明し、実際に作ってみました。ダイナミックモジュールとは一言でいうと「Emacs Lisp から呼び出せる関数を C 言語などで実装して追加できる」と書きました。邪道なところもあるとは思いますが、うまく応用すればかなりいろいろなことができると思います。本記事がダイナミックモジュール作成の助けになれば幸いです。


  1. 例えば C++ ならば、C99 に対応する C++11 以降であれば使うことができます。ただ、C++ で作ると libstdc++ などの C++ ランタイムライブラリをダイナミックリンクにするのかスタティックリンクにするのか、とかいう話も出てきます。 

  2. ダイナミックモジュールのファイル名は Emacs Lisp の .el ファイルと同様、基本的にはフィーチャ名+拡張子にします。そのため 32 bit/64 bit が違ってもダイナミックモジュールのファイル名が同じになってしまうので注意が必要です。Windows だと MinGW/Cygwin どちらでも同じファイル名になってしまいます。また、 32 bit/64 bit や MinGW/Cygwin といったシステム種別がダイナミックモジュールと Emacs で一致していないとロードできなかったり、あるいは最悪の場合はロードした瞬間に Emacs がクラッシュしてしまいます。 tr-ime では、複数の環境で同じディレクトリを使う可能性を考慮し、ダイナミックモジュールのファイル名をわざとフィーチャ名+拡張子ではなく、フィーチャ名に system-configurationx86_64-pc-cygwin のような名前)を加えたもの+拡張子として複数のシステムのファイルが一つのディレクトリで共存できるようにし、Emacs Lisp からダイナミックモジュールを require する際に system-configuration 名を加えたファイル名を指定することで、システム種別が Emacs と同じダイナミックモジュールを区別してロードできるようにしています。 

  3. 実は一旦ロードしてしまうとアンロードができません(少なくとも Emacs 27.1 や Emacs 28.0.50 では)。unload-feature はできるのですが、共有ライブラリはプロセスに残ったままになります。そのまま再度 loadrequire してもプロセスに残っていた共有ライブラリは読み直されることなく、残ったままの状態で初期化関数 emacs_module_init が呼ばれます。いちいち Emacs を終了させないと共有ライブラリが解放できません。つまり Emacs を起動したままだと、ダイナミックモジュールを修正・再ビルドしてからロードしなおす、みたいなことができないため、デバッグが大変面倒です。 

  4. Emacs Lisp が動作しているのと同じスレッドからであれば呼ぶことができますが、それ以外のスレッドから呼ぶとおかしくなります。具体的には、初期化関数 emacs_module_init の中や、後述のダイナミックモジュール関数の中であれば(そこで新たに作ったスレッドからでなければ)呼ぶことができます。一方で Windows のウィンドウプロシージャなどは別スレッドで動いているので、その中から呼ぶことはできません。この制約は何か特定のウィンドウメッセージを受け取ったら Emacs Lisp に通知したい、とかが簡単にはできないということなので、結構困りものです。こういう時にダイナミックモジュールから使うことができる汎用的なイベント通知の仕組みが欲しいところです。 

  5. emacs_value 型と C 言語の整数、浮動小数点数、文字列などの間を変換する C 関数が用意されているので、それらを使ってやり取りすることになります。emacs_value 型はリストのような複雑なデータも格納できますが、そういうものを直接取り扱う C 関数は用意されていません。リストなどをダイナミックモジュール内で扱いたい場合は、ダイナミックモジュール内から Emacs Lisp 関数を呼び出して扱う必要があります。 

  6. あくまでも funcall なので、呼び出せるのは関数だけです。setq とか defvar とかのスペシャルフォームを使うことはできません。setq ならば代わりに set 関数を使って実装するとかいう方法もありますが、defvar などはそういうわけにはいきません。こういうものはダイナミックモジュールではなくて Emacs Lisp でやった方が簡単です。どうしてもダイナミックモジュール内でやりたければ、list 関数を呼び出してスペシャルフォームを呼び出す形のリストを作って、それを引数に eval 関数を呼び出す、というかなり面倒なことをすればできます。 

  7. 何らかの方法で、このオブジェクトを Emacs Lisp に渡すことができれば、名前を付けなくても呼び出すことは可能なんでしょうが、普通は名前を付けると思います。ただ、ダイナミックモジュール内でのみ呼び出されるような関数(例えば、ダイナミックモジュール関数はインタラクティブ関数にできないので、インタラクティブ化するために別の defun で作った関数で包んで使い、直接 Emacs Lisp から呼び出すことがない関数など)には名前を付けないこともあると思います。 

  8. defalias の代わりに fset でも名前を付けることができます。しかし、fset だと通常の Emacs Lisp で defun して作った関数とは異なり、unload-feature したときに関数が未定義になってくれません。defalias ならば unload-feature すると関数が未定義になってくるので、この方がよいと思っています。 

  9. MELPA は残念ながらダイナミックモジュール(共有ライブラリ)のファイルを配布することはできないため、それらだけ別のサイトから自動でダウンロードできるようにしています。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
16
Help us understand the problem. What are the problem?