DEV Community

Cover image for Interop your .NET application with Rust
Guilherme Rocha
Guilherme Rocha

Posted on

Interop your .NET application with Rust

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

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:

# 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" 
Enter fullscreen mode Exit fullscreen mode

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.

# 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(())
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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()
}
Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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:

// InteropProject.Console/Program.cs

using InteropProject.Console.Interop;

InteropBindings.hello_world();
Enter fullscreen mode Exit fullscreen mode

Running dotnet run will show the message:

hello world from rust
Enter fullscreen mode Exit fullscreen mode

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:

#!/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
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)