无缝融合:Rust 与 C#的高效 FFI 互操作指南 -- csbindgen

文摘   2024-07-04 00:01   江苏  

无缝融合:Rust 与 C#的高效 FFI 互操作指南 -- csbindgen

前言

在当今的软件开发领域,跨语言互操作性日益成为项目成功的关键。Rust,以其卓越的性能和安全性,与 C#的广泛应用及其在.NET 和 Unity 平台上的强大功能相结合,为开发者提供了一个强大的工具集,以构建高效、可靠的应用程序。然而,将这两种语言无缝集成并非易事。本文将深入探讨如何使用 csbindgen 工具,一个专为 Rust 和 C#之间自动生成外部函数接口(FFI)代码的实用工具,来简化这一过程。

通过 csbindgen,开发者可以轻松地将 Rust 的 extern "C"函数代码转换为 C#的 DllImport 代码,实现原生代码与.NET 及 Unity 项目的无缝集成。本文将详细介绍 csbindgen 的安装、配置、使用方法以及如何通过一系列构建选项来定制代码生成过程,以满足不同项目的需求。

自动将 Rust 的 extern "C" 函数代码生成 C# 的 DllImport 代码,轻松将原生代码和 C 原生库带入 .NET 和 Unity。

特性

  • 从 Rust 的 extern "C" fn 代码自动生成 C# 的 DllImport 代码。
  • 优化生成的代码以适应 "Cdecl" 调用,简化跨平台构建。
  • 支持通过配置输出 .NET 和 Unity 的回调调用方法。
  • 利用 Rust 强大的跨平台构建工具链,简化 C 库在 C# 中的使用。

开始使用

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, 简单的 FFI 代码
#[no_mangle]
pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
    x + y
}

build.rs 中设置 csbindgen 代码。

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[1] 部分,了解如何解析 dll 文件路径。

C (到 Rust) 到 C

例如,构建 lz4[2] 压缩库。

// 使用 bindgen 生成绑定代码
bindgen::Builder::default()
    .header("c/lz4/lz4.h")
    .generate().unwrap()
    .write_to_file("lz4.rs").unwrap();

// 使用 cc,构建并链接 c 代码
cc::Build::new().file("lz4.c").compile("lz4");

