mruby_nginx_moduleあるいはngx_mrubyでPCREベースの正規表現を利用する際の注意

前回の記事で 深遠な理由によりmruby_nginx_modulengx_mrubyではmrbgemsとして実装されているPCREベースの正規表現エンジンを利用するのは困難と書きましたが、今回はこの理由について解説します。

mrubyと正規表現

このブログの執筆時点ではmrubyにはRegexpクラスが含まれていないので、 mruby正規表現を使うにはmruby-regexp-pcreのようにmrbgems化されている実装を使うか自分で実装する必要があります。

mruby_nginx_moduleとmruby-regexp-pcre

昨日、mruby_nginx_moduleでも正規表現が使えるように自分で実装しはじめたのですが、Rubyらしく使えるようにするのが思ったより大変だったので「もうmruby-regexp-pcre直接使えばいいか」と思ってmruby/build_config.rbに以下の行を追加したのでした。

conf.gem :git => 'https://github.com/iij/mruby-regexp-pcre/'

あとはmrubymruby_nginx_moduleをビルドし直せばmruby_nginx_moduleでも正規表現がこんな感じで使えるはずでした。

しかしこのlocationにアクセスすると500が返り、以下のエラーメッセージがログに出ます。

mrb_run failed. error: ArgumentError: invalid regular expression]

当初はmruby-regexp-pcreの使い方がおかしいかちゃんとビルドできてないのかと思ってたのですが、Nginxを介せずmruby単体で実行すればちゃんと動きます。

調べてみるとエラーを返しているのはpcre_compileで原因はNginxのPCREの使い方に起因してました。

    reg->re  = pcre_compile((const char *)RSTRING_PTR(source), coptions, &errstr, &erroff, NULL);

    if (reg->re == NULL) {
        mrb_raisef(mrb, E_ARGUMENT_ERROR, "invalid regular expression");
    }

NginxとPCRE

Nginxではconfigure時に「--with-pcre」を指定していると起動時に以下の関数が呼ばれます。

void
ngx_regex_init(void)
{   
    pcre_malloc = ngx_regex_malloc;
    pcre_free = ngx_regex_free;
}

とこんな感じで、pcre_(malloc|free)を上書きしていて、Nginxのメモリプールを使う関数に差し替えています。実際にpcre_compileを呼ぶ際はその前後でアロケーションの際に利用するメモリプールのアサインおよびリリース(ただのNULL代入)を行なっています。

    ngx_regex_malloc_init(rc->pool);

    re = pcre_compile((const char *) rc->pattern.data, (int) rc->options,
                      &errstr, &erroff, NULL);

    /* ensure that there is no current pool */
    ngx_regex_malloc_done();

ngx_regex_malloc_initの実装はこんな感じでグローバル変数のngx_pcre_poolに与えられたメモリプールへのポインタを代入しています。

static ngx_inline void
ngx_regex_malloc_init(ngx_pool_t *pool)
{
#if (NGX_THREADS)
    ngx_core_tls_t  *tls;

    if (ngx_threaded) {
        tls = ngx_thread_get_tls(ngx_core_tls_key);
        tls->pool = pool;
        return;
    }

#endif

    ngx_pcre_pool = pool;
}

で、ngx_regex_mallocで利用するメモリプールがこのngx_pcre_poolなわけです。

static void * ngx_libc_cdecl
ngx_regex_malloc(size_t size)
{
    ngx_pool_t      *pool;
#if (NGX_THREADS)
    ngx_core_tls_t  *tls;

    if (ngx_threaded) {
        tls = ngx_thread_get_tls(ngx_core_tls_key);
        pool = tls->pool;

    } else {
        pool = ngx_pcre_pool;
    }

#else

    pool = ngx_pcre_pool;

#endif

    if (pool) {
        return ngx_palloc(pool, size);
    }

    return NULL;
}

なのでNginxやNginxのモジュールからpcre_compileを直接呼ぶと必ず失敗します

また、残念なことにこのグローバル変数やngx_regex_malloc_(init|done)には頭にstatic修飾子が付いているため、Nginxの外部から利用することができません。

static ngx_pool_t  *ngx_pcre_pool;

mruby_nginx_moduleでmruby-regexp-pcreを利用できるようにする

Nginxがこのような実装になっているのでmruby_nginx_modulengx_mrubyではmruby-regexp-pcreのようなPCREベースのmrbgemsはそのまま利用することができません。

なのでmruby_nginx_moduleではmruby-regexp-pcreの実装を内部に取り込み、 pcre_compileの前後でpcre_(malloc|free)をさらに上書きしています。

    // As nginx orverrides pcre_(malloc|gree),                                                                                                                
    // calling pcre_compile directly fails                                                                                                                    
    // This is the workaround for it.                                                                                                                         
    ngx_mrb_pcre_pool = r->pool;
    old_pcre_malloc   = pcre_malloc;
    old_pcre_free     = pcre_free;
    pcre_malloc       = ngx_mrb_pcre_malloc;
    pcre_free         = ngx_mrb_pcre_free;
 
    reg->re  = pcre_compile((const char *)RSTRING_PTR(source), coptions, &errstr, &erroff, NULL);
 
    pcre_malloc = old_pcre_malloc;
    pcre_free   = old_pcre_free;

上書きしたmallocとfreeの実装はこんな感じ。

static ngx_pool_t *ngx_mrb_pcre_pool = NULL;
 
static void *(*old_pcre_malloc)(size_t);
static void (*old_pcre_free)(void *ptr);
 
static void *ngx_mrb_pcre_malloc(size_t size)
{   
    if (ngx_mrb_pcre_pool) {
        return ngx_palloc(ngx_mrb_pcre_pool, size);
    }
 
    return NULL;
}
 
static void ngx_mrb_pcre_free(void *ptr)
{
    if (ngx_mrb_pcre_pool) {
        ngx_pfree(ngx_mrb_pcre_pool, ptr);
        return;
    }
}

また、mruby-regexp-pcreは全部Cで書かれているわけではなく、結構な量のコードがmrubyで実装されているのでこいつら(regexp_pcre.rbとstring_pcre.rb)も取り込む必要があります。

これはどうするか悩んだのですが、Nginx起動する度にこのmrbファイルをrequireするわけにもいかないし、全部Cで再実装するのも大変だし、 かと言って事前にmrubyビルドする際にmrbcで組み込むのも難しいのでCの文字列に変換するスクリプト(util/regexpcodegen.rb)書いて#includeで取り込んだのをmrubyのステートマシンに 放り込む形にしました。

これはもう少しいい方法がないか考えたいです。

というわけで無事mruby_nginx_module正規表現が使えるようになりました。基本的にアロケータの差し替え以外はmruby-regexp-pcreそのままなので 非常に使いやすくて良いです。とてもいいmrbgemsを作ってくれたIIJさんに感謝!