GNU awk(以下awk)のDynamic Extension機能を使って、awkの内部でmrubyを実行できるようにしてみた!
上記の画像のように、mruby.soをローカルにダウンロードし、-l./mruby.so
オプションを付与するとmruby_eval関数が使えるようになる。
mruby.soはリポジトリのReleaseページからダウンロードできる。なお、linux-amd64環境のみサポートしている。
作ろうと思ったきっかけ
id:hibomaが会社Slackに「/etc/profile.d/gawk.sh
というスクリプトがあるがどういう目的なのか?」という趣旨の投稿をしていた。このスクリプトの中身をみるとAWKPATHとAWKLIBPATHという2つの環境変数を設定するものだった。
この2つの環境変数がawkの挙動にどのような影響を与えるかを調べていたところ、先述したDymanic Extension機能を発見した。
.soで機能拡張ができるのであればmrubyも組み込めるだろうという発想にいたるのは自然なことなので(?)実装してみたという感じ。
なお、ユースケースや実用性は全くわからない。
Dynamic Extensionを作るときのTips
Dynamic Extensionの作り方は公式のマニュアルをみれば理解できると思う。 また、ボイラープレートも用意されているのでこれを使えばすぐに始められると思う。 公式のサンプルは機能が豊富で最低限の部分だけを確認したい場合には読み込むのが大変なので、awk-mruby-extのv1.0.0のコードを見たほうがわかりやすいとおもう(mrubyのコードがあるが複雑なことはしていないしシンプルなはず…)。
それらを踏まえたうえでDynamic Extensionを作ったときに困ったことを書いておく
evalなどの関数はNamespace配下でも定義できない
GNU awk 5.0からNamespaceが使えるようになった。
awk-mruby-extではmruby_evalという関数を定義したが、Namespaceを活用して mruby::eval
にしたかった。
Dynamic ExtensionでNamespaceを使う場合はdl_load_funcマクロの第3引数に使いたいNamespaceの文字列をいれればよい。
で、mruby::evalを定義しようとしたのだが、実行時に以下のエラーがでた。
awk: fatal: make_builtin: cannot use gawk built-in `eval' as function name
ビルトインの関数と同じ名前の関数は、Namespace配下でも定義できないようだ…。
これはどうしようもなさそうなのでNamespaceを使わずにmruby_eval
とした。
awkの変数は基本的にはString
awk APIのget_argumentなどでawk側の変数を取得するときに、期待するデータ型(Type Requested)を指定するのだが、この指定値と実際のawk側の変数の型で取得できる変数の方が以下のようにきまる。
大抵String(AWK_STRING)を指定しておけば困らないだろう。 しかし、Dynamic Extension側で正しく型変換を行いたい場合はAWK_UNKNOWNを指定してget_argumentを実行して、実行後にawk_vault_tのval_typeを検査すればいいはず。
awk_value_t val; if (!get_argument(0, AWK_UNKNOWN, &val)) fatal(ext_id, "cannot retrive first argument"); switch(val->val_type) { case AWK_STRING: // 文字列のときの処理 :
make_const_stringなどの関数を使うことで戻り値も型をつけて返却することができる。 しかし、awk側でよしなにStringにされてしまうのであまり型を考えずにmake_const_stringで返してしまってもいいかもしれない…。 ただし、Booleanだけはmake_boolで返したほうがよい。というのも、awkにおいてtrueやfalseといった文字列を返却してしまうとどちらもTrue(真値)として解釈されてしまうのでmake_boolで返したほうがよいという感じ。まぁ、0がFalse、0以外がTrueとして認識されるので数値でもいいかもしれないが…。
Arrayは直接返却できない
create_arrayでArrayを作成できるのだが以下のようなエラーがでて直接返却できない。
awk: cmd. line:1: fatal: attempt to use array `(null)' in a scalar context
これを回避するにはsym_updateを使ってawkのシンボル(≒グローバル変数)を登録するとよい。
ary = create_array(); result->val_type = AWK_ARRAY; result->array_cookie = ary; // Arrayへの追加処理 sym_update("array", & val); // arrayとしてシンボルを登録 array = val.array_cookie; // この再代入は絶対やったほうがいいらしい
これでsym_updateで登録したarray変数にawk側からアクセスできるようになる。
おわりに
GNU awkのDynamic Extension機能を使ってmrubyの実行環境を実装してみた。 これを作ろうと思い立ってから2時間くらいで実装できてよかった。 mrubyも使いやすいAPIが揃っていて実装しやすいので色々なものに組み込むのおすすめです。