C# 调用Rust库方法 C#如何实现P/Invoke与Rust FFI交互

来源:这里教程网 时间:2026-02-21 17:42:24 作者:

确认Rust导出函数必须用
extern "C"
且禁用mangling

Rust默认不生成C兼容符号,直接

pub fn
无法被C# P/Invoke识别。必须显式声明为C ABI,并用
#[no_mangle]
防止符号重命名。否则C#调用时会报
System.EntryPointNotFoundException

在Rust中导出函数前加
extern "C" { ... }
块或直接写
extern "C" fn
必须加
#[no_mangle]
,否则链接器看到的是类似
_ZN3foo3bar17habc123...
的符号
避免返回Rust专有类型(如
String
Vec
&str
),只用C基本类型或手动管理的裸指针
如果需要返回字符串,推荐用
*const i8
(即
const char*
)并由C#负责
Marshal.PtrToStringAnsi
转换

C#端P/Invoke声明要严格匹配Rust函数签名

参数顺序、类型、调用约定(

CallingConvention.Cdecl
)三者必须与Rust侧完全一致,否则可能崩溃或读取垃圾内存。

务必在
[DllImport]
中指定
CallingConvention = CallingConvention.Cdecl
— Rust FFI默认用Cdecl,Windows上若省略会默认StdCall,导致栈不平衡
Rust的
i32
对应C#的
int
u64
对应
ulong
f64
对应
double
传入字符串时,Rust侧接收
*const i8
,C#用
string
+
[MarshalAs(UnmanagedType.LPStr)]
;传出字符串则Rust返回
*const i8
,C#用
IntPtr
接收后手动转换
结构体需用
[StructLayout(LayoutKind.Sequential)]
,字段对齐按Rust中
#[repr(C)]
定义

处理内存生命周期:谁分配谁释放

Rust和C#各自管理内存,跨FFI传递堆内存必须明确所有权边界,否则必然出现use-after-free或内存泄漏。

如果Rust函数返回堆分配的字符串(如
Box::into_raw(...)
包装的
*const i8
),必须配套提供一个
free_string(ptr: *mut u8)
函数供C#调用释放
C#不应尝试
Marshal.FreeHGlobal
释放Rust分配的内存 — 分配器不同(Rust用system allocator或jemalloc,.NET用自己的heap)
简单场景下,优先让Rust函数把结果写入C#预分配的缓冲区(
*mut i8
+
len: usize
),避免跨语言内存管理
回调函数需用
Box::new(FnOnce)
+
Box::into_raw
传指针,再由Rust在适当时机调用并
Box::from_raw
恢复销毁

构建与加载DLL:平台与ABI对齐是硬门槛

生成的Rust动态库(

.dll
/
.so
/
.dylib
)必须与C#运行时目标平台完全一致,否则
DllNotFoundException
或直接拒绝加载。

Rust crate需设
crate-type = ["cdylib"]
(不是
rlib
lib
编译目标必须匹配:C#项目是
x64
,Rust就得用
cargo build --release --target x86_64-pc-windows-msvc
(MSVC)或
x86_64-pc-windows-gnu
(GNU),不能混用
Windows上若Rust用GNU工具链(MinGW),C#需确保运行时能找到
libgcc
libstdc++
;MSVC更稳妥
将生成的
target\release\your_lib.dll
复制到C#输出目录(如
bin\Debug\net8.0\
),或用
[DllImport("your_lib.dll", EntryPoint = "...")]
显式指定路径

C#调用Rust最常卡在符号找不到或一调就崩,核心就两条:Rust端是否真正导出了C ABI符号,C#端是否用对了调用约定和内存契约。这两处错一点,后面所有逻辑都白搭。

相关推荐