2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

csbindgenを使ってみる

Last updated at Posted at 2024-09-30

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に必要なツールをインストールしてください。

また、今回の説明では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で開いたら以下のような感じになっていると思います。
image.png

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>と同一階層に追記してください。

ConsoleApp.csproj
<Project Sdk="Microsoft.NET.Sdk">

 <PropertyGroup>
   <!-- 省略 -->
 </PropertyGroup>
  
 <ItemGroup>
   <ProjectReference Include="..\..\src\CsbindgenHandsOn\CsbindgenHandsOn.csproj" />
 </ItemGroup>

</Project>

csbindgenの参照追加

コンソールアプリにFFIライブラリへの参照を追加します。
CsbindgenHandsOn.csprojを開いて以下の内容を<PropertyGroup>と同一階層に追記してください。

CsbindgenHandsOn.csproj
<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を開いて以下のコードと追記してください。

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.propsslnファイルと同階層に作成してください。ここはlinux/wslの人と、windowsの人で異なるので注意が必要です。

WSLの人

Directory.Build.props
<Project>
  <PropertyGroup>
    <RuntimeIdentifire>linux-x64</RuntimeIdentifire>
  </PropertyGroup>
</Project>

Windowsの人

Directory.Build.props
<Project>
  <PropertyGroup>
    <RuntimeIdentifire>win-x64</RuntimeIdentifire>
  </PropertyGroup>
</Project>

Rustセットアップ

FFI用のRustライブラリを作成します。vscodeで新しいターミナルを開いて以下のコマンドを実行してください。

cd ./src
cargo new --lib ./libcsbindgenhandson

Rustパッケージ設定

Cargo.tomlに以下の内容を追記してください。

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をリロードしましょう。

.vscode/settings.json
{
    "rust-analyzer.linkedProjects": [
        "src/libcsbindgenhandson/Cargo.toml"
    ],
}

Rustの関数を呼び出してみよう

ここからは実際にRustの関数を呼び出してみましょう。まず以下のようにlib.rsを書き換えます。

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が作成されるはずです。

build.rs
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();
}

また、CsbindgenHandsOnNativeMethods.DllImportResolver.csを作成して以下のコードをかきこみます。

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にしましょう。

CsbindgenHandsOn/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のコードを以下のコードに書き換えて実行してみましょう!  

ConsoleApp/Program.cs
using CsbindgenHandsOn;

var c = new BasicFunctionCall();
c.CallRustAdd(1,1);

ここまでできていれば。実行するとrust_add(1, 1): 2と表示されるはずです。

Rust -> C#時のcsbindgenの使い方

build.rscsbindgen::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

Cの関数を呼び出してみよう

それでは、Cの関数を呼び出してみましょう。

C/Rust側の準備

始めに、libcsbindgenhandson/srccディレクトリを作り、myMath.hを作成し以下のコードを書き込みます。

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を作成し以下のコードを書き込みます。

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.rsmain関数に書き込みましょう。
こうすることで、C -> RustのFFIコード生成(bindgen)とCのコンパイル(cc)をしてくれます。

build.rs
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の先頭に下記コードを追記します。

lib.rs
#[allow(non_snake_case)]
mod myMath;

#[allow(non_snake_case)]
mod myMath_ffi;

これをしないと生成されたmyMathmyMath_ffiをrustコンパイラが認識しないため、なぜかコンパイルしても関数がないとか言われる事態になります。

C#の準備

まず、CsbindgenHandsOn/BasicFunctionCall.csのクラスに以下の関数を足します。

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のコードに以下のコードを追加して実行しましょう!

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.rscsbindgen::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_prefixcsharp_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に以下のコードを追記します。

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のクラスに以下の関数を足します。

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のコードに以下のコードを追加して実行しましょう!

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を作成し以下のコードを書き込みます。

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を作成し以下のコードを書き込みます。

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.rsmain関数に下記コードを追記します。

build.rs
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コンパイラに生成したコードを認識させます。

lib.rs
#[allow(non_snake_case)]
mod myStack;

#[allow(non_snake_case)]
mod myStack_ffi;

C#の準備

始めに、CsbindgenHandsOn/NativeディレクトリにCNativeMethodsMyStack.csファイルを作成し下記コードを書き込みます。

CsbindgenHandsOn/Native/CNativeMethodsMyStack.cs
using GroupedNativeMethodsGenerator;

namespace CsbindgenHandsOn.Native
{
    [GroupedNativeMethods(removePrefix: "myStack")]
    internal static unsafe partial class CNativeMethodsMyStack { }
}

次に、CsbindgenHandsOnディレクトリにTestGroupedNativeMethods.csファイルを作成し下記コードを書き込みます。

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のコードに以下のコードを追加して実行しましょう!

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#ライフを!

2
2
0

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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?