Guilherme Rocha
Posted on June 20, 2022
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
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"
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(())
}
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()
}
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>
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();
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:
#!/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.
Posted on June 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.