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.