csbindgenってなに
nuneccさんが開発したライブラリで、C/C++ -> C#, Rust -> C#のFFI補助ツールとなっています。
FFI(Foreign Function Interface)とは
簡単にいうと別言語の関数を呼び出す機能で、たいてい既存のライブラリ(特にC/C++)を別言語でも使いたい時に用いられています。
csbindgenのここがすごい
ここからはcsbinfgenの利点を書いていきます。
[DLLImport]
の自動化
今まではFFIで使用したい関数すべてにつけないといけなかったのが、csbindgenを用いることで自動生成されるようになります。
このコード×数十とかいう日には苦痛でしかなかったのでありがたすぎます。
[DllImport("foge", EntryPoint = "foge_fuga",
CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern int fuga(int a);
オブジェクト指向的にFFI元関数を呼び出せる
FFIの都合上関数のみ呼び出せるのでfoge.fuga()
ではなくNativeMethods.fuga(hoge)
のようになってしまいます。
ここでGroupedNativeMethods
を用いると、ソースコードジェネレータでfoge.fuga()
用のfuga()
を自動生成してくれます。
RustとCの両方からFFIができる
csbindgen
によってRust -> C#のFFIが簡単にできるようになりました。また、Cのコードはbindgen
という別ライブラリでC->Rustを実現できます。
これを組み合わせてCの処理をRustで整えてC#に渡すといったことが簡単にできます。Cのコードベースが膨大でRust/C#に完全には移せないときにはかなり有効となります。
コンパイルが簡単
これはcsbindgenの機能ではないのですが、C/C++のコンパイルをRustのcc
/cmake
クレートを用いることで、楽にC/C++/Rust統合ネイティブライブラリを作成できるので、ネイティブライブラリ生成もcargo
のビルドシステムに乗せることでさらに簡略化できます。
さわってみる
環境の準備
以下のサイトから、csbindgenとbindgenに必要なツールをインストールしてください。
- rustのインストール
- NET 8のインストール
- LLVM(clang)のインストール
また、今回の説明ではvscodeを用いているのでそちらの方でやるとよいです。
- vscodeのインストール
プロジェクトの準備
今回の勉強会用リポジトリを立てたのでそちらからクローンしてきてください。
git clone https://github.com/aiueo-1234/csbindgen-handson.git
# sshがいい人は git clone git@github.com:aiueo-1234/csbindgen-handson.git
以下のリンクに今回使用したコードがあります。
手元で確認はいいやって方はこちらを参照してください
https://github.com/aiueo-1234/csbindgen-handson/tree/kc3
クローンしてvscodeで開いたら以下のような感じになっていると思います。
C#セットアップ
ソリューションファイルとFFI用ライブラリ・実行用コンソールアプリを作成します。
以下のコマンドをvscodeのターミナルで実行すると作成できます。
# wsl用
dotnet new sln -n CsbindgenHandsOn
dotnet new classlib -n CsbindgenHandsOn -o src/CsbindgenHandsOn
dotnet new console -n ConsoleApp -o sandbox/ConsoleApp
dotnet sln add src/CsbindgenHandsOn/CsbindgenHandsOn.csproj
dotnet sln add sandbox/ConsoleApp/ConsoleApp.csproj
なお、以降Windows本体で行っている人は適宜/
(スラッシュ)を\
(バックスラッシュ)か¥
に読み替えてください。
C#の依存関係設定
コンソールアプリにFFIライブラリへの参照を追加します。
ConsoleApp.csproj
を開いて以下の内容を<PropertyGroup>
と同一階層に追記してください。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- 省略 -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CsbindgenHandsOn\CsbindgenHandsOn.csproj" />
</ItemGroup>
</Project>
csbindgenの参照追加
コンソールアプリにFFIライブラリへの参照を追加します。
CsbindgenHandsOn.csproj
を開いて以下の内容を<PropertyGroup>
と同一階層に追記してください。
<ItemGroup>
<PackageReference Include="csbindgen" Version="1.9.3">
<IncludeAssets>
runtime; build; native; contentfiles; analyzers; buildtransitive
</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
C#ビルド設定
CsbindgenHandsOn
プロジェクトに以下の変更を加えます。
- unsafeコードを許可
- rustライブラリをC#のバイナリ側にコピーする設定
- rustのコンパイルをしてC#のプロジェクトにコピーする設定
CsbindgenHandsOn.csproj
を開いて以下のコードと追記してください。
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- ここ -->
</PropertyGroup>
<ItemGroup>
<None Include="runtimes\$(RuntimeIdentifire)\native\csbindgenhandson.dll"
Condition="$(RuntimeIdentifire.StartsWith(win))">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="runtimes\$(RuntimeIdentifire)\native\libcsbindgenhandson.so"
Condition="$(RuntimeIdentifire.StartsWith(linux))">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(Configuration)'=='Debug'">
<Exec Command="cargo build"
WorkingDirectory="$([System.IO.Path]::Combine($(ProjectDir),../libcsbindgenhandson))" />
<Copy Condition="$(RuntimeIdentifire.StartsWith(win))"
SourceFiles="$([System.IO.Path]::Combine($(ProjectDir),../libcsbindgenhandson/target/debug/csbindgenhandson.dll))"
DestinationFolder="$([System.IO.Path]::Combine($(ProjectDir),../CsbindgenHandsOn,runtimes\$(RuntimeIdentifire)\native))" />
<Copy Condition="$(RuntimeIdentifire.StartsWith(linux))"
SourceFiles="$([System.IO.Path]::Combine($(ProjectDir),../libcsbindgenhandson/target/debug/libcsbindgenhandson.so))"
DestinationFolder="$([System.IO.Path]::Combine($(ProjectDir),../CsbindgenHandsOn,runtimes\$(RuntimeIdentifire)\native))" />
</Target>
RuntimeIdentifireの決定
RIDを決めるため、Directory.Build.props
をsln
ファイルと同階層に作成してください。ここはlinux/wslの人と、windowsの人で異なるので注意が必要です。
WSLの人
<Project>
<PropertyGroup>
<RuntimeIdentifire>linux-x64</RuntimeIdentifire>
</PropertyGroup>
</Project>
Windowsの人
<Project>
<PropertyGroup>
<RuntimeIdentifire>win-x64</RuntimeIdentifire>
</PropertyGroup>
</Project>
Rustセットアップ
FFI用のRustライブラリを作成します。vscodeで新しいターミナルを開いて以下のコマンドを実行してください。
cd ./src
cargo new --lib ./libcsbindgenhandson
Rustパッケージ設定
Cargo.toml
に以下の内容を追記してください。
[lib]
crate-type = ["cdylib"]
name = "csbindgenhandson"
[build-dependencies]
csbindgen = "1.9.3"
cc = "1.1"
bindgen = "0.70"
rust-analyzer設定
.vscode/settings.json
を作成し以下の内容を追記してください。ここまで出来たらvscodeをリロードしましょう。
{
"rust-analyzer.linkedProjects": [
"src/libcsbindgenhandson/Cargo.toml"
],
}
Rustの関数を呼び出してみよう
ここからは実際にRustの関数を呼び出してみましょう。まず以下のようにlib.rs
を書き換えます。
#[no_mangle]
pub extern "C" fn rust_add(x: i32, y: i32) -> i32 {
x + y
}
次に、Cargo.toml
と同じ階層にbuild.rs
を作成して以下のコードを書き込みましょう。しばらくするとCsbindgenHandsOn/Native/NativeMethods.g.cs
が作成されるはずです。
fn main(){
csbindgen::Builder::default()
.input_extern_file("src/lib.rs")
.csharp_dll_name("csbindgenhandson")
.csharp_namespace("CsbindgenHandsOn.Native")
.generate_csharp_file("../CsbindgenHandsOn/Native/NativeMethods.g.cs")
.unwrap();
}
また、CsbindgenHandsOn
にNativeMethods.DllImportResolver.cs
を作成して以下のコードをかきこみます。
using System.Reflection;
using System.Runtime.InteropServices;
namespace CsbindgenHandsOn.Native
{
internal static unsafe partial class NativeMethods
{
static NativeMethods()
{
NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
}
static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName == __DllName)
{
var path = "runtimes/";
var extension = "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
path += "win-";
extension = ".dll";
}
else
{
path += "linux-";
extension = ".so";
}
if (RuntimeInformation.OSArchitecture == Architecture.X64)
{
path += "x64";
}
else if (RuntimeInformation.OSArchitecture == Architecture.Arm64)
{
path += "arm64";
}
path += $"/native/{(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)?"lib":"")}{__DllName}{extension}";
return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
}
return IntPtr.Zero;
}
}
}
さらに、CsbindgenHandsOn/Class1.cs
のコードを以下のコードに書き換えて、ファイルをBasicFunctionCall.cs
にしましょう。
namespace CsbindgenHandsOn;
public class BasicFunctionCall
{
public void CallRustAdd(int a, int b){
Console.WriteLine($"rust_add({a}, {b}): {Native.NativeMethods.rust_add(a,b)}");
}
}
最後に、ConsoleApp/Program.cs
のコードを以下のコードに書き換えて実行してみましょう!
using CsbindgenHandsOn;
var c = new BasicFunctionCall();
c.CallRustAdd(1,1);
ここまでできていれば。実行するとrust_add(1, 1): 2
と表示されるはずです。
Rust -> C#時のcsbindgenの使い方
build.rs
でcsbindgen::Builder::default()
に対して以下の関数をチェーンすれば使うことができます。
- input_extern_file("rust_file_path")
- FFI元の関数が書かれたファイルのパスを渡します
- csharp_dll_name("dll_name")
-
[DllImport]
のdllName
。つまりRustのライブラリ名です- 今回は
Cargp.toml
の[lib]
のname
を設定したのでそれと同じにします
- 今回は
-
- csharp_namespace("csharp_namespace")
- 生成したFFI用C#クラスの名前空間を設定します
- generate_csharp_file("csharp_file_path")
- 生成したFFI用C#クラスの出力先を指定します
- これを一番最後に呼びましょう
Rust側のコード解説
初めて見るキーワードが出てきた人もいるかもしれないので、ちょっと排泄しておきます。
- #[no_mangle]
- Rustコンパイラは、シンボル名をネイティブコードリンカが期待するものとは異なるものにマングル(難読化)するのでそれを防ぎます
- pub extern "C"
- ABI(Application Binary Interface)をCコンパイラがサポートするものにします
- C#のFFIもこの形式をサポートしているのでこれを設定しましょう
C#側のコード解説
- DllImportResolver
- 結びつけるネイティブライブラリを指定するために用います
- windowsとlinux,WSLではライブラリの拡張子やプレフィックスの有無などの違いがあるのでこれを用いて調整しましょう
- Windows
- 拡張子:.dll
- プレフィックス:なし
- linux, WSL
- 拡張子:.so
- プレフィックス:lib
- Windows
Cの関数を呼び出してみよう
それでは、Cの関数を呼び出してみましょう。
C/Rust側の準備
始めに、libcsbindgenhandson/src
にc
ディレクトリを作り、myMath.h
を作成し以下のコードを書き込みます。
#ifndef MYMATH_H_
#define MYMATH_H_
int myMath_mul(int a, int b);
int myMath_add(int a, int b);
#endif
次に、libcsbindgenhandson/src/c
ディレクトリで、myMath.c
を作成し以下のコードを書き込みます。
#include "myMath.h"
int myMath_mul(int a, int b){
return a * b;
}
int myMath_add(int a, int b){
return a + b;
}
さらに、以下のコードをbuild.rs
のmain
関数に書き込みましょう。
こうすることで、C -> RustのFFIコード生成(bindgen
)とCのコンパイル(cc
)をしてくれます。
bindgen::Builder::default()
.header("src/c/myMath.h").generate().unwrap()
.write_to_file("src/myMath.rs").unwrap();
cc::Build::new()
.file("src/c/myMath.c")
.try_compile("myMath").unwrap();
csbindgen::Builder::default()
.input_bindgen_file("src/myMath.rs")
.rust_method_prefix("cffi_")
.rust_file_header("use super::myMath::*;")
.csharp_entry_point_prefix("cffi_")
.csharp_dll_name("csbindgenhandson")
.csharp_namespace("CsbindgenHandsOn.Native")
.csharp_class_name("CNativeMethodsMyMath")
.generate_to_file(
"src/myMath_ffi.rs",
"../CsbindgenHandsOn/Native/CNativeMethodsMyMath.g.cs",
)
.unwrap();
また、lib.rs
の先頭に下記コードを追記します。
#[allow(non_snake_case)]
mod myMath;
#[allow(non_snake_case)]
mod myMath_ffi;
これをしないと生成されたmyMath
とmyMath_ffi
をrustコンパイラが認識しないため、なぜかコンパイルしても関数がないとか言われる事態になります。
C#の準備
まず、CsbindgenHandsOn/BasicFunctionCall.cs
のクラスに以下の関数を足します。
public void CallMyMathAdd(int a, int b){
Console.WriteLine($"myMath_add({a}, {b}): {Native.CNativeMethodsMyMath.myMath_add(a,b)}");
}
最後に、ConsoleApp/Program.cs
のコードに以下のコードを追加して実行しましょう!
c.CallMyMathAdd(2,2);
ここまでできていれば、実行すると前の章の結果に加えてmyMath_add(2, 2): 4
と表示されるはずです
C -> C#時のcsbindgenの使い方
まず、使いたいCファイルのヘッダファイルをbindgen
に読み込ませてC->RustのFFIコードを生成させます。
さらに、使いたいCファイルをcc
を用いてコンパイルしましょう。この時cc
がRustライブラリにCのコンパイル結果をバインドしてくれます。
bindgen
, cc
の詳しい使い方は本題ではないので省きますが、複雑なプログラムであればこれらのクレートを駆使することになります。
ここまでできたら、build.rs
でcsbindgen::Builder::default()
に対して以下の関数をチェーンします。
- input_bindgen_file("bindgen_file_path")
-
bindgen
が生成したファイルへのパス
-
- rust_file_header("use super::myMath::*;")
-
bindgen
が生成したファイルを、csbindgen
によって生成されたコードが読み込むためのuse
を書く
-
- rust_method_prefix("prefix_")
- csharp_entry_point_prefix("prefix_")
- RustとC#でのFFI関数をリンクする際にCの関数と被らないようにするためのもの。
-
rust_method_prefix
とcsharp_entry_point_prefix
は必ずそろえる
- csharp_dll_name("csbindgenhandson")
- csharp_namespace("CsbindgenHandsOn.Native")
- csharp_class_name("CNativeMethodsMyMath")
- 上三つはRust -> C#のときと同じです。
- generate_to_file("rust_ffi_path", "csharp_ffi_path")
- Rust, C#のFFIコードの出力先を指定します。
C -> Rust -> C#で連携してみる
これまでの知識をもちいて、Cの処理をRustで包んでC#に渡してみましょう。
具体的にはmyMath_mulをもちいてRustでpow関数を実装し、それをC#から呼べるようにしてみます。
始めに、lib.rs
に以下のコードを追記します。
use ::std::os::raw::c_int;
#[no_mangle]
pub unsafe extern "C" fn rust_pow(x: c_int, y: c_int) -> c_int {
let mut ret: c_int = 1;
for _ in 1..=y {
ret = myMath::myMath_mul(ret, x);
}
ret
}
次に、CsbindgenHandsOn/BasicFunctionCall.cs
のクラスに以下の関数を足します。
public void CallRustPow(int a, int b){
Console.WriteLine($"rust_pow({a}, {b}): {Native.NativeMethods.rust_pow(a,b)}");
}
最後に、ConsoleApp/Program.cs
のコードに以下のコードを追加して実行しましょう!
c.CallRustPow(2,3);
最終的な出力結果が以下のようになっているはずです。
rust_add(1, 1): 2
myMath_add(2, 2): 4
rust_pow(2, 3): 8
GroupedNativeMethodsを使ってみよう
csbindgen固有の機能となる、GroupedNativeMethodsを使ってみましょう。
GroupedNativeMethodsとは
オブジェクト指向的にFFI元関数を呼び出せるようにする機能です。
これは、FFIの都合上foge.fuga()
ではなくNativeMethods.fuga(hoge)
のようになってしまうため、ソースコードジェネレータでfoge.fuga()
用のfuga()
を自動生成してくれます。
今回はC言語でかなり簡素化したスタックを扱って試してみましょう。
C/Rust側の準備
始めに、libcsbindgenhandson/src/c
ディレクトリで、myStack.h
を作成し以下のコードを書き込みます。
#ifndef MYSTACK_H_
#define MYSTACK_H_
typedef struct MyStack
{
int index;
int *data;
} MyStack;
MyStack *myStack_create(int maxLength);
int myStack_pop(MyStack *myStack);
void myStack_push(MyStack *myStack, int val);
void myStack_delete(MyStack *myStack);
#endif
次に、libcsbindgenhandson/src/c
ディレクトリで、myStack.c
を作成し以下のコードを書き込みます。
#include "myStack.h"
#include <stdlib.h>
MyStack *myStack_create(int maxLength)
{
MyStack *ret = malloc(sizeof(MyStack));
if (ret == NULL)
{
return NULL;
}
int *data = malloc(sizeof(int) * maxLength);
if (data == NULL)
{
free(ret);
return NULL;
}
ret->index = -1;
ret->data = data;
return ret;
}
int myStack_pop(MyStack *myStack){
return myStack->data[myStack->index--];
}
void myStack_push(MyStack *myStack, int val){
myStack->data[++myStack->index]=val;
}
void myStack_delete(MyStack *myStack){
free(myStack->data);
free(myStack);
}
さらに、build.rs
のmain
関数に下記コードを追記します。
bindgen::Builder::default()
.header("src/c/myStack.h").generate().unwrap()
.write_to_file("src/myStack.rs").unwrap();
cc::Build::new()
.file("src/c/myStack.c").try_compile("myStack").unwrap();
csbindgen::Builder::default()
.input_bindgen_file("src/myStack.rs")
.rust_method_prefix("cffi_")
.rust_file_header("use super::myStack::*;")
.csharp_entry_point_prefix("cffi_")
.csharp_dll_name("csbindgenhandson")
.csharp_namespace("CsbindgenHandsOn.Native")
.csharp_class_name("CNativeMethodsMyStack")
.generate_to_file(
"src/myStack_ffi.rs",
"../CsbindgenHandsOn/Native/CNativeMethodsMyStack.g.cs",
).unwrap();
最後に、lib.rs
に下のコードを追記して、Rustコンパイラに生成したコードを認識させます。
#[allow(non_snake_case)]
mod myStack;
#[allow(non_snake_case)]
mod myStack_ffi;
C#の準備
始めに、CsbindgenHandsOn/Native
ディレクトリにCNativeMethodsMyStack.cs
ファイルを作成し下記コードを書き込みます。
using GroupedNativeMethodsGenerator;
namespace CsbindgenHandsOn.Native
{
[GroupedNativeMethods(removePrefix: "myStack")]
internal static unsafe partial class CNativeMethodsMyStack { }
}
次に、CsbindgenHandsOn
ディレクトリにTestGroupedNativeMethods.cs
ファイルを作成し下記コードを書き込みます。
using CsbindgenHandsOn.Native;
namespace CsbindgenHandsOn
{
public sealed unsafe class TestGroupedNativeMethods
: IDisposable
{
private bool _disposed;
private readonly MyStack* _stack;
public TestGroupedNativeMethods()
{
_stack = CNativeMethodsMyStack.myStack_create(5);
}
public void PushAndPop(ReadOnlySpan<int> numbers)
{
for (int i = 0; i < numbers.Length && i < 5; i++)
{
_stack->Push(numbers[i]);
Console.WriteLine($"pushed {numbers[i]}");
}
for (int i = 0;i < numbers.Length && i != 5; i++)
{
Console.WriteLine($"popped {_stack->Pop()}");
}
}
public void Dispose()
{
if (_disposed) return;
_stack->Delete();
_disposed = true;
}
}
}
最後に、ConsoleApp/Program.cs
のコードに以下のコードを追加して実行しましょう!
using var t = new TestGroupedNativeMethods();
t.PushAndPop([1,2,3,4,5]);
このコードの出力結果が以下のようになっているはずです。
pushed 1
pushed 2
pushed 3
pushed 4
pushed 5
popped 5
popped 4
popped 3
popped 2
popped 1
GroupedNativeMethodsの使い方
[GroupedNativeMethods]
属性をcsbindgen
で生成されたC#FFI用クラスに付けます。
この際、生成されたコードに対して属性をつけるのではなく、上書きされないように、partial
クラスで別ファイルにしてつけましょう。
[GroupedNativeMethods]
属性をつけたクラスに対してソースコードジェネレータがソースコードを生成してくれるので、bindgen
側でこの機能を使用したいメソッドをまとめるorヘッダファイルで分けてRust用コードを生成するのが楽です。
最後に
ここまでお付き合いくださりありがとうございました。
ここが違うとかがあればコメントで指摘していただけると幸いです。
それではよいC#ライフを!