marlin_verilator/
lib.rs

1// Copyright (C) 2024 Ethan Uppal.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License,
4// v. 2.0. If a copy of the MPL was not distributed with this file, You can
5// obtain one at https://mozilla.org/MPL/2.0/.
6
7//! This module implements the Verilator runtime for instantiating hardware
8//! modules. The Marlin Verilator runtime supports DPI, VCDs, and dynamic
9//! models in addition to standard models.
10//!
11//! For an example of how to use this runtime to add support for your own custom
12//! HDL, see `SpadeRuntime` (under the "language-support/spade/" directory),
13//! which just wraps [`VerilatorRuntime`].
14
15use std::{
16    cell::RefCell,
17    collections::{HashMap, hash_map::Entry},
18    ffi::{self, OsString},
19    fmt, fs,
20    hash::{self, Hash, Hasher},
21    io::Write,
22    os::fd::FromRawFd,
23    sync::{LazyLock, Mutex},
24    time::Instant,
25};
26
27use boxcar::Vec as BoxcarVec;
28use build_library::build_library;
29use camino::{Utf8Path, Utf8PathBuf};
30use dashmap::DashMap;
31use dpi::DpiFunction;
32use dynamic::DynamicVerilatedModel;
33use libloading::Library;
34use owo_colors::OwoColorize;
35use snafu::{ResultExt, Whatever, whatever};
36
37mod build_library;
38pub mod dpi;
39pub mod dynamic;
40pub mod vcd;
41
42pub use dynamic::AsDynamicVerilatedModel;
43
44/// Verilator-defined types for C FFI.
45pub mod types {
46    /// From the Verilator documentation: "Data representing 'bit' of 1-8 packed
47    /// bits."
48    pub type CData = u8;
49
50    /// From the Verilator documentation: "Data representing 'bit' of 9-16
51    /// packed bits"
52    pub type SData = u16;
53
54    /// From the Verilator documentation: "Data representing 'bit' of 17-32
55    /// packed bits."
56    pub type IData = u32;
57
58    /// From the Verilator documentation: "Data representing 'bit' of 33-64
59    /// packed bits."
60    pub type QData = u64;
61
62    /// From the Verilator documentation: "Data representing one element of
63    /// WData array."
64    pub type EData = u32;
65
66    /// From the Verilator documentation: "Data representing >64 packed bits
67    /// (used as pointer)."
68    pub type WData = EData;
69}
70
71/// <https://www.digikey.com/en/maker/blogs/2024/verilog-ports-part-7-of-our-verilog-journey>
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
73pub enum PortDirection {
74    Input,
75    Output,
76    Inout,
77}
78
79impl fmt::Display for PortDirection {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            PortDirection::Input => "input",
83            PortDirection::Output => "output",
84            PortDirection::Inout => "inout",
85        }
86        .fmt(f)
87    }
88}
89
90/// Configuration for a particular [`VerilatedModel`].
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
92pub struct VerilatedModelConfig {
93    /// If `None`, there will be no optimization. If a value from `0` to `3`
94    /// inclusive, the flag `-O<level>` will be passed. Enabling will slow
95    /// compilation times.
96    pub verilator_optimization: usize,
97
98    /// A list of Verilator warnings to disable on the Verilog source code for
99    /// this model.
100    pub ignored_warnings: Vec<String>,
101
102    /// Whether this model should be compiled with tracing support.
103    pub enable_tracing: bool,
104}
105
106/// You should not implement this `trait` manually. Instead, use a procedural
107/// macro like `#[verilog(...)]` to derive it for you.
108pub trait AsVerilatedModel<'ctx>: 'ctx {
109    /// The source-level name of the module.
110    fn name() -> &'static str;
111
112    /// The path of the module's definition.
113    fn source_path() -> &'static str;
114
115    /// The module's interface.
116    fn ports() -> &'static [(&'static str, usize, usize, PortDirection)];
117
118    #[doc(hidden)]
119    fn init_from(library: &'ctx Library, tracing_enabled: bool) -> Self;
120
121    #[doc(hidden)]
122    unsafe fn model(&self) -> *mut ffi::c_void;
123}
124
125/// Optional configuration for creating a [`VerilatorRuntime`]. Usually, you can
126/// just use [`VerilatorRuntimeOptions::default()`].
127#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128pub struct VerilatorRuntimeOptions {
129    /// The name of the `verilator` executable, interpreted in some way by the
130    /// OS/shell.
131    pub verilator_executable: OsString,
132
133    /// Whether Verilator should always be invoked instead of only when the
134    /// source files or DPI functions change.
135    pub force_verilator_rebuild: bool,
136
137    /// Whether to use the log crate.
138    pub log: bool,
139}
140
141impl Default for VerilatorRuntimeOptions {
142    fn default() -> Self {
143        Self {
144            verilator_executable: "verilator".into(),
145            force_verilator_rebuild: false,
146            log: false,
147        }
148    }
149}
150
151impl VerilatorRuntimeOptions {
152    /// The same as the [`Default`] implementation except that the log crate is
153    /// used.
154    pub fn default_logging() -> Self {
155        Self {
156            log: true,
157            ..Default::default()
158        }
159    }
160}
161
162#[derive(PartialEq, Eq, Hash, Clone)]
163struct LibraryArenaKey {
164    name: String,
165    source_path: String,
166    hash: u64,
167}
168
169/// Runtime for (System)Verilog code.
170pub struct VerilatorRuntime {
171    artifact_directory: Utf8PathBuf,
172    source_files: Vec<Utf8PathBuf>,
173    include_directories: Vec<Utf8PathBuf>,
174    dpi_functions: Vec<&'static dyn DpiFunction>,
175    options: VerilatorRuntimeOptions,
176    /// Mapping between hardware (top, path) and arena index of Verilator
177    /// implementations
178    library_map: RefCell<HashMap<LibraryArenaKey, usize>>,
179    /// Verilator implementations arena
180    library_arena: BoxcarVec<Library>,
181    /// SAFETY: These are dropped when the runtime is dropped. They will not be
182    /// "borrowed mutably" because the models created for this runtime must
183    /// not outlive it and thus will be all gone before these are dropped.
184    model_deallocators:
185        RefCell<Vec<(*mut ffi::c_void, extern "C" fn(*mut ffi::c_void))>>,
186}
187
188impl Drop for VerilatorRuntime {
189    fn drop(&mut self) {
190        for (model, deallocator) in
191            self.model_deallocators.borrow_mut().drain(..)
192        {
193            deallocator(model);
194        }
195    }
196}
197
198/* <Forgive me father for I have sinned> */
199
200// TODO: make cross-platform
201static STDERR: LazyLock<Mutex<fs::File>> =
202    LazyLock::new(|| Mutex::new(unsafe { fs::File::from_raw_fd(2) }));
203
204macro_rules! eprintln_nocapture {
205    ($($contents:tt)*) => {{
206        use snafu::ResultExt;
207
208        writeln!(
209            &mut STDERR.lock().expect("poisoned"),
210            $($contents)*
211        )
212        .whatever_context("Failed to write to non-captured stderr")
213    }};
214}
215
216#[derive(Default)]
217struct ThreadLocalFileLock;
218
219/// The file_guard handles locking across processes, but does not guarantee
220/// locking between threads in one process. Thus, we have this lock to
221/// synchronize threads for a given artifacts directoru.
222static THREAD_LOCKS_PER_BUILD_DIR: LazyLock<
223    DashMap<Utf8PathBuf, Mutex<ThreadLocalFileLock>>,
224> = LazyLock::new(DashMap::default);
225
226/* </Forgive me father for I have sinned> */
227
228fn one_time_library_setup(
229    library: &Library,
230    dpi_functions: &[&'static dyn DpiFunction],
231    tracing_enabled: bool,
232    options: &VerilatorRuntimeOptions,
233) -> Result<(), Whatever> {
234    if !dpi_functions.is_empty() {
235        let dpi_init_callback: extern "C" fn(*const *const ffi::c_void) =
236            *unsafe { library.get(b"dpi_init_callback") }
237                .whatever_context("Failed to load DPI initializer")?;
238
239        // order is important here. the function pointers will be
240        // initialized in the same order that they
241        // appear in the DPI array --- this is to match how the C
242        // initialization code was constructed in `build_library`.
243        let function_pointers = dpi_functions
244            .iter()
245            .map(|dpi_function| dpi_function.pointer())
246            .collect::<Vec<_>>();
247
248        (dpi_init_callback)(function_pointers.as_ptr_range().start);
249
250        if options.log {
251            log::info!("Initialized DPI functions");
252        }
253    }
254
255    if tracing_enabled {
256        let trace_ever_on_callback: extern "C" fn(bool) =
257            *unsafe { library.get(b"ffi_Verilated_traceEverOn") }
258                .whatever_context(
259                    "Model was not configured with tracing enabled",
260                )?;
261        trace_ever_on_callback(true);
262
263        if options.log {
264            log::info!("Initialized VCD tracing");
265        }
266    }
267
268    Ok(())
269}
270
271impl VerilatorRuntime {
272    /// Creates a new runtime for instantiating (System)Verilog modules as Rust
273    /// objects.
274    pub fn new(
275        artifact_directory: &Utf8Path,
276        source_files: &[&Utf8Path],
277        include_directories: &[&Utf8Path],
278        dpi_functions: impl IntoIterator<Item = &'static dyn DpiFunction>,
279        options: VerilatorRuntimeOptions,
280    ) -> Result<Self, Whatever> {
281        if options.log {
282            log::info!("Validating source files");
283        }
284        for source_file in source_files {
285            if !source_file.is_file() {
286                whatever!(
287                    "Source file {} does not exist or is not a file. Note that if it's a relative path, you must be in the correct directory",
288                    source_file
289                );
290            }
291        }
292
293        Ok(Self {
294            artifact_directory: artifact_directory.to_owned(),
295            source_files: source_files
296                .iter()
297                .map(|path| path.to_path_buf())
298                .collect(),
299            include_directories: include_directories
300                .iter()
301                .map(|path| path.to_path_buf())
302                .collect(),
303            dpi_functions: dpi_functions.into_iter().collect(),
304            options,
305            library_map: RefCell::new(HashMap::new()),
306            library_arena: BoxcarVec::new(),
307            model_deallocators: RefCell::new(vec![]),
308        })
309    }
310
311    /// Constructs a new model. Uses lazy and incremental building for
312    /// efficiency.
313    ///
314    /// See also: [`VerilatorRuntime::create_dyn_model`]
315    pub fn create_model_simple<'ctx, M: AsVerilatedModel<'ctx>>(
316        &'ctx self,
317    ) -> Result<M, Whatever> {
318        self.create_model(&VerilatedModelConfig::default())
319    }
320
321    /// Constructs a new model. Uses lazy and incremental building for
322    /// efficiency.
323    ///
324    /// See also: [`VerilatorRuntime::create_dyn_model`]
325    pub fn create_model<'ctx, M: AsVerilatedModel<'ctx>>(
326        &'ctx self,
327        config: &VerilatedModelConfig,
328    ) -> Result<M, Whatever> {
329        let library = self
330            .build_or_retrieve_library(
331                M::name(),
332                M::source_path(),
333                M::ports(),
334                config,
335            )
336            .whatever_context(
337                "Failed to build or retrieve verilator dynamic library. Try removing the build directory if it is corrupted.",
338            )?;
339
340        let delete_model: extern "C" fn(*mut ffi::c_void) = *unsafe {
341            library.get(format!("ffi_delete_V{}", M::name()).as_bytes())
342        }
343        .expect("failed to get symbol");
344
345        let model = M::init_from(library, config.enable_tracing);
346
347        self.model_deallocators.borrow_mut().push((
348            // SAFETY: todo
349            unsafe { model.model() },
350            delete_model,
351        ));
352
353        Ok(model)
354    }
355
356    // TODO: should this be unified with the normal create_model by having
357    // DynamicVerilatedModel implement VerilatedModel?
358
359    /// Constructs a new dynamic model. Uses lazy and incremental building for
360    /// efficiency. You must guarantee the correctness of the suppplied
361    /// information, namely, that `name` is precisely the name of the
362    /// Verilog module, `source_path` is, when canonicalized
363    /// using [`fs::canonicalize`], the relative/absolute path to the Verilog
364    /// file defining the module `name`, and `ports` is a correct subset of
365    /// the ports of the Verilog module.
366    ///
367    /// ```no_run
368    /// # use marlin_verilator::*;
369    /// # use marlin_verilator::dynamic::*;
370    /// # let runtime = VerilatorRuntime::new("".as_ref(), &[], &[], [], Default::default()).unwrap();
371    /// # || -> Result<(), snafu::Whatever> {
372    /// let mut main = runtime.create_dyn_model(
373    ///    "main",
374    ///    "src/main.sv",
375    ///    &[
376    ///        ("medium_input", 31, 0, PortDirection::Input),
377    ///        ("medium_output", 31, 0, PortDirection::Output),
378    ///    ],
379    ///    VerilatedModelConfig::default(),
380    /// )?;
381    /// # Ok(()) };
382    /// ````
383    ///
384    /// See also: [`VerilatorRuntime::create_model`]
385    pub fn create_dyn_model<'ctx>(
386        &'ctx self,
387        name: &str,
388        source_path: &str,
389        ports: &[(&str, usize, usize, PortDirection)],
390        config: VerilatedModelConfig,
391    ) -> Result<DynamicVerilatedModel<'ctx>, Whatever> {
392        let library = self
393            .build_or_retrieve_library(name, source_path, ports, &config)
394            .whatever_context(
395                "Failed to build or retrieve verilator dynamic library. Try removing the build directory if it is corrupted.",
396            )?;
397
398        let new_main: extern "C" fn() -> *mut ffi::c_void =
399            *unsafe { library.get(format!("ffi_new_V{name}").as_bytes()) }
400                .whatever_context(format!(
401                    "Failed to load constructor for module {}",
402                    name
403                ))?;
404        let delete_main =
405            *unsafe { library.get(format!("ffi_delete_V{name}").as_bytes()) }
406                .whatever_context(format!(
407                "Failed to load destructor for module {}",
408                name
409            ))?;
410        let eval_main =
411            *unsafe { library.get(format!("ffi_V{name}_eval").as_bytes()) }
412                .whatever_context(format!(
413                    "Failed to load evalulator for module {}",
414                    name
415                ))?;
416
417        let main = new_main();
418
419        let ports = ports
420            .iter()
421            .copied()
422            .map(|(port, high, low, direction)| {
423                (port.to_string(), (high - low + 1, direction))
424            })
425            .collect();
426
427        self.model_deallocators
428            .borrow_mut()
429            .push((main, delete_main));
430
431        Ok(DynamicVerilatedModel {
432            ports,
433            name: name.to_string(),
434            main,
435            eval_main,
436            library,
437        })
438    }
439
440    /// Invokes verilator to build a dynamic library for the Verilog module
441    /// named `name` defined in the file `source_path` and with signature
442    /// `ports`.
443    ///
444    /// If the library is already cached for the given module name/source path
445    /// pair, then it is returned immediately.
446    ///
447    /// It is required that the `ports` signature matches a subset of the ports
448    /// defined on the Verilog module exactly.
449    ///
450    /// If `self.options.force_verilator_rebuild`, then the library will always
451    /// be rebuilt. Otherwise, it is only rebuilt on (a conservative
452    /// definition) of change:
453    ///
454    /// - Edits to Verilog source code
455    /// - Edits to DPI functions
456    ///
457    /// Then, if this is the first time building the library, and there are DPI
458    /// functions, the library will be initialized with the DPI functions.
459    ///
460    /// See [`build_library::build_library`] for more information.
461    ///
462    /// # Safety
463    ///
464    /// This function is thread-safe.
465    fn build_or_retrieve_library(
466        &self,
467        name: &str,
468        source_path: &str,
469        ports: &[(&str, usize, usize, PortDirection)],
470        config: &VerilatedModelConfig,
471    ) -> Result<&Library, Whatever> {
472        if name.chars().any(|c| c == '\\' || c == ' ') {
473            whatever!("Escaped module names are not supported");
474        }
475
476        if self.options.log {
477            log::info!("Validating model source file");
478        }
479        if !self.source_files.iter().any(|source_file| {
480            match (
481                source_file.canonicalize_utf8(),
482                Utf8Path::new(source_path).canonicalize_utf8(),
483            ) {
484                (Ok(lhs), Ok(rhs)) => lhs == rhs,
485                _ => false,
486            }
487        }) {
488            whatever!(
489                "Module `{}` requires source file {}, which was not provided to the runtime",
490                name,
491                source_path
492            );
493        }
494
495        if let Some((port, _, _, _)) =
496            ports.iter().find(|(_, high, low, _)| high < low)
497        {
498            whatever!(
499                "Port {} on module {} was specified with the high bit less than the low bit",
500                port,
501                name
502            );
503        }
504        if let Some((port, _, _, _)) =
505            ports.iter().find(|(_, high, low, _)| high + 1 - low > 64)
506        {
507            whatever!(
508                "Port {} on module {} is greater than 64 bits",
509                port,
510                name
511            );
512        }
513
514        let mut hasher = hash::DefaultHasher::new();
515        ports.hash(&mut hasher);
516        config.hash(&mut hasher);
517        let library_key = LibraryArenaKey {
518            name: name.to_owned(),
519            source_path: source_path.to_owned(),
520            hash: hasher.finish(),
521        };
522
523        let library_idx = match self
524            .library_map
525            .borrow_mut()
526            .entry(library_key.clone())
527        {
528            Entry::Occupied(entry) => *entry.get(),
529            Entry::Vacant(entry) => {
530                let local_directory_name = format!(
531                    "{name}_{}_{}",
532                    source_path.replace("_", "__").replace("/", "_"),
533                    library_key.hash
534                );
535                let local_artifacts_directory =
536                    self.artifact_directory.join(&local_directory_name);
537
538                if self.options.log {
539                    log::info!(
540                        "Creating artifacts directory {}",
541                        local_artifacts_directory
542                    );
543                }
544                fs::create_dir_all(&local_artifacts_directory)
545                    .whatever_context(format!(
546                        "Failed to create artifacts directory {}",
547                        local_artifacts_directory,
548                    ))?;
549
550                //eprintln_nocapture!(
551                //    "on thread {:?}",
552                //    std::thread::current().id()
553                //)?;
554
555                if !THREAD_LOCKS_PER_BUILD_DIR
556                    .contains_key(&local_artifacts_directory)
557                {
558                    THREAD_LOCKS_PER_BUILD_DIR.insert(
559                        local_artifacts_directory.clone(),
560                        Default::default(),
561                    );
562                }
563                let thread_mutex = THREAD_LOCKS_PER_BUILD_DIR
564                    .get(&local_artifacts_directory)
565                    .expect("We just inserted if it didn't exist");
566
567                let _thread_lock = if let Ok(_thread_lock) =
568                    thread_mutex.try_lock()
569                {
570                    //eprintln_nocapture!(
571                    //    "thread-level try lock for {:?} succeeded",
572                    //    std::thread::current().id()
573                    //)?;
574                    _thread_lock
575                } else {
576                    eprintln_nocapture!(
577                        "{} waiting for file lock on build directory",
578                        "    Blocking".bold().cyan(),
579                    )?;
580                    let Ok(_thread_lock) = thread_mutex.lock() else {
581                        whatever!(
582                            "Failed to acquire thread-local lock for artifacts directory"
583                        );
584                    };
585                    _thread_lock
586                };
587
588                // # Safety
589                // build_library is not thread-safe, so we have to lock the
590                // directory
591                if self.options.log {
592                    log::info!("Acquiring file lock on artifact directory");
593                }
594                let lockfile = fs::OpenOptions::new()
595                .read(true)
596                .write(true)
597                .create(true)
598                .truncate(true)
599                .open(self.artifact_directory.join(format!("{local_directory_name}.lock")))
600                .whatever_context(
601                    "Failed to open lockfile for artifacts directory (this is not the actual lock itself, it is an I/O error)",
602                )?;
603
604                let _file_lock = file_guard::lock(
605                    &lockfile,
606                    file_guard::Lock::Exclusive,
607                    0,
608                    1,
609                )
610                .whatever_context(
611                    "Failed to acquire file lock for artifacts directory",
612                )?;
613                //eprintln_nocapture!(
614                //    "lockfile for {:?} succeeded",
615                //    std::thread::current().id()
616                //)?;
617
618                let start = Instant::now();
619
620                if self.options.log {
621                    log::info!("Building the dynamic library with verilator");
622                }
623                let (library_path, was_rebuilt) = build_library(
624                    &self.source_files,
625                    &self.include_directories,
626                    &self.dpi_functions,
627                    name,
628                    ports,
629                    &local_artifacts_directory,
630                    &self.options,
631                    config,
632                    self.options.log,
633                    || {
634                        eprintln_nocapture!(
635                            "{} {}#{} ({})",
636                            "   Compiling".bold().green(),
637                            name,
638                            library_key.hash,
639                            source_path
640                        )
641                    },
642                )
643                .whatever_context(
644                    "Failed to build verilator dynamic library",
645                )?;
646
647                if self.options.log {
648                    log::info!("Opening the dynamic library");
649                }
650                let library = unsafe { Library::new(library_path) }
651                    .whatever_context(
652                        "Failed to load verilator dynamic library",
653                    )?;
654
655                one_time_library_setup(
656                    &library,
657                    &self.dpi_functions,
658                    config.enable_tracing,
659                    &self.options,
660                )?;
661
662                let library_idx = self.library_arena.push(library);
663                entry.insert(library_idx);
664
665                let end = Instant::now();
666                let duration = end - start;
667
668                if was_rebuilt {
669                    eprintln_nocapture!(
670                        "{} `verilator-{}` profile target in {}.{:02}s",
671                        "    Finished".bold().green(),
672                        if config.verilator_optimization == 0 {
673                            "unoptimized".into()
674                        } else {
675                            format!("O{}", config.verilator_optimization)
676                        },
677                        duration.as_secs(),
678                        duration.subsec_millis() / 10
679                    )?;
680                }
681
682                library_idx
683            }
684        };
685
686        Ok(self
687            .library_arena
688            .get(library_idx)
689            .expect("bug: We just inserted the library"))
690    }
691}