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
12,282 每月下载量
用于 4 crates
120KB
2K SLoC
csbindgen
从 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 原生库。
展示
- lz4_bindgen.cs : LZ4 压缩库 C# 绑定
- zstd_bindgen.cs : Zstandard 压缩库的 C# 绑定
- quiche_bindgen.cs : cloudflare/quiche QUIC 和 HTTP/3 库的 C# 绑定
- bullet3_bindgen.cs : Bullet Physics SDK 的 C# 绑定
- sqlite3_bindgen.cs : SQLite 的 C# 绑定
- Cysharp/YetAnotherHttpHandler : 将 HTTP/2 (和 gRPC) 的功能带给 Unity 和 .NET Standard
- Cysharp/MagicPhysX : .NET PhysX 5 绑定,适用于所有平台(win, osx, linux)
入门指南
在 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_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(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 库通常以特定的前缀发布。例如,LZ4 是 LZ4
,ZStandard 是 ZSTD_
,quiche 是 quiche_
,Bullet Physics SDK 是 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
// 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# 版本不支持 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、CULong 结构。如果您想在 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(使用 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)
}
}
// 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# 的 String
、Vec<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