Open16

Rust版voicevox_coreのC# Bindingを作るために調べる

ちゅうこちゅうこ

READMEを読んでみると、FFIコードが書かれているファイルを input_extern_file で指定すればそのまま吐き出せそうなので、
voicevox_coreのc_apiが定義されているcratesを見て

https://github.com/VOICEVOX/voicevox_core/blob/a812bb8acc182a50786abf1e36d0ac4703e0027b/crates/voicevox_core_c_api/src/lib.rs

このファイルを指定して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が生成されてないみたい

ちゅうこちゅうこ

まずは 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になってしまった…

ちゅうこちゅうこ

https://docs.rs/bindgen/latest/bindgen/struct.Builder.html#enums
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 使っちゃった感じなのかな

ちゅうこちゅうこ

この typedef は #ifndef __cplusplus で、C++ 環境じゃなかったら int32_t で扱うよみたいなやつだから、コード生成の時に C++ だぞって指定すれば出来そう。

https://docs.rs/bindgen/latest/bindgen/struct.Builder.html#clang-arguments

ここで Clang の引数を指定できるので、-x オプションで C++ ファイルとして解釈させてみた。

https://github.com/yamachu/VoicevoxCoreSharp/blob/f98fe6faf60ccc2fb4de9f690dff79d279727d51/binding/build.rs#L4-L13

そうしたら問題になっていた typedef がなくなったため、正常に enum が吐き出されるようになった。

ちゅうこちゅうこ

とりあえず生成されたコードを触るのは好ましくないけど

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 に食わせたらいい感じになった

ちゅうこちゅうこ

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 コードがこんな感じで

https://github.com/VOICEVOX/voicevox_core/blob/e571d702b4c7e718ff1786fce0ee1892919a570d/crates/voicevox_core_c_api/src/lib.rs#L1059-L1075

bindgen で出力されるコードがこんな感じ
https://github.com/VOICEVOX/voicevox_core/blob/e571d702b4c7e718ff1786fce0ee1892919a570d/crates/voicevox_core_c_api/include/voicevox_core.h#L1078-L1080

そのため再度 csbindgen のみで対応するのを諦めて、bindgen 経由でコードの生成を行うようにした。

ここは手書き部分なのだが、なかなかに気づくのが難しいのでどうしたものか…

https://github.com/yamachu/VoicevoxCoreSharp/blob/f98fe6faf60ccc2fb4de9f690dff79d279727d51/src/VoicevoxCoreSharp.Core/Native/CoreUnsafe_.cs#L6-L17

どうすれば対応できるか csbindgen のコードをもうちょっと追ってみたい。
実は固定長スライスが問題なのではなくて、例えば NonNull とか Box が問題だったりするのだろうか。