erlangでオリジナルのBIFを追加する

これからerlangを使う上で「どうしても標準ライブラリでは足りない」と感じてしまったときのために、予め、erlangソースコードを編集して機能追加するためのノウハウを積んでおこうと思います。

そうそうオリジナルのBIFが必要なケースなんて無いと思いますが、お守り程度に知っておくだけでも、アイデア実現の幅が広がって良いかも知れません。

目的

  • beamで動作するビルトインモジュール"mymod"の追加。
  • mymod:true/0 # tureアトムを返すBIF
  • mymod:false/0 # falseアトムを返すBIF
  • mymod:muldiv/3 # 数値を掛けて割るBIF
  • mymod:serialize/1 # erlangデータを文字列に直列化するBIF

開発環境

注意

手順

先に言ってしまうと、

  1. bif.tabにエントリを書く
  2. bif.cに関数を追加
  3. make

これだけで済みます。
しかしこれだけだと元のソースに大幅な手を加えることになる他、単なるmakeの場合、コンパイルに膨大な時間が掛かったりするため、開発効率やオリジナルBIFの保守を考えて、もう少し深入りした工程を組んでみたいと思います。

1. erts/emulator/beam/bif.tabにエントリを加える

BIFテーブルにmymod:*をエントリさせます。

ファイルの最後に以下の行を追加します。

bif mymod:true/0
bif mymod:false/0
bif mymod:muldiv/3
bif mymod:serialize/1

この手続きによりmake時にmymod:*に関する様々なソース・ヘッダーファイルが自動生成され、BIFテーブルにもエントリされるようになります。

2. erts/emulator/beam/erl_bif_mymod.cを作成する

BIFをCで記述します。BIFは既にあるerts/emulator/beam/bif.cにも書くことができますが、今回はソースファイル及びオブジェクトファイルを分けることにします。

以下のような中身になります。

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "sys.h"
#include "erl_vm.h"
#include "global.h"
#include "erl_process.h"
#include "error.h"
#include "bif.h"

BIF_RETTYPE mymod_true_0(BIF_ALIST_0)
{
    BIF_RET(am_true);
}

BIF_RETTYPE mymod_false_0(BIF_ALIST_0)
{
    BIF_RET(am_false);
}

BIF_RETTYPE mymod_muldiv_3(BIF_ALIST_3)
{
    Sint n1 = signed_val(BIF_ARG_1);
    Sint n2 = signed_val(BIF_ARG_2);
    Sint n3 = signed_val(BIF_ARG_3);
    Sint r = n1 * n2 / n3; 
    BIF_RET(make_small(r));
}

BIF_RETTYPE mymod_serialize_1(BIF_ALIST_1)
{
    int pres;
    Eterm res;
    Eterm *hp;
    erts_dsprintf_buf_t *dsbufp = erts_create_tmp_dsbuf(64);    
    pres = erts_dsprintf(dsbufp, "%.*T", INT_MAX, BIF_ARG_1);
    if (pres < 0)
        erl_exit(1, "Failed to convert term to string: %d (s)\n", -pres, erl_errno_id(-pres));
    hp = HAlloc(BIF_P, 2*dsbufp->str_len); /* we need length * 2 heap words */
    res = buf_to_intlist(&hp, dsbufp->str, dsbufp->str_len, NIL);
    erts_destroy_tmp_dsbuf(dsbufp);
    BIF_RET(res);
}

erts/emulator/beam/*.cはmake dependにより自動的にコンパイルされるようになりますが、ファイル名は"erl_bif_[モジュール名].c"とするのが良いと思われます。
関数名は命名規則により決まっていて、"[モジュール名]_[関数名]_[引数の数]"にならなければなりません。これらはbif.tabの情報と同期させて下さい。
関数の中身はmymod_serialize_1以外は分かりやすいと思います。詳細な解説は別の記事にまとめるとして、次のステップに進みます。

3. erts/emulator/Makefile.inの書き換え

2.で、erts/emulator/beam/*.cは自動的にコンパイルMakefileの記述不要)されると書きましたが、リンカの設定は手動になります。ソースファイルを"erl_bif_mymod.c"としたなら、"erl_bif_mymod.o"をリンクさせるよう書き換えます。

RUN_OBJS = \
	.... \
	$(OBJDIR)/erl_bif_mymod.o

RUN_OBJSという変数にオブジェクトファイルを指定する記述があるので、そこに追加します。

4. configureの再実行

Makefile.inを書き換えたのでconfigureを再実行する必要があります。
3.と4.の手順はソースファイルを分けたときに一度だけ発生する作業です。

$ ./configure 

configureのオプションは必要に応じて記述して下さい。

5. コンパイルする

ソースのルートでmakeしてもコンパイルされますが、膨大な時間が掛かります。例え、"erts/emulator/beam/erl_bif_mymod.c"だけを書き換えたとしてもなぜか大部分が再コンパイルされます。
ですので"erts/emulator/beam/erl_bif_mymod.c"だけを再コンパイルしてリンクさせるようなmakeを行いたいところです。

$ cd erts/emulator
$ ERL_TOP=`pwd`/../.. make

erts/emulator/にMakefileがあるのでこれを利用します。このMakefileは編集されたソースのみ再コンパイルします。
このMakefileは別のMakefileから環境変数を継承して呼ばれることを想定しているので、そのまま実行するには足りない"ERL_TOP"を環境変数に加えた状態でmakeする必要があります。ERL_TOPはソースのルートディレクトリを指します。

6. テストする

5.の手順で実際にbeamが再コンパイルされます。さっそくオリジナルのBIFを試してみます。

$ bin/erl  # ソースのルートで実行。
1> A = mymod:true().
true
2> A = true.
true

mymod:true/1はtrueアトムを返すBIFとして定義しました。確かにtrueアトムを返しています。

次はmymod:muldiv/3を試します。第1引数と第2引数を掛けて第3引数で割った値を返します。(一般的なMulDivです)

3> B = mymod:muldiv(2,6,3).
4
4> B = 4.
4

こちらも正常に動作しています。Bは確かに、small-intである"4"に拘束されています。

最後にmymod:serialize/1を試します。これは第1引数のデータを文字列へ直列化したものを返します。

5> C = mymod:serialize([1,"hoge",atom,{ok,true}]).
"[1,\"hoge\",atom,{ok,true}]"

この文字列は、erlangでの一般的なeval実装や、file:consultと互換性があります。

もしここで期待される動作と異なった結果がもたらされた場合、2.の手順でソース修正、5.の手順でコンパイルし、再度テストを行います。

7. インストール

ここで作られたmymodは、erl,erlcなどで利用できます。
ソースのルートディレクトリで、以下のコマンドを実行してインストールします。

$ make clean
$ make 
# make install

以前のバイナリのバックアップは忘れずに。自作のBIF程怖いものはありません。

あとがき

bif.tabにエントリを記述し、同ディレクトリにソースコードを置くだけ。

次回は、

などまとめられたらと思います。