无缝融合: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_file
和 input_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]<int, int> 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] 是 LZ4
,ZStandard[4] 是 ZSTD_
,quiche[5] 是 quiche_
,Bullet Physics SDK[6] 是 b3
。
rust_method_prefix
和 csharp_method_prefix
或 csharp_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# 类型。
Rust | C# |
---|---|
i8 | sbyte |
i16 | short |
i32 | int |
i64 | long |
i128 | Int128 |
isize | nint |
u8 | byte |
u16 | ushort |
u32 | uint |
u64 | ulong |
u128 | UInt128 |
usize | nuint |
f32 | float |
f64 | double |
bool | [MarshalAs(UnmanagedType.U1)]bool |
char | uint |
() | void |
c_char | byte |
c_schar | sbyte |
c_uchar | byte |
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
| 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# 不支持 nint
和 nuint
。您可以使用 csharp_use_nint_types
使用 IntPtr
和 UIntPtr
替代它们:
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_long
和 c_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 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# 接收和返回函数到 Rust。
#[no_mangle]
pub extern "C" fn csharp_to_rust(cb: extern "C" fn(x: i32, y: i32) -> i32) {
let sum = cb(10, 20); // 调用 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]<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, 用 `&` 传递静态 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(20, 30);
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(使用 fixed
或 GCHandle.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<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#。String、Vec
#[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![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);
// 丢弃内部缓冲区,如果需要 Vec<u8>,请改用 buf.destroy_into_vec()。
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);
// 丢弃内部缓冲区,如果需要 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[] { 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 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 的强大能力。
#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
Rust 中有关 C 的可变参数的问题。: https://github.com/rust-lang/rust/issues/44930