diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e5e954c..166f464 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,8 +25,9 @@ jobs: - name: Test run: | cd verilog-support/example-project - cargo run -p example-verilog-project - cargo run -p example-verilog-project # rerun + cargo run --package example-verilog-project --bin tutorial + cargo run --package example-verilog-project --bin tutorial # rerun + cargo run --package example-verilog-project --bin dynamic_model test_spade: strategy: diff --git a/.gitignore b/.gitignore index 54c2353..3819ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ **/artifacts/ +**/artifacts2/ diff --git a/Cargo.lock b/Cargo.lock index 5915e14..a9e90b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,7 @@ name = "verilator" version = "0.0.0" dependencies = [ "camino", + "libc", "libloading", "log", "snafu", diff --git a/README.md b/README.md index 9cab07e..03c6bd0 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Still, a lot of these are less than optimal. - 🚀 Minimal overhead over directly using `verilator` - 🔌 Works completely drop-in in your existing projects +- 🪙 Declarative API for usability + Dynamic API for programmability - 🦀 Rust. Did I say Rust? ## ⚡️ Requirements @@ -68,6 +69,7 @@ I'll write more documentation once I get further in the development process. - [Testing a Verilog project](./docs/testing_verilog.md) - [Testing a Spade project](./docs/testing_spade.md) +- [Using dynamic Verilog models](./docs/verilog_dynamic_models.md) ## 💡 How it works diff --git a/docs/testing_verilog.md b/docs/testing_verilog.md index 9e5024a..785f0a7 100644 --- a/docs/testing_verilog.md +++ b/docs/testing_verilog.md @@ -4,7 +4,7 @@ > 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 -dumbname. You can find the full source code for this tutorial [here](../verilog-support/example-project/). We won't touch on the advanced aspects or features; the goal is just to provide a simple overfiew sufficient to get started. +dumbname. You can find the full source code for this tutorial [here](../verilog-support/example-project/) (in the `tutorial.rs` file). We won't touch on the advanced aspects or features; the goal is just to provide a simple overfiew sufficient to get started. ## Part 1: The Basics @@ -58,6 +58,8 @@ colog = "1.3.0" # optional, whatever version The only required package is `verilog` from dumbname; everything else is just for fun. +It's a good idea to fix a particular revision at this stage of development (and +make sure to update it frequently insofar as it doesn't break your code!). Finally, we'll want to actually write the code that drives our project in `main.rs`: diff --git a/docs/verilog_dynamic_models.md b/docs/verilog_dynamic_models.md new file mode 100644 index 0000000..5b0aa25 --- /dev/null +++ b/docs/verilog_dynamic_models.md @@ -0,0 +1,113 @@ + +# Using dynamic Verilog models + +> [!NOTE] +> This tutorial is aimed at Unix-like systems like macOS, Linux, and WSL. + +In this tutorial, we'll explore how to use dumbname to dynamically create +bindings to Verilog modules. +You can find the full source code for this tutorial [here](../verilog-support/example-project/) (in the `dyamic_model.rs` file). + +I'll be assuming you've read the [tutorial on testing Verilog projects](./testing_verilog.md); 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: The Basics + +Let's call our project "tutorial-project-2" (you are free to call it however you +like): +```shell +mkdir tutorial-project-2 +cd tutorial-project-2 +git init # optional, if using git +``` + +Let's use the same SystemVerilog module from the [Verilog tutorial](./testing_verilog.md). +```shell +mkdir sv +vi sv/main.sv +``` + +```systemverilog +// file: sv/main.sv +module main( + input[31:0] medium_input, + output[31:0] medium_output +); + assign medium_output = medium_input; +endmodule +``` + +## Part 2: Testing + +We'll create a new Rust project: +```shell +cargo init --bin . +``` + +Next, we'll add dumbname and other desired dependencies. +```toml +# file: Cargo.toml +[dependencies] +# other dependencies... +verilog = { git = "https://github.com/ethanuppal/dumbname" } +snafu = "0.8.5" # optional, whatever version +colog = "1.3.0" # optional, whatever version +``` + +The code for dynamic models is slightly more verbose. +It's not necessarily meant for human usage, though; this API is better suited for +using dumbname as a library (e.g., writing an interpreter). + +```rust +// file: src/main.rs +use snafu::Whatever; +use verilog::{verilog, VerilatorRuntime}; + +#[verilog(src = "sv/main.sv", name = "main")] +struct Main; + +#[snafu::report] +fn main() -> Result<(), Whatever> { + colog::init(); + + let mut runtime = VerilatorRuntime::new( + "artifacts2".into(), + &["sv/main.sv".as_ref()], + true, + )?; + + let mut main = runtime.create_dyn_model( + "main", + "sv/sv.main", + &[ + ("medium_input", 31, 0, PortDirection::Input), + ("medium_output", 31, 0, PortDirection::Output), + ], + )?; + + main.pin("medium_input", u32::MAX).whatever_context("pin")?; + println!("{}", main.read("medium_output").whatever_context("read")?); + assert_eq!( + main.read("medium_output").whatever_context("read")?, + 0u32.into() + ); + main.eval(); + println!("{}", main.read("medium_output").whatever_context("read")?); + assert_eq!( + main.read("medium_output").whatever_context("read")?, + u32::MAX.into() + ); + + Ok(()) +} +``` + +We can `cargo run` as usual to test. + +Make sure you pass in the correct filename to `create_dyn_model`. +You only need to pass in a correct _subset_ of the ports. + +One current issue is that if you use multiple dynamic models, since models are +lazy-built and cached, omitting ports in the first `create_dyn_model` for a +given module means that no later model can access those omitted ports. diff --git a/spade-support/example-project/swim.lock b/spade-support/example-project/swim.lock index e148fd9..2e56dea 100644 --- a/spade-support/example-project/swim.lock +++ b/spade-support/example-project/swim.lock @@ -1,2 +1,2 @@ [spade] -commit = "0c96a2248fe80b550430be86d3efc1dc9cd15c6f" +commit = "fd914099e8def300f269c033694afa0e6dfe6ce7" diff --git a/verilator/Cargo.toml b/verilator/Cargo.toml index aaf6278..6a83735 100644 --- a/verilator/Cargo.toml +++ b/verilator/Cargo.toml @@ -14,3 +14,4 @@ snafu.workspace = true camino.workspace = true libloading.workspace = true log.workspace = true +libc.workspace = true diff --git a/verilator/src/build_library.rs b/verilator/src/build_library.rs new file mode 100644 index 0000000..aecc3d0 --- /dev/null +++ b/verilator/src/build_library.rs @@ -0,0 +1,226 @@ +// Copyright (C) 2024 Ethan Uppal. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +// hardcoded knowledge: +// - output library is obj_dir/libV${top_module}.a +// - location of verilated.h +// - verilator library is obj_dir/libverilated.a + +use std::{fmt::Write, fs, process::Command}; + +use camino::{Utf8Path, Utf8PathBuf}; +use snafu::{prelude::*, Whatever}; + +use crate::PortDirection; + +fn build_ffi( + artifact_directory: &Utf8Path, + top: &str, + ports: &[(&str, usize, usize, PortDirection)], +) -> Result { + let ffi_wrappers = artifact_directory.join("ffi.cpp"); + + let mut buffer = String::new(); + writeln!( + &mut buffer, + r#" +#include "verilated.h" +#include "V{top}.h" + +extern "C" {{ + void* ffi_new_V{top}() {{ + return new V{top}{{}}; + }} + + + void ffi_V{top}_eval(V{top}* top) {{ + top->eval(); + }} + + void ffi_delete_V{top}(V{top}* top) {{ + delete top; + }} +"# + ) + .whatever_context("Failed to format utility FFI")?; + + for (port, msb, lsb, direction) in ports { + let width = msb - lsb + 1; + if width > 64 { + let underlying = format!( + "Port `{}` on top module `{}` was larger than 64 bits wide", + port, top + ); + whatever!(Err(underlying), "We don't support larger than 64-bit width on ports yet because weird C linkage things"); + } + let macro_prefix = match direction { + PortDirection::Input => "VL_IN", + PortDirection::Output => "VL_OUT", + PortDirection::Inout => "VL_INOUT", + }; + let macro_suffix = if width <= 8 { + "8" + } else if width <= 16 { + "16" + } else if width <= 32 { + "" + } else if width <= 64 { + "64" + } else { + "W" + }; + let type_macro = |name: Option<&str>| { + format!( + "{}{}({}, {}, {}{})", + macro_prefix, + macro_suffix, + name.unwrap_or("/* return value */"), + msb, + lsb, + if width > 64 { + format!(", {}", (width + 31) / 32) // words are 32 bits + // according to header + // file + } else { + "".into() + } + ) + }; + + if matches!(direction, PortDirection::Input | PortDirection::Inout) { + let input_type = type_macro(Some("new_value")); + writeln!( + &mut buffer, + r#" + void ffi_V{top}_pin_{port}(V{top}* top, {input_type}) {{ + top->{port} = new_value; + }} + "# + ) + .whatever_context("Failed to format input port FFI")?; + } + + if matches!(direction, PortDirection::Output | PortDirection::Inout) { + let return_type = type_macro(None); + writeln!( + &mut buffer, + r#" + {return_type} ffi_V{top}_read_{port}(V{top}* top) {{ + return top->{port}; + }} + "# + ) + .whatever_context("Failed to format output port FFI")?; + } + } + + writeln!(&mut buffer, "}} // extern \"C\"") + .whatever_context("Failed to format ending brace")?; + + fs::write(&ffi_wrappers, buffer) + .whatever_context("Failed to write FFI wrappers file")?; + + Ok(ffi_wrappers) +} + +fn needs_rebuild( + source_files: &[&str], + verilator_artifact_directory: &Utf8Path, +) -> Result { + if !verilator_artifact_directory.exists() { + return Ok(true); + } + + let Some(last_built) = fs::read_dir(verilator_artifact_directory) + .whatever_context(format!( + "{} exists but could not read it", + verilator_artifact_directory + ))? + .flatten() // Remove failed + .filter_map(|f| { + if f.metadata() + .map(|metadata| metadata.is_file()) + .unwrap_or(false) + { + f.metadata().unwrap().modified().ok() + } else { + None + } + }) + .max() + else { + return Ok(false); + }; + + for source_file in source_files { + let last_edited = fs::metadata(source_file) + .whatever_context(format!( + "Failed to read file metadata for source file {}", + source_file + ))? + .modified() + .whatever_context(format!( + "Failed to determine last-modified time for source file {}", + source_file + ))?; + if last_edited > last_built { + return Ok(true); + } + } + + Ok(false) +} + +pub fn build_library( + source_files: &[&str], + top_module: &str, + ports: &[(&str, usize, usize, PortDirection)], + artifact_directory: &Utf8Path, +) -> Result { + let ffi_artifact_directory = artifact_directory.join("ffi"); + fs::create_dir_all(&ffi_artifact_directory).whatever_context( + "Failed to create ffi subdirectory under artifacts directory", + )?; + let verilator_artifact_directory = artifact_directory.join("obj_dir"); + let library_name = format!("V{}_dyn", top_module); + let library_path = + verilator_artifact_directory.join(format!("lib{}.so", library_name)); + + if !needs_rebuild(source_files, &verilator_artifact_directory) + .whatever_context("Failed to check if artifacts need rebuilding")? + { + return Ok(library_path); + } + + let _ffi_wrappers = build_ffi(&ffi_artifact_directory, top_module, ports) + .whatever_context("Failed to build FFI wrappers")?; + + // bug in verilator#5226 means the directory must be relative to -Mdir + let ffi_wrappers = Utf8Path::new("../ffi/ffi.cpp"); + + let verilator_output = Command::new("verilator") + .args(["--cc", "-sv", "--build", "-j", "0"]) + .args(["-CFLAGS", "-shared -fpic"]) + .args(["--lib-create", &library_name]) + .args(["--Mdir", verilator_artifact_directory.as_str()]) + .args(["--top-module", top_module]) + //.arg("-O3") + .args(source_files) + .arg(ffi_wrappers) + .output() + .whatever_context("Invocation of verilator failed")?; + + if !verilator_output.status.success() { + whatever!( + "Invocation of verilator failed with nonzero exit code {}\n\n--- STDOUT ---\n{}\n\n--- STDERR ---\n{}", + verilator_output.status, + String::from_utf8(verilator_output.stdout).unwrap_or_default(), + String::from_utf8(verilator_output.stderr).unwrap_or_default() + ); + } + + Ok(library_path) +} diff --git a/verilator/src/dynamic.rs b/verilator/src/dynamic.rs new file mode 100644 index 0000000..caaf609 --- /dev/null +++ b/verilator/src/dynamic.rs @@ -0,0 +1,240 @@ +// Copyright (C) 2024 Ethan Uppal. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +use std::{collections::HashMap, fmt}; + +use libloading::Library; +use snafu::Snafu; + +use crate::{types, PortDirection}; + +pub struct DynamicVerilatedModel<'ctx> { + pub(crate) ports: HashMap, + pub(crate) name: String, + pub(crate) main: *mut libc::c_void, + pub(crate) eval_main: extern "C" fn(*mut libc::c_void), + pub(crate) delete_main: extern "C" fn(*mut libc::c_void), + pub(crate) library: &'ctx Library, + //cache: HashMap>, +} + +impl Drop for DynamicVerilatedModel<'_> { + fn drop(&mut self) { + (self.delete_main)(self.main); + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum VerilatorValue { + CData(types::CData), + SData(types::SData), + IData(types::IData), + QData(types::QData), +} + +impl fmt::Display for VerilatorValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VerilatorValue::CData(cdata) => cdata.fmt(f), + VerilatorValue::SData(sdata) => sdata.fmt(f), + VerilatorValue::IData(idata) => idata.fmt(f), + VerilatorValue::QData(qdata) => qdata.fmt(f), + } + } +} + +impl From for VerilatorValue { + fn from(value: types::CData) -> Self { + Self::CData(value) + } +} + +impl From for VerilatorValue { + fn from(value: types::SData) -> Self { + Self::SData(value) + } +} +impl From for VerilatorValue { + fn from(value: types::IData) -> Self { + Self::IData(value) + } +} + +impl From for VerilatorValue { + fn from(value: types::QData) -> Self { + Self::QData(value) + } +} + +#[derive(Debug, Snafu)] +pub enum DynamicVerilatedModelError { + #[snafu(display("Port {port} not found on verilated module {top}: did you forget to specify it in the runtime `create_dyn_model` constructor?: {source:?}"))] + NoSuchPort { + top: String, + port: String, + #[snafu(source(false))] + source: Option, + }, + #[snafu(display( + "Port {port} on verilated module {top} has width {width}, but used as if it was in the {attempted_lower} to {attempted_higher} width range" + ))] + InvalidPortWidth { + top: String, + port: String, + width: usize, + attempted_lower: usize, + attempted_higher: usize, + }, + #[snafu(display( + "Port {port} on verilated module {top} is an {direction} port, but was used as an {attempted_direction} port" + ))] + InvalidPortDirection { + top: String, + port: String, + direction: PortDirection, + attempted_direction: PortDirection, + }, +} + +impl DynamicVerilatedModel<'_> { + pub fn eval(&mut self) { + (self.eval_main)(self.main); + } + + pub fn read( + &self, + port: impl Into, + ) -> Result { + let port: String = port.into(); + let (width, direction) = *self.ports.get(&port).ok_or( + DynamicVerilatedModelError::NoSuchPort { + top: self.name.clone(), + port: port.clone(), + source: None, + }, + )?; + + if !matches!(direction, PortDirection::Output | PortDirection::Inout,) { + return Err(DynamicVerilatedModelError::InvalidPortDirection { + top: self.name.clone(), + port, + direction, + attempted_direction: PortDirection::Output, + }); + } + + macro_rules! read_value { + ($self:ident, $port:expr, $value_type:ty) => {{ + let symbol: libloading::Symbol< + extern "C" fn(*mut libc::c_void) -> $value_type, + > = unsafe { + self.library.get( + format!("ffi_V{}_read_{}", self.name, $port).as_bytes(), + ) + } + .map_err(|source| { + DynamicVerilatedModelError::NoSuchPort { + top: $self.name.to_string(), + port: $port.clone(), + source: Some(source), + } + })?; + + Ok((*symbol)($self.main).into()) + }}; + } + + if width <= 8 { + read_value!(self, port, types::CData) + } else if width <= 16 { + read_value!(self, port, types::SData) + } else if width <= 32 { + read_value!(self, port, types::IData) + } else if width <= 64 { + read_value!(self, port, types::QData) + } else { + unreachable!("Should have been caught in create_dyn_model") + } + } + + pub fn pin( + &mut self, + port: impl Into, + value: impl Into, + ) -> Result<(), DynamicVerilatedModelError> { + macro_rules! pin_value { + ($self:ident, $port:expr, $value:expr, $value_type:ty, $low:literal, $high:literal) => {{ + let symbol: libloading::Symbol< + extern "C" fn(*mut libc::c_void, $value_type), + > = unsafe { + self.library.get( + format!("ffi_V{}_pin_{}", self.name, $port).as_bytes(), + ) + } + .map_err(|source| { + DynamicVerilatedModelError::NoSuchPort { + top: $self.name.to_string(), + port: $port.clone(), + source: Some(source), + } + })?; + + let (width, direction) = $self + .ports + .get(&$port) + .ok_or(DynamicVerilatedModelError::NoSuchPort { + top: $self.name.clone(), + port: $port.clone(), + source: None, + })? + .clone(); + + if width > $high { + return Err(DynamicVerilatedModelError::InvalidPortWidth { + top: $self.name.clone(), + port: $port.clone(), + width, + attempted_lower: $low, + attempted_higher: $high, + }); + } + + if !matches!( + direction, + PortDirection::Input | PortDirection::Inout, + ) { + return Err( + DynamicVerilatedModelError::InvalidPortDirection { + top: $self.name.clone(), + port: $port, + direction, + attempted_direction: PortDirection::Input, + }, + ); + } + + (*symbol)($self.main, $value); + Ok(()) + }}; + } + + let port: String = port.into(); + match value.into() { + VerilatorValue::CData(cdata) => { + pin_value!(self, port, cdata, types::CData, 0, 8) + } + VerilatorValue::SData(sdata) => { + pin_value!(self, port, sdata, types::SData, 9, 16) + } + VerilatorValue::IData(idata) => { + pin_value!(self, port, idata, types::IData, 17, 32) + } + VerilatorValue::QData(qdata) => { + pin_value!(self, port, qdata, types::QData, 33, 64) + } + } + } +} diff --git a/verilator/src/lib.rs b/verilator/src/lib.rs index b803e41..e172839 100644 --- a/verilator/src/lib.rs +++ b/verilator/src/lib.rs @@ -6,14 +6,17 @@ use std::{ collections::{hash_map::Entry, HashMap}, - fmt::Write, - fs, - process::Command, + fmt, fs, }; +use build_library::build_library; use camino::{Utf8Path, Utf8PathBuf}; +use dynamic::DynamicVerilatedModel; use libloading::Library; -use snafu::{whatever, ResultExt, Whatever}; +use snafu::{prelude::*, Whatever}; + +mod build_library; +pub mod dynamic; /// Verilator-defined types for C FFI. pub mod types { @@ -43,12 +46,24 @@ pub mod types { } /// +#[derive(Debug, Clone, Copy)] pub enum PortDirection { Input, Output, Inout, } +impl fmt::Display for PortDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PortDirection::Input => "input", + PortDirection::Output => "output", + PortDirection::Inout => "inout", + } + .fmt(f) + } +} + /// You should not implement this `trait` manually. Instead, use a procedural /// macro like `#[verilog(...)]` to derive it for you. pub trait VerilatedModel { @@ -76,7 +91,7 @@ pub struct VerilatorRuntime { } impl VerilatorRuntime { - /// Creates a new runtime for instantiating (Systen)Verilog modules as Rust + /// Creates a new runtime for instantiating (System)Verilog modules as Rust /// objects. pub fn new( artifact_directory: &Utf8Path, @@ -89,7 +104,7 @@ impl VerilatorRuntime { for source_file in source_files { if !source_file.is_file() { whatever!( - "Source file {} does not exist or is not a file", + "Source file {} does not exist or is not a file. Note that if you specified relative paths, you must be in the correct directory", source_file ); } @@ -109,7 +124,80 @@ impl VerilatorRuntime { /// Constructs a new model. Uses lazy and incremental building for /// efficiency. pub fn create_model(&mut self) -> Result { - if M::name().chars().any(|c| c == '\\' || c == ' ') { + let library = self + .build_or_retrieve_library(M::name(), M::source_path(), M::ports()) + .whatever_context( + "Failed to build or retrieve verilator dynamic library", + )?; + + Ok(M::init_from(library)) + } + + /// Constructs a new dynamic model. Uses lazy and incremental building for + /// efficiency. You must guarantee the correctness of the suppplied + /// information, namely, that `name` is precisely the name of the + /// Verilog module, `source_path` is, when canonicalized + /// using [`fs::canonicalize`], the relative/absolute path to the Verilog + /// file defining the module `name`, and `ports` is a correct subset of + /// the ports of the Verilog module. + pub fn create_dyn_model<'ctx>( + &'ctx mut self, + name: &str, + source_path: &str, + ports: &[(&str, usize, usize, PortDirection)], + ) -> Result, Whatever> { + let library = self + .build_or_retrieve_library(name, source_path, ports) + .whatever_context( + "Failed to build or retrieve verilator dynamic library", + )?; + + let new_main: extern "C" fn() -> *mut libc::c_void = + *unsafe { library.get(format!("ffi_new_V{name}").as_bytes()) } + .whatever_context(format!( + "Failed to load constructor for module {}", + name + ))?; + let delete_main = + *unsafe { library.get(format!("ffi_delete_V{name}").as_bytes()) } + .whatever_context(format!( + "Failed to load destructor for module {}", + name + ))?; + let eval_main = + *unsafe { library.get(format!("ffi_V{name}_eval").as_bytes()) } + .whatever_context(format!( + "Failed to load evalulator for module {}", + name + ))?; + + let main = new_main(); + + let ports = ports + .iter() + .copied() + .map(|(port, high, low, direction)| { + (port.to_string(), (high - low + 1, direction)) + }) + .collect(); + + Ok(DynamicVerilatedModel { + ports, + name: name.to_string(), + main, + delete_main, + eval_main, + library, + }) + } + + fn build_or_retrieve_library( + &mut self, + name: &str, + source_path: &str, + ports: &[(&str, usize, usize, PortDirection)], + ) -> Result<&Library, Whatever> { + if name.chars().any(|c| c == '\\' || c == ' ') { whatever!("Escaped module names are not supported"); } @@ -119,21 +207,39 @@ impl VerilatorRuntime { if !self.source_files.iter().any(|source_file| { match ( source_file.canonicalize_utf8(), - Utf8Path::new(M::source_path()).canonicalize_utf8(), + Utf8Path::new(source_path).canonicalize_utf8(), ) { (Ok(lhs), Ok(rhs)) => lhs == rhs, _ => false, } }) { - whatever!("Module `{}` requires source file {}, which was not provided to the runtime", M::name(), M::source_path()); + whatever!("Module `{}` requires source file {}, which was not provided to the runtime", name, source_path); + } + + if let Some((port, _, _, _)) = + ports.iter().find(|(_, high, low, _)| high < low) + { + whatever!( + "Port {} on module {} was specified with the high bit less than the low bit", + port, + name + ); + } + if let Some((port, _, _, _)) = + ports.iter().find(|(_, high, low, _)| high + 1 - low > 64) + { + whatever!( + "Port {} on module {} is greater than 64 bits", + port, + name + ); } if let Entry::Vacant(entry) = self .libraries - .entry((M::name().to_string(), M::source_path().to_string())) + .entry((name.to_string(), source_path.to_string())) { - let local_artifacts_directory = - self.artifact_directory.join(M::name()); + let local_artifacts_directory = self.artifact_directory.join(name); if self.verbose { log::info!("Creating artifacts directory"); @@ -149,10 +255,10 @@ impl VerilatorRuntime { .iter() .map(|path_buf| path_buf.as_str()) .collect::>(); - let library_path = build( + let library_path = build_library( &source_files, - M::name(), - M::ports(), + name, + ports, &local_artifacts_directory, ) .whatever_context("Failed to build verilator dynamic library")?; @@ -165,225 +271,9 @@ impl VerilatorRuntime { entry.insert(library); } - let library = self + Ok(self .libraries - .get(&(M::name().to_string(), M::source_path().to_string())) - .unwrap(); - - Ok(M::init_from(library)) + .get(&(name.to_string(), source_path.to_string())) + .unwrap()) } } - -// hardcoded knowledge: -// - output library is obj_dir/libV${top_module}.a -// - location of verilated.h -// - verilator library is obj_dir/libverilated.a - -fn build_ffi( - artifact_directory: &Utf8Path, - top: &str, - ports: &[(&str, usize, usize, PortDirection)], -) -> Result { - let ffi_wrappers = artifact_directory.join("ffi.cpp"); - - let mut buffer = String::new(); - writeln!( - &mut buffer, - r#" -#include "verilated.h" -#include "V{top}.h" - -extern "C" {{ - void* ffi_new_V{top}() {{ - return new V{top}{{}}; - }} - - - void ffi_V{top}_eval(V{top}* top) {{ - top->eval(); - }} - - void ffi_delete_V{top}(V{top}* top) {{ - delete top; - }} -"# - ) - .whatever_context("Failed to format utility FFI")?; - - for (port, msb, lsb, direction) in ports { - let width = msb - lsb + 1; - if width > 64 { - let underlying = format!( - "Port `{}` on top module `{}` was larger than 64 bits wide", - port, top - ); - whatever!(Err(underlying), "We don't support larger than 64-bit width on ports yet because weird C linkage things"); - } - let macro_prefix = match direction { - PortDirection::Input => "VL_IN", - PortDirection::Output => "VL_OUT", - PortDirection::Inout => "VL_INOUT", - }; - let macro_suffix = if width <= 8 { - "8" - } else if width <= 16 { - "16" - } else if width <= 32 { - "" - } else if width <= 64 { - "64" - } else { - "W" - }; - let type_macro = |name: Option<&str>| { - format!( - "{}{}({}, {}, {}{})", - macro_prefix, - macro_suffix, - name.unwrap_or("/* return value */"), - msb, - lsb, - if width > 64 { - format!(", {}", (width + 31) / 32) // words are 32 bits - // according to header - // file - } else { - "".into() - } - ) - }; - - if matches!(direction, PortDirection::Input | PortDirection::Inout) { - let input_type = type_macro(Some("new_value")); - writeln!( - &mut buffer, - r#" - void ffi_V{top}_pin_{port}(V{top}* top, {input_type}) {{ - top->{port} = new_value; - }} - "# - ) - .whatever_context("Failed to format input port FFI")?; - } - - if matches!(direction, PortDirection::Output | PortDirection::Inout) { - let return_type = type_macro(None); - writeln!( - &mut buffer, - r#" - {return_type} ffi_V{top}_read_{port}(V{top}* top) {{ - return top->{port}; - }} - "# - ) - .whatever_context("Failed to format output port FFI")?; - } - } - - writeln!(&mut buffer, "}} // extern \"C\"") - .whatever_context("Failed to format ending brace")?; - - fs::write(&ffi_wrappers, buffer) - .whatever_context("Failed to write FFI wrappers file")?; - - Ok(ffi_wrappers) -} - -fn needs_rebuild( - source_files: &[&str], - verilator_artifact_directory: &Utf8Path, -) -> Result { - if !verilator_artifact_directory.exists() { - return Ok(true); - } - - let Some(last_built) = fs::read_dir(verilator_artifact_directory) - .whatever_context(format!( - "{} exists but could not read it", - verilator_artifact_directory - ))? - .flatten() // Remove failed - .filter_map(|f| { - if f.metadata() - .map(|metadata| metadata.is_file()) - .unwrap_or(false) - { - f.metadata().unwrap().modified().ok() - } else { - None - } - }) - .max() - else { - return Ok(false); - }; - - for source_file in source_files { - let last_edited = fs::metadata(source_file) - .whatever_context(format!( - "Failed to read file metadata for source file {}", - source_file - ))? - .modified() - .whatever_context(format!( - "Failed to determine last-modified time for source file {}", - source_file - ))?; - if last_edited > last_built { - return Ok(true); - } - } - - Ok(false) -} - -fn build( - source_files: &[&str], - top_module: &str, - ports: &[(&str, usize, usize, PortDirection)], - artifact_directory: &Utf8Path, -) -> Result { - let ffi_artifact_directory = artifact_directory.join("ffi"); - fs::create_dir_all(&ffi_artifact_directory).whatever_context( - "Failed to create ffi subdirectory under artifacts directory", - )?; - let verilator_artifact_directory = artifact_directory.join("obj_dir"); - let library_name = format!("V{}_dyn", top_module); - let library_path = - verilator_artifact_directory.join(format!("lib{}.so", library_name)); - - if !needs_rebuild(source_files, &verilator_artifact_directory) - .whatever_context("Failed to check if artifacts need rebuilding")? - { - return Ok(library_path); - } - - let _ffi_wrappers = build_ffi(&ffi_artifact_directory, top_module, ports) - .whatever_context("Failed to build FFI wrappers")?; - - // bug in verilator#5226 means the directory must be relative to -Mdir - let ffi_wrappers = Utf8Path::new("../ffi/ffi.cpp"); - - let verilator_output = Command::new("verilator") - .args(["--cc", "-sv", "--build", "-j", "0"]) - .args(["-CFLAGS", "-shared -fpic"]) - .args(["--lib-create", &library_name]) - .args(["--Mdir", verilator_artifact_directory.as_str()]) - .args(["--top-module", top_module]) - //.arg("-O3") - .args(source_files) - .arg(ffi_wrappers) - .output() - .whatever_context("Invocation of verilator failed")?; - - if !verilator_output.status.success() { - whatever!( - "Invocation of verilator failed with nonzero exit code {}\n\n--- STDOUT ---\n{}\n\n--- STDERR ---\n{}", - verilator_output.status, - String::from_utf8(verilator_output.stdout).unwrap_or_default(), - String::from_utf8(verilator_output.stderr).unwrap_or_default() - ); - } - - Ok(library_path) -} diff --git a/verilog-support/example-project/Cargo.toml b/verilog-support/example-project/Cargo.toml index 21d66d0..bfac596 100644 --- a/verilog-support/example-project/Cargo.toml +++ b/verilog-support/example-project/Cargo.toml @@ -9,6 +9,14 @@ repository.workspace = true readme.workspace = true license.workspace = true +[[bin]] +name = "tutorial" +path = "src/tutorial.rs" + +[[bin]] +name = "dynamic_model" +path = "src/dynamic_model.rs" + [dependencies] snafu.workspace = true colog.workspace = true diff --git a/verilog-support/example-project/src/dynamic_model.rs b/verilog-support/example-project/src/dynamic_model.rs new file mode 100644 index 0000000..9432d09 --- /dev/null +++ b/verilog-support/example-project/src/dynamic_model.rs @@ -0,0 +1,46 @@ +// Copyright (C) 2024 Ethan Uppal. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +use snafu::{ResultExt, Whatever}; +use verilog::{verilog, PortDirection, VerilatorRuntime}; + +#[verilog(src = "sv/main.sv", name = "main")] +struct Main; + +#[snafu::report] +fn main() -> Result<(), Whatever> { + colog::init(); + + let mut runtime = VerilatorRuntime::new( + "artifacts2".into(), + &["sv/main.sv".as_ref()], + true, + )?; + + let mut main = runtime.create_dyn_model( + "main", + "sv/main.sv", + &[ + ("medium_input", 31, 0, PortDirection::Input), + ("medium_output", 31, 0, PortDirection::Output), + ], + )?; + + main.pin("medium_input", u32::MAX).whatever_context("pin")?; + println!("{}", main.read("medium_output").whatever_context("read")?); + assert_eq!( + main.read("medium_output").whatever_context("read")?, + 0u32.into() + ); + main.eval(); + println!("{}", main.read("medium_output").whatever_context("read")?); + assert_eq!( + main.read("medium_output").whatever_context("read")?, + u32::MAX.into() + ); + + Ok(()) +} diff --git a/verilog-support/example-project/src/main.rs b/verilog-support/example-project/src/tutorial.rs similarity index 100% rename from verilog-support/example-project/src/main.rs rename to verilog-support/example-project/src/tutorial.rs diff --git a/verilog-support/verilog/src/lib.rs b/verilog-support/verilog/src/lib.rs index df363d0..a417529 100644 --- a/verilog-support/verilog/src/lib.rs +++ b/verilog-support/verilog/src/lib.rs @@ -10,5 +10,8 @@ pub mod __reexports { pub use verilator; } -pub use verilator::VerilatorRuntime; +pub use verilator::{ + dynamic::DynamicVerilatedModel, dynamic::DynamicVerilatedModelError, + dynamic::VerilatorValue, PortDirection, VerilatorRuntime, +}; pub use verilog_macro::verilog;