Rust版voicevox_coreのC# Bindingを作るために調べる
昔のC++実装のBindingは手作業で書いていたけど
最近 csbindgen っていうツールが出たので、それを使ってBindingの箇所を楽したい
こっちもRustだしね!
READMEを読んでみると、FFIコードが書かれているファイルを input_extern_file
で指定すればそのまま吐き出せそうなので、
voicevox_coreのc_apiが定義されているcratesを見て
このファイルを指定してbuildしてみよう
// 1.3.0 を使用
csbindgen::Builder::default()
.input_extern_file("./voicevox_core/crates/voicevox_core_c_api/src/lib.rs")
.csharp_dll_name("voicevox_core")
.csharp_class_name("CoreUnmanaged")
.csharp_namespace("VoicevoxEngineSharp.Core.Native")
.generate_csharp_file("../src/Native/CoreUnmanaged.g.cs")
.unwrap();
c_apiのlib.rsに書いてあるドキュメントがちょろっとバインディングコードに載って生成されていい感じ
生成された感じ良さそうなんだけど
/// <summary>@return 結果コード #VoicevoxResultCode</summary>
[DllImport(__DllName, EntryPoint = "voicevox_initialize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern VoicevoxResultCode voicevox_initialize(VoicevoxInitializeOptions options);
この VoicevoxResultCode
っていうenumが生成されてないみたい
この VoicevoxResultCode
は他のcratesに定義されていたやつっぽい
csbindgen は特にcargoとかでプロジェクトを見ているわけではなさそうなので、他のファイルに分散していると難しそうかも
方針転換でREADMEに書いてある、C(to Rust) to C# で触れられている方法で進めてみる
voicevox_core のビルドタスクにもあるように、cbindgen でC向けのHeaderを出力して、それを使って bindgen でRustのFFIコードを生成…みたいな流れで進めてみる
まずは cbindgen を触ってみる
// 0.24.3
cbindgen::generate("./voicevox_core/crates/voicevox_core_c_api")?
.write_to_file("./generated/voicevox_core.g.h");
こんな感じでHeaderが吐き出された
config 変えたらどうなるかを試すためにどうすればいいかなとなったんだけど
let mut config =
cbindgen::Config::from_root_or_default("./voicevox_core/crates/voicevox_core_c_api");
config.language = cbindgen::Language::C;
cbindgen::generate_with_config("./voicevox_core/crates/voicevox_core_c_api", config)?
.write_to_file("./generated/voicevox_core.g.h");
こんな感じで default のconfigを作って設定を変えれば良さそう
次は cbindgen で生成されたHeaderを使って bindgen でFFIコードを作成する
bindgen::Builder::default()
.header("./generated/voicevox_core.g.h")
.generate()
?.write_to_file("./generated/voicevox_core.g.rs");
こんな感じでとりあえず生成された
生成されたコードを眺めてみると enum が enum っぽくない感じで表現されている…
#[doc = " 実行環境に合った適切なハードウェアアクセラレーションモードを選択する"]
pub const VoicevoxAccelerationMode_VOICEVOX_ACCELERATION_MODE_AUTO: VoicevoxAccelerationMode = 0;
#[doc = " ハードウェアアクセラレーションモードを\"CPU\"に設定する"]
pub const VoicevoxAccelerationMode_VOICEVOX_ACCELERATION_MODE_CPU: VoicevoxAccelerationMode = 1;
#[doc = " ハードウェアアクセラレーションモードを\"GPU\"に設定する"]
pub const VoicevoxAccelerationMode_VOICEVOX_ACCELERATION_MODE_GPU: VoicevoxAccelerationMode = 2;
pub type VoicevoxAccelerationMode = i32;
そのこともあり、C# のBindingのコードも返り値の型がintになってしまった…
enumにもいくつかstyleがあるっぽいのでいくつか試してみたら
bindgen::Builder::default()
+ .default_enum_style(bindgen::EnumVariation::Rust {
+ non_exhaustive: false,
+ })
.header("./generated/voicevox_core.g.h")
.generate()
?.write_to_file("./generated/voicevox_core.g.rs");
こんな感じで生成されたのが一番それっぽい感じに見える
#[repr(u32)]
#[doc = " ハードウェアアクセラレーションモードを設定する設定値"]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum VoicevoxAccelerationMode {
#[doc = " 実行環境に合った適切なハードウェアアクセラレーションモードを選択する"]
VOICEVOX_ACCELERATION_MODE_AUTO = 0,
#[doc = " ハードウェアアクセラレーションモードを\"CPU\"に設定する"]
VOICEVOX_ACCELERATION_MODE_CPU = 1,
#[doc = " ハードウェアアクセラレーションモードを\"GPU\"に設定する"]
VOICEVOX_ACCELERATION_MODE_GPU = 2,
}
pub type VoicevoxAccelerationMode = i32;
しかし、#[repr(u32)]
?????????
下では i32 になってるが……
というか、同名定義していて不正なコードでは
元々のHeaderはこんな感じ
/**
* ハードウェアアクセラレーションモードを設定する設定値
*/
enum VoicevoxAccelerationMode
#ifdef __cplusplus
: int32_t
#endif // __cplusplus
{
/**
* 実行環境に合った適切なハードウェアアクセラレーションモードを選択する
*/
VOICEVOX_ACCELERATION_MODE_AUTO = 0,
/**
* ハードウェアアクセラレーションモードを"CPU"に設定する
*/
VOICEVOX_ACCELERATION_MODE_CPU = 1,
/**
* ハードウェアアクセラレーションモードを"GPU"に設定する
*/
VOICEVOX_ACCELERATION_MODE_GPU = 2,
};
#ifndef __cplusplus
typedef int32_t VoicevoxAccelerationMode;
#endif // __cplusplus
この typedef 使っちゃった感じなのかな
型が異なるのは
この辺りのIssue?あとtypedefとenumを同時に吐き出さないのはEnumVariation::Rust
で使ってるから対策できない感じかな
とりあえず生成されたコードを触るのは好ましくないけど
let generated_source = bindgen::Builder::default()
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
.header("./generated/voicevox_core.g.h")
.generate()?
.to_string()
// i32なのにu32に変わっている
.replace("#[repr(u32)]", "#[repr(i32)]")
// 同名のenumが存在かつpub typeでaliasを張っているとcsbindgenでi32に置き換えられてしまうことの対策
// むしろpub typeが吐き出されることがおかしいのでは…?defined multiple timesで怒られるはず
.replace(
"pub type VoicevoxAccelerationMode = i32;",
"// pub type VoicevoxAccelerationMode = i32;",
)
.replace(
"pub type VoicevoxResultCode = i32;",
"// pub type VoicevoxResultCode = i32;",
);
fs::write("./generated/voicevox_core.g.rs", generated_source)?;
csbindgen::Builder::default()
.input_bindgen_file("./generated/voicevox_core.g.rs")
// input_extern_file できれいな型が出力されるが
// voicevox_core_c_apiが依存しているvoicevox_coreのresult_codeを解決できない
// そのためcbindgenとbindgenから出力されるファイルを使用しbindingを生成している
// .input_extern_file("./voicevox_core/crates/voicevox_core_c_api/src/lib.rs")
.csharp_dll_name("voicevox_core")
.csharp_class_name("CoreUnmanaged")
.csharp_namespace("VoicevoxEngineSharp.Core.Native")
.generate_csharp_file("../src/Native/CoreUnmanaged.g.cs")
.unwrap();
こんな感じで書き換えたFFIコードを csbindgen に食わせたらいい感じになった
この辺りのIssue見たらBuilderにCallback食わせられるみたいなので、手でreplaceしなくてもいい感じに出来たりするのかな
HashSetじゃなくてHashMapにして、TypeKindとかを保持するようにする
has_typedef: boolじゃなくて、maybe_typekind: Option<TypeKind>とかにして
EnumVariation::Rust { .. } => {
if let Some(tk) = maybe_typedef {
attrs.insert(0, quote! { #[repr( ここにTypeKind???? )] });
} else {
// `repr` is guaranteed to be Rustified in Enum::codegen
attrs.insert(0, quote! { #[repr( #repr )] });
}
let tokens = quote!();
EnumBuilder::Rust {
attrs,
ident,
tokens,
emitted_any_variants: false,
}
}
とかで上手く行かないかなと見てみたけど、どうもだめそうだなぁ
csbindgen の 1.4.0 で複数のextern_fileがサポートされたため、そっちで試してみた
csbindgen::Builder::default()
// 追加順依存になるため、自分で依存される側のコードを先に解決する必要がある
.input_extern_file("./voicevox_core/crates/voicevox_core/src/result_code.rs")
.input_extern_file("./voicevox_core/crates/voicevox_core_c_api/src/lib.rs")
.csharp_dll_name("voicevox_core")
.csharp_class_name("CoreUnmanaged")
.csharp_namespace("VoicevoxEngineSharp.Core.Native")
.generate_csharp_file("../src/Native/CoreUnmanaged.g.cs")
.unwrap();
こんな感じで実行してみたら、1.3.0では含まれていなかった VoicevoxResultCode
のenumが生成されるようになった 🎉
input_extern_fileから生成しているため、コードにdocumentも追加されるので、これは嬉しい
コメントにも書いたように input_extern_file
の順序がキモなので、ここは注意が必要
これで cbindgen と bindgen が不要になったため、コード生成がかなりシンプルになった
しばらく csbindgen でコード生成をしていたが、[u8; 16]
のような固定長スライスを含む関数が出力されていないことに気づいた。
元の Rust コードがこんな感じで
bindgen で出力されるコードがこんな感じ
そのため再度 csbindgen のみで対応するのを諦めて、bindgen 経由でコードの生成を行うようにした。
ここは手書き部分なのだが、なかなかに気づくのが難しいのでどうしたものか…
どうすれば対応できるか csbindgen のコードをもうちょっと追ってみたい。
実は固定長スライスが問題なのではなくて、例えば NonNull
とか Box
が問題だったりするのだろうか。