Rust のライブラリ crate_type をまとめてみた

2022/01/13 追記
最初の投稿から 5 年も経ったらさすがに他の記事出てるだろうと思いきや、今でも Google で検索するといちばん上に出てきますね。
ということで改めて調べる必要があったことに加え、内容に不正確な点があったので、ほぼ全面的に書き直しました。過去の版は 編集履歴 から確認してください。

はじめに

2016/07/07 に Rust 1.10 が発表されました。もう 5 年前です。
Announcing Rust 1.10 – The Rust Programming Language Blog

Rust 1.10 では、新しく cdylib というが追加されました。
これは一体何者なのでしょうか。そもそも Rust の crate_type とは何でしょうか。

TL;DR

    • クレートの種類は bin, lib, proc-macro の 3 つ

 

    • lib クレートは次章に記載の通り、生成するファイルを *lib の形で指定することができる。

 

    • Rust を利用する人が指定する可能性があるのは基本的に rlib と cdylib の 2 つ。

rlib は Cargo の中で使用するために指定する(されている)。

cdylib は他の言語から動的に FFI するライブラリを作るために指定する。

答え

疑問に対する回答は Linkage – The Rust Reference にあります。違いについて要約すると、以下のことが書いてあります。

bin : 実行可能なファイルを生成します。

lib : Rust ライブラリを生成します。詳細はコンパイラに依存します。

dylib : 動的な Rust ライブラリを生成します。

staticlib : 静的な システム ライブラリを生成します。他言語アプリからリンクするために推奨されます。

cdylib : 動的な システム ライブラリを生成します。他言語からロードするために使用されます。

rlib : “Rust library” を生成します。静的な Rust ライブラリとも言え、中間生成物として使用されます。

proc-macro : 何を生成するかは特定していせん。関数マクロが定義されたクレートに設定します。

ここでは他にも解説がある bin と proc-macro については触れず、ライブラリに限定します。
内容が指定されていない lib を除くと、ライブラリの種類は下表の通りに整理できます。

Rust ライブラリシステムライブラリ動的(ロード)dylibcdylib静的(リンク)rlibstaticlib

ここでの「システムライブラリ」とは、他の言語から FFI などを使用して呼び出すことを想定したものです。
つまり、裏を返せば「Rust ライブラリ」とは、他の言語から使用されることを明確には想定していない形式とも言えます。

実際に rlib については 中間生成物 と書かれている通り、無指定でも Cargo の内部で使用されています。
外部パッケージを使っているクレートをビルドすると、rlib ファイルが ./target//deps の下へ大量に生成される様子が観察できます。

実験

とりあえず分かった気になれたので、ここで少し実験してみましょう。
環境は rustc 1.57.0 (f1edd0429 2021-11-29) on Windows 10 です。

サンプルとして以下のコードを使用します。

#[no_mangle]
pub fn saturating_add(a: u8, b: u8) -> u8 {
    a.saturating_add(b)
}

Rust 標準の u8::saturating_add をラップしただけの関数をエクスポートしています。
後ほど外部から呼び出せるように no_mangle 属性を指定しています。

これを以下の通りにコンパイルしてみます。なお、余計な情報が入らないよう最適化しています。
また歴史的な都合から dylib には prefer-dynamic というフラグがあるので、これを付けたものを追加します。

rustc add.rs -O --crate-type=cdylib --out-dir cdylib
rustc add.rs -O --crate-type=dylib --out-dir dylib
rustc add.rs -O --crate-type=dylib --out-dir dylib-dyn -C prefer-dynamic
rustc add.rs -O --crate-type=rlib --out-dir rlib
rustc add.rs -O --crate-type=staticlib --out-dir staticlib

ファイルの中身を確認する

生成された複数のファイルから、メインとなるものを抜粋すると以下の通りです。

crate_typefilesizecdylibadd.dll10752dylibadd.dll919040dylib + prefer-dynamicadd.dll11264rliblibadd.rlib4802staticlibadd.lib10398938

cdylib を覗いてみる

まずは cdylib を覗いてみましょう。
ファイル形式は拡張子に .dll とある通り Portable Executable なので、出力シンボルと依存先ライブラリを見てみます。

R:\crate_type> dumpbin /nologo /exports /dependents .\cdylib\add.dll

Dump of file .\cdylib\add.dll

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    VCRUNTIME140.dll
    api-ms-win-crt-runtime-l1-1-0.dll

  Section contains the following exports for add.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001360 rust_eh_personality = rust_eh_personality
          2    1 00001000 saturating_add = saturating_add

  Summary

        1000 .data
        1000 .pdata
        1000 .rdata
        1000 .reloc
        2000 .text

