この記事は、Xamarin Advent Calendar 2016の12日目の投稿です。
はじめに
わたしがC#を使っていていたく感動した点が、ネイティブライブラリ呼び出し(P/Invoke)の簡易さでした。
なんとなく関数の名前をあわせて、なんとなくDllImport属性をつければ、いいかんじにネイティブ呼び出しができる。それはとても良いことだと思うんですね。
ことXamarinにおいてもそれは変わらないので、そんなP/Invokeの良さを伝えたい!なにかサンプルを書こう!と思い立ちました。
P/Invokeするからには、何かしらネイティブライブラリを用意しなければなりません。
ところが、私は諸般の事情によりC言語を書くことができない体になってしまいましたので、C言語の代わりにRustでネイティブライブラリを作成してみようかと思います。
なお、やんごとなき事情によってiOSのみで動作確認をしています。
また、なんとなく既視感のある記事かもしれませんが、ご容赦ください…。
結論
取り急ぎ結論です。
CではなくRustで作ったライブラリだからといって、別段変わった点はなく、問題なくC#から呼び出すことができました。
「Xamarinを採用しようか検討しているけれど、Rust製のソフトウェア資産が活用できるかどうかだけが気がかりだった」という皆様におかれましては、どうぞ安心してXamarinを採用いただければと思います。
以下は作業記録です。
作業記録
サンプル
リポジトリ
- Rust : https://github.com/pochi1002/rustlib
- C#(Xamarin) : https://github.com/pochi1002/AppWithRust
デモ
ボタンを押すと足し算(1+2)をして、結果を表示するアプリです。
この足し算処理(1+2)をRustでかいてるんですよ!!!!!
準備
Xcodeのインストール
最新版をぜひ。
Visual studio for macのインストール
Xamarin studioでもいいです。(むしろ、たぶんそのほうがよい)
(2016/12/12現在、まだPreview版です)
rustupのインストール
手軽に、様々なプラットフォーム向けのRustのツールチェインをインストールできます。
(2016/12/12現在、まだBetaです)
cargo-lipoのインストール
とても手軽にUniversal Libraryの作成ができるようになります。
インストール後は、cargo-lipoのREADMEに書いてあるように、以下のようにしてiOS向けのtargetを追加しておきます。
$ rustup target add aarch64-apple-ios
$ rustup target add armv7-apple-ios
$ rustup target add armv7s-apple-ios
$ rustup target add i386-apple-ios
$ rustup target add x86_64-apple-ios
Rustでネイティブライブラリをつくる
プロジェクトの雛形作成
まず、cargo new
コマンドでプロジェクトの雛形を作ります。以下のファイルが生成されます。
src/lib.rs # ソースファイル
Cargo.toml # Cargoの設定ファイル。依存関係やビルド設定なんかを指定したりするみたい
コーディング
src/lib.rsを編集します。
こんなメソッドを用意してみます。ただの足し算です。
#[no_mangle]
pub extern fn rust_sum(x:i32, y:i32) -> i32{
x + y
}
ポイントは、[no_mangle]というAttributeをつけて、外向けのシンボルがメソッド名そのままとなるようにしています。
ビルド設定
また、staticlibを生成するため、Cargo.tomlに以下の記述も追加します。
- ライブラリ名はrustlib
- 生成するバイナリはスタティックライブラリ
[lib]
name="rustlib"
crate-type=["staticlib"]
ビルド
cargo lipo
コマンドを実行します。
すると、target/universal以下にUniversal binaryが生成されているはずです。
きちんとrust_sumメソッドがエクスポートされているか、nmコマンドで確認してみましょう。
$ nm target/universal/debug/librustlib.a | grep rust_sum
0000000000000000 T _rust_sum
大丈夫そうですね。
Xamarin側
プロジェクトの作成
お好みのXamarinプロジェクトを作成します。サンプルではXamarin.Formsプロジェクト(Shared)としました。
コーディング
NativeMethodsクラスを作成し、その中に、先ほど作成したネイティブライブラリがエクスポートしているメソッドの定義を作成します。
public static class NativeMethods
{
[DllImport("rustlib", CallingConvention=CallingConvention.Cdecl)]
internal static extern int rust_sum(int x, int y);
}
- ライブラリ名は、rustlib
- メソッド名は、Rust側で定義したとおりrust_sum
WindowsやAndroid環境では、これできちんとネイティブライブラリのメソッドを呼び出すことができます(たぶん)。
ところが、iOSではそうはいきません。rust_sumを呼び出すと例外が発生してしまいます。なんででしょう。困りましたね。
dllmap
DllImport属性にはdllNameというパラメータがあり、ここには呼び出すAPIを持つライブラリ名を指定します。
例えば、先ほど定義したrust_sumメソッドを呼び出す場合の動作は、ざっくり以下のような感じです。
- "rustlib.dll"だとか、"librustlib.so"だとか、そんな名前を持つライブラリを見つけだして読み込む(結構柔軟に探してくれる)
- 該当する名前を持つライブラリから、rust_sumという名前のシンボルを見つけ出して呼び出す
ところがXamarin.iOSでは、参照するライブラリはビルド時に静的リンクしてしまうので、ライブラリ名もへったくれもありません。なので、**DllImport属性のdllNameには"__Internal"を指定してね!**というお約束になっています。
じゃあ、dllImport("__Internal")
するか…というと、**今度はiOS以外で動かなくなります。**Android向けにはきちんとdllNameを指定したいけど、iOSでは"__Internal"にしなければいけません。
そんな悩みを解決するのがdllmapです。
ネイティブライブラリを呼び出すアセンブリのプロジェクト(サンプルコードではAppWithRust.iOS)にconfigファイルを作成し、以下のように記述します。1
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<dllmap dll="rustlib" target="__Internal"/>
</configuration>
すると、DllImport属性のdllNameに"rustlib"が指定されているとき、それを"__Internal"と読み替えてくれます。便利ですね。
ネイティブライブラリへの参照の追加
プロジェクトのNative Referencesに、先ほど生成したネイティブライブラリ(librustlib.a)を追加し、Propertyの「Force Load」をTrueにしておきます。
ライブラリを呼び出す
staticクラスのstaticメソッドを呼ぶのと何ら変わりません。サンプルではこんな感じで呼び出しています。
var intValue = NativeMethods.rust_sum(1, 2);
結果、ちゃんと3が返ってきました。素晴らしい足し算ですね。
まとめ
Rustの環境構築するのに右往左往したのでやたらと時間はかかってしまいましたが、
環境が整ってさえしまえば、あとはほとんど手間もなく、ハマることもありませんでした。足し算しかしてませんしね…。
そもそも、言語がRustであろうが何だろうが、C形式のABIを提供するネイティブライブラリを生成できているわけですから、それに対するXamarin側からの呼び出しに問題がないのは、当たり前といえば当たり前なんですが…。
あとは、構造体とか文字列の受け渡しとか、RustからのコールバックをC#側で受け取ったりとか、そこらへんまで確認できるといいですね。
参照
https://github.com/TimNN/cargo-lipo
https://www.rustup.rs/
rustup で Rust コンパイラーを簡単インストール
http://www.mono-project.com/docs/advanced/pinvoke/dllmap/
-
アプリのプロジェクトであれば、ファイル名は"app.config"でOKです。Dllのプロジェクトだったりすると、"<<アセンブリ名>>.config"だった気がします ↩