Intro
This is a guide for creating a Rust DLL and calling it from C#. We will cover native return values as well as structs. This guide assumes a windows environment, and also assumes that you have installed rust and set up a c# development environment.
Rust Project Setup and Dependencies
It's pretty simple to create a rust library and get it to compile into a DLL. First, navigate to the folder where you want your project and run the following command:
cargo new cs_call_rst
This will create a new folder named cs_call_rust, and initilize it with a 'src' folder and a cargo.toml file. We can build our new project by changing into the newly created cs_call_rust
folder and running:
cargo build
After running this command, you'll notice that there is now a new folder named target
and it contains a folder named debug
and in it are the output of our build. However, there's a problem, we didn't build a dll, we built a .rlib file. To tell the rust compiler to create a dll, open up cargo.toml
and make it look like the following:
[package]
name = "cs_call_rst"
version = "0.1.0"
authors = ["Jeremy Mill <jeremymill@gmail.com>"]
[lib]
name="our_rust"
crate-type = ["dylib"]
[dependencies]
The [package]
section tells the compiler some metadata about the package we're building, like who we are and what version this is. The next section, [lib]
is where we tell the compiler to create a DLL, and name it 'our_rust'. When you run cargo build
again, you should now see our_rust.dll
in the output directory.
First external rust function
Now that we've got our project all set up, lets add our first rust function, then call it from c#. Open up lib.rs
and add the following function:
#[no_mangle]
pub extern fn add_numbers(number1: i32, number2: i32) -> i32 {
println!("Hello from rust!");
number1 + number2
}
The first line, #[no_mangle]
tells the compiler to keep the name add_numbers
so that we can call it from external code. Next we define a public, externally available function, that takes in two 32 bit integers, and returns a 32 bit integer. The method prints a 'hello world' and returns the added numbers.
Run cargo build
to build our DLL, because we'll be calling this function in the next step.
C# project setup
I'm going to make the assumption that you're using visual studio for c# development, and that you already have a basic knowledge of c# and setting up a project. So, with that assumption, go ahead and create a new c# console application in visual studio. I'm naming mine rust_dll_poc
.
Before we write any code, we need to add our DLL into our project. Right click on our project and select add -> existing item -> our_rust.dll
. Next, in the bottom right 'properties' window (with the dll highlighted), make sure to change 'Copy Output Directory' from 'Do not copy' to 'Copy always'. This makes sure that the dll is copied to the build directory which will make debugging MUCH easier. Note, you will need to redo this step (or script it) with every change you make to the DLL.
Next, add the following using statement to the top of our application:
using System.Runtime.InteropServices;
This library will let us load our DLL and call it.
Next add the following private instance variable Program
class:
[DllImport("our_rust.dll")]
private static extern Int32 add_numbers(Int32 number1, Int32 number2);
This allows us to declare that we're importing an external function, named add_numbers, it's signature, and where we're importing it from. You may know that c# normally treats the int
as a 32 bit signed integer, however, when dealing with foreign functions, it is MUCH safer to be explicit in exactly what data type you're expecting on both ends, so we declared, explicitly, that we're expecting a 32 bit signed integer returned, and that the inputs should be 32 bit signed integers.
Now, lets, call the function. Add the following code into main
:
static void Main(string[] args)
{
var addedNumbers = add_numbers(10, 5);
Console.WriteLine(addedNumbers);
Console.ReadLine();
}
You should see the following output:
Hello from rust!
15
Note!: If you see a System.BadImageFormatException
When you try and run the above code, you (probably) have a mismatch in the build targets for our rust dll, and our c# application. C# and visual studio build for x86 by default, and rust-init will install a 64 bit compiler by default for a 64 bit architecture. You can build a 64 bit version of our c# application by following the steps outlined here
Returning a simple struct
Ok, awesome, we now know how to return basic values. But how about a struct? We will start with a basic struct that requires no memory allocation. First, lets define our struct, and a method that returns an instance of it in lib.rs
by adding the following code:
#[repr(C)]
pub struct SampleStruct {
pub field_one: i16,
pub field_two: i32,
}
#[no_mangle]
pub extern fn get_simple_struct() -> SampleStruct {
SampleStruct {
field_one: 1,
field_two: 2
}
}
Now we need to define the corresponding struct in c# that matches the rust struct, import the new function, and call it! Add the following into our program.cs
file:
Edit: As Kalyanov Dmitry pointed out, I missed adding a Struct Layout annotation. This annotation ensures that the C# compiler won't rearrange our struct and break our return values
namespace rust_dll_poc
{
[StructLayout(LayoutKind.Sequential)]
public struct SampleStruct
{
public Int16 field_one;
public Int32 field_two;
}
class Program
{
[DllImport("our_rust.dll")]
private static extern SampleStruct get_simple_struct();
...
and then we call it inside of Main
:
static void Main(string[] args)
{
var simple_struct = get_simple_struct();
Console.WriteLine(simple_struct.field_one);
Console.WriteLine(simple_struct.field_two);
....
You should see the following output (you remembered to move your updated DLL into the project directory, right?)
1
2
What about Strings?
Strings are, in my opinion, the most subtly complicated thing in programming. This is doubly true when working between two different languages, and even MORE true when dealing with an interface between managed and unmanaged code. Our strategy will be to store static string onto the heap and return a char *
in a struct to the memory address. We will store this address in a static variable in rust to make deallocation easier. We will also define a function free_string
which, when called by c#, will signal to rust that we're done with the string, and it is OK to deallocate that memory. It's worth noting here that this is VERY oversimplified and most definitely NOT thread safe. How this should 'actually' be implemented is highly dependent on the code you're writing.
Lets first add a using statement to the required standard libraries:
//external crates
use std::os::raw::c_char;
use std::ffi::CString;
Next we're going to create a mutable static variable which will hold the address of the string we're putting onto the heap:
static mut STRING_POINTER: *mut c_char = 0 as *mut c_char;
It's important to know that anytime we access this static variable, we will have the mark the block as unsafe. More information on why can be found here.
Next we're going to edit our struct to have a c_char field:
#[repr(C)]
pub struct SampleStruct {
pub field_one: i16,
pub field_two: i32,
pub string_field: *mut c_char,
}
Now, lets create two helper methods, one that stores strings onto the heap and transfers ownership (private) and one that frees that memory (public). Information on these methods, and REALLY important safety considerations can be found here
fn store_string_on_heap(string_to_store: &'static str) -> *mut c_char {
//create a new raw pointer
let pntr = CString::new(string_to_store).unwrap().into_raw();
//store it in our static variable (REQUIRES UNSAFE)
unsafe {
STRING_POINTER = pntr;
}
//return the c_char
return pntr;
}
#[no_mangle]
pub extern fn free_string() {
unsafe {
let _ = CString::from_raw(STRING_POINTER);
STRING_POINTER = 0 as *mut c_char;
}
}
Now, lets update get_simple_struct
to include our code:
#[no_mangle]
pub extern fn get_simple_struct() -> SampleStruct {
let test_string: &'static str = "Hi, I'm a string in rust";
SampleStruct {
field_one: 1,
field_two: 2,
string_field: store_string_on_heap(test_string),
}
}
Awesome! Our rust code is all ready! Lets edit our C# struct next. We will need to use the IntPtr type for our string field. We're supposed to be able to use the 'MarshalAs' data attributes to automatically turn this field into a string, but I have not been able to make it work.
[StructLayout(LayoutKind.Sequential)]
public struct SampleStruct
{
public Int16 field_one;
public Int32 field_two;
public IntPtr string_field;
}
and if we add the following line into main below the other Console.WriteLines, we should be able to see our text:
Console.WriteLine(Marshal.PtrToStringAnsi(simple_struct.string_field));
finally, we need to tell rust that it's OK to deallocate that memory, so we need to import the free_string
method just like we did with the other methods and call it `free_string();
The output should like this this:
1
2
Hi, I'm a string in rust
I hope all of this was useful to you! The complete c# can be found here and the complete rust code can be found here. Good luck, and happy coding!
Top comments (26)
I don't see strings as problems, i mean it was this easy to convert a c string to a rust string
then in c# it would be something like
the result is:
The rust code is thanks to this stackoverflow question
Yes but rusties don't like unsafe code ;)
That is wholly and completely untrue. We are not afraid of unsafe. There are times it is absolutely needed, and appropriate to use, and there's nothing wrong with that.
It just simply is, that a person should carefully consider whether they really need it, and if what they are doing is achievable perfectly in safe code; if they need unsafe, they should be careful to document and uphold the invariants in order to make sure things are ok and properly done.
If a person is doing ffi, it's pretty much a given that you are going to need unsafe somewhere. It comes with the territory.
Hey, thanks for the post. As you noted, the string handling is not ideal. I suggest you allocate a Box / Vec for the string, pass it to C#. From there, you just copy it into its native String type and call a Rust-defined free_string function. For users who are unexperienced with memory management / unsafety, the additional overhead seems justified for me.
Another minor I've noticed is the unideomatic return in one function (can be skipped at the end) ;)
Hey, thanks for the reply. I have a lot more experience with this now than when I wrote this. It definitely needs to be updated, i'll try and get around to it sooner than later
Any links on how to do that?
Don't have a link at hand, but I'd just return the char pointer directly (instead of storing it in a global) and ask for it again in the free function.
You should annotate the structs (on the C# side) with
[StructLayout(LayoutKind.Sequential)]
- otherwise CLR is free to reorder fields or add/remove padding which will break ABI.You're totally correct. I have that in the production code this is modeled after and I forgot it. I'll add it in, thanks!
I followed you article to bind up some common code. But got a problem whit bool always being true. Any thing special whit exposing a function using bool??
Yes, I had issues with bools as well, I didn't cover it in the article because it's weird. What I ended up doing was setting them up in c# as a byte and evaluating if it was a 1 or a 0, which SUCKS, but is totally doable. I tried getting a bunch of the marshal-as methods to work, but as of yet, none of them have. if you figure it out, let me know!
example:
rust:
c#:
Thanks, it worked. Had the solution of sending and revising a u8. But whit the byte at least i don't have to change anything on the rust side.
Some suggested to use types from libc like c_bool, but i need to get abit better at rust. Have just started out. I'll let you know if i find a good solution
I've been doing rust FFI at work for a few more months since I wrote this post. There's some things that I'll probably need to go back and update when I get the time. c_bool is a possible solution, there's also some shorthand in c# that may end up working, but I'll make sure to let you know if/when I get it working!
Thanks :-)
C# bools are Win32 BOOLs are 32-bit signed integers, for historical reasons.
Still, Marshall as book "should" work, correct?
bool on the Rust side but byte on the C# side, or (better) make a user-defined struct on the C# side, e.g. "ByteBool", that holds a single byte value and implements the conversions to/from System.Boolean.
[StructLayout(LayoutKind.Sequential)]
public struct test
{
public ByteBool isbool;
}
Did you tried to use
thread_local!
instead ofstatic mut
?Yeah. And
static mut
is pretty much almost always unneeded, since there is a safe way to do this, with interior mutability. (I'm coming from the future, but there are apis like OnceLock, LazyLock, etc)Nope, I'll look into it though. Thanks!
Hey, hope its not to late for a Question. I try your Tutorial, but i have a Problem to call a second function. It throw that he cant find an entryPoint for the second Function. Have you any Idea how i can call an another function in the same dll? Or have you maybe an Exemple?
The explicit Int32ing makes no sense. You're not being 'explicit' about the type - int is 100% always shorthand for Int32. It's as meaningless as writing Object instead of object.
What if we use fixed size char (byte) array? Would that make passing string simpler? Do you know how to do that?
I haven't done it yet, though I can think of no reason it wouldn't work. I'll see if I can throw together an example sometime today
Not sure if I should be digging up something this old, but here goes.
I wanted to send Dictionaries and Lists back and forth. What would be the best way to do something like that?
hey! Sorry I don't check this very often. I'd recommend serializing it and deserializing it. I doubt there's a good way to do it over FFI