さきほど実装した saturating_add と、no_std の Rust を書いたことがある人にはお馴染みの rust_eh_personality が見て取れます。それ以外に特筆する情報はなさそうです。

dylib を覗いてみる

続いて dylib の dll を見てみます。

R:\crate_type> dumpbin /nologo /exports /dependents .\dylib\add.dll

Dump of file .\dylib\add.dll

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    WS2_32.dll
    bcrypt.dll
    ADVAPI32.dll
    USERENV.dll
    VCRUNTIME140.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll

  Section contains the following exports for add.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
        2436 number of functions
        2436 number of names

    ordinal hint RVA      name

          1    0 0004ED70 _ZN100_$LT$core..ascii..EscapeDefault$u20$as$u20$core..iter..traits..double_ended..DoubleEndedIterator$GT$9next_back17h1498a44032c8561eE = _ZN100_$LT$core..ascii..EscapeDefault$u20$as$u20$core..iter..traits..double_ended..DoubleEndedIterator$GT$9next_back17h1498a44032c8561eE
          2    1 000104F0 _ZN101_$LT$std..fs..DirEntry$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..DirEntry$GT$$GT$8as_inner17h81e9e878d30be855E = _ZN101_$LT$std..fs..DirEntry$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..DirEntry$GT$$GT$8as_inner17h81e9e878d30be855E
          3    2 000104F0 _ZN101_$LT$std..fs..FileType$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..FileType$GT$$GT$8as_inner17he61019d1f94252c1E = _ZN101_$LT$std..fs..DirEntry$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..DirEntry$GT$$GT$8as_inner17h81e9e878d30be855E
          4    3 000104F0 _ZN101_$LT$std..fs..Metadata$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..FileAttr$GT$$GT$8as_inner17h9e589444f15fa30dE = _ZN101_$LT$std..fs..DirEntry$u20$as$u20$std..sys_common..AsInner$LT$std..sys..windows..fs..DirEntry$GT$$GT$8as_inner17h81e9e878d30be855E
          5    4 00019DA0 _ZN101_$LT$std..process..ExitStatusError$u20$as$u20$core..convert..Into$LT$std..process..ExitStatus$GT$$GT$4into17h6a80afb100a680cbE = _ZN101_$LT$std..process..ExitStatusError$u20$as$u20$core..convert..Into$LT$std..process..ExitStatus$GT$$GT$4into17h6a80afb100a680cbE
          6    5 0001C440 _ZN102_$LT$std..net..addr..SocketAddr$u20$as$u20$core..convert..From$LT$std..net..addr..SocketAddrV4$GT$$GT$4from17h35e23e4d3107c779E = _ZN102_$LT$std..net..addr..SocketAddr$u20$as$u20$core..convert..From$LT$std..net..addr..SocketAddrV4$GT$$GT$4from17h35e23e4d3107c779E
          7    6 0001C460 _ZN102_$LT$std..net..addr..SocketAddr$u20$as$u20$core..convert..From$LT$std..net..addr..SocketAddrV6$GT$$GT$4from17h31922aeaac04cd61E = _ZN102_$LT$std..net..addr..SocketAddr$u20$as$u20$core..convert..From$LT$std..net..addr..SocketAddrV6$GT$$GT$4from17h31922aeaac04cd61E
          8    7 0000FEE0 _ZN103_$LT$core..array..TryFromSliceError$u20$as$u20$core..convert..From$LT$core..convert..Infallible$GT$$GT$4from17h871ca03c104fd2bcE = _ZN103_$LT$core..array..TryFromSliceError$u20$as$u20$core..convert..From$LT$core..convert..Infallible$GT$$GT$4from17h871ca03c104fd2bcE
       ~~~~~~~~ 省略 ~~~~~~~~
       2429  97C 00076030 __unorddf2 = _ZN17compiler_builtins5float3cmp10__unorddf217h4f257f44582e6d90E
       2430  97D 00075F40 __unordsf2 = _ZN17compiler_builtins5float3cmp10__unordsf217h3bb3ef0e2ab6043aE
       2431  97E 0002D0C0 rust_begin_unwind = rust_begin_unwind
       2432  97F 0003E3A0 rust_eh_personality = _ZN58_$LT$libc..windows..FILE$u20$as$u20$core..clone..Clone$GT$5clone17hca7d4b7358131f48E
       2433  980 000E2000 rust_metadata_add_99d1d8310a660620 = rust_metadata_add_99d1d8310a660620
       2434  981 0002BDD0 rust_oom = rust_oom
       2435  982 0002D990 rust_panic = rust_panic
       2436  983 00001000 saturating_add = saturating_add

  Summary

        1000 .data
        6000 .pdata
       5F000 .rdata
        1000 .reloc
        1000 .rustc
       7B000 .text

