Calling Rust from Verilog

note

This tutorial is aimed at Unix-like systems like macOS, Linux, and WSL.

In this tutorial, we'll setup a SystemVerilog project and test our code with Marlin. You can find the full source code for this tutorial here (see in particular the dpi_tutorial.rs file).

I'll be assuming you've read the tutorial on testing Verilog projects; if not, read that first and come back. In particular, I won't be reexplaining things I discussed in that tutorial, although I will still walk through the entire setup.

Part 1: Setup

Let's call our project "tutorial-project" (you are free to call it however you like):

mkdir tutorial-project
cd tutorial-project
git init # optional, if using git

Here's what our project will look like in the end:

.
├── Cargo.toml
├── Cargo.lock
├── .gitignore
├── src
│   ├── lib.rs
│   ├── dpi.sv
└── tests
    └── dpi_test.rs

We'll have a simple SystemVerilog module that writes the result of three, a DPI function with a single integer output.

mkdir src
vi src/dpi.sv
// file: src/dpi.sv
import "DPI-C" function void three(output int out);

module main(output logic[31:0] out);
    int a = 0;
    initial begin
        three(a);
        $display("%d", a);
        out = a;
    end
endmodule

Part 2: Testing

We'll create a new Rust project:

cargo init --lib

Next, we'll add Marlin and other desired dependencies.

cargo add marlin --features verilog --dev
cargo add snafu --dev

caution

Please use snafu! 😂

Finally, we need the Rust file where we define the DPI function and drive the model.

mkdir tests
vi tests/dpi_test.rs
// file: tests/dpi_test.rs
use snafu::Whatever;
use marlin::{
    verilator::{VerilatorRuntime, VerilatorRuntimeOptions},
    verilog::prelude::*,
};

#[verilog::dpi]
pub extern "C" fn three(out: &mut u32) {
    *out = 3;
}

#[verilog(src = "src/dpi.sv", name = "main")]
struct Main;

//#[snafu::report]
fn main() -> Result<(), Whatever> {
    let runtime = VerilatorRuntime::new(
        "artifacts".into(),
        &["src/dpi.sv".as_ref()],
        &[],
        [three],
        VerilatorRuntimeOptions::default(),
    )?;

    let mut main = runtime.create_model_simple::<Main>()?;
    main.eval();
    assert_eq!(main.out, 3);

    Ok(())
}

caution

Using #[snafu::report] on the function gives error messages that are actually useful, but sometimes breaks LSP services like code completion. I recommend to only apply it to your test functions when you actually encounter an error.

We can cargo test as usual to test.

The magic happens here:

#![allow(unused)]
fn main() {
#[verilog::dpi]
pub extern fn three(out: &mut u32) {
    *out = 3;
}
}

By applying #[verilog::dpi], we turn our normal Rust function into a DPI one. We need to apply pub and extern (or extern "C") so that Rust exposes the function correctly to C.

DPI functions cannot have a return value and only take primitive integers (for input) or mutable references to primitive integers (for output/inout) as arguments. Beside that, there are no restrictions on the content --- write whatever Rust code you want!

Then, we told the runtime about this function:

    let runtime = VerilatorRuntime::new(
        "artifacts".into(),
        &["src/dpi.sv".as_ref()],
        &[],
-       [],
+       [three],
        VerilatorRuntimeOptions::default(),
        true,
    )?;

See the documentation for the #[verilog::dpi] macro for more details.