// csbindgen 代码,生成 Rust ffi 和 C# dll import
csbindgen::Builder::default()
    .input_bindgen_file("lz4.rs")            // 从 bindgen 生成的代码中读取
    .rust_file_header("use super::lz4::*;")     // 导入 bindgen 生成的模块(结构体/方法)
    .csharp_entry_point_prefix("csbindgen_"// 调整 Rust 方法和 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, 导入生成的代码。
#[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")        // 必需
    .csharp_dll_name("mynativelib")         // 必需
    .csharp_class_name("NativeMethods")     // 可选,默认:NativeMethods
    .csharp_namespace("CsBindgen")          // 可选,默认:CsBindgen
    .csharp_class_accessibility("internal"// 可选,默认:internal
    .csharp_entry_point_prefix("")          // 可选,默认:""
    .csharp_method_prefix("")               // 可选,默认:""
    .csharp_use_function_pointer(true)      // 可选,默认:true
    .csharp_disable_emit_dll_name(false)    // 可选,默认:false
    .csharp_imported_namespaces("MyLib")    // 可选,默认:空
    .csharp_generate_const_filter (|_|false// 可选,默认:`|_|false`
    .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR""__Internal"// 可选,默认:""
    .csharp_type_rename(|rust_type_name| match rust_type_name {
        "FfiConfiguration" => "Configuration".into(),
        _ => x,
    })
    .generate_csharp_file("../dotnet-sandbox/NativeMethods.cs")     // 必需
    .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(默认) 生成 delegate*
[DllImport(__DllName, EntryPoint = "callback_test", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern int callback_test(delegate* unmanaged[Cdecl]<intint> cb);

// 可以这样定义回调方法。
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static int Method(int x) => x * x;

// 并使用它。
callback_test(&Method);

// ---

// false 将生成 {method_name}_{parameter_name}_delegate, 对于 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 可以将回调方法定义为 MonoPInvokeCallback
[MonoPInvokeCallback(typeof(NativeMethods.callback_test_cb_delegate))]
static int Method(int x) => x * x;

// 并使用它。
callback_test(Method);

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

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

csbindgen::Builder::default()
    .input_bindgen_file("src/lz4.rs")             // 必需
    .method_filter(|x| { x.starts_with("LZ4") }) // 可选,默认:|x| !x.starts_with('_')
    .rust_method_prefix("csbindgen_")             // 可选,默认:"csbindgen_"
    .rust_file_header("use super::lz4::*;")       // 可选,默认:""
    .rust_method_type_path("lz4")                 // 可选,默认:""
    .csharp_dll_name("lz4")                       // 必需
    .csharp_class_name("NativeMethods")           // 可选,默认:NativeMethods
    .csharp_namespace("CsBindgen")                // 可选,默认:CsBindgen
    .csharp_class_accessibility("internal")       // 可选,默认:internal
    .csharp_entry_point_prefix("csbindgen_")      // 必需,必须与 rust_method_prefix 设置相同
    .csharp_method_prefix("")                     // 可选,默认:""
    .csharp_use_function_pointer(true)            // 可选,默认:true
    .csharp_imported_namespaces("MyLib")          // 可选,默认:空
    .csharp_generate_const_filter(|_|false)       // 可选,默认:|_|false
    .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR""__Internal")         // 可选,默认:""
    .csharp_type_rename(|rust_type_name| match rust_type_name.as_str() {    // 可选,默认:`|x| x`
        "FfiConfiguration" => "Configuration".into(),
        _ => x,
    })
    .csharp_file_header("#if !UNITY_WEBGL")       // 可选,默认:""
    .csharp_file_footer("#endif")                 // 可选,默认:""
    .generate_to_file("src/lz4_ffi.rs""../dotnet-sandbox/lz4_bindgen.cs"// 必需
    .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 库通常以特定的前缀发布。例如,LZ4[3]LZ4ZStandard[4]ZSTD_quiche[5]quiche_Bullet Physics SDK[6]b3

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
    // 库路径将搜索
    // 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# 端指定 Source Generator 来使用扩展方法对这些方法进行分组。

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

PM> Install-Package csbindgen[7]

// 创建新文件并写入与生成的扩展方法相同的类型名称和命名空间
namespace CsBindgen
{
    // 添加 `GroupedNativeMethods` 属性
    [GroupedNativeMethods]
    internal static unsafe partial class NativeMethods
    {
    }
}
// 原始方法
[DllImport(__DllName, EntryPoint = "counter_context_insert", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void counter_context_insert(counter_context* context, int value);

// 生成的方法
public static void Insert(this ref global::CsBindgen.counter_context @context, int @value)

// ----

counter_context* context
 = NativeMethods.create_counter_context();

// 标准风格
NativeMethods.counter_context_insert(context, 10);

// 生成的风格
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
    • 因此,建议使用在动词之前立即放置相同类型名称的命名约定。

类型 Marshalling


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

RustC#
i8sbyte
i16short
i32int
i64long
i128Int128
isizenint
u8byte
u16ushort
u32uint
u64ulong
u128UInt128
usizenuint
f32float
f64double
bool[MarshalAs(UnmanagedType.U1)]bool
charuint
()void
c_charbyte
c_scharsbyte
c_ucharbyte
c_shortshort
c_ushortushort
c_intint
c_uintuint
c_longCLong
c_ulongCULong
c_longlonglong
c_ulonglongulong

| c_float | float | | c_double | double | | c_void | void | | CString | sbyte | | NonZeroI8 | sbyte | | NonZeroI16 | short | | NonZeroI32 | int | | NonZeroI64 | long | | NonZeroI128 | Int128 | | NonZeroIsize | nint | | NonZeroU8 | byte | | 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![8] | [Flags]Enum | | extern "C" fn | delegate* unmanaged[Cdecl]<>Func<>/Action<> | | Option<extern "C" fn> | delegate* unmanaged[Cdecl]<>Func<>/Action<> | | *mut T | T* | | *const T | T* | | *mut *mut T | T** | | *const *const T | T** | | *mut *const T | T** | | *const *mut T | T** | | &T | T* | | &mut T | T* | | &&T | T** | | &*mut T | T** | | NonNull<T> | T* | | Box<T> | T* |

csbindgen 设计为返回不会引起 marshalling 的基本类型。最好自己从指针转换为 Span,而不是隐式地在黑箱中进行转换。这是最近的趋势,例如 .NET 7 中添加的 DisableRuntimeMarshalling[9]

旧版本的 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 之后转换为 CLong[10]CULong[11] 结构体。如果您想在 Unity 中进行转换,您将需要 Shim。

// 目前 Unity 是 .NET Standard 2.1,所以不存在 CLong 和 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
    }
}

结构体

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

// 如果您这样定义结构体...
#[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 生成这个 C# 结构体
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct MyVector3
{
    public float x;
    public float y;
    public float z;
}

还支持元组结构体,它将在 C# 中生成 Item* 字段。

#[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;
}

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

// Rust 中的 0 字节
#[repr(C)]
pub struct MyContext;
// C# 中的 1 字节
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct MyContext
{
}

联合体

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;
}

枚举

支持 #[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,
}

bitflags 枚举

csbindgen 支持 bitflags[12] 包。

bitflags! {
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    struct EnumFlagsu32 {
        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# 接收和返回函数到 Rust。

#[no_mangle]
pub extern "C" fn csharp_to_rust(cb: extern "C" fn(x: i32, y: i32) -> i32) {
    let sum = cb(1020); // 调用 C# 方法
    println!("{sum}");
}

#[no_mangle]
pub extern "C" fn rust_to_csharp() -> extern fn(x: i32, y: i32) -> i32 {
    sum // 返回 Rust 方法
}

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]<intintint> cb);

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

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

// C# -> Rust, 用 `&` 传递静态 UnmanagedCallersOnly 方法
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConv
Cdecl) })]
static int Sum(int x, int y) => x + y;