はい、シンボル名は思いきり省略しました。冒頭に 2436 number of functions とある通り、2436 個の関数が表示されます。さきほど実装した saturating_add は、そのうち 2436 番目となっていました。
増えた関数の name には見慣れた名前があります。これら cdylib にはなかった 2400 あまりの関数は、Rust の標準ライブラリを再出力したものであることが見て取れます。

それから依存先のほうにも、さきほどは存在しなかった dll が見て取れます。例えば WS2_32.dll は Windows Sockets 2 (Winsock) なので、ネットワークに関係しています。
これについてもコード上からは使用していない Rust の標準ライブラリを再出力しているため、このような結果となります。

それともう一つ、Summary のところに .rustc という領域があります。ここには型定義を含めたコンパイラのメタ情報が入っています。後ほど確認します。

dylib + prefer-dynamic を覗いてみる

次は dylib に -C prefer-dynamic フラグをつけたものです。

R:\crate_type> dumpbin /nologo /exports /dependents .\dylib-dyn\add.dll

Dump of file .\dylib-dyn\add.dll

File Type: DLL

  Image has the following dependencies:

    VCRUNTIME140.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    KERNEL32.dll
    std-f87c887dcbebcf7e.dll

  Section contains the following exports for add.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           6 number of functions
           6 number of names

    ordinal hint RVA      name

          1    0 00001B92 __rust_alloc = __rust_alloc
          2    1 00001B98 __rust_alloc_zeroed = __rust_alloc_zeroed
          3    2 00001B9E __rust_dealloc = __rust_dealloc
          4    3 00001BA4 __rust_realloc = __rust_realloc
          5    4 00005000 rust_metadata_add_99d1d8310a660620 = rust_metadata_add_99d1d8310a660620
          6    5 00001000 saturating_add = saturating_add

  Summary

        1000 .data
        1000 .pdata
        1000 .rdata
        1000 .reloc
        1000 .rustc
        1000 .text

標準ライブラリが見つからないどころか、今度は依存先として指定されています。

rlib を覗いてみる

続いて rlib を見てみましょう。
こちらは拡張子が .rlib なので、まずはファイル形式を特定してみましょう。テキストエディタで開くと以下の記述で始まります。

