#bindings #bindings-generator #c-sharp #code-generation

csbindgen

从 Rust 生成 C# FFI,自动将本地代码和 C 本地库带到 .NET 和 Unity

21 个稳定版本

1.9.3 2024 年 7 月 30 日
1.9.1 2024 年 3 月 8 日
1.8.3 2023 年 9 月 20 日
1.7.3 2023 年 5 月 22 日
0.1.2 2023 年 2 月 28 日

#15 in FFI

Download history 477/week @ 2024-05-04 405/week @ 2024-05-11 598/week @ 2024-05-18 840/week @ 2024-05-25 604/week @ 2024-06-01 1178/week @ 2024-06-08 2160/week @ 2024-06-15 2983/week @ 2024-06-22 2670/week @ 2024-06-29 2520/week @ 2024-07-06 2386/week @ 2024-07-13 1142/week @ 2024-07-20 3750/week @ 2024-07-27 1434/week @ 2024-08-03 5550/week @ 2024-08-10 1449/week @ 2024-08-17

12,282 每月下载量
用于 4 crates

MIT 许可证

120KB
2K SLoC

csbindgen

Crates Api Rustdoc

从 Rust 生成 C# FFI,自动将本地代码和 C 本地库带到 .NET 和 Unity。

自动从 Rust 的 extern "C" fn 代码生成 C# 的 DllImport 代码。由于 DllImport 默认使用 Windows 调用约定,并且需要大量配置以进行 C 调用,因此 csbindgen 生成的代码针对 "Cdecl" 调用进行了优化。此外,.NET 和 Unity 使用不同的回调调用方法(.NET 使用函数指针,而 Unity 使用 MonoPInvokeCallback),但您可以通过配置输出任一代码。

当与 Rust 的出色 C 集成一起使用时,您还可以将 C 库引入 C#。

使用 C 库与 C# 一起通常会有很多麻烦。不仅创建绑定很困难,跨平台构建也非常困难。在这个时代,您必须为多个平台和架构构建,包括 windows、osx、linux、android、ios,每个平台和架构都有 x64、x86、arm。

Rust 拥有出色的跨平台构建工具链,以及 cc 包cmake 包 允许将 C 代码集成到构建中。此外,rust-bindgen 可以从 .h 生成绑定,功能强大且非常稳定。

csbindgen 可以通过 Rust 轻松将本地 C 库引入 C#。csbindgen 生成 Rust extern 代码和 C# DllImport 代码,以与从 C 生成的代码配合使用。通过 cc 包或 cmake 包,将 C 代码链接到单个 rust 原生库。

展示

入门指南

Cargo.toml 中将其安装为 build-dependencies,并在 build.rs 上设置 bindgen::Builder

[package]
name = "example"
version = "0.1.0"

[lib]
crate-type = ["cdylib"]

[build-dependencies]
csbindgen = "1.8.0"

Rust 到 C#。

您可以将 Rust FFI 代码带到 C#。

// lib.rs, simple FFI code
#[no_mangle]
pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
    x + y
}

将 csbindgen 代码设置为 build.rs

fn main() {
    csbindgen::Builder::default()
        .input_extern_file("lib.rs")
        .csharp_dll_name("example")
        .generate_csharp_file("../dotnet/NativeMethods.g.cs")
        .unwrap();
}

csharp_dll_name 用于在 C# 端指定 [DllImport({DLL_NAME}, ...)],该名称应与 dll 二进制文件名称匹配。请参阅 #library-loading 部分,了解如何解决 dll 文件路径。

[!NOTE] 在此示例中,csharp_dll_name 的值由您设置的 Rust 项目输出。在上面的例子中,Cargo.toml 中的 package.name 被设置为 "example"。默认情况下,以下二进制文件应输出到 Rust 项目的 target/ 文件夹。

  • Windows: example.dll
  • Linux: libexample.so
  • macOS: libexample.dylib

应指定没有扩展名的文件名到 DllImport。请注意,在某些环境中,rust 编译器默认将文件名前缀为 "lib"。因此,如果您想在 macOS 上直接尝试此示例,csharp_dll_name 将为 "libexample"。

然后,让我们运行 cargo build,它将生成此 C# 代码。