NativeMethods.csharp_to_rust(&Sum);

// Rust -> C#, 获取类型化的 delegate*
var f = NativeMethods.rust_to_csharp();

var v = f(2030);
Console.WriteLine(v); // 50

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

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();

// 做任何事情...

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)
    }
}

// 没有 repr(C)
pub struct CounterContext {
    pub set: HashSet<i32>,
}
// csbindgen 生成这个处理程序类型
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct CounterContext
{
}

// 您可以持有指针实例
CounterContext* ctx = NativeMethods.create_counter_context();

NativeMethods.insert_counter_context(ctx, 10);
NativeMethods.insert_counter_context(ctx, 20);

NativeMethods.delete_counter_context(ctx);

在这种情况下,建议与 Grouping Extension Methods[14] 一起使用。

如果你想传递空指针,在 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!"),
    };
}
// 在 C# 中,通过 null 调用。
NativeMethods.null_pointer_test(null);

字符串和数组(Span)

Rust 的 String、Array(Vec) 和 C# 的 String、Array 是不同的事物。由于它不能共享,需要用指针传递,并用 slice(Span) 处理,或者必要时具体化。

CString 是以空字符结尾的字符串。它可以以 *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) };
}
// 空字符终止的 `byte*` 或 sbyte* 可以通过 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.");

        // 保持内存直到调用 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# 端 span 实用工具
partial struct ByteBuffer
{
    public unsafe Span<byteAsSpan()
    {
        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#。String、Vec、Vec、Rust -> C# 的模式如下。

#[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);
    // 丢弃内部缓冲区,如果需要 String,请改用 String::from_utf8_unchecked(buf.destroy_into_vec())。
    buf.destroy();
}

#[no_mangle]
pub extern "C" fn alloc_u8_buffer() -> *mut ByteBuffer {
    let vec: Vec<u8> = vec![110100];
    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);
    // 丢弃内部缓冲区,如果需要 Vec<u8>,请改用 buf.destroy_into_vec()。
    buf.destroy();
}

#[no_mangle]
pub extern "C" fn alloc_i32_buffer() -> *mut ByteBuffer {
    let vec: Vec<i32> = vec![110100100010000];
    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);
    // 丢弃内部缓冲区,如果需要 Vec<i32>,请改用 buf.destroy_into_vec_struct::<i32>()。
    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 创建 slice。

#[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, 测试 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[] { 110100255 };
fixed (byte* p = bytes)
{
    NativeMethods.csharp_to_rust_bytes(p, bytes.Length);
}

再次强调,重要的是在 Rust 中分配的内存必须在 Rust 中释放,在 C# 中分配的内存必须在 C# 中释放。

构建跟踪


csbindgen 会静默跳过任何具有无法生成类型的任何方法。如果您使用 cargo build -vv 构建,如果未生成,您将获得这些消息。

  • csbindgen can't handle this parameter type so ignore generate, method_name: {} parameter_name: {}
  • csbindgen can't handle this return type so ignore generate, method_name: {}

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


csbindgen 不处理 C 的可变参数,因为这会导致未定义的行为,因为此功能在 C# 和 Rust 中都不稳定。 在 C# 中,C 的可变参数有 __arglist 关键字。`__arglist` 在 Windows 环境之外有很多问题。[15]Rust 中有关 C 的可变参数的问题。[16]

附录

https://github.com/Cysharp/csbindgen

结语

随着软件项目变得越来越复杂,能够高效地在不同编程语言之间桥接差异,成为了开发者必须掌握的技能。csbindgen 作为一个强大的工具,不仅简化了 Rust 与 C#之间的 FFI 生成过程,还为跨平台开发提供了强有力的支持。通过本文的深入解析,我们希望读者能够充分理解并利用 csbindgen 的功能,无论是在.NET 框架下还是在 Unity 游戏开发中,都能够更加自如地整合 Rust 的强大能力。

Reference
[1]

#library-loading: #库加载

[2]

lz4: https://github.com/lz4/lz4

[3]

LZ4: https://github.com/lz4/lz4

[4]

ZStandard: https://github.com/facebook/zstd

[5]

quiche: https://github.com/cloudflare/quiche

[6]

Bullet Physics SDK: https://github.com/bulletphysics/bullet3

[7]

csbindgen: https://www.nuget.org/packages/csbindgen

[8]

bitflags!: https://crates.io/crates/bitflags

[9]

DisableRuntimeMarshalling: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.disableruntimemarshallingattribute

[10]

CLong: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.clong

[11]

CULong: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.culong

[12]

bitflags: https://crates.io/crates/bitflags

[13]

Unity Callback: #unity-callback

[14]

Grouping Extension Methods: #grouping-extension-methods

[15]

__arglist 在 Windows 环境之外有很多问题。: https://github.com/dotnet/runtime/issues/48796

[16]

Rust 中有关 C 的可变参数的问题。: https://github.com/rust-lang/rust/issues/44930


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章