Rust跨语言绑定生成工具csbindgen的使用,实现Rust与C#无缝互操作的高效代码生成

Rust跨语言绑定生成工具csbindgen的使用,实现Rust与C#无缝互操作的高效代码生成

简介

csbindgen是一个用于自动生成C# FFI代码的工具,可以从Rust的extern "C" fn代码生成C#的DllImport代码。它针对"Cdecl"调用进行了优化,并且可以配置为输出适用于.NET或Unity的代码。

快速开始

安装

Cargo.toml中添加为build-dependencies

[package]
name = "example"
version = "0.1.0"

[lib]
crate-type = ["cdylib"]

[build-dependencies]
csbindgen = "1.8.0"

Rust到C#的绑定生成

Rust代码示例

// lib.rs, 简单的FFI代码
#[no_mangle]
pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
    x + y
}

build.rs配置

fn main() {
    csbindgen::Builder::default()
        .input_extern_file("lib.rs")
        .csharp_dll_name("example")
        .generate_csharp_file("../dotnet/NativeMethods.g.cs")
        .unwrap();
}

生成的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压缩库

// 使用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# DllImport代码
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();

生成的Rust FFI代码

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

生成的C#代码

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

完整示例Demo

Rust侧代码

// lib.rs
#[repr(C)]
pub struct Point {
    pub x: f32,
    pub y: f32,
}

#[no_mangle]
pub extern "C" fn create_point(x: f32, y: f32) -> *mut Point {
    Box::into_raw(Box::new(Point { x, y }))
}

#[no_mangle]
pub extern "C" fn get_point_distance(p: *const Point) -> f32 {
    unsafe {
        let point = &*p;
        (point.x * point.x + point.y * point.y).sqrt()
    }
}

#[no_mangle]
pub extern "C" fn free_point(p: *mut Point) {
    unsafe {
        Box::from_raw(p);
    }
}

// 回调函数示例
#[no_mangle]
pub extern "C" fn call_with_callback(cb: extern "C" fn(i32) -> i32) -> i32 {
    cb(42)
}

build.rs配置

// build.rs
fn main() {
    csbindgen::Builder::default()
        .input_extern_file("src/lib.rs")
        .csharp_dll_name("point_lib")
        .csharp_class_name("PointNativeMethods")
        .csharp_namespace("Geometry")
        .generate_csharp_file("../dotnet/PointNativeMethods.g.cs")
        .unwrap();
}

生成的C#代码

// PointNativeMethods.g.cs
using System;
using System.Runtime.InteropServices;

namespace Geometry
{
    internal static unsafe partial class PointNativeMethods
    {
        const string __DllName = "point_lib";

        [StructLayout(LayoutKind.Sequential)]
        public struct Point
        {
            public float x;
            public float y;
        }

        [DllImport(__DllName, EntryPoint = "create_point", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern Point* create_point(float x, float y);

        [DllImport(__DllName, EntryPoint = "get_point_distance", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern float get_point_distance(Point* p);

        [DllImport(__DllName, EntryPoint = "free_point", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
        public static extern void free_point(Point* p);

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

C#使用示例

// Program.cs
using System;
using System.Runtime.InteropServices;

class Program
{
    [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }]
    static int CallbackMethod(int value) => value * 2;

    static void Main()
    {
        // 创建点
        var point = PointNativeMethods.create_point(3.0f, 4.0f);
        try
        {
            // 计算距离
            var distance = PointNativeMethods.get_point_distance(point);
            Console.WriteLine($"Distance: {distance}"); // 应该输出5
            
            // 使用回调
            var result = PointNativeMethods.call_with_callback(&CallbackMethod);
            Console.WriteLine($"Callback result: {result}"); // 应该输出84
        }
        finally
        {
            // 释放点
            PointNativeMethods.free_point(point);
        }
    }
}

类型映射

csbindgen支持多种Rust和C#之间的类型映射:

Rust类型 C#类型
i32 int
f32 float
bool [MarshalAs(UnmanagedType.U1)]bool
*mut T T*
#[repr(C)]Struct [StructLayout(LayoutKind.Sequential)]Struct

高级功能

Unity回调支持

对于Unity项目,可以配置生成使用MonoPInvokeCallback的回调:

csbindgen::Builder::default()
    .input_extern_file("src/lib.rs")
    .csharp_use_function_pointer(false) // 为Unity生成兼容的回调
    .generate_csharp_file("../unity/NativeMethods.cs");

库加载路径解析

internal static unsafe partial class NativeMethods
{
    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";
            }

            path += "/native/" + __DllName + extension;
            return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
        }
        return IntPtr.Zero;
    }
}

总结

csbindgen提供了强大的工具来简化Rust和C#之间的互操作,支持从简单的函数调用到复杂的数据结构传递。通过自动生成绑定代码,它可以显著减少手动编写FFI代码的工作量,并帮助开发者构建高性能的跨语言应用程序。


1 回复

Rust跨语言绑定生成工具csbindgen的使用指南

介绍

csbindgen是一个专门为Rust和C#互操作设计的绑定生成工具,它能够自动生成Rust与C#之间的FFI(外部函数接口)绑定代码,简化跨语言调用的开发流程。

主要特性:

  • 自动生成C#调用Rust代码所需的P/Invoke声明
  • 支持Rust到C#的类型自动转换
  • 生成安全的类型封装
  • 简化内存管理和错误处理
  • 支持各种调用约定

安装方法

将csbindgen添加到你的Cargo.toml中:

[dependencies]
csbindgen = "0.6"

或者使用cargo安装:

cargo install csbindgen

基本使用方法

1. 准备Rust代码

首先在Rust中编写需要暴露给C#的函数,使用#[no_mangle]extern "C"

// lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn get_greeting(name: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(name) };
    let input = c_str.to_str().unwrap();
    let greeting = format!("Hello, {}!", input);
    let output = CString::new(greeting).unwrap();
    output.into_raw()
}

#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
    unsafe {
        if s.is_null() { return; }
        CString::from_raw(s)
    };
}

2. 创建配置文件

在项目根目录创建csbindgen.toml

[build]
input_path = "src/lib.rs"
output_path = "bindings/generated.cs"
include_header = "// Auto-generated by csbindgen"

3. 生成绑定

运行生成命令:

csbindgen generate

这将生成C#绑定代码到指定路径。

4. 在C#中使用

生成的C#代码可以直接使用:

// C#示例
using System;
using System.Runtime.InteropServices;

class Program {
    [DllImport("your_rust_library")]
    public static extern int add_numbers(int a, int b);
    
