C++の共有ライブラリの作成と利用

programing

C++の共有ライブラリの生成と利用に関して。備忘録。

OS : ubuntu 18.04
コンパイラ : g++ 7.4.0

共有ライブラリと静的ライブラリ

Linuxにおいて、C++のライブラリには静的ライブラリ(拡張子a)と共有ライブラリ(拡張子so)がある。

静的ライブラリは、ビルド時に組み込まれるので、静的ライブラリが存在しない場でも実行ファイルだけで正しく動く。一方、共有ライブラリは、ビルド時には組み込まれず、リンクされるのみで、実行時に共有ライブラリを探査して見つけたものを利用する。

そのため、共有ライブラリを利用した実行ファイルは利用している共有ライブラリが存在しない場では正しく動くことができず、また、共有ライブラリを実行時に探査される場に置く(または探査する場を指定して見つけられるようにする)必要がある。

共有ライブラリの作成

共有ライブラリの作成は、g++コマンドに-sharedオプションを付けてコンパイルする。このとき、さらにいくつか追加でオプションを渡す必要がある。

以下のようなcppファイルがあったとする。

`mylib.cpp’

#include<iostream>

int lib_func(){
    std::cout << "lib_func called" << std::endl;
}

これを、共有ライブラリにするには、以下のオプションと共にg++コマンドを使う。

-shared 共有ライブラリを生成するオプション
-fPIC PositionIndependentCodeなアセンブラのソースを生成するオプション※
-o 出力ファイル(この場合はライブラリファイル)の指示

結果、下記のようにする。

$ g++ -shared -fPIC ./myib.cpp -o libmine.so

このとき、-oオプションで指定するライブラリの名前は、lib + ライブラリ名 + .soとすること。これは決められたもので、sonameと呼ばれている。

以上で、libmine.soという共有ライブラリが生成されるはず。

[ -fPICオプションについて ]

fPICオプションは、位置独立実行形式(PositionIndependentCode)なアセンブラのソースを生成することを指定するオプション。これを付けずに -shared オプションのみで g++ を利用すると、 -fPIC を付けるように警告が出るはずだ。
そもそも、初期のPCではプログラムはメインメモリに配置される位置にプログラムが依存していたらしい。そのため、複数のプログラムを走らせるときは、使用しているメモリが重複しないように注意しながらプログラムを走らせる必要があった。
しかし、これだと大変なので、位置独立コードが発明された。この位置独立のコードは、共有オブジェクトを作成する場合に推奨されている。

共有ライブラリの利用

前項で作成した共有ライブラリを利用したい場合、下記のようにする。

main.cpp

int lib_func();

int main(){
    lib_func();
    return 0;
}

これを、g++でコンパイルする時に、ライブラリを利用することをコンパイラに知らせる。以下のようになる。

$ g++ -o main main.cpp -lmine -L./

ここで、-lオプションと-Lオプションを利用する。(実際には-oオプションも)

-lオプションでは、使用するライブラリを指定している。このとき、ライブラリの名前はsonameの接頭語のlibと接尾語の.soを除いた部分によって指定する。

-Lオプションでは、使用するライブラリのあるディレクトリパスを指定している。上記の場合、main.cppと同じディレクトリにlibmine.soが存在していることをコンパイラに教えている。

これで、mainという名前の実行ファイルが生成され、それが実行されれば、コンソールに"lib_func called"の文字が表示されるはずだ。

実は、実行してみれば分かるが、恐らく正しく実行されずに、下記のようなエラーが出る。

./main: error while loading shared libraries: libmine.so: cannot open shared object file: No such file or directory

読んでみるとそのままだが、libmine.soが見つからないとエラーを吐いてプログラムが止まっている。

最初にも述べたとおり、共有ライブラリはリンクされるのみで実行ファイルの中には組み込まれず、実行時に動的なリンカがリンクされている共有ライブラリ(今回だとlibmine.so)を探査し、読み込んでから実行ファイルが実行される。

つまり、動的なリンカが共有ライブラリを見つけられなかったということだ。では、動的なリンカはどこを探しているのか、何をすれば探査場所を指定することが出来るのかという話になる。

結論から言うと、/etc/ld.so.confにパスを書くか、環境変数LD_LIBRARY_PATHにパスを追加する。

動的リンカの探査

共有ライブラリは、実行時に動的リンカに探査され、発見された後にその中にあるプログラムが利用される。この動的リンカの探査場所は、/etc/ld.so.confと、環境変数LD_LIBRARY_PATHに書かれる。

では/etc/ld.so.confにパスを書けばよいだけかというとそうではない。動的なリンカは探しに行く時にいつも/etc/ld.so.confの中身を確認しているわけではないからだ。普通、/etc/ld.so.cacheというキャッシュを確認している。そのため、/etc/ld.so.confの中身を書き換えたときは、その内容をldconfigコマンドを使って/etc/ld.so.cacheに反映させてやらなくてはいけない。

動的リンカに共有ライブラリの場所を教えてあげる、または、デフォルトで設定されているであろう/usr/local/libにここまでで作った共有ライブラリを入れて、再度 実行ファイルmainを実行すると、プログラムは期待した動きをするはずだ。

CMakeを使う

ここまででやったこと(共有ライブラリの作成と利用)をCMakeを使って行う。

# cmakeの必須バージョン(最低)
cmake_minimum_required(VERSION 3.8)

# ライブラリの生成。2つ目の引数は共有ライブラリならSHARED,静的ライブラリならSTATIC
add_library(mine SHARED mylib.cpp)

# 実行ファイルの生成
add_executable(main main.cpp)

# ライブラリのリンク librariesと複数形、typo注意
target_link_libraries(main hello)

なお、add_libraryの1つめの引数は、特に何もしなければライブラリの名前に使われるが、実際にはcmake内で使われるターゲット名で、これはcmake実行環境内でユニークでなくてはいけない。

つまり、下記のように同じ名前のターゲットを作ることは出来ない。

# 意図としては、同じ名前の共有ライブラリと静的ライブラリの両方を同時に作りたい
add_library(mine SHARED mylib.cpp)
add_library(mine STATIC mylib.cpp) # これはだめ

では、一度のコンパイルで同じ名前の共有ライブラリと静的ライブラリの両方を出力したいときにはどうするか。

結論から言うと、一報を別のターゲット名に設定した後、そのターゲットのプロパティを変更する。下記のように書く。

add_library(mine SHARED mylib.cpp)

# 一度、別の名前に避難
add_library(mine-static STATIC mylib.cpp)
# ターゲットのOUTPUT_NAMEプロパティを変更して同じ名前にする
set_target_properties(mine-static PROPERTIES OUTPUT_NAME mine)

これで同じ名前の共有ライブラリと静的ライブラリが同時に作れる。

今回はライブラリを生成してその場でリンクしているのでコレで良い。

だが、外部の(ダウンロードした)ライブラリを利用するときは、そのライブラリがどこにあるのかを教えてあげる必要がある。

また、可能なものであれば、ライブラリなどを探査するモジュールもあるので、それを使ってライブラリをモジュールに探させることも出来る。

ダウンロードしたライブラリを使う

ダウンロードしたライブラリ(又は自分でビルドしたライブラリ)の多くは、includeと名付けられたディレクトリにヘッダファイルが、libと名付けられたディレクトリに共有ライブラリや静的ライブラリが収められている。

実際、/usr/localの中を見てみると、includelibディレクトリがあり、その中にそれぞれ対応するファイルが収められているのが分かると思う。

これをcmakeでどう書くかというと、includeディレクトリ、つまりヘッダファイルをcmakeに認識させたい場合は、include_direcotriesを使い、libディレクトリの中にあるライブラリを認識させたい場合は、link_directoriesを使う。

cmake_minimum_required(VERSION 3.0)

# インクルードディレクトリの指定
include_directories(somewhere/include)

# リンクディレクトリの指定
link_directories(somewhere/lib)

add_executable(main main.cpp)

target_link_libraries(main some_library)

上記だと、指定したsomewhere/libディレクトリの中にlibsome_library.soがあるので、それを使いたいということになる。また、libsome_library.soを使う際に利用するヘッダファイルはsomewhere/incliudeの中にあるので、それを使いたいということでもある。

ちなみに、somewhere/libの中に、libsome_library.solibsome_library.aの両方が合った場合、共有ライブラリが優先される。静的ライブラリを使いたいバイは、target_link_libraries(main some_library.a).aだと明記する)と書けば良い。

上記では、リンクディレクトリやインクルードディレクトリのパスはハードコードしたが、実際はこんなことはしない。パッケージ(ヘッダ+ライブラリ)を探査するfind_packageというモジュールを使う。

find_package

find_packageある方法 によって外部ライブラリを探し、

にその結果を代入してくれる。(XXXはパッケージ名)

例えば、find_package(SOMEPACKAGE REQUIRED)とcmakeファイル上に書き込めば、find_packageモジュールはSOMEPACKAGEを探し、見受かればSOMEPACKAGE_FOUNDにtrueの旨の値を代入する。といった具合。
ちなみに、REQUIREDオプションは、パッケージが見つからなければその場で処理を中断させるオプション。

では、その ある方法 とは何か。その方法は2つあり、モジュールモードコンフィグモードがある。

モジュールモード

モジュールモードでは、/usr/share/cmake-3.10/Modules(3.10の部分はバージョン)の中にあるFindXXX.cmakeに従って、必要な情報を集める。

例えば、GTK2というパッケージを見つけたければ、find_package(GTK2 REQUIRED)と書く。すると、この探査モジュールは/usr/share/cmake-3.10/Modules/FindGTK2.cmakeという名前のファイルを探し、それに従って必要な値を代入していく。

このFindXXX.cmakeファイルはaptなどでインストールするとパッケージ提供側が用意してくれていて、それをaptが配置してくれたりする。そのため。当然だが自分でビルドしてcmakeファイルを適切な場所に配置されなかったパッケージは、このモジュールモードを使って見つけることは出来ない。

find_packageがcmakeファイルを探査するのは/usr/share/cmake-3.10/Modulesの中の他に、CMAKE_MODULE_PATH変数で指定された場所を探す。

コンフィグモード

コンフィグモードでは、<package名>Config.cmakeまたは<lower-case-package名>-config.cmakeという名前のファイルに従ってパッケージを探す。探してからは、モジュールモードと同じ。

では上記2パターンのうちのいずれかのファイルをどこに配置すればよいかと言う話になるが、かなり複雑になるのでここでは書かない。使う時に調べてみれば良い。

その他

find_package以外にも、探査用のモジュールはいくつかあるので、使う時に調べながら使ってみると良い。

参考

https://www.ibm.com/developerworks/jp/linux/library/l-shlibs/index.html