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!