loading...

Calling Rust from C#

living_syn profile image Jeremy Mill Updated on ・6 min read

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!

Discussion

pic
Editor guide
Collapse
eivbsmed profile image
Eivind Brandth Smedseng

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??

Collapse
living_syn profile image
Jeremy Mill Author

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:

#[repr(C)]
pub struct test {
 pub isbool: bool,
}

c#:

[StructLayout(LayoutKind.Sequential)]
public struct test 
{
    public byte isbool;
}
Collapse
eivbsmed profile image
Eivind Brandth Smedseng

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

Thread Thread
living_syn profile image
Jeremy Mill Author

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!

Thread Thread
eivbsmed profile image
Collapse
yaketycats profile image
Paul

C# bools are Win32 BOOLs are 32-bit signed integers, for historical reasons.

Thread Thread
living_syn profile image
Jeremy Mill Author

Still, Marshall as book "should" work, correct?

Thread Thread
yaketycats profile image
Paul

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;
}

Collapse
dmitryvk profile image
Kalyanov Dmitry

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.

Collapse
living_syn profile image
Jeremy Mill Author

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!

Collapse
torkleyy profile image
Thomas Schaller

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) ;)

Collapse
living_syn profile image
Jeremy Mill Author

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

Collapse
kelsonball profile image
Kelson Ball

Any links on how to do that?

Collapse
torkleyy profile image
Thomas Schaller

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.

Collapse
peshor profile image
PeShor

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?

Collapse
pepyakin profile image
Sergey Pepyakin

Did you tried to use thread_local! instead of static mut?

Collapse
living_syn profile image
Jeremy Mill Author

Nope, I'll look into it though. Thanks!

Collapse
mateuszlewko profile image
Mateusz Lewko

What if we use fixed size char (byte) array? Would that make passing string simpler? Do you know how to do that?

Collapse
living_syn profile image
Jeremy Mill Author

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

Collapse
pieflavor profile image
Adam Spofford

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.