60
40

More than 3 years have passed since last update.

RustでつくるDLLの話

Posted at

こんにちは。 私はメインで使ってるOSがWindowsなので、共有ライブラリといえばDLL派です。 だいたいsoってなんだよドレミファの次にあるやつかよ。 なのでこの記事では共有ライブラリのことをDLLって呼びますがご了承ください。

Rustの得意分野の一つが、FFI (Foreign Function Interface) と呼ばれる他言語との連携ですね。 他の言語で書かれたライブラリをRustから呼び出したりするのはもちろんですが、Rustで書かれたライブラリを他の言語から呼び出してもらうことももちろんできます。
その際には、こういった言語間連携の事実上の標準である、C言語の呼び出し規約に準拠した関数を仕込んだDLLファイルを作成して、それを介して呼んだり呼ばれたりするのが標準なわけですね。

この記事では、Rustの関数を他言語から呼び出してもらうためのDLLの作り方やちょっとした小ネタを書いていきます。

RustでDLLファイルを作るには

とりあえず、拡張子 .dll のファイルを出力するだけであれば、クレートのsrcフォルダの下にlib.rsというファイルを置き、次のような記述をCargo.tomlに追加するだけです:

Cargo.toml
[lib]
crate-type = ["cdylib"]

もしクレートを cargo new クレート名 --lib のコマンドで作成したのなら、上述のlib.rsはもうできているはずなので、Cargo.tomlに上記の内容を追加するだけですね。 あとはいつもと同じようにビルド(厳密にはcargo build --lib)してやれば、target/debug/フォルダ以下になんかDLLができあがっているはずです。

ただこれだけだと、DLLは作ったものの中には特に何もないので、DLLで公開する関数を教えてやらなければいけません。

DLLで公開する関数をつくる

5秒でわかる

関数を書くときに、#[no_mangle]pubextern "C"をこの順にfnの前に書きます。

もっと厳密に

いろいろ試してみた感じ、DLLで公開される関数の厳密な条件はたぶん以下のような感じです:

  1. DLLを作るクレートから、pubで公開されており、
  2. extern "C"で定義されており、
  3. 関数名がマングル(難読化みたいなの)されておらず、
  4. 関数の実体が、DLLを作るクレートから見て静的にリンクされている

他クレートで定義された関数をDLLで公開する

これは上述の1.の条件を利用します。 DLLのクレートの依存先の別のクレートで、

#[no_mangle] pub extern "C" fn hogehoge() {
  // ...
}

のように定義されている関数があった場合、DLLのクレートでこの関数をpub useしてやれば、それがそのままDLLで公開されるわけです。 ただし注意しなければいけないのは上述の4.の条件で、依存先のクレートが動的にリンクされるとこの関数はDLLに入らなくなってしまいます。 なので、依存先のクレートは必ず静的にリンクされるrlibの設定がされていないといけません:

Cargo.toml
[lib]
crate-type = ["rlib"]

一応、デフォルト設定であるlibrlibの代わりに入っていても、だいたいは問題なく動くと思いますが、ドキュメントによるとlibは静的・動的どちらかをコンパイラが自由に選択するという設定なので、この場合はrlibを書いておくのが正解でしょう。

extern "C" は2種類の文法があり、それぞれ微妙な違いがある

あるんです。

主に関数をインポートするときに使う、extern "C" {}ブロックの中に関数宣言(関数本体部分{...}を書かずに、;で終わらせるやつ)を書いていく方式と、関数をエクスポートするときに使う、extern "C"のあとに関数定義を書いていく方式の2通りがあります。

これ、インポートするときとエクスポートするときで対称になってるんやろなぁと思うじゃないですか、ところが微妙に違うらしいんです。

違いは名前のマングリングで、前者は勝手に#[no_mangle]相当の非マングル化された関数名をインポートしますが、後者は関数ひとつひとつに明示的に#[no_mangle]と書かなければ関数名がマングルされます。

私の感覚では、前者の動作のほうがちょっとおかしい気はします。 extern "C"が指定しているのはあくまでも呼び出し規約(関数を呼び出すときに引数を左から渡すか右から渡すかとか、関数の呼び出し元と呼び出し先のどちらがスタックポインタを動かすかなどの取り決め)であって、関数名は呼び出し規約とは直接関係ないところだからです。 とはいえ、普通はどうせextern "C"したら#[no_mangle]も同時に書くに決まってるんだから、こうした方がコード書くときにタイプ量が減って楽だという気持ちも分からんでもないです。 問題は言語内で統一が取れていないところですね……。

なんでこんなことになってるのか軽く調べてみましたが、2016年ぐらいにこの問題に気づいた方がいて修正しようとしてみたものの、互換性の問題で修正できなさそうみたいな結論になっているスレを発見できたぐらいでした。

60
40
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
40