わかっているようであやふやな基礎をメモします。なんか変だったら教えてください。
分割コンパイルとライブラリ
Linux などの OS で動くプログラムは C とか C++ のような言語で書かれたソースコードをコンパイルして作られます(ここでは Android の話はしません)。C とか C++ で大規模なプログラムを作るときは、ソースコードが1ファイルで数万行などとすると扱いづらいので適当に分割します。C だったら *.c という名前、C++ だったら *.cc とか *.cpp とかいうファイルですね。
数百、数千ファイルのソースコードが作られる場合は、たぶん一人で作業しているのではないでしょう。巨大なプロジェクトの一部は完成済みのひとまとまりになっていて、それを利用して残りの部分を開発することになるでしょう。ライブラリとは、その「完成済みのひとまとまり」です。
例1:非分割
例があったほうがいいと思うのでむりやり作ります。三角関数表を作ろうと思ったとします。さらに、これは良い例ではありませんが、標準の sin(), cos() を知らないか使えない事情があったとします(面白そうだからやってみたかっただけ)。プログラムを分割しない場合はこんなふうですね。
#include <stdio.h>
#include <math.h> /* fmod(3M) のために */
#define PI 0x1.921fb54442d18p+1 /* 十六進指数表記 */
double
c_cos(double x)
{
double s = 1.0;
x = fmod(x, PI*2.);
if (x > PI) { x = PI - x; s = -1.0; }
if (x < -PI) { x = -PI - x; s = -1.0; }
double x2 = x * x;
/* 理科年表にも載っている式 */
return s*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2
/(14.*13.))/(12.*11.))/(10.*9.))/(8.*7.))/(6.*5.))/(4.*3.))/2.);
}
double
c_sin(double x)
{
double s = 1.;
/* sin のテイラー展開は精度が悪いのでcosに切り替える */
x = fmod(x - PI/2., PI*2.);
if (x > PI) { x = PI - x; s = -1.; }
if (x < -PI) { x = -PI - x; s = -1.; }
double x2 = x * x;
return s*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2
/(14.*13.))/(12.*11.))/(10.*9.))/(8.*7.))/(6.*5.))/(4.*3.))/2.);
}
int
main(int argc, const char **argv)
{
const double DEG2RAD = PI / 180.0;
for (int ideg = 0; ideg <= 90; ideg += 5) {
double x = DEG2RAD * ideg;
printf("%4i %10.6f %10.6f\n", ideg, c_sin(x), c_cos(x));
}
return 0;
}
例1(非分割)のコンパイル・リンク・実行
C コンパイラは gcc
を例にしますが、インテルコンパイラだったら icc
ですね。どれでもほぼ共通に、オプション -c
がない場合はコンパイル・リンク(後述)がまとめて行われ、オプション -o
で指定される名前(下の例では trigtab1, デフォルトつまり -o
を欠く場合は a.out
)の実行ファイルができます。
今回、標準ライブラリ関数 fmod() を使ったのでソースコードの #include <math.h>
やリンク時の -lm
が必要になっています。そのことは fmod() のドキュメント を読んで知ることができますが、とりあえず呪文と思ってください。
$ gcc -o trigtab1 trigtab1.c -lm
$ ./trigtab1
0 -0.000000 1.000000
5 0.087156 0.996195
10 0.173648 0.984808
15 0.258819 0.965926
20 0.342020 0.939693
25 0.422618 0.906308
30 0.500000 0.866025
35 0.573576 0.819152
40 0.642788 0.766044
45 0.707107 0.707107
50 0.766044 0.642788
55 0.819152 0.573576
60 0.866025 0.500000
65 0.906308 0.422618
70 0.939693 0.342020
75 0.965926 0.258819
80 0.984808 0.173648
85 0.996195 0.087156
90 1.000000 -0.000000
例2:分割コンパイルしてからリンク
さてソースコードを関数ごとに3つのファイルに分割してみます。関数 c_cos(), c_sin() 自体には変更がなく、共通で用いられる定数 PI
を mymath.h というファイルに移しています。
#include <math.h>
#include "mymath.h"
double
c_cos(double x)
{
double s = 1.0;
x = fmod(x, PI*2.);
if (x > PI) { x = PI - x; s = -1.0; }
if (x < -PI) { x = -PI - x; s = -1.0; }
double x2 = x * x;
return s*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2
/(14.*13.))/(12.*11.))/(10.*9.))/(8.*7.))/(6.*5.))/(4.*3.))/2.);
}
#include <math.h>
#include "mymath.h"
double
c_sin(double x)
{
double s = 1.;
x = fmod(x - PI/2., PI*2.);
if (x > PI) { x = PI - x; s = -1.; }
if (x < -PI) { x = -PI - x; s = -1.; }
double x2 = x * x;
return s*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2*(1.-x2
/(14.*13.))/(12.*11.))/(10.*9.))/(8.*7.))/(6.*5.))/(4.*3.))/2.);
}
さてこれら関数 c_cos(), c_sin() を使うメインロジックはこうなります。
#include <stdio.h>
#include "mymath.h"
int
main(int argc, const char **argv)
{
const double DEG2RAD = PI / 180.0;
for (int ideg = 0; ideg <= 90; ideg += 5) {
double x = DEG2RAD * ideg;
printf("%4i %10.6f %10.6f\n", ideg, c_sin(x), c_cos(x));
}
return 0;
}
trigtab.c では fmod() を使わなくなったので #include <math.h>
は不要になりました。ヘッダファイルの取り込み #include "mymath.h"
は定数 PI
のためと、関数の仕様を教えてもらうために必要になります。
#define PI 0x1.921fb54442d18p+1
extern double c_cos(double x);
extern double c_sin(double x);
上記のように、ヘッダファイル *.h には通例、各ソースファイル *.c で共通に用いられる定数や関数プロトタイプ宣言(引数や返却値の型を示すもの)などが書かれます。上記の例で関数プロトタイプ宣言を削ってしまうと、 trigtab.c をコンパイルしているときに c_cos() や c_sin() が何型を引数にして何型を返却値にするのかわからないので警告がでます。
例2のコンパイル・リンク
ソースコードを分割した場合は、それぞれをコンパイルだけする(cc
等の -c
オプションで処理する)と、オブジェクトファイル(名前は *.o)ができます。最後にオブジェクトファイルを全部集めてリンク(結合)すると実行ファイル(ここでは trigtab2)ができます。
# コンパイル
$ gcc -c c_cos.c # c_cos.o ができる
$ gcc -c c_sin.c # c_sin.o ができる
$ gcc -c trigtab.c # trigtab.o ができる
# リンク
$ gcc -o trigtab2 trigtab.o c_cos.o c_sin.o -lm
$ ./trigtab2
0 -0.000000 1.000000
5 0.087156 0.996195
(中略)
85 0.996195 0.087156
90 1.000000 -0.000000
多数のソースコードがある場合、最新の変更を加えたソースだけをコンパイルして、実行ファイルを作り直す(リビルド)すると速くできるわけです。手でやっていると間違えますから make などの道具を使いたいことになりますが、また別の機会に説明することにしましょう。
例3:いったんライブラリを作ってからリンクする
次は、オブジェクトファイルたち(c_cos.o, c_sin.o)をいったんひとまとめにライブラリとしてみましょう。
# コンパイル
$ gcc -c c_cos.c # c_cos.o ができる
$ gcc -c c_sin.c # c_sin.o ができる
# ライブラリ作成
$ ar rf libmymath.a c_cos.o c_sin.o # libmymath.a ができる
これでライブラリ libmymath.a ができます。これと mymath.h だけがあれば後の処理はできます。この例では2つのソースコードがひとまとめになりましたが、もっと多数のソースコードがある場合にはコマンドラインが短くなって助かります。
$ gcc -c trigtab.c # trigtab.o ができる
$ gcc -o trigtab3 trigtab.o -L. -lmymath -lm
$ ./trigtab3
(後略)
上の例のように、libなんとか.a をリンクするためには cc
等に -lなんとか
オプションを与えます。cc コマンドは libmymath.a がカレントディレクトリにあることを知らないので -L.
オプションで教えてあげています。
ここまで説明すると、今まで呪文であったリンク時の -lm
オプションは実は libm.a を探してリンクしているらしいということがわかります。興味があれば探してみるとよいですが、コンパイラごとにライブラリサーチパス(-L
を指定せずに探しに行けるディレクトリ群)が決まっていて、そこに libm.a(後述する libm.so しかないかもしれません)が置かれています。オプション -L
はそのサーチパスに追加するというものだったのでした。
(ついでにいうと、ヘッダファイル mymath.h が自分ではない作成者の管理下など、カレントディレクトリではない所にある場合、コンパイル時に -I
オプションでそのことを教えてやることで見つけられるようになります。コンパイルするときに必要なものですからリンク時に指定しても効果はありません)
例4:共有オブジェクトを作る
オブジェクトファイルには上記(例2~3)の *.o の他に共有オブジェクト *.so というのがあります。次のようにすると、実行ファイル trigtab4 ができて、同じように実行できるのですが、これまでとは違い実行ファイル trigtab4 の中には共有オブジェクト c_cos.so や c_sin.so の内容は含まれていません。実行時に c_cos.so や c_sin.so が探索されて、その中の関数コードが実行されます。これをダイナミックリンクと言います。
gcc -shared -fPIC -o c_cos.so c_cos.c # c_cos.so ができる
gcc -shared -fPIC -o c_sin.so c_sin.c # c_sin.so ができる
gcc -c trigtab.c # trigtab.o ができる
gcc -o trigtab4 trigtab.o -Wl,-rpath,'$ORIGIN' c_cos.so c_sin.so -lm
上の例で -Wl,-rpath,'$ORIGIN'
というけったいなオプションを与えていますが、これが trigtab4 に「共有オブジェクトは実行ファイルと同じディレクトリ($ORIGIN
)にありますよ」と教えてあげるものです。もっと難しいことがしたい場合は ld.so(8)のマニュアルぺージ を見てください。
プログラムを実行しなくても共有オブジェクトが揃っているかを確認することができるのが ldd コマンドです。次のようにフルパスが表示されればダイナミックリンクは可能です。
$ ldd trigtab4
linux-vdso.so.1 => (0x00007ffef43e0000)
c_cos.so => /var/www/html/2022/so/./c_cos.so (0x00007f390b849000)
c_sin.so => /var/www/html/2022/so/./c_sin.so (0x00007f390b647000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f390b334000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f390af69000)
/lib64/ld-linux-x86-64.so.2 (0x0000557663b45000)
共有オブジェクトが欠けているか、不適切な場所にある場合は、 c_cos.so => not found
などの表示になることで知られます。
上記例を見るからに、 libm.so.6 というものがダイナミックリンクされていることがわかります。C 標準の数学関数(fmod() や標準 cos(), sin() など)はその中にありますし、その他の C 標準の関数(printf() など)は libc.so.6 の中にあります。
例5:共有ライブラリを作る
ここまで見て来れば、libm.so.6 のようなものを自分で作りたくなりますね。次のようにします。
$ gcc -shared -fPIC -c -o c_sin.o c_sin.c
$ gcc -shared -fPIC -c -o c_cos.o c_cos.c
$ gcc -shared -fPIC -o libmymath.so c_sin.o c_cos.o
そうすると、利用側プログラムでは(パス情報指定 -Wl,... -L. をすれば) libm と同じ使用感で自作ライブラリ libmymath を動的リンクできます。
$ gcc -c trigtab.c
$ gcc -o trigtab5 trigtab.o -Wl,-rpath,'$$ORIGIN' -L. -lmymath -lm
なお、ここまで実行すると、カレントディレクトリに libmymath.so ができていると思いますが、その状態で上記の例3を再実行すると、libmymath.a は使われなくなってしまいます。ご注意。
コマンドラインオプションまとめ
gcc の -c
と -shared -fPIC
は綺麗に直交しているのでまとめておきます。
例 | -c | -shared -fPIC | 結果ファイル数 | 形式 |
---|---|---|---|---|
例1 | なし | なし | 1(リンクする) | 実行ファイル |
例2,3 | -c | なし | ソースと同数(リンクしない) | オブジェクト |
例5 | なし | -shared -fPIC | 1(リンクする) | 共有ライブラリ |
例4 | -c | -shared -fPIC | ソースと同数(リンクしない) | 共有オブジェクト |
まあ、実際問題としては、 make を使って更新されたソースだけコンパイルするようにするなら、 -c
はひとつのソースコードについてだけ実行するのが通例ですけど。
qiitaではmermaidでER図が描けるので、上記のファイル形式を流れにしてみるとこんなかんじですね。