Generate C# FFI from Rust for automatically brings native code and C native library to .NET and Unity.
Go to file
2023-03-05 15:30:41 +09:00
.vscode initial code 2023-02-27 03:31:44 +09:00
csbindgen callback complete 2023-03-05 15:30:41 +09:00
csbindgen-tests callback complete 2023-03-05 15:30:41 +09:00
dotnet-sandbox callback complete 2023-03-05 15:30:41 +09:00
.gitattributes feat: add .gitattributes to indicate vendored 2023-02-28 18:18:49 +09:00
.gitignore carg 2023-02-28 15:33:01 +09:00
Cargo.toml initial code 2023-02-27 03:31:44 +09:00
LICENSE Create LICENSE 2023-02-28 15:48:31 +09:00
README.md CLong 2023-03-01 16:08:32 +09:00

csbindgen

Crates Api Rustdoc

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

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
{
    internal 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
{
    internal 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("LZ4") } )
    .rust_method_prefix("csbindgen_")
    .rust_file_header("use super::lz4;")
    .rust_method_type_path("lz4")
    .csharp_class_name("LibLz4")
    .csharp_class_accessibility("public")
    .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_ulong_convert("uint")
    .generate_to_file("src/lz4_ffi.rs", "../dotnet-sandbox/lz4_bindgen.cs")
    .unwrap();

It will be embedded in the placeholder of the output file.

#[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}
{
    {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
    }

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

Adjust rust_file_header and rust_method_type_path to match your module configuration.

method_filter allows you to specify which methods to exclude; if unspecified, methods prefixed with _ are excluded by default.

rust_method_prefix and csharp_method_prefix or csharp_entry_point_prefix must be adjusted to match the method name to be called.

csharp_dll_name_if is optional. If specified, #if allows two DllName to be specified, which is useful if the name must be __Internal at iOS build.

If the file path to be loaded needs to be changed depending on the operating system, the following load code can be used.

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

    static NativeMethods()
    {
        NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
    }

    static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
    {
        if (libraryName == __DllName)
        {
            var path = "runtimes/";
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                path += "win-";

            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                path += "osx-";
            }
            else
            {
                path += "linux-";
            }

            if (RuntimeInformation.OSArchitecture == Architecture.X86)
            {
                path += "x86";
            }
            else if (RuntimeInformation.OSArchitecture == Architecture.X64)
            {
                path += "x64";
            }
            else if (RuntimeInformation.OSArchitecture == Architecture.Arm64)
            {
                path += "arm64";
            }

            path += "/native/" + __DllName;

            return NativeLibrary.Load(path, assembly, searchPath);
        }

        return IntPtr.Zero;
    }
}

csharp_c_long_convert and csharp_c_ulong_convert configure how handles c_long and c_ulong to C# type. default is to int and uint because LLP64 is 32bit representation but you can change it to 64bit or use CLong/CULong struct after .NET 6.

Builder options: Rust to C#

Rust to C# is similar workflow as C to C#, use the input_extern_file -> setup options -> generate_csharp_file.

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

generate_csharp_file does not generate Rust file so no need to use rust_ option.

License

This library is licensed under the MIT License.