Tips for Developing Apache 2.0.x modules

[ はじめに | 排他処理 | 共有メモリ | ファイル入出力 | 書式指定 | リクエストの処理 | C++ | デバッグ | 参考文献 ]

# はじめに

このページでは，私が Apache 2.x 向けのモジュール開発の過程で得た Tips について紹介しています．

# 排他処理

他のプロセス及びスレッドに対して排他処理を行うには apr_global_lock_t が，他のスレッドに対して排他処理を行うには apr_thread_mutex_t や apr_thread_rwlock_t が利用できます．

## apr_global_mutex_t の使い方

基本的には，次の手順で使用します．

1. post_config ステージ
   - apr_global_mutex_create を使って apr_global_mutex_t を生成．
   - 必要に応じて unixd_set_global_mutex_perms を使ってパーミッションを変更．
2. child_init ステージ
   - apr_global_mutex_child_init を使っ て apr_global_mutex_t に attach．
3. クリティカルセクションの開始
   - apr_global_mutex_lock または apr_global_mutex_trylock を使ってロック．
4. クリティカルセクションの終了
   - apr_global_mutex_unlock を使ってアンロッ ク．

クリティカルセクションの開始/終了の詳細については，上記の関数を呼ぶ だけなので，apr_global_mutex.h のドキュメントを参照してください．関 数の戻り値のチェックだけ気をつけておけば，特につまずく点はないと思い ます．また，apr_global_mutex_t のリソースは Apache の終了時に自動的 に解放されるので，明示的 に apr_global_mutex_destroy を呼ぶ必要はあ りません．

post_config ステージにおい て unixd_set_global_mutex_perms を実行すべ きかどうかは AP_NEED_SET_MUTEX_PERMS が define されているかどうかで判断すれば良いのですが，このマクロは Apache 2.0 系には用意されていません．Apache 2.0 系のモジュールを作成 する場合は次のようにしてモジュール側で define してやる必要があります． (AP_SERVER_MAJORVERSION_NUMBER が Apache 2.1 系のみに存在することを 利用しています)

    /* Apache 2.0 系の場合は，AP_NEED_SET_MUTEX_PERMS の define を試みる． */
    #ifndef AP_SERVER_MAJORVERSION_NUMBER
    #if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
    #define AP_NEED_SET_MUTEX_PERMS
    #endif
    #endif

以上をふまえると，モジュールのコードは次のようになります．

    #ifndef AP_SERVER_MAJORVERSION_NUMBER
    #if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
    #define AP_NEED_SET_MUTEX_PERMS
    #endif
    #endif

    #include "apr_global_mutex.h"
    #ifdef AP_NEED_SET_MUTEX_PERMS
    #include "unixd.h"
    #endif

    typedef struct ServerConfig {
        char *mutex_path;
        apr_global_mutex_t *mutex;
    } sconfig;

    static void *create_server_config(apr_pool_t *p, server_rec *s)
    {
        sconfig *config;

        config = (sconfig *)(apr_palloc(p, sizeof(sconfig)));

        /* ここで指定されるファイルは，必要に応じて自動的に生成されます． */
        /* あらかじめ作成しておく必要はありません． */
        config->mutex_path = "/path/to/global/mutex/file";

        return config;
    }

    static int post_config(apr_pool_t *pconf, apr_pool_t *plog,
                           apr_pool_t *ptemp, server_rec *s)
    {
        sconfig *config;
        void *user_data;
        apr_status_t status;

        /* post_config は Apache の起動時に二度呼ばれるので，ダミーの */
        /* user_data を使って一度目では apr_global_mutex_t を生成しない */
        /* ようにする． */
        apr_pool_userdata_get(&user_data, USER_DATA_KEY, s->process->pool);
        if (user_data == NULL) {
            apr_pool_userdata_set((const void *)(1), USER_DATA_KEY,
                                  apr_pool_cleanup_null, s->process->pool);
            return OK;
        }

        do {
            config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

            status = apr_global_mutex_create(&(config->glock), config->glock_path,
                                             APR_LOCK_DEFAULT, pconf);
            if (status != APR_SUCCESS) {
                return HTTP_INTERNAL_SERVER_ERROR;
            }
        } while ((s = s->next) != NULL);

    #ifdef AP_NEED_SET_MUTEX_PERMS
        status = unixd_set_global_mutex_perms(config->glock);
        if (status != APR_SUCCESS) {
            return HTTP_INTERNAL_SERVER_ERROR;
        }
    #endif

        return OK;
    }

    static void child_init(apr_pool_t *p, server_rec *s)
    {
        sconfig *config;

        do {
            config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

            if (apr_global_mutex_child_init(&(config->glock),
                                            config->glock_path, p) != APR_SUCCESS) {
                SERROR("Can not attach to global mutex (%s).", config->glock_path);

                return;
            }
        } while ((s = s->next) != NULL);
    }

    static void register_hooks(apr_pool_t *pool)
    {
        ap_hook_post_config(post_config, NULL, NULL, APR_HOOK_REALLY_FIRST);
        ap_hook_child_init(child_init, NULL, NULL, APR_HOOK_REALLY_FIRST);
        /* (略)*/
    }

    module AP_MODULE_DECLARE_DATA uploader_module = {
        STANDARD20_MODULE_STUFF,
        NULL,
        NULL,
        create_server_config,
        NULL,
        NULL,
        register_hooks
    };

