id:papix です. この記事は, Perl Advent Calendar 2019の24日目の記事です. 昨日はmp0liiuさんの「Perlのスタックトレースを見やすく扱う方法」でした.
qiita.com
Syntax::Keyword::Try
さて, Perl Advent Calendar 2019の2日目に, Syntax::Keyword::Tryの紹介をしました.
papix.hatenablog.com
そしてその最後に, このような予告をしていました.
「キーワードプラグインを使ったモジュールの実装について」をご紹介させてもらえればと思います!!!!!!
...が, 当初予定していた16日では準備が間に合わず, 他の話題でお茶を濁したりしました.
papix.hatenablog.com
このエントリでは, 今度こそSyntax::Keyword::Tryを題材に, キーワードプラグインの実装について触れていきたいと思います.
...が, 後述しますが, この記事単体では到底説明を終われそうにありません!!!! 今後, 数記事に分けて紹介していきますのでその点ご了承ください...
諸注意
- papixのXS力はゴミです
- 以下の記述は, 実際のコードやPerlの各種ドキュメントを読みながら, 推測(自信がない状態)で書いています
- この記事で紹介しているSyntax::Keyword::Tryの実装やXSの知識が正しい保証は出来ません
Syntax::Keyword::Tryのソースコード
さて, いよいよSyntax::Keyword::Tryを通じて, Perlのキーワードプラグインについて, その実装について触れていきましょう.
今回は, 現時点での最新版である0.11のソースコードを題材にします.
metacpan.org
さあ, まずはlib/Syntax/Keyword/Try.pm を見ていきましょう.
...import
, import_into
という, パッケージをuse
するときに使われる関数を除けば, このファイルで実行している処理はたったこれだけです:
require XSLoader;
XSLoader::load( __PACKAGE__, $VERSION );
XSLoader. というわけで, ここからはXSの世界へと突入していきます.
XSとは?
そもそもXSとは何か. XSは, PerlとC言語を紐付けるための言語... という表現が一番わかり易いのではないかと思います. C言語で書かれたプログラムをPerlの世界から呼ぶためには, XSという言語で紐付けてあげる必要がある, という感じです.
日本語の資料で言えば, 下記の「CによるPerl拡張入門(α)」が有用です.
xsubtut.github.io
Try.xsを読み解く
というわけで, Syntax::Keyword::TryのコアにあたるXSで記述されたlib/Syntax/Keyword/Try.xsを見ていきましょう.
...そもそも, XSの世界でキーワードプラグインを有効にするにはどうすればいいのでしょうか? いろいろ調べていくと, perlapiの「Global Variables」の節に, 次のような記載があります.
PL_keyword_plugin
NOTE: this function is experimental and may change or be removed without notice.
これっぽいですね. PL_keyword_pluginというグローバル変数は, キーワードプラグインを扱う関数を指す関数ポインタとのこと. つまりここに適当な関数ポインタ(キーワードプラグイン用の)をセットすると, Perlはそれを使ってよしなにキーワードプラグインを有効にしてくれそうです. そして, ここで指す関数は, 以下のように宣言されている必要があると書かれています.
int keyword_plugin_function(pTHX_
char *keyword_ptr, STRLEN keyword_len,
OP **op_ptr)
これをもとに, Try.xsを見ていくと, まず static void S_wrap_keyword_plugin(pTHX_ Perl_keyword_plugin_t func, Perl_keyword_plugin_t *var)
という関数の中でグローバル変数 PL_keyword_plugin
に対し, 引数 func
を代入していることがわかります.
更にこれは, define wrap_keyword_plugin(func, var) S_wrap_keyword_plugin(aTHX_ func, var)
というマクロが定義されていて, wrap_keyword_plugin
は Try.xs の末尾に, 次のような形で呼び出されています.
BOOT:
(中略)
wrap_keyword_plugin(&my_keyword_plugin, &next_keyword_plugin);
このコードは, BOOT:
から始まるBOOTキーワードの中にあります. BOOT以下に書かれたコードは, このXSを使ったモジュールを実行する際, 最初に実行されるブートストラップのコードです.
さて. ここで &my_keyword_plugin
は関数 static int my_keyword_plugin(pTHX_ char *kw, STRLEN kwlen, OP **op)
のアドレス(これは, 先に紹介した PL_keyword_plugin に代入可能な形で宣言されています), そして &next_keyword_plugin
は定数 next_keyword_plugin
, つまり static int (*next_keyword_plugin)(pTHX_ char *, STRLEN, OP **);
のアドレスになります. これを踏まえて, 改めて関数 S_wrap_keyword_plugin
(マクロである wrap_keyword_plugin
を展開した先)を見てみましょう.
/* papix注釈
* func = &my_keyword_plugin, *var = &next_keyword_plugin
*/
static void S_wrap_keyword_plugin(pTHX_ Perl_keyword_plugin_t func, Perl_keyword_plugin_t *var)
{
/* BOOT can potentially race with other threads (RT123547) */
/* Perl doesn't really provide us a nice mutex for doing this so this is the
* best we can find. See also
* https://rt.perl.org/Public/Bug/Display.html?id=132413
*/
if(*var)
return;
OP_CHECK_MUTEX_LOCK;
if(!*var) {
*var = PL_keyword_plugin;
PL_keyword_plugin = func;
}
OP_CHECK_MUTEX_UNLOCK;
}
重要なのは以下のコードです.
if(!*var) {
*var = PL_keyword_plugin;
PL_keyword_plugin = func;
}
つまり, 既にグローバル変数 PL_keyword_plugin
にあるキーワードプラグイン用の関数を, *var
すなわち定数 next_keyword_plugin
に退避して, 改めて PL_keyword_plugin
に func
, すなわちSyntax::Keyword::Tryを提供するための関数, my_keyword_plugin
をセットしている訳です. PL_keyword_plugin
がグローバル変数なら, キーワードプラグインを読み込むために上書きされていくはずで, どうやって複数のキーワードプラグインを実現するのだろう...? と思っていましたが, こういう実装になっている... のだと思います. 多分.
そして改めて関数 my_keyword_plugin
を見てみると,
static int my_keyword_plugin(pTHX_ char *kw, STRLEN kwlen, OP **op)
{
HV *hints;
if(PL_parser && PL_parser->error_count)
return (*next_keyword_plugin)(aTHX_ kw, kwlen, op);
if(!(hints = GvHV(PL_hintgv)))
return (*next_keyword_plugin)(aTHX_ kw, kwlen, op);
if(kwlen == 3 && strEQ(kw, "try") &&
hv_fetchs(hints, "Syntax::Keyword::Try/try", 0))
return try_keyword(aTHX_ op);
return (*next_keyword_plugin)(aTHX_ kw, kwlen, op);
}
...となっています. Syntax::Keyword::Tryのためのコードは, ここから改めてtry_keyword
という関数を呼び出して実行しており, エラーが発生している場合や, Syntax::Keyword::Tryのコードが無事処理に成功した時は, (*next_keyword_plugin)(aTHX_ kw, kwlen, op);
として, 先程退避した(別の)キーワードプラグインを実行する... という構成になっているようです.
今回のまとめ
XS, 本当に難しいですね... 一応大学時代, 修士論文はXSのモジュールを書いて卒業したのですが, その時の知見もほぼ失われ, 改めてイチから咀嚼しながらこの記事を書きました.
かなり憶測というか, 「実際のコードやperldocの記載を見るとこうではないか...?」と, 自信のないまま書いているので, 間違いなどは多々あるかもしれません. その時は, Twitterやコメントなどで教えて頂けると幸いです.
XS, そしてキーワードプラグインは興味深い内容なので, これからも少しずつ読み解いていきたいと思います. 次は, 実際にSyntax::Keyword::Tryのための処理が書かれている... であろう, try_keyword
についてコードを読んでいこうと思います.
明日のPerl Advent Calendar 2019の担当は sy250f さんです. 宜しくおねがいします.