Gnu Emacs 25.1から導入されたDynamic Modulesを使ってオーディオファイルを再生する

本稿はKLab Advent Calendar 2017の6日目の記事です.

はじめに

Gnu Emacs 25.1(以下Emacs)では新たにShared Libraryを動的にロードする機能が導入されました. この機能を使う事で, これまでは外部のプロセスを利用しないと実現できなかった事がEmacsのみで完結できるようになりました. 今回はその一例としてwavファイルを再生する簡単なDynamic Moduleを作ります.

下準備

Emacsのビルド

まず, Emacsをビルドします. なぜなら, configure時に–with-modulesオプションを付与した状態でビルドされたEmacsでなければこの機能を使う事ができないからです1.

macOS環境でEmacsをビルドする際のShell Scriptを以下に示します. なお, このsnippetはHacker's patchを適用しています. また, 各バージョンは2017年12月5日現在の最新のバージョンです.

EMACS_VERSION=25.3
PATCH_VERSION=6.8

## Emacsとpatchを取得して
curl -O http://ftp.gnu.org/pub/gnu/emacs/emacs-${EMACS_VERSION}.tar.gz
curl -O ftp://ftp.math.s.chiba-u.ac.jp/emacs/emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}.tar.gz

## 伸張
tar zxvf emacs-${EMACS_VERSION}.tar.gz
tar zxvf emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}.tar.gz

cd emacs-${EMACS_VERSION}

