macOS で動的ライブラリを動的リンクするとき
macOSにて、自作のCやC++アプリケーションで動的ライブラリを実行時に読み込ませる方法について調べてみたのでまとめてみます。動的ロードではなく、動的リンクの話です。
実行環境: macOS 12.1
目次:
動的ライブラリをどこに置いておくか
まず、動的ライブラリをどこに置いておくかですが、自分だけが利用するようなライブラリであれば /usr/local/lib
あたりに置いておけば基本的に問題なさそうです。なぜなら、アプリを実行する時にリンカがそこを探してくれるからですね。
しかし、作ったアプリを外部に配布するとなると、配布先のPCの /usr/lib
や/usr/local/lib
あたりにアプリで利用するライブラリが存在するとは限らないので、その場合はアプリと一緒に動的ライブラリも配布する必要があります。それだけでなく、配布する実行アプリが同梱した動的ライブラリを読み込めるようにしてあげる必要もあります。実は、今回はこの話がメインです。
動的ライブラリの詳細を追う
まずは、動的ライブラリの詳細を追ってみましょう。
以下のような単純な動的ライブラリを作ってみます。
mylib.cpp:
int my_sum(int a, int b) { return a + b; }
# コンパイル $ g++ -c mylib.cpp # 動的ライブラリ生成 $ g++ -shared mylib.o -o libmylib.dylib
次に、作ったライブラリを利用する実行ファイルを作ってみます。
main.cpp:
#include <iostream> #include <mylib.h> int main () { int a = my_sum(1, 5); std::cout << a << std::endl; }
フォルダ構成は以下のように多少整理しておきました。
. ├── main ├── main.cpp └── mylib ├── include │ └── mylib.h ├── lib │ └── libmylib.dylib ├── mylib.cpp └── mylib.o
コンパイルとビルド時リンクを行っていきます。
$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -o main
このコマンドにより実行ファイルmain
が出来上がります。実行ファイル main
があるディレクトリでこのファイルを実行すると...
$ ./main dyld[17548]: Library not loaded: libmylib.dylib
残念ながらmain
を実行したときにリンカが ./mylib/lib/libmylib.dylib
を見つけることができなかったようです。
さて、実行時にリンカがアプリで利用される動的ライブラリをどのように見つけるかがここでは問題になっています。デフォルトではリンカは /usr/local/lib
や実行ファイルを実行したディレクトリ等を探すようになっています。では、自分好みの位置に配置した動的ライブラリをリンカが見つけられるようにするにはどうしたら良いのでしょう。
これを知るには、実行ファイルが実行時にどのようにライブラリをロードするかを見てみる必要があります。
実行ファイルや動的ライブラリの中にはいろんな情報が書き込まれているのですが、macOS ではこれらの情報を otool
コマンドにより調べることが可能です。
# 動的ライブラリのID名(自身のパス)を表示 $ otool -D libmylib.dylib # 依存する動的ライブラリを表示 $ otool -L libmylib.dylib # ロード時のコマンドを表示 $ otool -l libmylib.dylib # 他にもあります
-l
オプションでロード時のコマンドを見ることができるので、これで main
を見てみましょう。
$ otool -l main main: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000100000000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0x0 ... 略 ... Load command 8 cmd LC_LOAD_DYLINKER cmdsize 32 name /usr/lib/dyld (offset 12) Load command 9 cmd LC_UUID cmdsize 24 uuid E9747806-B1DE-3C85-BB7A-A7C1AC3A3502 Load command 10 cmd LC_BUILD_VERSION cmdsize 32 platform 1 minos 12.0 sdk 12.1 ntools 1 tool 3 version 711.0 Load command 11 cmd LC_SOURCE_VERSION cmdsize 16 version 0.0 Load command 12 cmd LC_MAIN cmdsize 24 entryoff 15616 stacksize 0 Load command 13 cmd LC_LOAD_DYLIB cmdsize 40 name libmylib.dylib (offset 24) time stamp 2 Thu Jan 1 09:00:02 1970 current version 0.0.0 compatibility version 0.0.0 Load command 14 cmd LC_LOAD_DYLIB cmdsize 48 name /usr/lib/libc++.1.dylib (offset 24) time stamp 2 Thu Jan 1 09:00:02 1970 current version 1200.3.0 compatibility version 1.0.0 Load command 15 cmd LC_LOAD_DYLIB cmdsize 56 name /usr/lib/libSystem.B.dylib (offset 24) time stamp 2 Thu Jan 1 09:00:02 1970 current version 1311.0.0 compatibility version 1.0.0 Load command 16 cmd LC_FUNCTION_STARTS cmdsize 16 dataoff 49840 datasize 16 Load command 17 cmd LC_DATA_IN_CODE cmdsize 16 dataoff 49856 datasize 0
重要なのは、cmd LC_LOAD_DYLIB
の部分。LC_LOAD_DYLIB
は 「このパスで指定されたネイティブライブラリをロードしてくれ!」という意味を表します。
つまり、cmd LC_LOAD_DYLIB
の部分をいい感じにいじってあげることで好みの位置に配置したライブラリを読み込むようにできる訳ですね。
絶対パスで動的ライブラリが読み込まれるようにする
macOSには実行ファイルの中身をいじることができるinstall_name_tool
コマンドが用意されています。これを使って main
の中身を書き換えてみましょう。
$ install_name_tool -change libmylib.dylib /Path/To/Workspace/Folder/mylib/lib/libmylib.dylib main
これは libmylib.dylib
を /Path/To/Workspace/Folder/mylib/lib/libmylib.dylib
に置き換えるコマンドです。
これにより、main
を実行した時に/Path/To/Workspace/Folder/mylib/lib/libmylib.dylib
をロードするように変更することができました。この段階で、main
はどのディレクトリにいても実行できるようになっています。
@executable_path と @rpath
自分の好みの位置にある動的ライブラリを読み込ませることに成功しました。しかし、これでは配布するアプリには不適切です。なぜなら、アプリを開発したマシン上の絶対パスをそのまま利用しているからですね。かといって、相対パスにすると「実行時のカレントディレクトリからの相対パスで動的ライブラリのパスを指定する」ことになるので、アプリを実行できるディレクトリが制限されてしまいます。
ロードするライブラリは絶対パスで設定したいけど、この絶対パスは実行時のパスを元にして作り上げたいですね。
これを可能にするのが@executable_path
や@rpath
といったものです。
例えば、main
を配布する際に以下のようなフォルダ構成で配布することを考えましょう。
distribution ├── lib │ └── libmylib.dylib └── main
この時、以下のコマンドを実行することで main
の中身を編集します(main
は新しく作り直したことを想定)。
$ install_name_tool -add_rpath @executable_path/lib main $ install_name_tool -change libmylib.dylib @rpath/libmylib.dylib main
@executable_path
には実行ファイルが実行された時のそのファイルが存在するディレクトリへの絶対パスが展開されます。例えば、distribution フォルダが /Users/<ユーザー名>/Downloads
フォルダにあるとしましょう。
Downloads/distribution ├── lib │ └── libmylib.dylib └── main
この時、main
を実行すると、@executable_path
は /Users/<ユーザー名>/Downloads/distribution
に展開されることになります。
install_name_tool -add_rpath @executable_path/lib main
は、@executable_path/lib
というパスを rpath に追加するコマンドになっています。
rpath はリンカが動的ライブラリを検索するパスです。つまり、今回の場合は実行ファイルがあるディレクトリと同じディレクトリにあるlibディレクトリの中からライブラリを探索するように設定していることになります。
さらに、ライブラリ自体のパスを @rpath/libmylib.dylib
に書き換えることで、ライブラリ自体のパスを実質的に @executable_path/lib/libmylib.dylib
に設定しています。
こうしておくことで、どんなmacOS環境でも、どのディレクトリからでも main
を実行できるようになります。
まとめ
今回はmacOS で動的ライブラリを動的リンクするときのことについて書きました。
- 自分だけが利用するようなライブラリであれば
/usr/local/lib
あたりにおいておけばOK。 - 自作のアプリを外部に配布するとき、動的ライブラリも一緒に配布する必要があるならば、アプリが動的ライブラリを読み込めるように正しく設定してあげる必要がある。
おまけ
利用するライブラリのID(インストールパス)を @rpath/libXXX.dylib に書き換えておいて...
$ install_name_tool -id @rpath/libmylib.dylib libmylib.dylib # 確認 $ otool -D libmylib.dylib libmylib.dylib: @rpath/libmylib.dylib
そのライブラリをリンクすると...
$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -o main
$ otool -l main main: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000100000000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0x0 ... 略 ... Load command 13 cmd LC_LOAD_DYLIB cmdsize 48 name @rpath/libmylib.dylib (offset 24) time stamp 2 Thu Jan 1 09:00:02 1970 current version 0.0.0 compatibility version 0.0.0 ... 略 ...
mylib の name
(ID) が @rpath/libmylib.dylib
になります。
さらに、rpath はコンパイルオプションで設定できます。
$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -Wl,-rpath,@executable_path/lib -o main
otool -l main main: Load command 0 cmd LC_SEGMENT_64 cmdsize 72 segname __PAGEZERO vmaddr 0x0000000000000000 vmsize 0x0000000100000000 fileoff 0 filesize 0 maxprot 0x00000000 initprot 0x00000000 nsects 0 flags 0x0 ... 略 ... Load command 16 cmd LC_RPATH cmdsize 40 path @executable_path/lib (offset 12) Load command 17 cmd LC_FUNCTION_STARTS cmdsize 16 dataoff 49840 datasize 16 Load command 18 cmd LC_DATA_IN_CODE cmdsize 16 dataoff 49856 datasize 0
Load command 16
に rpath のことが書いてありますね。
つまり、あらかじめ利用するライブラリのIDを @rpath/libXXX.dylib に書き換えておいた上でアプリのビルド時に -Wl,-rpath,@executable_path/lib
というオプションをつけてビルドすることで、install_name_tool
コマンドを使う必要がなくなる訳ですね。