TL;DR
- APR DBDを使うApacheモジュールをC++で書いた
- Apache起動時にリンクエラー発生!
- 直した
APR DBDってなんだ
APR(Apache Portable Runtime)というApacheのサポートライブラリがあります。もともとはApacheの一部でしたが独立したプロジェクトとして便利関数群を抽出したものみたいです。
APRは大きく3種類に分けられています。
- 基本的な関数をまとめたAPR
- 便利関数をまとめたAPR-util
- 文字コードを扱うためのAPR-iconv
APR DBDは、APR-utilに含まれる、各種RDBMSへの接続や操作を共通化されたAPIから利用できるようにするモジュールです。このAPIを使うことでMySQLとかPostgreSQLとかを意識せずにDB操作できるようになるわけですね。
このAPR DBDを使い、データベースを操作するApacheモジュールを書いてみようと思いました。C++で。
DockerでMySQLを動かす
前置きが長かったですが、本題はまだです。データベース操作をするためにはデータベースが必要です。
手っ取り早くDockerでMySQLサーバを動かしましょう。以下のコマンドでさくっとサーバを立てて接続できます。パスワードの入力を求められるのでpassword
と入力します。
$ sudo docker run --name mysqld -e MYSQL_ROOT_PASSWORD=password -d -p 13306:3306 mysql
$ sudo docker exec -it mysqld mysql -u root -p
なお、MySQLコマンドラインツールが使えるなら以下のやり方でも接続できます。
$ mysql -u root -h 127.0.0.1 -P 13306 -p
ポートフォワードを使って13306->3306に転送されるようにしたのはこのためです(私が試した時は、ホストでmysqlが動ているとそっちにつなぎに行っちゃってました。もしかしたら勘違いかもしれないのでよく調べます)。
接続できたら、データベースとテーブルを作って何かをINSERTしておきます。
mysql> CREATE DATABASE testdb DEFAULT CHARACTER SET utf8;
mysql> USE testdb;
mysql> CREATE TABLE test (id INTEGER AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255), body VARCHAR(255)) DEFAULT CHARSET=utf8;
mysql> INSERT INTO test(title, body) VALUES('aaa', 'bbb');
mysql> INSERT INTO test(title, body) VALUES('hoge', 'fuga');
これでデータベースが用意出来ました。
Apacheモジュールを作る
それではこのMySQLサーバのtestdbに接続し、SELECT * FROM test
の結果を表示するようなApacheモジュールを書いてみます。
ぐぐって色々調べながら最初に書いたコードはこちらです。
https://gist.github.com/d9magai/763f936c4cbbabd08986ef387bcce8c5
下のようなconfを用意して/etc/httpd/conf.d
とかに置いておきます。curl localhost/condb
とコマンドを打てばモジュールの実行結果が見れるようにします。
LoadModule condb_module /usr/lib64/httpd/modules/mod_condb.so
<location /condb>
SetHandler condb
</Location>
DBDriver mysql
DBDParams "host=127.0.0.1,user=root,pass=password,dbname=testdb,port=13306,sock=/tmp/mysql.sock"
DBDPersist ON
DBDKeep 5
DBDMax 5
DBDMin 2
DBDExptime 100
準備ができたらモジュールをコンパイルしてインストールします。
$ g++ -c -fPIC -I`apxs -q INCLUDEDIR` -I/usr/include/apr-1/ `apxs -q CFLAGS` `apxs -q CFLAGS_SHLIB` -Wall -o mod_condb.o mod_condb.cpp
$ g++ -fPIC -shared -o mod_condb.so mod_condb.o `apxs -q LIBS_SHLIB`
$ sudo apxs -A -i -a -n 'condb' mod_condb.so
さて、満を持してApacheを起動するとエラーがでて動かないと思います。
httpd: Syntax error on line 360 of /etc/httpd/conf/httpd.conf: Syntax error on line 1 of /etc/httpd/conf.d/mod_condb.conf:Cannot load /usr/lib64/httpd/modules/mod_condb.so into server: /usr/lib64/httpd/modules/mod_condb.so: undefined symbol: _Z14ap_dbd_acquireP11request_rec
undefined symbol: _Z14ap_dbd_acquireP11request_rec
というのがエラーの原因ですね。詳しく調べてみました。
名前マングリングとシンボル名
シンボルが見つからないというエラーですが、そもそもシンボルってなんでしょう。
シンボルの説明の前に、C/C++のコードが実行可能になるまでの処理の流れを調べたので説明します。大きく分けて以下の4つになるみたいです。
- プリプロセス(
gcc -E main.c
でプリプロセス後のソースを見れる) - コンパイル(
gcc -S main.c
でコンパイル直後のアセンブラソースに変換した段階で処理を止める。.s
ファイルができる) - アセンブル(
as -o main.o main.s
アセンブラソースをアセンブルして、オブジェクトファイル(バイナリ)を生成する) - リンク(
gcc -o main main.o
オブジェクトファイルを実行可能にする)
アセンブルとリンクの間に生成されファイルがオブジェクトファイルです。CやC++において関数名は、オブジェクトファイルの中ではシンボル名として扱われています。
ちょっと実験をしてみます。オブジェクトファイルはバイナリといいましたが、中を覗けるコマンドがあります。nmです。
オブジェクトファイルを覗いてみる
実験に使うコードはこちらです。
#include<stdio.h>
void hoge() {
printf("Hello World\n");
}
int main(void) {
hoge();
return 0;
}
先ずはgccを使ってオブジェクトファイルを作った時です。
bash-4.2# gcc -c main.c -o main.o
bash-4.2# nm main.o
0000000000000000 T hoge
0000000000000010 T main
U puts
hoge
とmain
というシンボル名ができているのがわかります。関数名がそのままシンボル名になっています。わかりやすいですね。
次にg++を使ってみます。
bash-4.2# g++ -c main.c -o main.o
bash-4.2# nm main.o
0000000000000000 T _Z4hogev
0000000000000010 T main
U puts
main
は分かりますが_Z4hogev
という感じで、hoge
になんだか変な文字がくっついてごちゃごちゃしています。
どうしてシンボル名がごちゃごちゃしてる?
これは、C++には関数のオーバーロードや名前空間、クラスのメンバ関数という概念があることと関係しています。C++はCと異り、同じ名前を持つ関数が複数存在することが有り得ます。関数名をシンボル名に使ってしまっては衝突してしまいます。
C++コンパイラはそういった付加情報をシンボル名に埋め込み、リンク時に適切な関数と結合できるようにしています。このような処理を名前マングリング(name mangling)と言います。
もう一度実験してみます。今度は次のようなコードです。
#include<stdio.h>
void hoge() {
printf("Hello World\n");
}
void hoge(int n) {
printf("Hello World\n");
}
int main(void){
hoge();
return 0;
}
引数の違うhoge
関数が2つあります。さて、オブジェクトファイルを作って中身を覗いてみましょう。
bash-4.2# g++ -c main.c -o main.o
bash-4.2# nm main.o
0000000000000010 T _Z4hogei
0000000000000000 T _Z4hogev
0000000000000027 T main
U puts
2つの異なるシンボル名があるのが確認できました。
extern "C"を使って回避
名前マングリングの解説をしてきました。改めて問題のエラーを見返してみます。
undefined symbol: _Z14ap_dbd_acquireP11request_rec
このエラーは、ap_dbd_acquire()
と関数をコールしている箇所をC++コンパイラが名前マングリングでシンボル名に付加情報を埋め込み、Cコンパイラでコンパイルしたライブラリをダイナミックリンクをしようしたら_Z14ap_dbd_acquireP11request_rec
というシンボルは見つからなかった、という意味になるかと思います。
Cコンパイラによってap_dbd_acquire
という感じの素直なシンボル名で定義されているにも関わらず、モジュールは_Z14ap_dbd_acquireP11request_rec
という複雑なシンボル名を期待して、undefined symbol
と言ってるわけですね。
Cで書いた関数をC++から呼びたい、もしくはその逆の場合、C++コンパイラに対して名前マングリングせずにC言語と同じく関数名をそのままシンボル名にしてほしい、ということを教えなければなりません。そこでextern "C"
の出番です。これは名前マングリングを回避したいのでC言語として解釈して下さい、とg++に伝えたい時に使われます。
問題のap_dbd_acquire
という関数はmod_dbd.h
で宣言されていました。
https://ci.apache.org/projects/httpd/trunk/doxygen/mod__dbd_8h_source.html
ですので、#include <mod_dbd.h>
をextern "C"
で囲んであげます。
extern "C"{
#include <mod_dbd.h>
}
こうすることでap_dbd_acquire
はマングルしなくなるため、モジュールはap_dbd_acquire
というシンボルを探すようになります。
名前マングリングを回避したら動いた
これでApacheを実行してもリンクエラーは発生しなくなりました。動作確認しますと、ちゃんとDB操作できていました。
$ curl localhost/condb
1,aaa,bbb,
2,hoge,fuga,
The sample page from mod_condb.c
Dockerで再現できるようにした
もしも、自分のとこでも動かしてみたい、と思われた方が居た時に簡単に試せるようにDockerで再現できるようにしてみました。
https://github.com/d9magai/docker-mod_condb
./run.sh
を実行しますとデータベース作成からモジュールをビルドしてApacheを起動するところまで動きます。