Interop your .NET application with Rust
Tags: csharp, rust, dotnet
Published: 2022-06-20 00:54:35

Foreign Functional Interface is one of the most interesting topics on the Computer Science world, allowing you to use the generated result from one language to another, in this post I'm going to show you how to efficiently interop Rust with .NET Core.

## Why?

Rust and C# have very strong aspects in both languages, while C# is used mostly in the enterprise world, specially because of ASP.NET and it's efficiency, Rust has high level simplicity with low level performance, peaking languages like C++ and C. Being able to combine the power of both languages could result in a very interesting results when correctly applied. so let's go.

## Requirements
 - Cargo
 - .NET 6

## Let's go

First create a dotnet project and solution and the folder where you want your Rust project to be with the following commands, I'm going to call the dotnet app "InteropProject", creating a folder called Native to store the Rust code:

```sh
mkdir InteropProject
cd InteropProject/
dotnet new sln
mkdir InteropProject.Native
cd InteropProject.Native/
cargo new --lib my_lib
cd ..
dotnet new console -o InteropProject.Console
dotnet sln add InteropProject.Console
cd InteropProject.Console/
mkdir Interop
```

Let's first work on the Rust code, what we want to do is to generate a .cs file with all the equivalent bindings and types, doing that by hand is boring and time consuming, luckly there is a library for easily do all the heavy stuff for us called [interoptopus](https://github.com/ralfbiedert/interoptopus).

Inside my_lib add interoptopus and since it's a multibackend library you are going to need it's C# implementation called interoptopus_backend_csharp:

```toml
# Cargo.toml

[package]
name = "my_lib"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
interoptopus = "0.14.5"
interoptopus_backend_csharp = "0.14.5" 
```

The way interoptopus works is by generating all your generated files in the test process. create a folder called tests with `mkdir tests` and add your `bindings.rs` file inside of it.

```rust
# tests/bindings.rs

use interoptopus::util::NamespaceMappings;
use interoptopus::{Error, Interop};

#[test]
fn bindings_csharp() -> Result<(), Error> {
    use interoptopus_backend_csharp::{Config, Generator};

    Generator::new(
        Config {
            class: "InteropBindings".to_string(),
            dll_name: "my_lib".to_string(),
            namespace_mappings: NamespaceMappings::new("InteropProject.Console.Interop"),
            ..Config::default()
        },
        my_lib::my_inventory(),
    )
    .write_file("../../InteropProject.Console/Interop/InteropBindings.cs")?;

    Ok(())
}
```

At first it's not going to compile because it's missing the my_inventory function. It's the function containing all the function and types registered to be generated as C# code returning an Inventory, let's modify our `lib.rs` to create it.

```rust
// src/lib.rs

use interoptopus::{ffi_function, function, Inventory, InventoryBuilder};

#[ffi_function]
#[no_mangle]
pub extern "C" fn hello_world() {
    println!("hello world from rust");
}

pub fn my_inventory() -> Inventory {
    InventoryBuilder::new()
        .register(function!(hello_world))
        .inventory()
}
```

Here we are creating and registering a function called hello_wold that will be extern with the C interface, so every function needs to be included as `extern "C"`. Now that we created our Rust code lets build it. run `cargo test && cargo build --target release` to generate the library binaries and generate the C# file.

## Configuring our .csproj

For our project to run, it needs to have all the binaries in the same folder as our C# application bin folder, csproj allow us to configure our dotnet build command to always copy our binary. change your current directory to the console App and change the `InteropProject.Console.csproj`

```xml
<!-- InteropProject.Console/InteropProject.Console.csproj -->

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
     <Content Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'" Include="$(MSBuildProjectDirectory)/../InteropProject.Native/my_lib/target/release/libmy_lib.so">
        <Link>%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </Content>
      <Content Condition="'$(OS)' == 'Windows_NT'" Include="$(MSBuildProjectDirectory)/../InteropProject.Native/my_lib/target/release/my_lib.dll">
        <Link>%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </Content>
  </ItemGroup>

</Project>

```

Now everytime you run `dotnet build` it will add the `.so` if you are on Linux or `.dll` if on Windows generate your config.

Let's now call our function from or `Program.cs`:

```csharp
// InteropProject.Console/Program.cs

using InteropProject.Console.Interop;

InteropBindings.hello_world();
```

Running `dotnet run` will show the message:

```
hello world from rust
```

## Congratulations

Now you are able to interop your .NET application with rust. you can even create a shell script or powershell script to automate the build and run process with:

```bash
#!/usr/bin/env bash

PROJECT_DIR="$(pwd)"

function main() {
    cd "$(PROJECT_DIR)/InteropProject.Native/my_lib"
    cargo test && cargo build --target release
    cd "$(PROJECT_DIR)/InteropProject.Console/"
    dotnet build
}

main
```

## Thank you for your time

If you liked this tutorial share and give it a like! And if you have any doubt leave it a comment that I will do my best to help you.