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コマンドを使う必要がなくなる訳ですね。