## apr_thread_mutex_t の使い方

次のようにして使用します．

1. 初期化
   - apr_thread_mutex_create を使っ て apr_thread_mutex_t を生成．
2. クリティカルセクションの開始
   - apr_thread_mutex_lock または apr_thread_mutex_trylock を使ってロック．
3. クリティカルセクションの終了
   - apr_thread_mutex_unlock を使ってアンロック．
4. 後始末
   - apr_thread_mutex_destroy を使っ て apr_thread_mutex_t のリソースを解放．

環境によってはスレッドを扱えない場合があるので，マク ロ APR_HAS_THREADS が 0 以外の時だ け apr_thread_mutex_t を使うようにすると良いです．

    #include "apr.h"
    #if APR_HAS_THREADS
    #include "apr_thread_mutex.h"
    #endif

    /* (略)*/

    apr_status_t lock()
    {
    #if APR_HAS_THREADS
        return apr_thread_mutex_lock(mutex);
    #else
        return APR_SUCCESS;
    #endif
    }

使用に関しては特につまずく点は無いと思います．apr_thread_mutex.h の ドキュメントを参照してください．

## apr_thread_rwlock_t の使い方

変更はあまりされないけど，頻繁に参照されるデータの保護に使用します． ロックの方法は増えてますが，基本的な使用方法 は apr_thread_mutex_t と同じです．

# 共有メモリ

他のプロセスとメモリの共有を行うには apr_shm_t を利用します．実際に 動くコンパクトなサンプルとしては， mod_shm_counter がお薦めです．

## apr_shm_t の使い方

基本的には，次の手順で使用します．

1. post_config ステージ
   - apr_shm_create を使って apr_shm_t を生成．
   - apr_shm_baseaddr_get を使ってプロセスが実際に読み書きするアドレスを設定．
2. child_init ステージ
   - 必要に応じて apr_shm_attach を使っ て apr_shm_t に attach．
   - apr_shm_baseaddr_get を使ってプロセスが実際に読み書きするアドレスを設定．

