Masteries

技術的なことや仕事に関することを書いていきます.

Syntax::Keyword::TryとPerlのキーワードプラグイン (その2)

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_pluginfunc, すなわち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 さんです. 宜しくおねがいします.