Rustで静的なライブラリを作ろうと思った時に、ちょっとややこしかったのでメモ。
タイトルにある「Cとリンク可能な」というのは、「Rust専用の形式ではなく使用している環境での一般的な形式の」という意味で、この記事ではLinuxシステムを前提としているので.aファイルのライブラリということになる。
なお、この記事は少し中途半端なところで終わっているので、もし参考にする場合はご注意下さい。
ライブラリの作成
まずはcargoでライブラリ用のプロジェクトを作成する。
$ cargo new --lib mylib
デフォルトではRust用のライブラリ(.rlib)としてビルドされてしまうので、システムの静的ライブラリ(.a)としてビルドするようにcrate-typeをstaticlibに設定する。
...
[lib]
crate-type = ["staticlib"]
cargo buildによりビルドすると.aファイルが生成される。
$ cargo build
(出力は省略)
$ ls target/debug/*.a
target/debug/libmylib.a
Cとのリンク
作成したライブラリとリンクするようにCのプログラムをコンパイルする。
しかし、書いた覚えのない関数が見つからないとのエラーが大量に出てしまう。
$ gcc main.c -o main -L /project_path/mylib/target/debug/ -lmylib
usr/bin/ld: /project_path/mylib/target/debug//libmylib.a(std-c32b051c3aafd36c.std.4p3qj3su-cgu.0.rcgu.o): in function `std::sys::unix::mutex::Mutex::init':
/rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:46: undefined reference to `pthread_mutexattr_init'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:48: undefined reference to `pthread_mutexattr_settype'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:52: undefined reference to `pthread_mutexattr_destroy'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:46: undefined reference to `pthread_mutexattr_init'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:48: undefined reference to `pthread_mutexattr_settype'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:52: undefined reference to `pthread_mutexattr_destroy'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:46: undefined reference to `pthread_mutexattr_init'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:48: undefined reference to `pthread_mutexattr_settype'
/usr/bin/ld: /rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447//src/libstd/sys/unix/mutex.rs:52: undefined reference to `pthread_mutexattr_destroy'
...
collect2: error: ld returned 1 exit status
関数名から察するに、ここで見つからない関数はlibpthreadやlibdlに含まれるもののようだ。試しにこれらをリンクするように指定してみると、問題なくコンパイルは成功する。(大抵の場合、これらのライブラリはシステムにインストールされておりパスも通っているので、ライブラリを指定するだけでリンクできる。)
$ gcc main.c -o main -L /project_path/mylib/target/debug/ -lmylib -lpthread -ldl
なぜこうなるかというと、RustはデフォルトでlibpthreadなどのCで書かれたライブラリに依存しているためだ。
試しに「Hello, world!」を出力するだけのプログラムにどのようなライブラリがリンクされているか見てみると、libpthreadもlibdlもリンクされていることが分かる。
今回作成したライブラリでlibrtは必要にならなかったが、ライブラリで実装している処理によってはlibrtもリンクしないとコンパイルできなかったりもするだろう。libgcc_sについてはどのようなケースで必要になるのかちょっとよく分からなかった、、、
$ ./hello_rust
Hello, world!
$ ldd hello_rust
linux-vdso.so.1 (0x00007ffe4e091000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f044fcb8000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f044fcae000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f044fc8d000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f044fc73000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f044fab2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f044fd0b000)
ちなみに同じようなプログラムをCで実装してコンパイルした場合は、もう少しリンクされるライブラリは少ない。いくつかライブラリがリンクされているが、これらはgccでコンパイルすれば勝手にリンクしてくれる。(Rustでも同じライブラリがリンクされていることを確認できる。)
$ ./hello_c
Hello, world!
$ ldd hello_c
linux-vdso.so.1 (0x00007ffced5d1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd426a3f000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd426c20000)
ちょっと分かりづらくない?
自分で静的ライブラリを作ってリンクしたつもりなのに、自分で書いてない関数でエラーが出たりして、しかもRustがデフォルトでリンクしているライブラリとか普段意識してないし、、、ライブラリを使うときに自分でそれらを指定しなきゃいけないのも結構面倒。
さっくり自分で作成したライブラリだけ指定すればいいようにできないのだろうか?
結果から言うと、なんとなくやりたいことはできたが、ちょっと強引なやり方になってしまった。
Rustのmuslターゲットを使う
上の例でみたように、Rustではデフォルトでいくつかのライブラリが動的にリンクされるが、ターゲットを変えることで静的にリンクさせることができる。
rustupで選択可能なターゲットを確認してみる。
$ rustup target list
...
x86_64-unknown-linux-gnu (installed)
...
x86_64-unknown-linux-musl
...
リストを確認すると、ほとんどのターゲットにおいて-gnuと-muslが対になっていることが分かる。-gnuをターゲットにした場合はglibcが、-muslをターゲットにした場合はmusl libcがリンクされる。そして、glibcは動的に、musl libcは静的にリンクされる。
musl libcが静的リンクを前提としているらしく、それを知ったときはなるほどという感じだったが、ターゲットの名前からリンクのやり方が違うということを読み取れなかったので最初は少し戸惑った。
-muslのターゲットもrustupで簡単にインストール可能だ。
$ rustup target add x86_64-unknown-linux-musl
このmuslターゲットのインストール、日本語による検索でヒットするページだと古い公式ドキュメントなど、musl libcを使うようにrustcを自分でビルドする手順が結構出てくる(2020/4/28時点)。しかし、調べてみると今はrustupで簡単にインストールできるようになっており、勢いのある言語は整備がどんどん進んでありがたいな~と思うのであった。
ターゲットがインストールできたら、cargoにオプションを渡すだけで簡単にターゲットを切り替えられる。
ターゲットを指定した場合、成果物が置かれるディレクトリが変わるので注意。
$ cargo build --target=x86_64-unknown-linux-musl
(出力は省略)
$ ls target/x86_64-unknown-linux-musl/debug/*.a
target/x86_64-unknown-linux-musl/debug/libmylib.a
これで、Rustそのものに必要なライブラリも静的にリンクされている自前のライブラリを作ることができた。
再度、Cとのリンク
ここまでくれば、あとは最初と同じようにコンパイルするだけだ、と思った。
$ gcc main.c -o main -L /project_path/mylib/target/x86_64-unknown-linux-musl/debug/ -lmylib
実際、自分が作成したライブラリだけ指定すれば問題なくコンパイルできたのだが、実行するとSegmentation faultで落ちてしまった。
正確な原因はつかめていないのだが、自作のライブラリにはmusl libcが含まれているにも関わらずC側のプログラムがglibcを動的にリンクしているため、musl libcの処理が呼ばれるべきところでglibcの処理が呼ばれてしまい、何かおかしくなっているのかもしれない。
ちなみに-staticを付けてコンパイルするとうまくいって、当初やりたかったことができる。
$ gcc main.c -o main -L /project_path/mylib/target/x86_64-unknown-linux-musl/debug/ -lmylib -static
Rustのライブラリ側に含まれるmusl libcを優先的に使ってくれるようになるのだろうか、、、うーん、よく分からん。
まとめ
たぶん、Rustのライブラリ側にmusl libcを使うなら、Cのプログラム側もmusl libcを使うようにgccを設定すべきで、glibcとmusl libcをごちゃまぜにして使うのはよくないと思う。
そのあたりも調べてみたいが体力がもたなかったので、今度気が向いたら調べてみよう、、、勉強不足だな~