モジュールのコードは次のようになります．

    #include "apr_shm.h"
    typedef struct ServerConfig {
        char *shm_path;
        /* 共有メモリ */
        apr_shm_t *shm_data;
        /* プロセスが実際に読み書きするメモリ */
        char *data;
    } sconfig;

    static void *create_server_config(apr_pool_t *p, server_rec *s)
    {
        sconfig *config;

        config = (sconfig *)(apr_palloc(p, sizeof(sconfig)));

        /* ここで指定されるファイルは，必要に応じて自動的に生成されます． */
        /* あらかじめ作成しておく必要はありません． */
        config->shm_path = "/path/to/shm/file";
        config->data_shm = NULL;

        return config;
    }

    static int post_config(apr_pool_t *pconf, apr_pool_t *plog,
                           apr_pool_t *ptemp, server_rec *s)
    {
        sconfig *config;
        void *user_data;
        apr_status_t status;

        /* post_config は Apache の起動時に二度呼ばれるので，ダミーの */
        /* user_data を使って一度目では apr_shm_t を生成しない */
        /* ようにする． */
        apr_pool_userdata_get(&user_data, USER_DATA_KEY, s->process->pool);
        if (user_data == NULL) {
            apr_pool_userdata_set((const void *)(1), USER_DATA_KEY,
                                  apr_pool_cleanup_null, s->process->pool);
            return OK;
        }

        do {
            config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

            /* 1024 バイトの共有メモリを設定 */
            status = apr_shm_create(&(config->shm_data), sizeof(char)*1024,
                                    config->shm_path, pconf);
            if (status != APR_SUCCESS) {
                return HTTP_INTERNAL_SERVER_ERROR;
            }

            config->data = (char *)(apr_shm_baseaddr_get(config->shm_data);

            /* 必要に応じて初期化 */
            memset(config->shm_data, 0, sizeof(char)*1024);
        } while ((s = s->next) != NULL);

        return OK;
    }

    static void child_init(apr_pool_t *p, server_rec *s)
    {
        sconfig *config;

        do {
            config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

            /* この条件が成り立つときのみ attach する． */
            if (!config->data_shm) {
                if (apr_shm_attach(&(config->data_shm), config->shm_path,
                                   p) != APR_SUCCESS) {
                    return;
                }
            }

            config->data = (char *)(apr_shm_baseaddr_get(config->data_shm));
        } while ((s = s->next) != NULL);
    }

# ファイル入出力

ファイル入出力がらみの注意点について説明します．

## バッファリング

APR は，デフォルトでは apr_file_write, apr_file_read 等での入出力の際にバッファリン グを行いません．次のように，apr_file_open の際に， APR_BUFFERED フラグを立ててやることでバッファ リングするようになります．

    apr_file_t *file;
    const char *file_path;
    apr_pool_t *pool;

    apr_file_open(&file, file_name, APR_READ|APR_BUFFERED, APR_OS_DEFAULT, pool);

## mmap

mmap を使ってファイルへの書き込みを行う場合， apr_mmap_create のフラグに は APR_MMAP_WRITE だけでな く APR_MMAP_READ を指定する必要があります． APR_MMAP_WRITE だけの場合，一部の環境で正常 に動作しません．

また，apr_file_open の際に APR_BUFFERED フラグを立ててた開いたファイル は mmap を使って読み書きできません．apr_mmap_create を呼ぶ と APR_EBADF が返されて失敗します．

# 書式指定

Apache および APR には，stdio.h や stdarg.h の *printf 関数の代わり に使える次の関数が用意されています．

- apr_pvsprintf(apr_pool_t *p, const char *fmt, va_list ap)
- apr_psprintf(apr_pool_t *p, const char *fmt, ...)
- apr_snprintf(char *buf, apr_size_t len, const char *format, ...)
- apr_vsnprintf(char *buf, apr_size_t len, const char *format, va_list ap)
- ap_vrprintf(request_rec *r, const char *fmt, va_list vlist)
- ap_rprintf(request_rec *r, const char *fmt, ...)

これらに渡す書式指定 format には，通常の *printf 関数と同じものが使 えますが，数値に関しては注意が必要です．通常，数値に対しては「%d」や 「%lu」等の変換指定を用いますが，多くのアーキテクチャで動作させるこ とを考えた場合，これはあまり望ましくありません．あるアーキテクチャで 正常に動いていても別のアーキテクチャでは，違った値が出力されなかった り，不正なメモリアクセスが発生したりします． APR では，この問題を解決するために apr.h で次のような変換指定文字マ クロが定義されています．

- APR_SIZE_T_FMT
- APR_OFF_T_FMT
- APR_PID_T_FMT
- APR_INT64_T_FMT
- APR_UINT64_T_FMT
- APR_UINT64_T_HEX_FMT

これらを用いることで，特定のアーキテクチャに依存しない安全なコードに することができます． たとえば， apr_size_t， apr_off_t や apt_time_t を出力する際は，次のようにし ます．

    apr_size_t size;
    apr_off_t offset;
    apr_time_t time;
    const char *str;

    /* (略)*/

    str = apr_psprintf(pool,
                       "size = %" APR_SIZE_T_FMT ", "
                       "offset = %" APR_OFF_T_FMT ", "
                       "time = %" APR_UINT64_T_FMT,
                       size, offset, time);

# リクエストの処理

Apache 2.x のコンテンツハンドラで POST されたデータを読み込むには次 の二つの方法があります．

- apr_brigade_create, ap_get_brigade, apr_bucket_read を利用
- ap_setup_client_block, ap_should_client_block, ap_get_client_block を利用

原因は調べ切れていないのですが，後者の方法を使った場合，POST された データのサイズに比例した(大体 1/40)のメモリをApache 内部で消費してし まうようです．巨大なデータを扱うモジュールを作成する場合は注意してく ださい．

# C++

Apche のモジュールを C++ で開発する場合，いくつか注意点があります．基本的な物については， Apache API C++ Cookbook を参照してください．Apache 1.x 向けに書かれた物ですが， ほとんどの項目はそのまま Apache 2.x にも使えます． 以下では，その他の注意点について説明します．

## APR_INT64_C

C++ でモジュールを書いていて，「INT64_C が未定義です．」みたいなエラー が出た場合は，マク ロ __STDC_CONSTANT_MACROS を define してや りましょう．

参考: /usr/include/stdint.h

    /* The ISO C99 standard specifies that in C++ implementations these
       should only be defined if explicitly requested.  */
    #if !defined __cplusplus || defined __STDC_CONSTANT_MACROS
    ... (INT64_C の define)
    #endif

# デバッグ

Apache のモジュールを開発する際の便利なデバッグ方法を紹介します．

## Apache の Debug ビルド

モジュールを開発する場合，APR Pool のデバッグ機能を有効にした Apache を用いることで，メモリ関連のバグを早い段階で見つけることがで きます．Unix の場合は次のようにすることで Debug ビルドが行えます． (参考: APR_POOL_DEBUG is functioning again WAS: RE: Don't use APR_POOL_DEBUG )

    $ CPPFLAGS=-DAPR_POOL_DEBUG ./configure ...
    $ make

WIndows の場合は，次のようにします． (参考: Compiling Apache for Microsoft Windows )

    > vsvar32.bat
    > nmake /f Makefile.win _apached

## UNIX でのデバッグ

メモリ関連のバグの場合，デバッグオプション(-g) をつけてコンパイルし， Valgrind 上で Apache を実行するのが手軽です．Apache にデバッグモード (-X) を指定するのがポイントです．

    $ valgrind -v --leak-check=yes --show-reachable=yes --tool=memcheck /usr/sbin/apache2 -X -f /path/to/httpd.conf 2>&1 | tee valgrind.log

Apache をデバッグモードで実行した場合，標準出力への出力がコンソール に表示されるので，いつもの(?) printf デバッグも行えます．

## WIndows でのデバッグ

普通の Windows アプリケーションの場合と同様，デバッグオプション(/Od /MDd /GS /RTCsu /Zi) をつけてコンパイルして，VisualStudio を使うのが お薦めです．

# 参考文献

モジュールの開発に役立つ文献を紹介します．

Apacheモジュール プログラミングガイド
  Apache のモジュール作成に必要になる事柄を一通り説明した本です．痒い ところに手が届いているので，手元に置いておくと重宝します．
Advanced Topics in Module Design: Threadsafety and Portability
  Apache2 でモジュールを作成するときに必要になってくるテクニックが解 説されたパワポです．そんなに長くないので，モジュール書く前にさらっと読 んでおきましょう．
Apache API C++ Cookbook
  C++ を使って Apache のモジュールを作成する際の注意事項について説明 したサイトです．
libapr(apache portable runtime) programming tutorial
  APR のチュートリアルです．サンプルコードおよび，間違いやすい点につ いての記述が多いので重宝します．