!<arch>
/               0           0     0     0       24        `
~~~~~~~~ 以下、省略 ~~~~~~~~

ということで(?)ファイル形式は Ar アーカイブです。
展開するとオブジェクトファイルやらメタデータやらが入っていますが、このままでは何だか分かりません。rustc 専用のファイルといったところです。

……staticlib は?

staticlib は dylib と OS のライブラリを静的リンクしたようなもので、貼りつけるには大きすぎるので省略します。

Rust から呼んでみる

せっかくライブラリを作ったので、呼び出してみましょう。以下のコードを使用します。
当然 $200 + 200 = 400$ ですが、saturating されれば u8::MAX の $255$ になるはずです。

extern crate add;

fn main() {
    println!("{}", add::saturating_add(200, 200));
}

Rust から rlib を呼んでみる

まずはいつも (Cargo で) 使っている rlib から。
rustc の -L オプションによって、リンカの検索対象を指定します。

R:\crate_type> rustc main.rs -L rlib

R:\crate_type> main.exe
255

難なく実行できました。

Rust から dylib を呼んでみる

続いて dylib です。

R:\crate_type> rustc main.rs -L dylib
error: cannot satisfy dependencies so `std` only shows up once
  |
  = help: having upstream crates all available in one format will likely make this go away

error: cannot satisfy dependencies so `core` only shows up once
  |
  = help: having upstream crates all available in one format will likely make this go away

       ~~~~~~~~ 省略 ~~~~~~~~

error: cannot satisfy dependencies so `panic_unwind` only shows up once
  |
  = help: having upstream crates all available in one format will likely make this go away

error: aborting due to 13 previous errors

まずは二重定義だと怒られました。これから std をコンパイルしようというのに、依存元に既に含まれるのは確かに変ですね。
それならばということで、ここで prefer-dynamic を登場させましょう。

R:\crate_type> rustc main.rs -L dylib-dyn

R:\crate_type> main.exe

R:\crate_type> echo %errorlevel%
-1073741515

コンパイルは通りましたが、実行時にエラーが出ました。なお -1073741515 (0xc0000135) は STATUS_DLL_NOT_FOUND です。
答えは .exe をエクスプローラから起動すると出ます。

grafik.png

それはそうですね。私の場合は rustup を使用しているので、下記の場所にありました。

R:\crate_type> copy C:\Users\etoilevi\.rustup\toolchains\stable-x86_64-pc-windows-msvc\bin\std-f87c887dcbebcf7e.dll
1 個のファイルをコピーしました。

R:\crate_type> main.exe
255

なんとか動きました。

Rust から cdylib, staticlib を呼んでみる

続いてシステムライブラリの cdylib, staticlib を呼んでみます。

R:\crate_type> rustc main.rs -L cdylib
 WARN rustc_metadata::locator no metadata found: no `.rustc` section in '\\?\R:\crate_type\cdylib\add.dll'
 WARN rustc_metadata::locator no metadata found: no `.rustc` section in '\\?\R:\crate_type\cdylib\add.dll'
error[E0462]: found staticlib `add` instead of rlib or dylib
 --> main.rs:1:1
  |
1 | extern crate add;
  | ^^^^^^^^^^^^^^^^^
  |
  = help: please recompile that crate using --crate-type lib
  = note: the following crate versions were found:
          crate `add`: cdylib\add.dll.lib

error: aborting due to previous error


R:\crate_type> rustc main.rs -L staticlib
error[E0462]: found staticlib `add` instead of rlib or dylib
 --> main.rs:1:1
  |
1 | extern crate add;
  | ^^^^^^^^^^^^^^^^^
  |
  = help: please recompile that crate using --crate-type lib
  = note: the following crate versions were found:
          crate `add`: staticlib\add.lib

error: aborting due to previous error


どちらも同じ理由で怒られます。これは生成物の中に rustc が使用するメタデータが含まれておらず、関数の型などが分からないためです。dylib にあった .rustc セクションのことです。
ということで外部関数の宣言を追加します。

mod add {
    #[link(name = "add.dll")] // cdylib のみ
    extern "system" {
        pub fn saturating_add(a: u8, b: u8) -> u8;
    }
}

fn main() {
    unsafe {
        println!("{}", add::saturating_add(200, 200));
    }
}

再度コンパイルしてみましょう。

R:\crate_type> rustc main.rs -L cdylib

R:\crate_type> main.exe
255

~~~~ ここで #[link(name = "add")] をコメントアウト ~~~~

R:\crate_type> rustc main.rs -L staticlib -l add

R:\crate_type> main.exe
255

実行できました。

C から cdylib を呼んでみる

ところで cdylib には標準ライブラリそのものは入っていませんでしたが、ちゃんと機能するのでしょうか。
以下のような C のコードを書いて、さきほど作った DLL の関数を動的に呼び出してみましょう。

#include <stdio.h>
#include <Windows.h>

typedef UINT8(CALLBACK *SATURATING_ADD)(UINT8, UINT8);

int main()
{
    HMODULE hLib;
    SATURATING_ADD pProc;
    UINT8 ab = 200;

    hLib = LoadLibraryA("add");
    if (hLib == NULL)
    {
        printf("%u", GetLastError());
        return 1;
    }

    pProc = (SATURATING_ADD)GetProcAddress(hLib, "saturating_add");
    if (pProc == NULL)
    {
        return 2;
    }

    printf("%u\n", pProc(ab, ab));
    return 0;
}

実行してみましょう。

R:\crate_type>main.exe && echo exitcode: %errorlevel%
255
exitcode: 0

saturating してます。正常に動作しているようです。
ちなみに dylib でもシンボルはエクスポートされているため、同様に動きます。

まとめ

以上 Rust のライブラリ用 crate_type の違いを、生成物の中身とともに確認してみました。

最終的な実行ファイルまでを Rust だけで開発する場合には Cargo を通したビルドのみで事足ります。ですので最初に「Rust ライブラリ」と書いてあったものについては、あえて指定する機会もないと思います。

最終品が実行ファイルでない、つまり C やその他の言語から Rust の関数を呼ぶ際には、動的リンク/ロードでよければ cdylib の、musl を使用するなど動的な依存ができない場合は staticlib の指定になると思います。

以上、開発のお役に立てれば幸いです。

おまけ (WebAssembly)

ちなみに WebAssembly のライブラリを作成する際、初期状態の Cargo.toml でビルドすると

crate-type = ["cdylib", "rlib"]

と書けと怒られますが、これについても「おまじない」ではなくなりましたね。
ここでの cdylib は最終成果物の .wasm ファイル(=動的ライブラリ)を、rlib は Cargo が内側で使う .rlib ファイルを生成します。