确认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#端是否用对了调用约定和内存契约。这两处错一点,后面所有逻辑都白搭。
