MySQL Native Driverプラグイン のアーキテクチャ

このセクションでは、mysqlndプラグイン のアーキテクチャについての概要を示します。

MySQL Native Driver の概要

mysqlndプラグイン を開発する前に、mysqlnd そのものがどのように成り立っているのかを少し知っておくと役に立ちます。mysqlnd は次に示すモジュールからなります:

mysqlnd のモジュール毎のソースコードの組み合わせを示した表
モジュールの統計情報 mysqlnd_statistics.c
データベース接続 mysqlnd.c
結果セット mysqlnd_result.c
結果セットのメタデータ mysqlnd_result_meta.c
プリペアドステートメント mysqlnd_ps.c
ネットワーク mysqlnd_net.c
Wire protocol mysqlnd_wireprotocol.c

C言語のオブジェクト指向パラダイム

ソースコードレベルで、mysqlnd は オブジェクト指向を実装するためのパターンを採用しています。

C言語では、オブジェクトを表現するために struct(構造体) を使います。struct のメンバがオブジェクトのプロパティを表現します。関数を指している struct のメンバがメソッドを表現します。

C++ や Java のような言語と異なり、C言語におけるオブジェクト指向のパラダイムでは決まった継承のルールがありませんが、従う必要があるルールがいくつかあります。このルールについては後に述べます。

PHP のライフサイクル

PHP のライフサイクルを考えるとき、ふたつの基本的なサイクルが存在します。

  • PHPエンジンの起動と終了までのサイクル

  • リクエスト毎のライフサイクル

PHPエンジンが起動するとき、PHP はモジュールを初期化する (MINIT) 関数を登録されたエクステンションごとに呼び出します。これによって、各々のモジュールが PHPエンジンが処理を行うライフサイクルの間存在するリソースを割り当てたり、変数を定義することができます。PHPエンジンが終了するときには、 エンジンが終了(MSHUTDOWN)関数をエクステンション毎に呼び出します。

PHPエンジンが起動している間、エンジンはたくさんのリクエストを受けとります。それぞれのリクエストは別のライフサイクルを構成します。リクエスト毎にPHPエンジンはリクエストの初期化関数をエクステンション毎に呼び出します。エクステンション側では、リクエストの処理に必要な変数の定義やリソースの割り当てを行うことができます。リクエストのサイクルが終了するときは、PHPエンジンがリクエストの終了(RSHUTDOWN)関数をエクステンション毎に呼び出します。これによって、エクステンションは必要となるあらゆるクリーンアップ処理を行うことができます。

プラグインはどうやって動くか

mysqlndプラグイン は mysqlnd を使うエクステンションが mysqlnd を呼び出すときの制御を奪うことによって動作します。これは mysqlnd の関数テーブルを取得し、バックアップし、カスタムの関数テーブルと置き換えることによって実現されます。この関数テーブルが、プラグインが必要とする関数を呼び出すのです。

次のコードは、mysqlnd の関数テーブルを置き換える方法を示しています:

/* a place to store original function table */
struct st_mysqlnd_conn_methods org_methods;

