ngx_mrubyから学ぶnginxモジュールの作り方

ngx_mrubyを作るにあたって、nginxモジュールの実装方法が分かってきたので、それを連々と書いていこうと思います。nginxモジュールといっても、Apacheモジュールの実装方法と似ていたので、Apacheモジュールを書ける人は同様にnginxモジュールも実装できると思います。

ngx_mruby用のディレクティブを追加

nginxモジュールはApacheモジュールと同じで、基本的にはnginxの内部ルールに従って、nginxに処理させたい関数をフックさせる方式で実装します。nginxのconfに新たな設定を追加したい時、例えば、以下のようなnginxの設定を新たに作りたいとします。

location /mruby {
    mrubyHandler /path/to/file.rb;
}

これをモジュールで実現するためには、以下のように、まずngx_command_tを定義します。Apacheモジュールでいうcommand_recです。

static ngx_command_t ngx_http_mruby_commands[] = {
    { ngx_string("mrubyHandler"),
      NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
      ngx_http_mruby_handler_phase,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL },

    ngx_null_command
};

まずは、このように記述すると覚えて下さい。Locationディレクティブ上で有効な設定を追加したい場合はNGX_HTTP_LOC_CONFを指定し、今回の新しい設定であるmrubyHandlerディレクティブに引数を一つ指定したい場合はNGX_CONF_TAKE1を指定します。この辺りもApacheに非常に似ています。また、この設定が記述された場合に呼び出す関数のポインタを指定します。上記の例の場合は、ngx_http_mruby_handler_phaseを指定します。この関数については、実装の流れ上、最後に説明するので一旦頭のすみに置いておいて下さい。

ディレクティブの設定値を保存する構造体を定義

次に、mrubyHandlerの設定の値を保存しておくための構造体を定義します。今回はLocationディレクティブ内で有効なディレクティブなので以下のように構造体を定義します。Apacheモジュールでも同様です。

typedef struct {

    char *handler_code_file;
    // 後で必要になったらメンバを増やす

} ngx_http_mruby_loc_conf_t;

そして、この構造体をmrubyHandlerディレクティブとしてnginxに認識させるために呼び出す登録・初期化関数ngx_http_mruby_loc_confのポインタをstatic ngx_http_module_t構造体に定義しておきます。Apacheモジュールでのmodule AP_MODULE_DECLARE_DATAにあたります。例えば、以下のようになります。

static ngx_http_module_t ngx_http_mruby_module_ctx = {
    NULL,                          /* preconfiguration */
    ngx_http_mruby_init,           /* postconfiguration */

    NULL,                          /* create main configuration */
    NULL,                          /* init main configuration */

    NULL,                          /* create server configuration */
    NULL,                          /* merge server configuration */

    ngx_http_mruby_loc_conf,       /* create location configuration */
    NULL                           /* merge location configuration */
};

ディレクティブの設定値を保存する構造体の初期化

では、ngx_http_module_tに登録した関数のポインタに関して、まずはmrubyHandlerディレクティブの登録・初期化を行う関数ngx_http_mruby_loc_confを定義しましょう。Apacheモジュールでのcreate dir configにあたります。以下のように記述します。

static void *ngx_http_mruby_loc_conf(ngx_conf_t *cf)
{
    ngx_http_mruby_loc_conf_t *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mruby_loc_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    conf->handler_code_file = NULL;

    return conf;
}

上記のように、ngx_pcallocで設定を保存する構造体のメモリを確保し、設定が格納されるメンバhandler_code_fileを初期化しています。この構造体の値はすべてのリクエストが保持すると理解して下さい。Locationディレクティブの中にmrubyHandlerディレクティブを記述していた場合、そのLocationのURLにアクセスがあったリクエストだけ、handler_code_fileに指定した引数がコピー(実際にコピーする関数は最後に説明します)されており、それ以外のリクエストはNULLのままになります。これによって、Locationディレクティブで設定したURLにアクセスしたかどうかを判断することができます。この辺りもApacheと同様です。

モジュールのフック関数を登録

さらに、上記のngx_module_tのpostconfigurationにngx_http_mruby_initという関数のポインタを指定しています。これは、nginx起動時のこのモジュール初期化処理時に呼び出される関数になります。Apacheモジュールでいうregister_hooksにあたります。この初期化処理は以下のように実装します。

static ngx_int_t ngx_http_mruby_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt *h;
    ngx_http_core_main_conf_t *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_mruby_handler;

    return NGX_OK;
}

このngx_mrubyモジュールの初期化時に、リクエストからレスポンスを返すまでのどのフェーズでどの関数をフックさせるかを、ngx_http_core_moduleから呼び出したモジュールのphaseが保存されている配列cmcfに登録します。今回は、NGX_HTTP_CONTENT_PHASE、つまりはコンテンツを処理するフェーズでngx_http_mruby_handlerを呼び出すようにします。Apacheモジュールでは、ap_hook_***にあたります。nginxのフックフェーズとしては、以下のフェーズが存在します。

