csbindgen/README.md
2023-02-28 18:12:04 +09:00

5.9 KiB

csbindgen

Crates Api Rustdoc

Generate C# FFI from Rust for brings C native library to .NET and Unity easily.

There are usually many pains involved in using the C Library with C#. Not only is it difficult to create bindings, but cross-platform builds are very difficult. In this day and age, you have to build for multiple platforms and architectures, windows, osx, linux, android, ios, each with x64, x86, arm.

Rust has an excellent toolchain for cross-platform builds, as well as cc crate, cmake crate allow C source code to be integrated into the build. And rust-bindgen, which generates bindings from .h, is highly functional and very stable.

csbindgen can easily bring native C libraries into C# through Rust. csbindgen generates Rust extern code and C# DllImport code to work with C# from code generated from C by bindgen. With cc crate or cmake crate, C code is linked to the single rust native library.

Of course, you can also output pure FFI Rust code (or a wrapper layer to make it easier to bring C, C++ libraries into C#) to C#.

Getting Started

Install on Cargo.toml as build-dependencies and set up bindgen::Builder on build.rs.

[build-dependencies]
csbindgen = "0.1.1"

C (to Rust) to C#

For example, build lz4 compression library.

// 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
    .csharp_dll_name("liblz4")
    .generate_to_file("lz4_ffi.rs", "../dotnet/NativeMethods.lz4.g.cs")
    .unwrap();

It will generates like these code.

// lz4_ffi.rs

#[allow(unused)]
use ::std::os::raw::*;

use super::lz4;

#[no_mangle]
pub extern "C" fn csbindgen_LZ4_compress_default(src: *const c_char, dst: *mut c_char, srcSize:  c_int, dstCapacity:  c_int) ->  c_int
{
    unsafe {
        return lz4::LZ4_compress_default(src, dst, srcSize, dstCapacity);
    }
}
// NativeMethods.lz4.g.cs

using System;
using System.Runtime.InteropServices;

namespace CsBindgen
{
    public static unsafe partial class NativeMethods
    {
        const string __DllName = "liblz4";

        [DllImport(__DllName, EntryPoint = "csbindgen_LZ4_compress_default", CallingConvention = CallingConvention.Cdecl)]
        public static extern int LZ4_compress_default(byte* src, byte* dst, int srcSize, int dstCapacity);
    }
}

Finally import generated module on 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 to C#.

You can bring simple Rust FFI code to C#.

// lib.rs, simple FFI code
#[no_mangle]
pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
    x + y
}

Setup csbindgen code to build.rs.

csbindgen::Builder::default()
    .input_extern_file("lib.rs")
    .csharp_dll_name("nativelib")
    .generate_csharp_file("../dotnet/NativeMethods.g.cs")
    .unwrap();

It will generate this C# code.

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

namespace CsBindgen
{
    public static unsafe partial class NativeMethods
    {
        const string __DllName = "nativelib";

        [DllImport(__DllName, EntryPoint = "my_add", CallingConvention = CallingConvention.Cdecl)]
        public static extern int my_add(int x, int y);
    }
}

Builder options(configure template)

input_bindgen_file -> setup options -> generate_to_file to use C to C# workflow. Here are full option guide.

csbindgen::Builder::default()
    .input_bindgen_file("src/lz4.rs")
    .method_filter(|x| { !x.starts_with("_") && !x.starts_with("XXH") } )
    .rust_method_prefix("csbindgen_")
    .rust_file_header("use super::lz4;")
    .rust_method_type_path("lz4")
    .csharp_class_name("LibLz4")
    .csharp_namespace("CsBindgen")
    .csharp_dll_name("csbindgen_tests")
    .csharp_dll_name_if("UNITY_IOS && !UNITY_EDITOR", "__Internal")
    .csharp_entry_point_prefix("csbindgen_")
    .csharp_method_prefix("")
    .csharp_c_long_convert("int")
    .csharp_c_long_convert("uint")
    .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 extern "C" fn {rust_method_prefix}LZ4_versionNumber() ->  c_int
{
    unsafe {
        return {rust_method_type_path}::LZ4_versionNumber()
    }
}
using System;
using System.Runtime.InteropServices;

namespace {csharp_namespace}
{
    public 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
    }

    [DllImport(__DllName, EntryPoint = "{csharp_entry_point_prefix}LZ4_versionNumber", CallingConvention = CallingConvention.Cdecl)]
    public static extern int {csharp_method_prefix}LZ4_versionNumber();
}

Builder options: Rust to C#

csbindgen::Builder::default()
    .input_extern_file("src/lib.rs")
    .csharp_class_name("LibRust")
    .csharp_dll_name("csbindgen_tests")
    .generate_csharp_file("../dotnet-sandbox/method_call.cs")
    .unwrap();

License

This library is licensed under the MIT License.