    [DllImport("your_rust_library")]
    public static extern IntPtr get_greeting(string name);
    
    [DllImport("your_rust_library")]
    public static extern void free_string(IntPtr str);
    
    static void Main() {
        int sum = add_numbers(5, 7);
        Console.WriteLine($"Sum: {sum}");
        
        IntPtr greetingPtr = get_greeting("Rust");
        string greeting = Marshal.PtrToStringAnsi(greetingPtr);
        free_string(greetingPtr);
        Console.WriteLine(greeting);
    }
}

高级用法

自定义类型映射

// Rust端
#[repr(C)]
pub struct Point {
    pub x: f32,
    pub y: f32,
}

#[no_mangle]
pub extern "C" fn create_point(x: f32, y: f32) -> Point {
    Point { x, y }
}

生成的C#代码将包含对应的结构体:

[StructLayout(LayoutKind.Sequential)]
public struct Point {
    public float x;
    public float y;
}

[DllImport("your_rust_library")]
public static extern Point create_point(float x, float y);

回调函数支持

// Rust端
type Callback = extern "C" fn(i32);

#[no_mangle]
pub extern "C" fn register_callback(cb: Callback) {
    cb(42);
}

C#端使用:

[DllImport("your_rust_library")]
public static extern void register_callback(Action<int> callback);

// 使用
register_callback(value => {
    Console.WriteLine($"Callback received: {value}");
});

完整示例demo

Rust项目结构

rust-csharp-demo/
├── Cargo.toml
├── csbindgen.toml
└── src/
    └── lib.rs

Cargo.toml

[package]
name = "rust-csharp-demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
csbindgen = "0.6"

src/lib.rs

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// 简单加法函数
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

// 字符串处理函数
#[no_mangle]
pub extern "C" fn get_greeting(name: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(name) };
    let input = c_str.to_str().unwrap();
    let greeting = format!("Hello, {}!", input);
    CString::new(greeting).unwrap().into_raw()
}

// 释放字符串内存
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
    unsafe {
        if s.is_null() { return; }
        CString::from_raw(s)
    };
}

// 自定义结构体
#[repr(C)]
pub struct Point {
    pub x: f32,
    pub y: f32,
}

#[no_mangle]
pub extern "C" fn create_point(x: f32, y: f32) -> Point {
    Point { x, y }
}

// 回调函数示例
type Callback = extern "C" fn(i32);

#[no_mangle]
pub extern "C" fn register_callback(cb: Callback) {
    cb(42);
}

csbindgen.toml

[build]
input_path = "src/lib.rs"
output_path = "../csharp-demo/GeneratedBindings.cs"
include_header = "// Auto-generated by csbindgen"

C#项目使用

Program.cs

using System;
using System.Runtime.InteropServices;

namespace RustCSharpDemo
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Point
    {
        public float x;
        public float y;
    }

    class Program
    {
        [DllImport("rust_csharp_demo")]
        public static extern int add_numbers(int a, int b);
        
        [DllImport("rust_csharp_demo")]
        public static extern IntPtr get_greeting(string name);
        
        [DllImport("rust_csharp_demo")]
        public static extern void free_string(IntPtr str);
        
        [DllImport("rust_csharp_demo")]
        public static extern Point create_point(float x, float y);
        
        [DllImport("rust_csharp_demo")]
        public static extern void register_callback(Action<int> callback);

        static void Main(string[] args)
        {
            // 基本类型示例
            int sum = add_numbers(5, 7);
            Console.WriteLine($"5 + 7 = {sum}");

            // 字符串处理示例
            IntPtr greetingPtr = get_greeting("World");
            string greeting = Marshal.PtrToStringAnsi(greetingPtr);
            free_string(greetingPtr);
            Console.WriteLine(greeting);

            // 结构体示例
            Point p = create_point(1.5f, 2.5f);
            Console.WriteLine($"Point created: ({p.x}, {p.y})");

            // 回调函数示例
            register_callback(value => {
                Console.WriteLine($"Callback received value: {value}");
            });
        }
    }
}

最佳实践

  1. 内存管理:Rust分配的内存应由Rust释放,反之亦然
  2. 错误处理:考虑使用错误码或Result类型进行跨语言错误传递
  3. 类型安全:尽量使用简单类型或明确标记了#[repr(C)]的类型
  4. 性能考虑:减少跨语言调用次数,批量处理数据

常见问题解决

  1. 类型不匹配:确保Rust和C#端使用相同大小的类型
  2. 调用约定:检查extern "C"CallingConvention设置
  3. DLL加载:确保C#能找到Rust生成的动态库
  4. 内存泄漏:确保成对使用分配和释放函数
回到顶部