void minit_register_hooks(TSRMLS_D) {
  /* active function table */
  struct st_mysqlnd_conn_methods * current_methods
    = mysqlnd_conn_get_methods();

  /* backup original function table */
  memcpy(&org_methods, current_methods,
    sizeof(struct st_mysqlnd_conn_methods);

  /* install new methods */
  current_methods->query = MYSQLND_METHOD(my_conn_class, query);
}

接続関数テーブルの管理は、モジュールを初期化(MINIT)している間に行わなければなりません。関数テーブルはグローバルに共有されるリソースです。マルチスレッド環境で、TSRMを有効にしてPHPをビルドした環境では、グローバルに共有されたリソースをリクエストを処理している間に操作すると、ほぼ確実に衝突が起こります。

注意:

mysqlnd の関数テーブルを管理するときに、固定サイズを割り当てるロジックは絶対に使わないでください。新しいメソッドが関数テーブルの最後に追加される可能性があるからです。関数テーブルは将来どんな場合でも変更される可能性があります。

親クラスのメソッドを呼び出す

オリジナルの関数テーブルをバックアップしている場合、オリジナルの関数テーブルのエントリに含まれる関数を呼び出すことができます - これが親メソッドです。

場合によっては、Connection::stmt_init() のように、派生メソッドで他のあらゆる処理より先に親メソッドを呼び出すことが決定的に重要な場合があります。

MYSQLND_METHOD(my_conn_class, query)(MYSQLND *conn,
  const char *query, unsigned int query_len TSRMLS_DC) {

  php_printf("my_conn_class::query(query = %s)\n", query);

  query = "SELECT 'query rewritten' FROM DUAL";
  query_len = strlen(query);

  return org_methods.query(conn, query, query_len); /* return with call to parent */
}

プロパティを拡張する

mysqlndオブジェクトは C言語の構造体で表現されます。実行時に、C言語の構造体に新たにメンバを追加することは不可能です。mysqlndオブジェクト のユーザーは、プロパティを単純にオブジェクトに追加することはできません。

mysqlnd_plugin_get_plugin_<object>_data()ファミリーの適切な関数を使って、任意のデータ (プロパティ) を mysqlnd オブジェクトに追加することができます。オブジェクトをメモリに割り付ける際に、mysqlnd はオブジェクトの最後に 任意のデータ向けの void * ポインタを保持するためのメモリ空間を予約しておきます。 mysqlnd は プラグインひとつにつき、ひとつの void *ポインタ を保持する空間を予約しています。

次の表で、特定のプラグインでポインタの位置を計算する方法を示します:

mysqlnd のポインタの位置を計算する方法
メモリアドレス メモリの内容
0 mysqlndオブジェクト を表現する構造体の開始
n mysqlndオブジェクト を表現する構造体の終了
n + (m x sizeof(void*)) m 番目のプラグインのオブジェクトデータを表現する void* ポインタ

mysqlndオブジェクト のコンストラクタを継承する計画がある場合、それが許可されていることを必ず頭にいれておいてください!

次のコードはプロパティを拡張する方法を示しています:

/* any data we want to associate */
typedef struct my_conn_properties {
  unsigned long query_counter;
} MY_CONN_PROPERTIES;

/* plugin id */
unsigned int my_plugin_id;

void minit_register_hooks(TSRMLS_D) {
  /* obtain unique plugin ID */
  my_plugin_id = mysqlnd_plugin_register();
  /* snip - see Extending Connection: methods */
}

static MY_CONN_PROPERTIES** get_conn_properties(const MYSQLND *conn TSRMLS_DC) {
  MY_CONN_PROPERTIES** props;
  props = (MY_CONN_PROPERTIES**)mysqlnd_plugin_get_plugin_connection_data(
    conn, my_plugin_id);
  if (!props || !(*props)) {
    *props = mnd_pecalloc(1, sizeof(MY_CONN_PROPERTIES), conn->persistent);
    (*props)->query_counter = 0;
  }
  return props;
}

プラグインの開発者には、プラグイン用のデータに使われるメモリを管理する責任があります。

mysqlnd のメモリアロケータを使うことを推奨します。これらのメモリアロケータ関数は次のような規約に従って命名されています: mnd_*loc() mysqlnd のメモリアロケーターには役に立つ機能がいくつかあります。たとえばデバッグビルドでない環境でデバッグ用のアロケータを使う機能などです。

いつ、どのように継承するか
  いつ継承するか? 各々のインスタンスが自分のプライベートな関数テーブルを持っているか? どのように継承するか?
Connection (MYSQLND) MINIT No mysqlnd_conn_get_methods()
Resultset (MYSQLND_RES) MINIT (モジュール初期化) 時 またはその後 Yes mysqlnd_result_get_methods() または、オブジェクトのメソッド関数テーブルを変更する
Resultset Meta (MYSQLND_RES_METADATA) MINIT (モジュール初期化) 時 No mysqlnd_result_metadata_get_methods()
Statement (MYSQLND_STMT) MINIT (モジュール初期化) 時 No mysqlnd_stmt_get_methods()
Network (MYSQLND_NET) MINIT (モジュール初期化) 時 またはその後 Yes mysqlnd_net_get_methods() または、オブジェクトのメソッド関数テーブルを変更する
Wire protocol (MYSQLND_PROTOCOL) MINIT (モジュール初期化) 時 またはその後 Yes mysqlnd_protocol_get_methods() または、オブジェクトのメソッド関数テーブルを変更する

上記の表で許可されていない場合は、モジュールを初期化した(MINIT)後のいかなる場合であっても関数テーブルを変更してはいけません。

クラスによっては、メソッドの関数テーブルへのポインタが含まれている場合があります。このようなクラスのインスタンスはすべて、同じ関数テーブルを共有しています。混乱を避けるため、特にマルチスレッドの環境下では、このような関数テーブルは MINIT (モジュール初期化) 時にだけ変更するようにしてください。

そうでないクラスでは、グローバルに共有された関数テーブルのコピーを使っています。クラスの関数テーブルのコピーがオブジェクトとともに作成されます。それぞれのオブジェクトは自分の関数テーブルを使います。これによって開発者は二つの選択肢が得られます: MINIT(モジュール初期化) 時にオブジェクトのデフォルトの関数テーブルを変更するか、同じクラスの他のインスタンスに影響を与えることなくオブジェクトのメソッドを追加で変更することができます。

関数テーブルを共有する利点は、パフォーマンスの向上です。関数テーブルをそれぞれの、すべてのオブジェクトにコピーする必要がないからです。

コンストラクタの状態
  メモリ割り当て、オブジェクトの生成、リセット 変更可能か? 呼び出し元
Connection (MYSQLND) mysqlnd_init() No mysqlnd_connect()
Resultset(MYSQLND_RES)

メモリ割り当て:

  • Connection::result_init()

リセットし、再初期化されるタイミング:

  • Result::use_result()

  • Result::store_result

Yes, ただし親メソッドを呼び出すこと!
  • Connection::list_fields()

  • Statement::get_result()

  • Statement::prepare() (メタデータのみ)

  • Statement::resultMetaData()

Resultset Meta (MYSQLND_RES_METADATA) Connection::result_meta_init() Yes, ただし親メソッドを呼び出すこと! Result::read_result_metadata()
Statement (MYSQLND_STMT) Connection::stmt_init() Yes, ただし親メソッドを呼び出すこと! Connection::stmt_init()
Network (MYSQLND_NET) mysqlnd_net_init() No Connection::init()
Wire protocol (MYSQLND_PROTOCOL) mysqlnd_protocol_init() No Connection::init()

コンストラクタを全面的に置き換えないことを強く推奨します。コンストラクタはメモリへの割り当てを実行します。mysqlndプラグインAPI と オブジェクトのロジックにとってメモリへの割り当ては決定的に重要です。開発者が警告を無視してコンストラクタへのフックを強行する場合、コンストラクタで何かをする前に親のコンストラクタを少なくとも呼び出すべきです。

すべての警告に関わらず、コンストラクタを継承することが役に立つ場合があります。コンストラクタは、オブジェクトの関数テーブルを共有されていないオブジェクトの関数テーブルと一緒に修正するのにぴったりな場所です。共有されていないオブジェクトの関数テーブルの例としては、結果セットやネットワーク、wire protocol が挙げられます。

オブジェクトの破棄に関する状態
Type 継承したメソッドは親メソッドを呼ばねばならないか? デストラクタ
Connection Yes, メソッドの実行後に呼び出さなければなりません free_contents(), end_psession()
Resultset Yes, メソッドの実行後に呼び出さなければなりません free_result()
Resultset Meta Yes, メソッドの実行後に呼び出さなければなりません free()
Statement Yes, メソッドの実行後に呼び出さなければなりません dtor(), free_stmt_content()
Network Yes, メソッドの実行後に呼び出さなければなりません free()
Wire protocol Yes, メソッドの実行後に呼び出さなければなりません free()

デストラクタは、mysqlnd_plugin_get_plugin_<object>_data() で取得したプロパティを破棄するのに適切な場所です。

ここで挙げたデストラクタは、オブジェクトそのものを破棄する 実際の mysqlnd メソッドと一致しないかもしれません。しかし、これらのデストラクタは、開発者がフックし、プラグインデータを解放する最良の場所なのです。コンストラクタに関しては、開発者がメソッドを完全に置き換えることができるものの、推奨されません。上の表に複数のメソッドが示されていた場合、mysqlnd がどのメソッドをはじめに呼び出したかに関わらず、開発者はここで示されているすべてのメソッドをフックし、プラグインのデータを解放する必要があります。

プラグインに推奨されるやり方は、単純にメソッドをフックし、メモリを解放した後に親クラスの実装を速やかに呼び出すことです。

警告

PHP 5.3.0 から 5.3.3 までは、PHPに存在するバグによって、プラグインはプラグイン固有のデータを持続的接続に結びつけることができませんでした。これは、ext/mysqlext/mysqlimysqlnd で必要な end_psession() メソッドを呼び出していなかったため、プラグインがメモリリークしていた可能性があったためでした。このバグは PHP 5.3.4 で修正されています。