// NativeMethods.g.cs
using System;
using System.Runtime.InteropServices;

namespace CsBindgen
{
    internal static unsafe partial class NativeMethods
    {
        const string __DllName = "example";

        [DllImport(__DllName, EntryPoint = "my_add", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern int my_add(int x, int y);
    }
}

C (到 Rust) 到 C#

例如,构建 lz4 压缩库。

// using bindgen, generate binding code
bindgen::Builder::default()
    .header("c/lz4/lz4.h")
    .generate().unwrap()
    .write_to_file("lz4.rs").unwrap();

// using cc, build and link c code
cc::Build::new().file("lz4.c").compile("lz4");

// csbindgen code, generate both rust ffi and C# dll import
csbindgen::Builder::default()
    .input_bindgen_file("lz4.rs")            // read from bindgen generated code
    .rust_file_header("use super::lz4::*;")     // import bindgen generated modules(struct/method)
    .csharp_entry_point_prefix("csbindgen_") // adjust same signature of rust method and C# EntryPoint
    .csharp_dll_name("liblz4")
    .generate_to_file("lz4_ffi.rs", "../dotnet/NativeMethods.lz4.g.cs")
    .unwrap();

它将生成如下代码。

// lz4_ffi.rs

#[allow(unused)]
use ::std::os::raw::*;

use super::lz4::*;

#[no_mangle]
pub unsafe extern "C" fn csbindgen_LZ4_compress_default(src: *const c_char, dst: *mut c_char, srcSize:  c_int, dstCapacity:  c_int) ->  c_int
{
    LZ4_compress_default(src, dst, srcSize, dstCapacity)
}
// NativeMethods.lz4.g.cs

using System;
using System.Runtime.InteropServices;

namespace CsBindgen
{
    internal static unsafe partial class NativeMethods
    {
        const string __DllName = "liblz4";

        [DllImport(__DllName, EntryPoint = "csbindgen_LZ4_compress_default", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern int LZ4_compress_default(byte* src, byte* dst, int srcSize, int dstCapacity);
    }
}

最后,在 lib.rs 中导入生成的模块。

// lib.rs, import generated codes.
#[allow(dead_code)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(non_upper_case_globals)]
mod lz4;

#[allow(dead_code)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
mod lz4_ffi;

构建器选项(配置模板)

构建器选项:Rust 到 C#

Rust 到 C#,使用 input_extern_file -> 设置选项 -> generate_csharp_file

csbindgen::Builder::default()
    .input_extern_file("src/lib.rs")        // required
    .csharp_dll_name("mynativelib")         // required
    .csharp_class_name("NativeMethods")     // optional, default: NativeMethods
    .csharp_namespace("CsBindgen")          // optional, default: CsBindgen
    .csharp_class_accessibility("internal") // optional, default: internal
    .csharp_entry_point_prefix("")          // optional, default: ""
    .csharp_method_prefix("")               // optional, default: ""
    .csharp_use_function_pointer(true)      // optional, default: true
    .csharp_disable_emit_dll_name(false)    // optional, default: false
    .csharp_imported_namespaces("MyLib")    // optional, default: empty
    .csharp_generate_const_filter (|_|false) // optional, default: `|_|false`
    .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR", "__Internal") // optional, default: ""
    .csharp_type_rename(|rust_type_name| match rust_type_name {     // optional, default: `|x| x`
        "FfiConfiguration" => "Configuration".into(),
        _ => x,
    })
    .generate_csharp_file("../dotnet-sandbox/NativeMethods.cs")     // required
    .unwrap();

csharp_* 配置将嵌入到输出文件的占位符中。

using System;
using System.Runtime.InteropServices;
using {csharp_imported_namespaces};

namespace {csharp_namespace}
{
    {csharp_class_accessibility} static unsafe partial class {csharp_class_name}
    {
#if {csharp_dll_name_if(if_symbol,...)}
        const string __DllName = "{csharp_dll_name_if(...,if_dll_name)}";
#else
        const string __DllName = "{csharp_dll_name}";
#endif
    }

    {csharp_generate_const_filter}

