1use core::convert::Into;
16use std::{
17 cell::RefCell,
18 cmp,
19 collections::{HashMap, hash_map::Entry},
20 ffi::{self, OsStr, OsString},
21 fmt, fs,
22 hash::{self, Hash, Hasher},
23 process::Command,
24 slice,
25 sync::{LazyLock, Mutex},
26 time::Instant,
27};
28
29use boxcar::Vec as BoxcarVec;
30use build_library::build_library;
31use camino::{Utf8Path, Utf8PathBuf};
32use dashmap::DashMap;
33use dpi::DpiFunction;
34use dynamic::DynamicVerilatedModel;
35use libloading::Library;
36use owo_colors::OwoColorize;
37use snafu::{OptionExt, ResultExt, Whatever, whatever};
38
39mod build_library;
40pub mod dpi;
41pub mod dynamic;
42pub mod ffi_names;
43pub mod nocapture;
44pub mod vcd;
45
46pub use dynamic::AsDynamicVerilatedModel;
47
48use crate::{
49 dynamic::DynamicPortInfo,
50 ffi_names::{DPI_INIT_CALLBACK, TRACE_EVER_ON},
51};
52
53pub mod types {
55 pub type CData = u8;
58
59 pub type SData = u16;
62
63 pub type IData = u32;
66
67 pub type QData = u64;
70
71 pub type EData = u32;
74
75 pub type WData = EData;
78
79 pub type WDataInP = *const WData;
82
83 pub type WDataOutP = *mut WData;
86}
87
88pub const fn compute_wdata_word_count_from_width_not_msb(
93 width: usize,
94) -> usize {
95 width.div_ceil(types::WData::BITS as usize)
96}
97
98pub const fn compute_approx_width_from_wdata_word_count(
103 word_count: usize,
104) -> usize {
105 word_count * (types::WData::BITS as usize)
106}
107
108#[derive(PartialEq, Eq, Hash, Clone, Debug)]
112pub struct WideIn<const WORDS: usize> {
113 inner: [types::WData; WORDS],
114}
115
116impl<const WORDS: usize> WideIn<WORDS> {
117 pub fn new(value: [types::WData; WORDS]) -> Self {
118 Self { inner: value }
119 }
120
121 pub fn value(&self) -> &[types::WData; WORDS] {
122 &self.inner
123 }
124
125 #[doc(hidden)]
129 pub fn as_ptr(&self) -> types::WDataInP {
130 self.inner.as_ptr()
131 }
132}
133
134impl<const WORDS: usize> Default for WideIn<WORDS> {
135 fn default() -> Self {
136 Self { inner: [0; WORDS] }
137 }
138}
139
140#[derive(PartialEq, Eq, Hash, Clone, Debug)]
142pub struct WideOut<const WORDS: usize> {
143 inner: [types::WData; WORDS],
144}
145
146impl<const WORDS: usize> WideOut<WORDS> {
147 pub fn value(&self) -> &[types::WData; WORDS] {
148 &self.inner
149 }
150
151 #[allow(clippy::not_unsafe_ptr_arg_deref)]
155 #[doc(hidden)]
156 pub fn from_ptr(raw: types::WDataOutP) -> Self {
157 let mut inner = [0; WORDS];
158 inner.copy_from_slice(unsafe { slice::from_raw_parts(raw, WORDS) });
159 Self { inner }
160 }
161}
162
163impl<const WORDS: usize> From<WideOut<WORDS>> for [types::WData; WORDS] {
164 fn from(val: WideOut<WORDS>) -> Self {
165 val.inner
166 }
167}
168
169impl<const WORDS: usize> Default for WideOut<WORDS> {
170 fn default() -> Self {
171 Self { inner: [0; WORDS] }
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
176pub enum PortDirection {
177 Input,
178 Output,
179 Inout,
180}
181
182impl fmt::Display for PortDirection {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 match self {
185 PortDirection::Input => "input",
186 PortDirection::Output => "output",
187 PortDirection::Inout => "inout",
188 }
189 .fmt(f)
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub enum CxxStandard {
197 Cxx98,
198 Cxx11,
199 Cxx14,
200 Cxx17,
201 Cxx20,
202 Cxx23,
203 Cxx26,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Hash)]
208pub struct VerilatedModelConfig {
209 pub verilator_optimization: usize,
212
213 pub ignored_warnings: Vec<String>,
216
217 pub enable_tracing: bool,
219
220 pub cxx_executable: String,
223
224 pub cxx_standard: Option<CxxStandard>,
226}
227
228impl Default for VerilatedModelConfig {
229 fn default() -> Self {
230 Self {
231 verilator_optimization: 0,
232 ignored_warnings: Default::default(),
233 enable_tracing: Default::default(),
234 cxx_executable: "c++".into(),
235 cxx_standard: Some(CxxStandard::Cxx14),
236 }
237 }
238}
239
240impl VerilatedModelConfig {
241 pub fn verilator_optimization(self, level: usize) -> Self {
242 Self {
243 verilator_optimization: level,
244 ..self
245 }
246 }
247
248 pub fn enable_tracing(self, tracing_enabled: bool) -> Self {
249 Self {
250 enable_tracing: tracing_enabled,
251 ..self
252 }
253 }
254
255 pub fn cxx_executable(self, cxx_executable: String) -> Self {
256 Self {
257 cxx_executable,
258 ..self
259 }
260 }
261
262 pub fn cxx_standard(self, cxx_standard: Option<CxxStandard>) -> Self {
263 Self {
264 cxx_standard,
265 ..self
266 }
267 }
268}
269
270pub trait AsVerilatedModel<'ctx>: 'ctx {
273 fn name() -> &'static str;
275
276 fn source_path() -> &'static str;
278
279 fn ports() -> &'static [(&'static str, usize, usize, PortDirection)];
282
283 #[doc(hidden)]
284 fn init_from(library: &'ctx Library, tracing_enabled: bool) -> Self;
285
286 #[doc(hidden)]
287 unsafe fn model(&self) -> *mut ffi::c_void;
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
293pub struct VerilatorRuntimeOptions {
294 pub verilator_executable: OsString,
297
298 pub allow_unsupported_verilator: Option<VerilatorVersion>,
301
302 pub force_verilator_rebuild: bool,
305
306 pub log: bool,
308}
309
310impl Default for VerilatorRuntimeOptions {
311 fn default() -> Self {
312 Self {
313 verilator_executable: "verilator".into(),
314 allow_unsupported_verilator: None,
315 force_verilator_rebuild: false,
316 log: false,
317 }
318 }
319}
320
321impl VerilatorRuntimeOptions {
322 pub fn default_logging() -> Self {
325 Self {
326 log: true,
327 ..Default::default()
328 }
329 }
330
331 pub fn verilator_executable(self, verilator_executable: OsString) -> Self {
332 Self {
333 verilator_executable,
334 ..self
335 }
336 }
337
338 pub fn allow_unsupported_verilator(
339 self,
340 version: Option<VerilatorVersion>,
341 ) -> Self {
342 Self {
343 allow_unsupported_verilator: version,
344 ..self
345 }
346 }
347
348 pub fn force_verilator_rebuild(
349 self,
350 force_verilator_rebuild: bool,
351 ) -> Self {
352 Self {
353 force_verilator_rebuild,
354 ..self
355 }
356 }
357}
358
359#[derive(PartialEq, Eq, Hash, Clone)]
360struct LibraryArenaKey {
361 name: String,
362 source_path: String,
363 hash: u64,
364}
365
366struct ModelDeallocator {
367 model: *mut ffi::c_void,
368 deallocator: extern "C" fn(*mut ffi::c_void),
369}
370
371#[derive(Clone, Copy)]
372enum BuildTarget {
373 Linux,
374 MacOS,
375}
376
377pub struct VerilatorRuntime {
379 artifact_directory: Utf8PathBuf,
380 build_target: BuildTarget,
381 source_files: Vec<Utf8PathBuf>,
382 include_directories: Vec<Utf8PathBuf>,
383 dpi_functions: Vec<&'static dyn DpiFunction>,
384 options: VerilatorRuntimeOptions,
385 verilator_version: VerilatorVersion,
386 library_map: RefCell<HashMap<LibraryArenaKey, usize>>,
389 library_arena: BoxcarVec<Library>,
391 model_deallocators: RefCell<Vec<ModelDeallocator>>,
395}
396
397impl Drop for VerilatorRuntime {
398 fn drop(&mut self) {
399 for ModelDeallocator { model, deallocator } in
400 self.model_deallocators.borrow_mut().drain(..)
401 {
402 deallocator(model);
404 }
405 }
406}
407
408#[derive(Default)]
411struct ThreadLocalFileLock;
412
413static THREAD_LOCKS_PER_BUILD_DIR: LazyLock<
417 DashMap<Utf8PathBuf, Mutex<ThreadLocalFileLock>>,
418> = LazyLock::new(DashMap::default);
419
420fn one_time_library_setup(
423 library: &Library,
424 dpi_functions: &[&'static dyn DpiFunction],
425 tracing_enabled: bool,
426 options: &VerilatorRuntimeOptions,
427) -> Result<(), Whatever> {
428 if !dpi_functions.is_empty() {
429 let dpi_init_callback: extern "C" fn(*const *const ffi::c_void) =
430 *unsafe { library.get(DPI_INIT_CALLBACK.as_bytes()) }
431 .whatever_context("Failed to load DPI initializer")?;
432
433 let function_pointers = dpi_functions
438 .iter()
439 .map(|dpi_function| dpi_function.pointer())
440 .collect::<Vec<_>>();
441
442 (dpi_init_callback)(function_pointers.as_ptr_range().start);
443
444 if options.log {
445 log::info!("Initialized DPI functions");
446 }
447 }
448
449 if tracing_enabled {
450 let trace_ever_on_callback: extern "C" fn(bool) =
451 *unsafe { library.get(TRACE_EVER_ON.as_bytes()) }
452 .whatever_context(
453 "Model was not configured with tracing enabled",
454 )?;
455 trace_ever_on_callback(true);
456
457 if options.log {
458 log::info!("Initialized VCD tracing");
459 }
460 }
461
462 Ok(())
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
466pub struct VerilatorVersion {
467 pub major: usize,
468 pub minor: usize,
469}
470
471#[macro_export]
472macro_rules! verilator_version {
473 ($major:literal $minor:literal) => {
474 $crate::VerilatorVersion {
475 major: $major,
476 #[allow(clippy::zero_prefixed_literal)]
477 minor: $minor,
478 }
479 };
480}
481
482impl fmt::Display for VerilatorVersion {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 write!(f, "{}.{:03}", self.major, self.minor)
485 }
486}
487
488impl cmp::PartialOrd for VerilatorVersion {
489 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
490 Some(self.cmp(other))
491 }
492}
493
494impl cmp::Ord for VerilatorVersion {
495 fn cmp(&self, other: &Self) -> cmp::Ordering {
496 self.major
497 .cmp(&other.major)
498 .then(self.minor.cmp(&other.minor))
499 }
500}
501
502pub const MINIMUM_SUPPORTED_VERILATOR: VerilatorVersion =
503 verilator_version!(5 025);
504
505fn retrieve_verilator_version(
506 verilator_executable: &OsStr,
507) -> Result<VerilatorVersion, Whatever> {
508 let output = Command::new(verilator_executable)
509 .arg("--version")
510 .output()
511 .whatever_context("Failed to retrieve Verilator --version")?;
512 let stdout = String::from_utf8(output.stdout).whatever_context(
513 "Failed to decode Verilator --version output as UTF-8",
514 )?;
515 let (major, minor_and_rest) = stdout
516 .strip_prefix("Verilator ")
517 .and_then(|rest| rest.split_once('.'))
518 .whatever_context("Unexpected Verilator --version output format")?;
519 let minor = minor_and_rest
520 .split_once(' ')
521 .map(|(minor, _)| minor)
522 .unwrap_or(minor_and_rest);
523 Ok(VerilatorVersion {
524 major: major.parse().whatever_context(
525 "Failed to parse major version from Verilator --version output",
526 )?,
527 minor: minor.parse().whatever_context(
528 "Failed to parse minor version from Verilator --version output",
529 )?,
530 })
531}
532
533fn check_verilator_version(version: VerilatorVersion) -> Result<(), Whatever> {
534 if MINIMUM_SUPPORTED_VERILATOR.major != version.major
535 || version.minor < MINIMUM_SUPPORTED_VERILATOR.minor
536 {
537 whatever!(
538 "Unsupported Verilator version {version} (see `VerilatorRuntimeOptions::allow_unsupported_verilator`)"
539 );
540 }
541 Ok(())
542}
543
544impl VerilatorRuntime {
545 pub fn new(
548 artifact_directory: &Utf8Path,
549 source_files: &[&Utf8Path],
550 include_directories: &[&Utf8Path],
551 dpi_functions: impl IntoIterator<Item = &'static dyn DpiFunction>,
552 options: VerilatorRuntimeOptions,
553 ) -> Result<Self, Whatever> {
554 if options.log {
555 log::info!("Validating Verilator version");
556 }
557 let verilator_version =
558 retrieve_verilator_version(&options.verilator_executable)?;
559 if let Some(allowed_version) = options.allow_unsupported_verilator {
560 if verilator_version < allowed_version {
561 whatever!(
562 "Unsupported Verilator version {verilator_version} ({allowed_version} was explicitly allowed)"
563 )
564 }
565 } else {
566 check_verilator_version(verilator_version)?;
567 }
568
569 if options.log {
570 log::info!("Validating source files");
571 }
572 for source_file in source_files {
573 if !source_file.is_file() {
574 whatever!(
575 "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",
576 source_file
577 );
578 }
579 }
580
581 let uname_output = Command::new("uname")
582 .output()
583 .whatever_context("Invocation of uname failed")?;
584
585 if !uname_output.status.success() {
586 whatever!(
587 "Invocation of uname failed with nonzero exit code {}\n\n--- STDOUT ---\n{}\n\n--- STDERR ---\n{}",
588 uname_output.status,
589 String::from_utf8_lossy(&uname_output.stdout),
590 String::from_utf8_lossy(&uname_output.stderr)
591 );
592 }
593
594 let build_target = if String::from_utf8(uname_output.stdout)
595 .map(|s| s.trim() == "Darwin")
596 .unwrap_or(false)
597 {
598 BuildTarget::MacOS
599 } else {
600 BuildTarget::Linux
601 };
602
603 Ok(Self {
604 artifact_directory: artifact_directory.to_owned(),
605 build_target,
606 source_files: source_files
607 .iter()
608 .map(|path| path.to_path_buf())
609 .collect(),
610 include_directories: include_directories
611 .iter()
612 .map(|path| path.to_path_buf())
613 .collect(),
614 dpi_functions: dpi_functions.into_iter().collect(),
615 options,
616 verilator_version,
617 library_map: RefCell::new(HashMap::new()),
618 library_arena: BoxcarVec::new(),
619 model_deallocators: RefCell::new(vec![]),
620 })
621 }
622
623 pub fn create_model_simple<'ctx, M: AsVerilatedModel<'ctx>>(
628 &'ctx self,
629 ) -> Result<M, Whatever> {
630 self.create_model(&VerilatedModelConfig::default())
631 }
632
633 pub fn create_model<'ctx, M: AsVerilatedModel<'ctx>>(
642 &'ctx self,
643 config: &VerilatedModelConfig,
644 ) -> Result<M, Whatever> {
645 let library = self
646 .build_or_retrieve_library(
647 M::name(),
648 M::source_path(),
649 M::ports(),
650 config,
651 )
652 .whatever_context(
653 "Failed to build or retrieve verilator dynamic library. Try removing the build directory if it is corrupted.",
654 )?;
655
656 let delete_model: extern "C" fn(*mut ffi::c_void) = *unsafe {
657 library.get(format!("ffi_delete_V{}", M::name()).as_bytes())
658 }
659 .expect("failed to get symbol");
660
661 let model = M::init_from(library, config.enable_tracing);
662
663 self.model_deallocators.borrow_mut().push(ModelDeallocator {
664 model: unsafe { model.model() },
668 deallocator: delete_model,
669 });
670
671 Ok(model)
672 }
673
674 pub fn create_dyn_model<'ctx>(
704 &'ctx self,
705 name: &str,
706 source_path: &str,
707 ports: &[(&str, usize, usize, PortDirection)],
708 config: VerilatedModelConfig,
709 ) -> Result<DynamicVerilatedModel<'ctx>, Whatever> {
710 let library = self
711 .build_or_retrieve_library(name, source_path, ports, &config)
712 .whatever_context(
713 "Failed to build or retrieve verilator dynamic library. Try removing the build directory if it is corrupted.",
714 )?;
715
716 let new_main: extern "C" fn() -> *mut ffi::c_void =
717 *unsafe { library.get(format!("ffi_new_V{name}").as_bytes()) }
718 .whatever_context(format!(
719 "Failed to load constructor for module {name}"
720 ))?;
721 let delete_main =
722 *unsafe { library.get(format!("ffi_delete_V{name}").as_bytes()) }
723 .whatever_context(format!(
724 "Failed to load destructor for module {name}"
725 ))?;
726 let eval_main =
727 *unsafe { library.get(format!("ffi_V{name}_eval").as_bytes()) }
728 .whatever_context(format!(
729 "Failed to load evalulator for module {name}"
730 ))?;
731
732 let main = new_main();
733
734 let ports = ports
735 .iter()
736 .copied()
737 .map(|(port, high, low, direction)| {
738 (
739 port.to_string(),
740 DynamicPortInfo {
741 width: high + 1 - low,
742 direction,
743 },
744 )
745 })
746 .collect();
747
748 self.model_deallocators.borrow_mut().push(ModelDeallocator {
749 model: main,
750 deallocator: delete_main,
751 });
752
753 Ok(DynamicVerilatedModel {
754 ports,
755 name: name.to_string(),
756 main,
757 eval_main,
758 library,
759 })
760 }
761
762 fn build_or_retrieve_library(
788 &self,
789 name: &str,
790 source_path: &str,
791 ports: &[(&str, usize, usize, PortDirection)],
792 config: &VerilatedModelConfig,
793 ) -> Result<&Library, Whatever> {
794 if name.chars().any(|c| c == '\\' || c == ' ') {
795 whatever!("Escaped module names are not supported");
796 }
797
798 if self.options.log {
799 log::info!("Validating model source file");
800 }
801 if !self.source_files.iter().any(|source_file| {
802 match (
803 source_file.canonicalize_utf8(),
804 Utf8Path::new(source_path).canonicalize_utf8(),
805 ) {
806 (Ok(lhs), Ok(rhs)) => lhs == rhs,
807 _ => false,
808 }
809 }) {
810 whatever!(
811 "Module `{}` requires source file {}, which was not provided to the runtime",
812 name,
813 source_path
814 );
815 }
816
817 if let Some((port, _, _, _)) =
818 ports.iter().find(|(_, high, low, _)| high < low)
819 {
820 whatever!(
821 "Port {} on module {} was specified with the high bit less than the low bit",
822 port,
823 name
824 );
825 }
826
827 let mut hasher = hash::DefaultHasher::new();
828 ports.hash(&mut hasher);
829 config.hash(&mut hasher);
830 let library_key = LibraryArenaKey {
831 name: name.to_owned(),
832 source_path: source_path.to_owned(),
833 hash: hasher.finish(),
834 };
835
836 let library_idx = match self
837 .library_map
838 .borrow_mut()
839 .entry(library_key.clone())
840 {
841 Entry::Occupied(entry) => *entry.get(),
842 Entry::Vacant(entry) => {
843 let local_directory_name = format!(
844 "{name}_{}_{}",
845 source_path.replace("_", "__").replace("/", "_"),
846 library_key.hash
847 );
848 let local_artifacts_directory =
849 self.artifact_directory.join(&local_directory_name);
850
851 if self.options.log {
852 log::info!(
853 "Creating artifacts directory {}",
854 local_artifacts_directory
855 );
856 }
857 fs::create_dir_all(&local_artifacts_directory)
858 .whatever_context(format!(
859 "Failed to create artifacts directory {local_artifacts_directory}",
860 ))?;
861
862 if !THREAD_LOCKS_PER_BUILD_DIR
868 .contains_key(&local_artifacts_directory)
869 {
870 THREAD_LOCKS_PER_BUILD_DIR.insert(
871 local_artifacts_directory.clone(),
872 Default::default(),
873 );
874 }
875 let thread_mutex = THREAD_LOCKS_PER_BUILD_DIR
876 .get(&local_artifacts_directory)
877 .expect("We just inserted if it didn't exist");
878
879 let _thread_lock = if let Ok(_thread_lock) =
880 thread_mutex.try_lock()
881 {
882 _thread_lock
887 } else {
888 eprintln_nocapture!(
889 "{} waiting for file lock on artifact directory",
890 " Blocking".bold().green(),
891 )?;
892 let Ok(_thread_lock) = thread_mutex.lock() else {
893 whatever!(
894 "Failed to acquire thread-local lock for artifacts directory"
895 );
896 };
897 _thread_lock
898 };
899
900 if self.options.log {
904 log::info!("Acquiring file lock on build directory");
905 }
906 let lockfile = fs::OpenOptions::new()
907 .read(true)
908 .write(true)
909 .create(true)
910 .truncate(true)
911 .open(self.artifact_directory.join(format!("{local_directory_name}.lock")))
912 .whatever_context(
913 "Failed to open lockfile for artifacts directory (this is not the actual lock itself, it is an I/O error)",
914 )?;
915
916 let _file_lock = file_guard::lock(
917 &lockfile,
918 file_guard::Lock::Exclusive,
919 0,
920 1,
921 )
922 .whatever_context(
923 "Failed to acquire file lock for artifacts directory",
924 )?;
925 let start = Instant::now();
931
932 if self.options.log {
933 log::info!("Building the dynamic library with verilator");
934 }
935 let (library_path, was_rebuilt) = build_library(
936 &self.source_files,
937 self.build_target,
938 &self.include_directories,
939 &self.dpi_functions,
940 name,
941 ports,
942 &local_artifacts_directory,
943 &self.options,
944 config,
945 self.verilator_version,
946 self.options.log,
947 || {
948 eprintln_nocapture!(
949 "{} {}#{} ({})",
950 " Compiling".bold().green(),
951 name,
952 library_key.hash,
953 source_path
954 )
955 },
956 )
957 .whatever_context(
958 "Failed to build verilator dynamic library",
959 )?;
960
961 if self.options.log {
962 log::info!("Opening the dynamic library");
963 }
964 let library = unsafe { Library::new(library_path) }
965 .whatever_context(
966 "Failed to load verilator dynamic library",
967 )?;
968
969 one_time_library_setup(
970 &library,
971 &self.dpi_functions,
972 config.enable_tracing,
973 &self.options,
974 )?;
975
976 let library_idx = self.library_arena.push(library);
977 entry.insert(library_idx);
978
979 let end = Instant::now();
980 let duration = end - start;
981
982 if was_rebuilt {
983 eprintln_nocapture!(
984 "{} `verilator-{}` profile [{}] target in {}.{:02}s",
985 " Finished".bold().green(),
986 if config.verilator_optimization == 0 {
987 "O0".into()
988 } else {
989 format!("O{}", config.verilator_optimization)
990 },
991 if config.verilator_optimization == 0 {
992 "unoptimized"
993 } else {
994 "optimized"
995 },
996 duration.as_secs(),
997 duration.subsec_millis() / 10
998 )?;
999 }
1000
1001 library_idx
1002 }
1003 };
1004
1005 Ok(self
1006 .library_arena
1007 .get(library_idx)
1008 .expect("bug: We just inserted the library"))
1009 }
1010}