Generate C# FFI from Rust for automatically brings native code and C native library to .NET and Unity.
Generate C# FFI from Rust for automatically brings native code and C native library to .NET and Unity.
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:
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"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);
}
}For example, build lz4 compression library.
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"…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"]).
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.
…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;Rust to C#, use the input_extern_file -> setup options -> generate_csharp_file.
…csharp_* configuration will be embedded in the placeholder of the output file.
…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. This option allows multiple call.
csharp_disable_emit_dll_name is optional, if set to true then don't emit const string __DllName. It is useful for generate same class-name from different builder.
csharp_generate_const_filter is optional, if set a filter fun, then generate filter C# const field from Rust const.
input_extern_file and input_bindgen_file allow mulitple call, if you need to add dependent struct, use this.
csbindgen::Builder::default()
.input_extern_file("src/lib.rs")
.input_extern_file("src/struct_modules.rs")
.generate_csharp_file("../dotnet-sandbox/NativeMethods.cs");also csharp_imported_namespaces can call multiple times.
always_included_types is optional. Types and enums that exist in the input file are all trimmed and not generated if they are not used in method definitions. If set name, do not trim and generate type to C#.
csharp_use_function_pointer configures how generate function pointer. The default is to generate a delegate*, but Unity does not support it; setting it to false will generate a Func/Action that can be used with MonoPInvokeCallback.
…input_bindgen_file -> setup options -> generate_csharp_file to use C to C# workflow. input_bindgen_file -> setup options -> generate_to_file to use C to Rust to C# workflow.
…It will be embedded in the placeholder of the output file.
#[allow(unused)]
use ::std::os::raw::*;
{rust_file_header}
#[no_mangle]
pub unsafe extern "C" fn {rust_method_prefix}LZ4_versionNumber() -> c_int
{
{rust_method_type_path}::LZ4_versionNumber()
}csharp_* option template is same as Rust to C#, see above documentation.
Adjust rust_file_header for match your module configuration, recommend to use ::*, also using rust_method_type_path that add explicitly resolve path.
method_filter allows you to specify which methods to exclude; if unspecified, methods prefixed with _ are excluded by default. C libraries are usually published with a specific prefix. For example, LZ4 is LZ4, ZStandard is ZSTD_, quiche is quiche_, Bullet Physics SDK is b3.
rust_method_prefix and csharp_method_prefix or csharp_entry_point_prefix must be adjusted to match the method name to be called.
If the file path to be loaded needs to be changed depending on the operating system, the following load code can be used.
…The naming convention {platform}-{arch}/native/ is used in NuGet packaging for native libraries. When pushing to NuGet, if files are arranged according to this naming convention, the DllImportResolver mentioned above is unnecessary and the load path is automatically specified. If not using NuGet, even if files are arranged with the same naming convention, the Resolver mentioned above is still needed.
If Unity, configure Platform settings in each native library's inspector.
In an object-oriented style, it is common to create methods that take a pointer to a state (this) as their first argument. With csbindgen, you can group these methods using extension methods by specifying a Source Generator on the C# side.
Install csbindgen from NuGet, and specify [GroupedNativeMethods] for the partial class of the generated extension methods.
PM> Install-Package csbindgen
// create new file and write same type-name with same namespace
namespace CsBindgen
{
// append `GroupedNativeMethods` attribute
[GroupedNativeMethods]
internal static unsafe partial class NativeMethods
{No open issues yet, or sync has not completed.