ゆべねこの足跡

IT系のはなしを残しておく場所です

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

参考