Csbindgen
Generate C# FFI from Rust for automatically brings native code and C native library to .NET and Unity.
Install / Use
/learn @Cysharp/CsbindgenREADME
csbindgen
Generate C# FFI from Rust for automatically brings native code and C native library to .NET and Unity.
Automatically generates C# DllImport code from Rust extern "C" fn code. Whereas DllImport defaults to the Windows calling convention and requires a lot of configuration for C calls, csbindgen generates code optimized for "Cdecl" calls. Also .NET and Unity have different callback invocation methods (.NET uses function pointers, while Unity uses MonoPInvokeCallback), but you can output code for either by configuration.
When used with Rust's excellent C integration, you can also bring C libraries into C#. rust-bindgen is a proven .h parsing library with extensive track record. By using it together with csbindgen, you can generate code to call C code. In this case, you don't need a Rust native library - all you need is the C/C++ binary and C#.
Additionally, Rust has an excellent toolchain for cross-platform builds, and the cc crate and cmake crate allow C source code to be integrated into the build. When used together with these, it also supports consolidating multiple native calls and simplifying them into Rust calls.
showcase:
- lz4_bindgen.cs : LZ4 compression library C# binding
- zstd_bindgen.cs : Zstandard compression library C# binding
- quiche_bindgen.cs : cloudflare/quiche QUIC and HTTP/3 library C# binding
- bullet3_bindgen.cs : Bullet Physics SDK C# binding
- sqlite3_bindgen.cs : SQLite C# binding
- Cysharp/YetAnotherHttpHandler : brings the power of HTTP/2 (and gRPC) to Unity and .NET Standard
- Cysharp/NativeCompressions : LZ4 and Zstandard binding to Unity and .NET Standard
- Cysharp/MagicPhysX : .NET PhysX 5 binding to all platforms(win, osx, linux)
Getting Started
Install on Cargo.toml as build-dependencies and set up bindgen::Builder on build.rs.
[package]
name = "example"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[build-dependencies]
csbindgen = "1.9.6"
Rust to C#.
You can bring 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, use builder and input_extern_file.
fn main() {
csbindgen::Builder::default()
.input_extern_file("lib.rs")
.csharp_dll_name("example")
.generate_csharp_file("../dotnet/NativeMethods.g.cs")
.unwrap();
}
csharp_dll_name is for specifying [DllImport({DLL_NAME}, ...)] on the C# side, which should match the name of the dll binary.
See #library-loading section for how to resolve the dll file path.
[!NOTE] In this example, the value of
csharp_dll_nameis output by the Rust project you set up. In the above,package.namein the Cargo.toml is set to "example". By default, the following binaries should be output to thetarget/folder of the Rust project.
- Windows: example.dll
- Linux: libexample.so
- macOS: libexample.dylib
The filename without the extension should be specified to DllImport. Be careful that by default, rust compiler prefixes filenames with "lib" in some environments. So if you want to try this example as is on macOS,
csharp_dll_namewould be "libexample".
Then, let's run cargo build 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 = "example";
[DllImport(__DllName, EntryPoint = "my_add", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern int my_add(int x, int y);
}
}
C (to Rust) to C#
For example, build lz4 compression library.
C to C#
It's almost the same as Rust to C#, but add a reference to bindgen, and first add bindgen generation to build.rs. Then in csbindgen's builder, specify input_bindgen_file to load the file.
[package]
name = "example"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[build-dependencies]
csbindgen = "1.9.6"
bindgen = "0.72.1"
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// using bindgen, generate binding code
bindgen::Builder::default()
.header("c/lz4/lz4.h")
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
.generate()?
.write_to_file("lz4.rs")?;
// csbindgen code, generate C# dll import
csbindgen::Builder::default()
.input_bindgen_file("lz4.rs") // read from bindgen generated code
// .csharp_generate_const_filter(|x| x.starts_with("LZ4_")) // use csharp_generate_const_filter if you want to generate const
.generate_csharp_file("../dotnet/NativeMethods.lz4.g.cs")?;
Ok(())
}
In this case, you won't use the Rust binary built. Instead, build the C/C++ library separately using make, cmake, or other tools, and place it accordingly. When targeting OSS libraries, they typically come with make/cmake configurations, so it's best to follow their build procedures to generate the library.
bindgen's default_enum_style generates consts. When handling in C#, it would be more practical to generate them as enums. Also, csbindgen does not generate consts by default. By enabling csharp_generate_const_filter, consts will be generated on the C# side.
Additionally, types and enums that exist in the input file are all trimmed and not generated if they are not used in method definitions. If you want to generate types or enums on the C# side, you explicitly specify them using always_included_types. For example, .always_included_types(["ZL_StandardGraphID", "ZL_StandardNodeID"]).
C to Rust to C#
You can incorporate and build C code into a Rust library by using the cc crate or cmake crate. In that case, you need to create a flow where C# calls Rust, and Rust calls C. You can automate this entire flow by generating both Rust and C# files with generate_to_file.
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// using bindgen, generate binding code
bindgen::Builder::default()
.header("c/lz4/lz4.h")
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
.generate().unwrap()
.write_to_file("lz4.rs")?;
// 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
.rust_file_header("use super::lz4::*;") // import bindgen generated modules(struct/method)
.csharp_entry_point_prefix("csbindgen_") // adjust same signature of rust method and C# EntryPoint
.csharp_dll_name("liblz4")
// .csharp_generate_const_filter(|x| x.starts_with("LZ4_")) // use csharp_generate_const_filter if you want to generate const
.generate_to_file("lz4_ffi.rs", "../dotnet/NativeMethods.lz4.g.cs")?;
}
It will generates like these code.
// 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)
}
// 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);
}
}
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;
Builder options(configure template)
Builder options: Rust to C#
Rust to C#, use the input_extern_file -> setup options -> generate_csharp_file.
csbindgen::Builder::default()
.input_extern_file("src/lib.rs") // required
.csharp_dll_name("mynativelib") // required
.csharp_class_name("NativeMethods") // optional, default: NativeMethods
.csha