NGX_HTTP_POST_READ_PHASE
NGX_HTTP_SERVER_REWRITE_PHASE
NGX_HTTP_FIND_CONFIG_PHASE
NGX_HTTP_REWRITE_PHASE
NGX_HTTP_POST_REWRITE_PHASE
NGX_HTTP_PREACCESS_PHASE
NGX_HTTP_ACCESS_PHASE
NGX_HTTP_POST_ACCESS_PHASE
NGX_HTTP_TRY_FILES_PHASE
NGX_HTTP_CONTENT_PHASE
NGX_HTTP_LOG_PHASE

この中から任意のフックしたいフェーズを選択することが可能で、コンテンツを処理する前や後ろのフェーズを指定したりすることができます。

フック関数の処理を実装

では、ここで今回登録したngx_http_mruby_handlerを実装してみましょう。以下のようになります。

static ngx_int_t ngx_http_mruby_handler(ngx_http_request_t *r)
{   
    ngx_http_mruby_loc_conf_t *clcf = ngx_http_get_module_loc_conf(r, ngx_http_mruby_module);

    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD)))
        return NGX_DECLINED;

    if (clcf->handler_code_file == NULL)
        return NGX_DECLINED;

    return ap_ngx_mrb_run(r, clcf->handler_code_file);
}

処理しているリクエストの情報が保存されているngx_http_request_t構造体を利用して、現在処理しているリクエストがmrubyHandlerディレクティブの設定がされたLocationディレクティブへのアクセスであるかを評価します。ngx_http_get_module_loc_conf関数でngx_http_mruby_moduleに関する設定が保存された構造体を呼び出します。

上記で説明した通り、もし、clcf->handler_code_fileがNULLだった場合は、指定したLocationへのアクセスではない事がわかります。また、Locationへのアクセスだった場合は、必ずmrubyHandlerディレクティブに渡していた引数・ファイル名がclcf->handle_code_fileに保存されているはずです。保存されていれば、そのファイルをap_ngx_mrb_run()によって、mrubyのvm上で実行します。ap_ngx_mrb_runは以下のような処理になっています。このあたりがngx_mruby特有のもので、ここを任意の処理にすることで、自分のnginxモジュールを作る事ができます。

static int ap_ngx_mrb_run(ngx_http_request_t *r, char *code_file)
{
    FILE *mrb_file;

    mrb_state *mrb = mrb_open();
    ap_ngx_mrb_class_init(mrb);

    if ((mrb_file = fopen((char *)code_file, "r")) == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "mrb_file open failed");
    }

    struct mrb_parser_state* p = mrb_parse_file(mrb, mrb_file, NULL);
    int n = mrb_generate_code(mrb, p->tree);
    mrb_pool_close(p->pool);
    ap_ngx_mrb_push_request(r);
    mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_nil_value());

    return NGX_OK;
}

上記は、mrubyの定番の実行組み込み方で、mrb_open()でmrb_stateを初期化し、ap_ngx_mrb_class_init()で定義したメソッド(本エントリでは説明を省略します)を読み込みます。指定されたファイルをmrb_parse_fileで構文木解析して、mrb_generate_codeでバイトコードを生成し、mrb_runでvm上でバイトコードを実行します。指定されたファイルはRubyのコードが書かれていて、例えばNginx.rputs(“hello world”)と書かれていてれば、それをbodyに出力します。これが、ngx_mrubyの基本的な動作です。

mrubyHandlerディレクティブの引数の値を構造体に保存

ngx_http_mruby_handle_phaseは実装の流れ上、最後に説明すると最初に述べました。このようにLocationディレクティブ内のmrubyHandlerディレクティブの設定値は、最初に説明したngx_command_tに登録している関数ngx_http_mruby_handler_phaseによって保存されます。Apacheモジュールでいうcommand_recに登録する関数ポインタと同じです。実際にngx_http_mruby_handler_phaseを書くと以下のようになります。

static char * ngx_http_mruby_handler_phase(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{   
    ngx_str_t *value;
    ngx_http_mruby_loc_conf_t *flcf = conf;

    value = cf->args->elts;
    flcf->handler_code_file = (char *)value[1].data;

    return NGX_CONF_OK;
}

この関数によって、Locationディレクティブで設定したmrubyHandler /path/to/fileの/path/to/file文字列がvalue[1].dataに入っているので、それをflcf->handler_code_fileにポインタをわたしておきます。この処理によって、Location /mruby、つまりは http://example.com/mruby 以下にアクセスした場合は、flcf->handle_code_fileに/path/to/fileがコピーされる事になります。

これらを組み合わせた実装がngx_mruby

これらの関数を組み合わせたコードが、ngx_mrubyのコードになります。どうでしょう、nginxのモジュールも何とか実装できる気がしてきたのではないでしょうか。本エントリの実装を理解するだけでも、ある程度のnginxモジュールを作成できると思いますので、色々作ってみて下さい。