    [DllImport(__DllName, EntryPoint = "{csharp_entry_point_prefix}LZ4_versionNumber", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
    public static extern int {csharp_method_prefix}LZ4_versionNumber();
}

csharp_dll_name_if 是可选的。如果指定,#if 允许指定两个 DllName,这在 iOS 构建中名称必须为 __Internal 时很有用。

csharp_disable_emit_dll_name 是可选的,如果设置为 true,则不发出 const string __DllName。这对于从不同构建器生成相同的类名很有用。

csharp_generate_const_filter 是可选的,如果设置一个过滤函数,则从 Rust const 生成 C# 的 const 过滤字段。

input_extern_fileinput_bindgen_file 允许多次调用,如果您需要添加依赖结构,请使用此功能。

csbindgen::Builder::default()
    .input_extern_file("src/lib.rs")
    .input_extern_file("src/struct_modules.rs")
    .generate_csharp_file("../dotnet-sandbox/NativeMethods.cs");

此外,csharp_imported_namespaces 也可以多次调用。

Unity 回调

csharp_use_function_pointer 配置如何生成函数指针。默认情况下,它将生成一个 delegate*,但 Unity 不支持它;将其设置为 false 将生成一个 Func/Action,可以与 MonoPInvokeCallback 一起使用。

// true(default) generates delegate*
[DllImport(__DllName, EntryPoint = "callback_test", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern int callback_test(delegate* unmanaged[Cdecl]<int, int> cb);

// You can define like this callback method.
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static int Method(int x) => x * x;

// And use it.
callback_test(&Method);

// ---

// false will generates {method_name}_{parameter_name}_delegate, it is useful for Unity
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int callback_test_cb_delegate(int a);

[DllImport(__DllName, EntryPoint = "callback_test", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern int callback_test(callback_test_cb_delegate cb);

// Unity can define callback method as MonoPInvokeCallback
[MonoPInvokeCallback(typeof(NativeMethods.callback_test_cb_delegate))]
static int Method(int x) => x * x;

// And use it.
callback_test(Method);

构建器选项:C (到 Rust) 到 C#

input_bindgen_file -> 设置选项 -> generate_to_file 使用 C 到 C# 工作流程。

csbindgen::Builder::default()
    .input_bindgen_file("src/lz4.rs")             // required
    .method_filter(|x| { x.starts_with("LZ4") } ) // optional, default: |x| !x.starts_with('_')
    .rust_method_prefix("csbindgen_")             // optional, default: "csbindgen_"
    .rust_file_header("use super::lz4::*;")       // optional, default: ""
    .rust_method_type_path("lz4")                 // optional, default: ""
    .csharp_dll_name("lz4")                       // required
    .csharp_class_name("NativeMethods")           // optional, default: NativeMethods
    .csharp_namespace("CsBindgen")                // optional, default: CsBindgen
    .csharp_class_accessibility("internal")       // optional, default: internal
    .csharp_entry_point_prefix("csbindgen_")      // required, you must set same as rust_method_prefix
    .csharp_method_prefix("")                     // optional, default: ""
    .csharp_use_function_pointer(true)            // optional, default: true
    .csharp_imported_namespaces("MyLib")          // optional, default: empty
    .csharp_generate_const_filter(|_|false)       // optional, default:|_|false
    .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR", "__Internal")         // optional, default: ""
    .csharp_type_rename(|rust_type_name| match rust_type_name.as_str() {    // optional, default: `|x| x`
        "FfiConfiguration" => "Configuration".into(),
        _ => x,
    })
    .csharp_file_header("#if !UNITY_WEBGL")       // optional, default: ""
    .csharp_file_footer("#endif")                 // optional, default: ""
    .generate_to_file("src/lz4_ffi.rs", "../dotnet-sandbox/lz4_bindgen.cs") // required
    .unwrap();

它将被嵌入到输出文件的占位符中。

#[allow(unused)]
use ::std::os::raw::*;

{rust_file_header}

#[no_mangle]
pub unsafe extern "C" fn {rust_method_prefix}LZ4_versionNumber() ->  c_int
{
    {rust_method_type_path}::LZ4_versionNumber()
}

csharp_* 选项模板与 Rust 到 C# 的文档相同。

调整 rust_file_header 以匹配您的模块配置,建议使用 ::*,同时使用 rust_method_type_path 来显式解析路径。

method_filter 允许您指定要排除的方法;如果未指定,则默认排除以 _ 为前缀的方法。C 库通常以特定的前缀发布。例如,LZ4LZ4ZStandardZSTD_quichequiche_Bullet Physics SDKb3

rust_method_prefixcsharp_method_prefixcsharp_entry_point_prefix 必须调整以匹配要调用的方法名。

库加载

如果需要根据操作系统更改要加载的文件路径,可以使用以下加载代码。

internal static unsafe partial class NativeMethods
{
    // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform
    // Library path will search
    // win => __DllName, __DllName.dll
    // linux, osx => __DllName.so, __DllName.dylib

    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 if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                path += "osx-";
                extension = ".dylib";
            }
            else
            {
                path += "linux-";
                extension = ".so";
            }

            if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
            {
                path += "x86";
            }
            else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
            {
                path += "x64";
            }
            else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
            {
                path += "arm64";
            }

            path += "/native/" + __DllName + extension;

            return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
        }

        return IntPtr.Zero;
    }
}

如果是在 Unity 中,请在每个本地库的检查器中配置平台设置。

分组扩展方法

在面向对象的风格中,创建以状态指针(this)为第一个参数的方法很常见。使用 csbindgen,您可以通过在 C# 端指定源生成器来使用扩展方法将这些方法分组。

从 NuGet 安装 csbindgen,并为生成的扩展方法的部分类指定 [GroupedNativeMethods]。

PM> Install-Package csbindgen

// create new file and write same type-name with same namespace
namespace CsBindgen
{
    // append `GroupedNativeMethods` attribute
    [GroupedNativeMethods]
    internal static unsafe partial class NativeMethods
    {
    }
}
// original methods
[DllImport(__DllName, EntryPoint = "counter_context_insert", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void counter_context_insert(counter_context* context, int value);

// generated methods
public static void Insert(this ref global::CsBindgen.counter_context @context, int @value)

// ----

counter_context* context = NativeMethods.create_counter_context();

// standard style
NativeMethods.counter_context_insert(context, 10);

// generated style
context->Insert(10);

GroupedNativeMethods 有四个配置参数。

public GroupedNativeMethodsAttribute(
    string removePrefix = "",
    string removeSuffix = "",
    bool removeUntilTypeName = true,
    bool fixMethodName = true)

使用此功能时函数名的约定如下

  • 第一个参数必须是指针类型。
  • removeUntilTypeName 将在方法名中找到类型名之前删除。
    • 例如 foo_counter_context_insert(countext_context* foo) -> Insert
    • 因此,建议使用命名约定,其中类型名紧接在动词之前。

类型打包

Rust 类型将映射到以下 C# 类型。

Rust C#
i8 sbyte
i16 short
i32 int
i64 long
i128 Int128
isize nint
u8 字节
u16 ushort
u32 uint
u64 ulong
u128 UInt128
usize nuint
f32 float
f64 double
bool [MarshalAs(UnmanagedType.U1)]bool
char uint
() void
c_char 字节
c_schar sbyte
c_uchar 字节
c_short short
c_ushort ushort
c_int int
c_uint uint
c_long CLong
c_ulong CULong
c_longlong long
c_ulonglong ulong
c_float float
c_double double
c_void void
CString sbyte
NonZeroI8 sbyte
NonZeroI16 short
NonZeroI32 int
NonZeroI64 long
NonZeroI128 Int128
NonZeroIsize nint
NonZeroU8 字节
NonZeroU16 ushort
NonZeroU32 uint
NonZeroU64 ulong
NonZeroU128 UInt128
NonZeroUsize nuint
#[repr(C)]Struct [StructLayout(LayoutKind.Sequential)]Struct
#[repr(C)]Union [StructLayout(LayoutKind.Explicit)]Struct
#[repr(u*/i*)]Enum Enum
bitflags! [Flags]Enum
extern "C" fn delegate* unmanaged[Cdecl]<>Func<>/Action<>
Option<extern "C"fn> delegate* unmanaged[Cdecl]<>Func<>/Action<>
*mutT T*
*constT T*
*mut *mutT T**
*const *constT T**
*mut *constT T**
*const *mutT T**
&T T*
&mutT T*
&&T T**
&*mutT T**
NonNull<T> T*
Box<T> T*

csbindgen 旨在返回不会引起封装的原始类型。自己从指针转换为 Span 比隐式地在黑盒中进行转换要好。这是一个近期趋势,例如 .NET 7 中添加了 DisableRuntimeMarshalling

较老的 C# 版本不支持 nintnuint。您可以使用 csharp_use_nint_types 在其位置使用 IntPtrUIntPtr

    csbindgen::Builder::default()
        .input_extern_file("lib.rs")
        .csharp_dll_name("nativelib")
        .generate_csharp_file("../dotnet/NativeMethods.g.cs")
        .csharp_use_nint_types(false)
        .unwrap();

c_longc_ulong 在 .NET 6 后将转换为 CLongCULong 结构。如果您想在 Unity 中进行转换,则需要 Shim。

// Currently Unity is .NET Standard 2.1 so does not exist CLong and CULong
namespace System.Runtime.InteropServices
{
    internal struct CLong
    {
        public int Value; // #if Windows = int, Unix x32 = int, Unix x64 = long
    }

    internal struct CULong
    {
        public uint Value; // #if Windows = uint, Unix x32 = uint, Unix x64 = ulong
    }
}

Struct

csbindgen 支持 Struct,您可以在方法参数或返回值上定义 #[repr(C)] 结构。

// If you define this struct...
#[repr(C)]
pub struct MyVector3 {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

#[no_mangle]
pub extern "C" fn pass_vector3(v3: MyVector3) {
    println!("{}, {}, {}", v3.x, v3.y, v3.z);
}
// csbindgen generates this C# struct
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct MyVector3
{
    public float x;
    public float y;
    public float z;
}

它还支持元组结构,它将在 C# 中生成 [FieldOffset(0)] 字段。

#[repr(C)]
pub struct MyIntVec3(i32, i32, i32);
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct MyIntVec3
{
    public int Item1;
    public int Item2;
    public int Item3;
}

它还支持单元结构,但 C# 中没有与 Rust 的单元结构(0 字节)同义的 C# 结构,因此无法实现。建议使用类型化指针而不是 void*。

// 0-byte in Rust
#[repr(C)]
pub struct MyContext;
// 1-byte in C#
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct MyContext
{
}

Union

Union 将生成 [FieldOffset(0)] 结构。

#[repr(C)]
pub union MyUnion {
    pub foo: i32,
    pub bar: i64,
}

#[no_mangle]
pub extern "C" fn return_union() -> MyUnion {
    MyUnion { bar: 53 }
}
[StructLayout(LayoutKind.Explicit)]
internal unsafe partial struct MyUnion
{
    [FieldOffset(0)]
    public int foo;
    [FieldOffset(0)]
    public long bar;
}

Enum

#[repr(i*)]#[repr(u*)] 定义的 Enum 是受支持的。

#[repr(u8)]
pub enum ByteEnum {
    A = 1,
    B = 2,
    C = 10,
}
internal enum ByteTest : byte
{
    A = 1,
    B = 2,
    C = 10,
}

位标志 Enum

csbindgen 支持 bitflags crate。

bitflags! {
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    struct EnumFlags: u32 {
        const A = 0b00000001;
        const B = 0b00000010;
        const C = 0b00000100;
        const ABC = Self::A.bits() | Self::B.bits() | Self::C.bits();
    }
}
[Flags]
internal enum EnumFlags : uint
{
    A = 0b00000001,
    B = 0b00000010,
    C = 0b00000100,
    ABC = A | B | C,
}

函数

您可以接收、从/向 C# 返回函数。

#[no_mangle]
pub extern "C" fn csharp_to_rust(cb: extern "C" fn(x: i32, y: i32) -> i32) {
    let sum = cb(10, 20); // invoke C# method
    println!("{sum}");
}

#[no_mangle]
pub extern "C" fn rust_to_csharp() -> extern fn(x: i32, y: i32) -> i32 {
    sum // return rust method
}

extern "C" fn sum(x:i32, y:i32) -> i32 {
    x + y
}

默认情况下,csbindgen 生成 extern "C" fn 作为 delegate* unmanaged[Cdecl]<>

[DllImport(__DllName, EntryPoint = "csharp_to_rust", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void csharp_to_rust(delegate* unmanaged[Cdecl]<int, int, int> cb);

[DllImport(__DllName, EntryPoint = "rust_to_csharp", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern delegate* unmanaged[Cdecl]<int, int, int> rust_to_csharp();

您可以在 C# 中这样使用。

// C# -> Rust, pass static UnmanagedCallersOnly method with `&`
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static int Sum(int x, int y) => x + y;

NativeMethods.csharp_to_rust(&Sum);

// Rust -> C#, get typed delegate*
var f = NativeMethods.rust_to_csharp();

var v = f(20, 30);
Console.WriteLine(v); // 50

Unity 不能使用 C# 9.0 函数指针,csbindgen 必须使用 MonoPInvokeCallback 选项。见:Unity 回调 部分。

Rust FFI 支持 Option<fn>,它可以接收空指针。

#[no_mangle]
pub extern "C" fn nullable_callback_test(cb: Option<extern "C" fn(a: i32) -> i32>) -> i32 {
    match cb {
        Some(f) => f(100),
        None => -1,
    }
}
var v = NativeMethods.nullable_callback_test(null); // -1

指针

堆中分配的 Rust 内存可以通过指针和 Box::into_raw 以及 Box::from_raw 发送到 C#。

#[no_mangle]
pub extern "C" fn create_context() -> *mut Context {
    let ctx = Box::new(Context { foo: true });
    Box::into_raw(ctx)
}

#[no_mangle]
pub extern "C" fn delete_context(context: *mut Context) {
    unsafe { Box::from_raw(context) };
}

#[repr(C)]
pub struct Context {
    pub foo: bool,
    pub bar: i32,
    pub baz: u64
}
var context = NativeMethods.create_context();

// do anything...

NativeMethods.delete_context(context);

您还可以将 C# 分配的内存传递给 Rust(使用 fixedGCHandle.Alloc(Pinned))。重要的是,Rust 中分配的内存必须在 Rust 中释放,C# 中分配的内存必须在 C# 中释放。

如果您想传递非 FFI 安全的结构引用,csbindgen 生成空的 C# 结构。

#[no_mangle]
pub extern "C" fn create_counter_context() -> *mut CounterContext {
    let ctx = Box::new(CounterContext {
        set: HashSet::new(),
    });
    Box::into_raw(ctx)
}

#[no_mangle]
pub unsafe extern "C" fn insert_counter_context(context: *mut CounterContext, value: i32) {
    let mut counter = Box::from_raw(context);
    counter.set.insert(value);
    Box::into_raw(counter);
}

#[no_mangle]
pub unsafe extern "C" fn delete_counter_context(context: *mut CounterContext) {
    let counter = Box::from_raw(context);
    for value in counter.set.iter() {
        println!("counter value: {}", value)
    }
}

// no repr(C)
pub struct CounterContext {
    pub set: HashSet<i32>,
}
// csbindgen generates this handler type
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct CounterContext
{
}

// You can hold pointer instance
CounterContext* ctx = NativeMethods.create_counter_context();
    
NativeMethods.insert_counter_context(ctx, 10);
NativeMethods.insert_counter_context(ctx, 20);

NativeMethods.delete_counter_context(ctx);

在这种情况下,建议与 分组扩展方法 一起使用。

如果您想传递空指针,在 Rust 端,通过 as_ref() 转换为 Option。

#[no_mangle]
pub unsafe extern "C" fn null_pointer_test(p: *const u8) {
    let ptr = unsafe { p.as_ref() };
    match ptr {
        Some(p2) => print!("pointer address: {}", *p2),
        None => println!("null pointer!"),
    };
}
// in C#, invoke by null.
NativeMethods.null_pointer_test(null);

字符串和数组(Span)

Rust 的 String、Array(Vec) 和 C# 的 String、Array 是不同的事物。由于它们无法共享,可以通过指针传递它们,并使用切片(Span)或必要时进行实例化来处理它们。

CString 是以 null 结尾的字符串。它可以通过 *mut c_char 传递,并在 C# 中作为 byte* 接收。

#[no_mangle]
pub extern "C" fn alloc_c_string() -> *mut c_char {
    let str = CString::new("foo bar baz").unwrap();
    str.into_raw()
}

#[no_mangle]
pub unsafe extern "C" fn free_c_string(str: *mut c_char) {
    unsafe { CString::from_raw(str) };
}
// null-terminated `byte*` or sbyte* can materialize by new String()
var cString = NativeMethods.alloc_c_string();
var str = new String((sbyte*)cString);
NativeMethods.free_c_string(cString);

Rust 的 String 是 UTF-8(Vec<u8>),但 C# 的 String 是 UTF-16。另外,Vec<> 不能发送到 C#,因此需要手动转换指针和控制内存。以下是 FFI 的缓冲区管理器。

#[repr(C)]
pub struct ByteBuffer {
    ptr: *mut u8,
    length: i32,
    capacity: i32,
}

impl ByteBuffer {
    pub fn len(&self) -> usize {
        self.length
            .try_into()
            .expect("buffer length negative or overflowed")
    }

    pub fn from_vec(bytes: Vec<u8>) -> Self {
        let length = i32::try_from(bytes.len()).expect("buffer length cannot fit into a i32.");
        let capacity =
            i32::try_from(bytes.capacity()).expect("buffer capacity cannot fit into a i32.");

        // keep memory until call delete
        let mut v = std::mem::ManuallyDrop::new(bytes);

        Self {
            ptr: v.as_mut_ptr(),
            length,
            capacity,
        }
    }

    pub fn from_vec_struct<T: Sized>(bytes: Vec<T>) -> Self {
        let element_size = std::mem::size_of::<T>() as i32;

        let length = (bytes.len() as i32) * element_size;
        let capacity = (bytes.capacity() as i32) * element_size;

        let mut v = std::mem::ManuallyDrop::new(bytes);

        Self {
            ptr: v.as_mut_ptr() as *mut u8,
            length,
            capacity,
        }
    }

    pub fn destroy_into_vec(self) -> Vec<u8> {
        if self.ptr.is_null() {
            vec![]
        } else {
            let capacity: usize = self
                .capacity
                .try_into()
                .expect("buffer capacity negative or overflowed");
            let length: usize = self
                .length
                .try_into()
                .expect("buffer length negative or overflowed");

            unsafe { Vec::from_raw_parts(self.ptr, length, capacity) }
        }
    }

    pub fn destroy_into_vec_struct<T: Sized>(self) -> Vec<T> {
        if self.ptr.is_null() {
            vec![]
        } else {
            let element_size = std::mem::size_of::<T>() as i32;
            let length = (self.length * element_size) as usize;
            let capacity = (self.capacity * element_size) as usize;

            unsafe { Vec::from_raw_parts(self.ptr as *mut T, length, capacity) }
        }
    }

    pub fn destroy(self) {
        drop(self.destroy_into_vec());
    }
}
// C# side span utility
partial struct ByteBuffer
{
    public unsafe Span<byte> AsSpan()
    {
        return new Span<byte>(ptr, length);
    }

    public unsafe Span<T> AsSpan<T>()
    {
        return MemoryMarshal.CreateSpan(ref Unsafe.AsRef<T>(ptr), length / Unsafe.SizeOf<T>());
    }
}

使用 ByteBuffer,您可以发送 Vec<> 到 C#。Rust -> C# 的 StringVec<u8>Vec<i32> 的模式如下。

#[no_mangle]
pub extern "C" fn alloc_u8_string() -> *mut ByteBuffer {
    let str = format!("foo bar baz");
    let buf = ByteBuffer::from_vec(str.into_bytes());
    Box::into_raw(Box::new(buf))
}

#[no_mangle]
pub unsafe extern "C" fn free_u8_string(buffer: *mut ByteBuffer) {
    let buf = Box::from_raw(buffer);
    // drop inner buffer, if you need String, use String::from_utf8_unchecked(buf.destroy_into_vec()) instead.
    buf.destroy();
}

#[no_mangle]
pub extern "C" fn alloc_u8_buffer() -> *mut ByteBuffer {
    let vec: Vec<u8> = vec![1, 10, 100];
    let buf = ByteBuffer::from_vec(vec);
    Box::into_raw(Box::new(buf))
}

#[no_mangle]
pub unsafe extern "C" fn free_u8_buffer(buffer: *mut ByteBuffer) {
    let buf = Box::from_raw(buffer);
    // drop inner buffer, if you need Vec<u8>, use buf.destroy_into_vec() instead.
    buf.destroy();
}

#[no_mangle]
pub extern "C" fn alloc_i32_buffer() -> *mut ByteBuffer {
    let vec: Vec<i32> = vec![1, 10, 100, 1000, 10000];
    let buf = ByteBuffer::from_vec_struct(vec);
    Box::into_raw(Box::new(buf))
}

#[no_mangle]
pub unsafe extern "C" fn free_i32_buffer(buffer: *mut ByteBuffer) {
    let buf = Box::from_raw(buffer);
    // drop inner buffer, if you need Vec<i32>, use buf.destroy_into_vec_struct::<i32>() instead.
    buf.destroy();
}
var u8String = NativeMethods.alloc_u8_string();
var u8Buffer = NativeMethods.alloc_u8_buffer();
var i32Buffer = NativeMethods.alloc_i32_buffer();
try
{
    var str = Encoding.UTF8.GetString(u8String->AsSpan());
    Console.WriteLine(str);

    Console.WriteLine("----");

    var buffer = u8Buffer->AsSpan();
    foreach (var item in buffer)
    {
        Console.WriteLine(item);
    }

    Console.WriteLine("----");

    var i32Span = i32Buffer->AsSpan<int>();
    foreach (var item in i32Span)
    {
        Console.WriteLine(item);
    }
}
finally
{
    NativeMethods.free_u8_string(u8String);
    NativeMethods.free_u8_buffer(u8Buffer);
    NativeMethods.free_i32_buffer(i32Buffer);
}

C# 转换为 Rust 会稍微简单一些,只需要传递 byte* 和长度。在 Rust 中,使用 std::slice::from_raw_parts 来创建切片。

#[no_mangle]
pub unsafe extern "C" fn csharp_to_rust_string(utf16_str: *const u16, utf16_len: i32) {
    let slice = std::slice::from_raw_parts(utf16_str, utf16_len as usize);
    let str = String::from_utf16(slice).unwrap();
    println!("{}", str);
}

#[no_mangle]
pub unsafe extern "C" fn csharp_to_rust_utf8(utf8_str: *const u8, utf8_len: i32) {
    let slice = std::slice::from_raw_parts(utf8_str, utf8_len as usize);
    let str = String::from_utf8_unchecked(slice.to_vec());
    println!("{}", str);
}


#[no_mangle]
pub unsafe extern "C" fn csharp_to_rust_bytes(bytes: *const u8, len: i32) {
    let slice = std::slice::from_raw_parts(bytes, len as usize);
    let vec = slice.to_vec();
    println!("{:?}", vec);
}
var str = "foobarbaz:あいうえお"; // ENG:JPN(Unicode, testing for UTF16)
fixed (char* p = str)
{
    NativeMethods.csharp_to_rust_string((ushort*)p, str.Length);
}

var str2 = Encoding.UTF8.GetBytes("あいうえお:foobarbaz");
fixed (byte* p = str2)
{
    NativeMethods.csharp_to_rust_utf8(p, str2.Length);
}

var bytes = new byte[] { 1, 10, 100, 255 };
fixed (byte* p = bytes)
{
    NativeMethods.csharp_to_rust_bytes(p, bytes.Length);
}

同样,重要的是在 Rust 中分配的内存必须在 Rust 中释放,在 C# 中分配的内存必须在 C# 中释放。

构建跟踪

csbindgen 会静默跳过任何具有不可生成类型的非方法。如果您使用 cargo build -vv 构建,如果没有生成,您将看到这些消息。

  • csbindgen 可以处理此参数类型 因此忽略生成,方法名: {}参数名: {}
  • csbindgen 可以处理此返回 类型 因此忽略生成,方法名: {}

不可生成的方法:C 的可变参数/变量参数方法

csbindgen 不处理 C 的可变参数,这会导致未定义的行为,因为这种特性在 C# 和 Rust 中都不稳定。在 C# 中有一个用于 C 可变参数的 __arglist 关键字。 __arglist 除了 Windows 环境,还有许多问题。 Rust 中有一个关于 C 可变参数的问题。

许可证

此库根据 MIT 许可证授权。

依赖关系

~2.4–4MB
~70K SLoC