## パッチを適用し
patch -p 1 < ../emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}/patch-mac
cp -r ../emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}/mac mac
cp ../emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}/src/* src
cp ../emacs-${EMACS_VERSION}-mac-${PATCH_VERSION}/lisp/term/mac-win.el lisp/term
cp nextstep/Cocoa/Emacs.base/Contents/Resources/Emacs.icns mac/Emacs.app/Contents/Resources/Emacs.icns

## ビルド
./configure --with-mac --with-modules --without-x  # ここでDynamic Moduleの機能を有効にする.
make -j16

sudo make install

ここで用意したEmacsのソースコードの一部を後で利用するので, インストール終わったからといって捨ててはいけません.

外部ライブラリの準備

自分で作るのが面倒なので, オーディオファイルを再生するために必要な外部ライブラリを用意します. 今回はportaudiolibsndfileを利用します.

macOS環境では手っ取り早くhomebrewを使ってインストールします. Windows環境下でもpacmanなどを利用すればお手軽にインストールできると思います.

brew install portaudio
brew install libsndfile

Dynamic Moduleの作成

いよいよDynamic Moduleの開発に入っていきます.

エントリーポイント

各Dynamic Moduleは extern "C" されたemacs-module-initという関数をエントリーポイントとします. ですので, この関数の中にDynamic Moduleの実装をしていくことになります. また, EmacsがロードするDynamic ModuleはGPL互換ライセンスである必要があります. その宣言をModule内で表すために, plugin_is_GPL_compatibleというシンボルをexportしなければなりません.

int plugin_is_GPL_compatible; // module-loadされる時にこの名称のシンボルがShared Library内に存在するかどうかを確認するためだけに必要.

int
emacs_module_init(emacs_runtime* ert)
{
  // Dynamic Moduleの処理を記述
}

struct emacs_runtimeはEmacsのソースコード郡の中にある, src/emacs-module.h内で定義されています. そのため, このヘッダファイルへのパスを通すか, コピーしてDynamic Moduleのソースコードがあるディレクトリに持ってくるかした上で, #includeする必要があります.

自module用のemacs_envを取得する.

emacs_module_initの引数で渡されるemacs_runtime型の変数からemacs_env型のオブジェクトを取得します. これはこのDynamic Module用で, 他のDynamic Moduleとは独立しています.

auto env = ert->get_environment(ert);

関数の生成と束縛

このenvを利用してEmacs側から利用したいシンボルや関数を作っていきます.

make_function

関数を作るには, emacs_envのmake_functionを利用します.

struct emacs_env
{
  emacs_value (*make_function) (emacs_env *env,
                                ptrdiff_t min_arity, // 受け取る引数の数の内 必須な物の数
                                ptrdiff_t max_arity, // 受け取る引数の数の最大値
                                // Emacs側から呼び出したい関数へのポインタ
                                emacs_value (*function) (emacs_env *env,
                                                           ptrdiff_t nargs, // argsの数
                                                           emacs_value args[], // 関数へ渡される引数
                                                           void *) EMACS_NOEXCEPT, // ユーザーデータ
                                const char *documentation, // 関数のドキュメント文字列
                                void *data); // ユーザーデータ
};

Emacs内で扱うシンボルや関数などあらゆる物はemacs_valueという抽象的な型の元で実装されており, 基本的にはこのemacs_value型でやりとりを行います. make_functionも例に盛れず, Emacsから呼び出したい関数をfunction引数に渡すと, その他の引数と共にEmacsで利用できるようにemacs_valueとして返されます. ただし, これだけだと, lambdaを作ったような物で名前がなく, Emacsから呼び出す事が難しいので名前を付けます.

intern

新しいシンボルを取得するにはEmacs Lispと同様にintern2する必要があります. Dynamic Moduleからinternを呼び出すには, emacs_envが持っているintern関数ポインタを介します.

struct emacs_env
{
  emacs_value (*intern) (emacs_env *env,
                         const char *symbol_name); // internしたいシンボル名

};

これを呼び出す事で, symbol_nameを持つシンボルとしてemacs_valueを取得できます.

funcall

internしたシンボルとmake_functionで作成した関数を結びつけるにはfset3を使いますが, fset関数はemacs-module.h内には用意されていないので, emacs_envのfuncall4関数ポインタを利用してfsetを呼び出します.

struct emacs_env
{
  emacs_value (*funcall) (emacs_env *env,
                          emacs_value function, // funallで呼び出す関数
                          ptrdiff_t nargs, // funcall時, functionに渡される引数の数
                          emacs_value args[]); // funcall時, functionに渡される引数
};

fset

fsetは頻繁に使う事になるので, 一つの関数として定義します.

void
fset(emacs_env* env, emacs_value symbol, emacs_value function)
{
  auto fset = env->intern(env,
                          "fset");
  std::array<emacs_value, 2> args{{symbol,
                                   function}};
  env->funcall(env,
               fset,
               args.size(),
               args.data());
}

例えば, オーディオファイルへのパスを引数に受け取り, そのファイルのデータを再生するaudio-player-play-audioという関数を実装するには以下のようになります.

// 関数の実体をどこかで定義.
emacs_value play_audio(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* user_data) EMACS_NOEXCEPT;

int
emacs_module_init(emacs_runtime* ert)
{
  auto env = ert->get_environment(ert);
  auto function = env->make_function(env,
                                     1, // 引数としてファイルパスを1つ取るので1
                                     1, // optional等は使わないのでmaxも1
                                     play_audio,
                                     "Play audio from file.",
                                     nullptr);
  auto symbol = env->intern(env,
                            "audio-player-play-audio");
  fset(env,
       symbol,
       function);
}

provide

これで関数を生成しシンボルと結びつける事はできましたが, それだけではまだ足りません. Emacs Lispでpackageを作る時と同様にprovide5する必要があります. provideもfsetと同じようにinternでprovideのemacs_valueを取得し, それをfuncallに渡して呼び出します.

void
provide(emacs_env* env, const char* feature_name)
{
  auto provide = env->intern(env,
                             "provide");
  auto feature = env->intern(env,
                             feature_name);
  std::array<emacs_value, 1> args{{feature}};
  env->funcall(env,
               provide,
               args.size(),
               args.data());
}

上記のprovide関数を使ってaudio-playerという名前で登録します.

int
emacs_module_init(emacs_runtime* ert)
{
  auto env = ert->get_environment(ert);
  // なんやかんや処理をして

  provide(env,
          "audio-player");

  return 0;
}

これをビルドしてsite-lispなどパスの通っている処にDynamic Moduleを置いてrequireすると各関数が呼び出せるようになります.

(require 'audio-player)
(audio-player-play-audio "/path/to/your/audio_data.wav")

注意点

Module名

この時作成されたDynamic Moduleは, macOSではlibaudio-player.dylib, LINUXなどPOSIX環境下ではllibaudio-player.soとなります. prefixのlibが付いた状態だとEmacsはそれをDynamic Moduleとして自動的に認識する事ができません. ですので, 接頭辞のlibを削除するか, module-loadで明示的にロードする必要があります.

またmacOS環境の場合, 拡張子dylibをEmacsはDynamic Moduleと認識しないため, .soに変更する必要もあります. これはmodule-file-suffixという変数で設定されている拡張子のみをDynamic Moduleとして認識するためです. なお, module-file-suffixの値をinit.elなどで".dylib"に変更しても読み込むことはないので大人しく拡張子を変更するか, やはりmodule-loadで明示的にロードする必要があります.

例外

EMACS_NOEXCEPTが指定されている事からもわかる通り, Emacs側に例外を投げる事はできません. ですので, 例外はModule内部で全て処理する必要があります.

むすび

今回作成したDynamic Moduleのソースコードは, 上記で説明していないmessage関数の呼び出し方や, 文字列の取得方法, オーディオデータの再生部分等も含めてgithub repositoryに全て置いてあります.

これまでは, 別途プロセスを作成しそのプロセスと通信して処理をしなければならなかった事も, Dynamic Moduleが実装された事でEmacsのプロセス内で完結させる事ができるようになり, Emacsの可能性がさらに広がる事となりました. 筆者は現在EmacsをVSTホストアプリケーションとするDynamic Moduleを作成中です. これによってVST Pluginの実装からデバッグまで全てEmacsだけで完結できるようになり益々開発が捗ります.

Emacsは本当に素晴らしいエディタですね.

それでは, 素敵なEmacs Lifeを!

脚注

1 https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Modules.html

2 https://www.gnu.org/software/emacs/manual/html_node/elisp/Creating-Symbols.html#index-intern-577

3 https://www.gnu.org/software/emacs/manual/html_node/elisp/Function-Cells.html#index-fset-897

4 https://www.gnu.org/software/emacs/manual/html_node/elisp/Calling-Functions.html#index-funcall-866

5 https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html#index-provide-1052

著者: m13o
Emacs 25.3.1 (Org mode 9.1.3)

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.