はじめに
本エントリは MySQL Advent Calendar 2025 の 9 日目の記事です。
昨日は @HrsUed さんの wait/io/table/sql/handlerの正体 でした。
本エントリのあらすじとしては、libmysqlclient を用いたデータベースドライバの実装・処理の流れを追うことで MySQL C API を用いたライブラリの開発・使用の勘所を掴みたいというモチベーションのもと、Perl の代表的な MySQL ドライバである DBD::mysql の実行フローを追いながら理解を深めるぞ、というものになります。
いくつかのメソッドの実行フローを眺めようと思っていたのですが、quote だけでほどほどのボリュームになってしまったので、他のメソッドの動作については別のエントリでまた書こうと思います。
DBD::mysql とは?
Perl で MySQL データベースを操作するために使用されるデータベースドライバ。
DBI という Perl のデータベースインタフェースに準拠した実装を提供している。
登場人物紹介
mysql.xs
DBD::mysql の一部。
mysql.xs では、Perl の実装と C の実装とのインタフェースが定義されている。
ここより下の実装は Dynamic Loading によってメモリに読み込まれる (libmysqlclient は Dynamic Linker によって解決される)。
libmysqlclient
C 言語で記述されたクライアントアプリケーションが MySQL Server と通信するために使用される C ベースの API の実装。
MySQL :: MySQL 8.4 C API Developer Guide :: 2 MySQL C API Implementations
quote メソッドの実行フローを追ってみる
DBD::mysql (が準拠する DBI) には $dbh->quote($value, $data_type) というメソッドが定義されている。
これは $value に含まれる特殊な文字をエスケープするなどして、SQL 文中に用いるリテラル値として使えるようにするために使用される。
undef が与えられた場合は "NULL" が出力される。
また、$data_type が与えられた場合は、その情報を基に $value に対して施すクオートの処理内容を決定する。
== Example == $dbh->quote("' OR 1=1 --") == Output == '\' OR 1=1 --'
処理は大まかに以下の図に示した流れで進む。
sequenceDiagram participant A as Perlアプリケーション participant B as mysql.pm participant C as mysql.xs participant D as dbdimp.c participant E as libmysqlclient A->>B: call activate B B->>C: dispatch deactivate B activate C C->>D: call deactivate C activate D D->>E: call deactivate D activate E E->>D: return deactivate E activate D D->>C: return deactivate D activate C C->>A: return deactivate C
ここからは、各ステップでどのような処理が行われているかを具体的に読み進めていく。
1. Perl アプリケーション
$dbh->quote() が呼び出される。
2. mysql.pm
mysql.pm に定義されている DBD::mysql::db パッケージには、DBI の要求する quote メソッドが定義されていない。
このとき、DBI は mysql.xs で定義されている C 言語実装の XSUB を動的に"ディスパッチ"する。
The DBI "dispatches" the method calls to the appropriate driver for actual execution. The DBI is also responsible for the dynamic loading of drivers, error checking and handling, providing default implementations for methods, and many other non-database specific duties.
Architecture of a DBI Application
3. mysql.xs
mysql.xs には quote という C 言語で書かれた関数が定義されており、DBI によって透過的に呼び出される*1。
https://github.com/perl5-dbi/DBD-mysql/blob/aead7bede40dd335412efe35f41d35e3144d01dd/mysql.xs#L413-L429
quote は Perl の引数 (dbh、str、type) を受け取り、非同期クエリの実行中チェックなどをした後に dbdimp.c のヘルパー関数 dbd_db_quote() を呼び出し、処理を委譲する。
4. dbdimp.c
mysql.xs から呼び出された dbd_db_quote は引数 (dbh、str、type) を受け取り、以下の順に処理を行なっていく。
https://github.com/perl5-dbi/DBD-mysql/blob/aead7bede40dd335412efe35f41d35e3144d01dd/dbdimp.c#L4586-L4642
a. 渡された文字列 str の有効性を確認し、undef である場合は "NULL" を返す
b. 型情報 type が与えられている場合はその型に対応する sql_type_info_t 構造体を取得し、数値型などのクオートすべきでない型であることが判明した場合は Nullsv を返して処理を切り上げる (この Nullsv は文字列としての "NULL" とは異なるので注意)
c. エスケープ後の文字列を格納するための新しい Perl スカラ値を作成し、メモリを割り当てる
メモリは 元の文字列の長さ * 2 + 3 の長さで割り当てられる。
なぜこうなっているのかと言うと、全ての文字がバックスラッシュでエスケープされる場合に最長で元の長さの 2 倍になる可能性があり、さらに開始の引用符 (')、終了の引用符 (') および C 言語において文字列の終端を表すヌル文字 (\0) が追加されるため最長で 元の文字列の長さ * 2 + 3 になるためだと思われる。
d. libmysqlclient ライブラリの mysql_real_escape_string_quote() を元の文字列を引数として渡した上で呼び出す
5. libmysqlclient
与えられた文字列にエスケープ処理を施し、エンコード済み文字列の長さを返す。
MySQL :: MySQL 8.4 C API Developer Guide :: 5.4.61 mysql_real_escape_string_quote()
6. dbdimp.c (戻り)
libmysqlclient によってエスケープが実行された文字列に対し*2、開始と終了の引用符を書き込みヌル文字 (\0) を追加したものを返す。