Skip to main content

rustdv_gpi/
rustdv_gpi.rs

1//! # rustdv-gpi
2//!
3//! Safe wrapper over the simulator programming interface (design-doc §3.3).
4//! Invariants upheld here so everything above is safe Rust:
5//!
6//! 1. Handles are opaque and non-null; fallible acquisition is `Result`.
7//! 2. Object-handle lifetime = simulation lifetime (freely `Copy`able IDs).
8//!    Callback registrations are modeled by RAII ([`CallbackHandle`]):
9//!    dropping a live handle removes it, and fired one-shots remove themselves
10//!    from inside the trampoline while both supported simulators still accept
11//!    the registration handle.
12//! 3. Strings are copied at the boundary, every call.
13//! 4. No unwinding across FFI: every trampoline wraps the closure in
14//!    `catch_unwind`; panics are routed to the panic sink.
15//! 5. Callback user-data ownership: an `Rc` whose C-side reference is
16//!    reclaimed exactly once (on fire for one-shots, on removal otherwise).
17//!
18//! Thread affinity (§3.4): all types here hold raw pointers and are
19//! therefore `!Send`/`!Sync` — the compiler rejects moving them off the
20//! simulator thread.
21
22use std::cell::{Cell, RefCell};
23use std::ffi::{CStr, CString};
24use std::fmt;
25use std::panic::{catch_unwind, AssertUnwindSafe};
26use std::rc::Rc;
27
28use rustdv_gpi_sys as sys;
29
30// Test executables need vpi_* symbol definitions (the simulator provides
31// them for the real cdylib) — see rustdv-vpi-stubs.
32#[cfg(test)]
33use rustdv_vpi_stubs as _;
34
35pub mod value;
36pub use value::{Logic, LogicArray};
37
38// ===========================================================================
39// Errors
40// ===========================================================================
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum HandleError {
44    /// Name did not resolve (cocotb raises AttributeError here; rustdv
45    /// returns this — design-doc §0.6).
46    NotFound { name: String, scope: String },
47    /// Handle exists but is not the requested kind.
48    WrongKind { name: String, expected: &'static str, actual: String },
49    NoTopModule,
50}
51
52impl fmt::Display for HandleError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            HandleError::NotFound { name, scope } => {
56                write!(f, "no object named '{name}' in scope '{scope}'")
57            }
58            HandleError::WrongKind { name, expected, actual } => {
59                write!(f, "'{name}' is a {actual}, expected {expected}")
60            }
61            HandleError::NoTopModule => write!(f, "no top-level module found"),
62        }
63    }
64}
65impl std::error::Error for HandleError {}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ValueError {
69    /// Value contains x/z bits and was asked for as an integer.
70    FourState(String),
71    Width { want: u32, have: usize },
72}
73
74impl fmt::Display for ValueError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            ValueError::FourState(s) => write!(f, "value '{s}' has x/z bits"),
78            ValueError::Width { want, have } => write!(f, "width mismatch: want {want}, have {have}"),
79        }
80    }
81}
82impl std::error::Error for ValueError {}
83
84// ===========================================================================
85// Handles
86// ===========================================================================
87
88/// Raw non-null simulator object handle. Valid for the whole simulation
89/// (invariant 2), hence `Copy`. `!Send` because it wraps a raw pointer.
90#[derive(Copy, Clone, PartialEq, Eq)]
91pub struct ObjHandle(sys::vpiHandle);
92
93impl ObjHandle {
94    fn new(h: sys::vpiHandle) -> Option<Self> {
95        if h.is_null() { None } else { Some(ObjHandle(h)) }
96    }
97    fn get(self, prop: i32) -> i32 {
98        unsafe { sys::vpi_get(prop, self.0) }
99    }
100    fn get_str(self, prop: i32) -> String {
101        // Invariant 3: copy immediately.
102        unsafe {
103            let p = sys::vpi_get_str(prop, self.0);
104            if p.is_null() {
105                String::new()
106            } else {
107                CStr::from_ptr(p).to_string_lossy().into_owned()
108            }
109        }
110    }
111}
112
113/// Any simulator object, classified by type (design-doc §3.3 downcasting).
114#[derive(Copy, Clone)]
115pub enum AnyHandle {
116    Hierarchy(HierarchyHandle),
117    Logic(LogicHandle),
118    Other(ObjHandle),
119}
120
121impl AnyHandle {
122    pub fn classify(h: ObjHandle) -> AnyHandle {
123        match h.get(sys::vpiType) {
124            sys::vpiModule => AnyHandle::Hierarchy(HierarchyHandle { h }),
125            sys::vpiNet | sys::vpiReg | sys::vpiIntegerVar | sys::vpiPort | sys::vpiMemory
126            | sys::vpiLongIntVar | sys::vpiShortIntVar | sys::vpiIntVar | sys::vpiByteVar
127            | sys::vpiEnumVar | sys::vpiBitVar => {
128                // Signal width is immutable for the lifetime of a VPI
129                // object. Cache it at discovery so every value read/write
130                // does not pay for another vpi_get(vpiSize) crossing.
131                let width = h.get(sys::vpiSize).max(0) as u32;
132                AnyHandle::Logic(LogicHandle { h, width })
133            }
134            _ => AnyHandle::Other(h),
135        }
136    }
137
138    pub fn as_logic(self) -> Result<LogicHandle, HandleError> {
139        match self {
140            AnyHandle::Logic(l) => Ok(l),
141            AnyHandle::Hierarchy(h) => Err(HandleError::WrongKind {
142                name: h.full_name(),
143                expected: "signal",
144                actual: "module".into(),
145            }),
146            AnyHandle::Other(o) => Err(HandleError::WrongKind {
147                name: o.get_str(sys::vpiFullName),
148                expected: "signal",
149                actual: format!("vpiType {}", o.get(sys::vpiType)),
150            }),
151        }
152    }
153
154    pub fn as_hierarchy(self) -> Result<HierarchyHandle, HandleError> {
155        match self {
156            AnyHandle::Hierarchy(h) => Ok(h),
157            AnyHandle::Logic(l) => Err(HandleError::WrongKind {
158                name: l.full_name(),
159                expected: "module",
160                actual: "signal".into(),
161            }),
162            AnyHandle::Other(o) => Err(HandleError::WrongKind {
163                name: o.get_str(sys::vpiFullName),
164                expected: "module",
165                actual: format!("vpiType {}", o.get(sys::vpiType)),
166            }),
167        }
168    }
169}
170
171/// A module/scope handle. Port of cocotb's HierarchyObject
172/// (cocotb: handle.py), with `Result`-returning lookup (mapping row 19).
173#[derive(Copy, Clone)]
174pub struct HierarchyHandle {
175    h: ObjHandle,
176}
177
178impl HierarchyHandle {
179    /// A handle to nothing, for unit tests that need a `RustdvCtx` but never
180    /// touch the DUT. Any VPI call through it goes to `rustdv-vpi-stubs`,
181    /// which panics — so a test that *does* touch the DUT fails loudly
182    /// instead of reading garbage.
183    pub fn null_for_test() -> HierarchyHandle {
184        HierarchyHandle { h: ObjHandle(std::ptr::null_mut()) }
185    }
186
187    /// Dynamic child lookup: `dut.child("clk")?` (design-doc OQ-6 lean).
188    pub fn child(&self, name: &str) -> Result<AnyHandle, HandleError> {
189        let cname = CString::new(name).expect("NUL in signal name");
190        let h = unsafe { sys::vpi_handle_by_name(cname.as_ptr(), self.h.0) };
191        match ObjHandle::new(h) {
192            Some(h) => Ok(AnyHandle::classify(h)),
193            None => Err(HandleError::NotFound { name: name.into(), scope: self.full_name() }),
194        }
195    }
196
197    /// Shorthand: child that must be a signal.
198    pub fn signal(&self, name: &str) -> Result<LogicHandle, HandleError> {
199        self.child(name)?.as_logic()
200    }
201
202    pub fn name(&self) -> String {
203        self.h.get_str(sys::vpiName)
204    }
205    pub fn full_name(&self) -> String {
206        self.h.get_str(sys::vpiFullName)
207    }
208
209    /// Iterate child objects (modules, nets, regs) — serves the
210    /// `visit_children` debug-print role at the DUT level.
211    pub fn children(&self) -> Vec<AnyHandle> {
212        let mut out = Vec::new();
213        for t in [sys::vpiModule, sys::vpiNet, sys::vpiReg] {
214            unsafe {
215                let it = sys::vpi_iterate(t, self.h.0);
216                if it.is_null() {
217                    continue;
218                }
219                loop {
220                    let c = sys::vpi_scan(it);
221                    if c.is_null() {
222                        break; // scan returning NULL frees the iterator
223                    }
224                    if let Some(h) = ObjHandle::new(c) {
225                        out.push(AnyHandle::classify(h));
226                    }
227                }
228            }
229        }
230        out
231    }
232}
233
234/// A value-bearing signal handle (net/reg/var). Port of cocotb's
235/// LogicObject surface: explicit `get()`/`set()` (mapping row 20).
236/// Buffered-write semantics live a layer up in rustdv-sim; the methods
237/// here apply values immediately.
238#[derive(Copy, Clone, PartialEq, Eq)]
239pub struct LogicHandle {
240    h: ObjHandle,
241    width: u32,
242}
243
244impl LogicHandle {
245    pub fn name(&self) -> String {
246        self.h.get_str(sys::vpiName)
247    }
248    pub fn full_name(&self) -> String {
249        self.h.get_str(sys::vpiFullName)
250    }
251    pub fn size(&self) -> u32 {
252        self.width
253    }
254
255    /// Current value as a binary string, e.g. "0101", "xxxx".
256    pub fn get_binstr(&self) -> String {
257        let mut val = sys::t_vpi_value {
258            format: sys::vpiBinStrVal,
259            value: sys::u_vpi_value_union { integer: 0 },
260        };
261        unsafe {
262            sys::vpi_get_value(self.h.0, &mut val);
263            let p = val.value.str_;
264            if p.is_null() {
265                String::new()
266            } else {
267                CStr::from_ptr(p).to_string_lossy().into_owned()
268            }
269        }
270    }
271
272    /// Current value as a LogicArray (4-state).
273    pub fn get(&self) -> LogicArray {
274        LogicArray::from_binstr(&self.get_binstr())
275    }
276
277    /// Current value as u64; `Err` if any bit is x/z (design-doc §0.6:
278    /// conversion failures are Results, not exceptions).
279    pub fn get_u64(&self) -> Result<u64, ValueError> {
280        let width = self.size().max(1);
281        if width > 64 {
282            return Err(ValueError::Width {
283                want: width,
284                have: 64,
285            });
286        }
287        let mut val = sys::t_vpi_value {
288            format: sys::vpiVectorVal,
289            value: sys::u_vpi_value_union {
290                vector: std::ptr::null_mut(),
291            },
292        };
293        unsafe {
294            sys::vpi_get_value(self.h.0, &mut val);
295            let words = val.value.vector;
296            if words.is_null() {
297                return Ok(0);
298            }
299            let word_count = width.div_ceil(32) as usize;
300            let mut result = 0u64;
301            for index in 0..word_count {
302                let word = *words.add(index);
303                let used_bits = if index + 1 == word_count && !width.is_multiple_of(32) {
304                    width % 32
305                } else {
306                    32
307                };
308                let mask = if used_bits == 32 {
309                    u32::MAX
310                } else {
311                    (1u32 << used_bits) - 1
312                };
313                if word.bval & mask != 0 {
314                    return Err(ValueError::FourState(self.get_binstr()));
315                }
316                result |= ((word.aval & mask) as u64) << (index * 32);
317            }
318            Ok(result)
319        }
320    }
321
322    fn put_binstr_flags(&self, bin: &str, flags: i32) {
323        let c = CString::new(bin).expect("NUL in binstr");
324        let mut val = sys::t_vpi_value {
325            format: sys::vpiBinStrVal,
326            value: sys::u_vpi_value_union { str_: c.as_ptr() as *mut _ },
327        };
328        unsafe {
329            sys::vpi_put_value(self.h.0, &mut val, std::ptr::null_mut(), flags);
330        }
331    }
332
333    fn put_vector_words(&self, words: &mut [sys::t_vpi_vecval]) {
334        let mut val = sys::t_vpi_value {
335            format: sys::vpiVectorVal,
336            value: sys::u_vpi_value_union {
337                vector: words.as_mut_ptr(),
338            },
339        };
340        unsafe {
341            sys::vpi_put_value(self.h.0, &mut val, std::ptr::null_mut(), sys::vpiNoDelay);
342        }
343    }
344
345    /// Immediate (NoDelay) write of an integer value, zero-extended /
346    /// truncated to the signal width. This is the "setimmediatevalue"
347    /// analog; scheduled writes are layered above (design-doc §4.1(4)).
348    pub fn set_u64_now(&self, v: u64) {
349        let width = self.size().max(1);
350        let word_count = width.div_ceil(32) as usize;
351        let mut words = if word_count <= 2 {
352            let words = [
353                sys::t_vpi_vecval {
354                    aval: v as u32,
355                    bval: 0,
356                },
357                sys::t_vpi_vecval {
358                    aval: (v >> 32) as u32,
359                    bval: 0,
360                },
361            ];
362            words[..word_count].to_vec()
363        } else {
364            let mut words = vec![sys::t_vpi_vecval::default(); word_count];
365            words[0].aval = v as u32;
366            words[1].aval = (v >> 32) as u32;
367            words
368        };
369        if !width.is_multiple_of(32) {
370            let mask = (1u32 << (width % 32)) - 1;
371            let last = words.last_mut().expect("positive width has a vector word");
372            last.aval &= mask;
373            last.bval &= mask;
374        }
375        self.put_vector_words(&mut words);
376    }
377
378    /// Immediate write of a 4-state value.
379    pub fn set_now(&self, v: &LogicArray) {
380        self.put_binstr_flags(&v.to_binstr(), sys::vpiNoDelay);
381    }
382}
383
384/// All top-level modules in the design.
385pub fn top_modules() -> Vec<HierarchyHandle> {
386    let mut out = Vec::new();
387    unsafe {
388        let it = sys::vpi_iterate(sys::vpiModule, std::ptr::null_mut());
389        if it.is_null() {
390            return out;
391        }
392        loop {
393            let m = sys::vpi_scan(it);
394            if m.is_null() {
395                break;
396            }
397            if let Some(h) = ObjHandle::new(m) {
398                out.push(HierarchyHandle { h });
399            }
400        }
401    }
402    out
403}
404
405/// The first top-level module (the DUT in single-top designs).
406pub fn top_module() -> Result<HierarchyHandle, HandleError> {
407    top_modules().into_iter().next().ok_or(HandleError::NoTopModule)
408}
409
410// ===========================================================================
411// Time
412// ===========================================================================
413
414/// Current simulation time in simulator precision steps.
415pub fn sim_time_steps() -> u64 {
416    let mut t = sys::t_vpi_time { type_: sys::vpiSimTime, high: 0, low: 0, real: 0.0 };
417    unsafe { sys::vpi_get_time(std::ptr::null_mut(), &mut t) };
418    ((t.high as u64) << 32) | (t.low as u64)
419}
420
421/// Simulator time precision as a power of ten (e.g. -9 = 1 ns).
422pub fn time_precision() -> i32 {
423    thread_local! {
424        static PREC: Cell<Option<i32>> = const { Cell::new(None) };
425    }
426    PREC.with(|p| match p.get() {
427        Some(v) => v,
428        None => {
429            let v = unsafe { sys::vpi_get(sys::vpiTimePrecision, std::ptr::null_mut()) };
430            p.set(Some(v));
431            v
432        }
433    })
434}
435
436/// End the simulation (vpi_control(vpiFinish)).
437pub fn finish() {
438    unsafe {
439        sys::vpi_control(sys::vpiFinish, 0i32);
440    }
441}
442
443// ===========================================================================
444// Panic sink (invariant 4)
445// ===========================================================================
446
447thread_local! {
448    static PANIC_SINK: RefCell<Option<Box<dyn Fn(String)>>> = const { RefCell::new(None) };
449}
450
451/// Install the handler invoked when a callback closure panics (the runner
452/// routes this to "fail the current test", mirroring cocotb catching
453/// BaseException per task).
454pub fn set_panic_sink(f: Box<dyn Fn(String)>) {
455    PANIC_SINK.with(|s| *s.borrow_mut() = Some(f));
456}
457
458fn report_panic(payload: Box<dyn std::any::Any + Send>) {
459    let msg = if let Some(s) = payload.downcast_ref::<&str>() {
460        s.to_string()
461    } else if let Some(s) = payload.downcast_ref::<String>() {
462        s.clone()
463    } else {
464        "panic (non-string payload)".to_string()
465    };
466    PANIC_SINK.with(|s| {
467        if let Some(f) = s.borrow().as_ref() {
468            f(msg.clone());
469        } else {
470            eprintln!("rustdv: panic in simulator callback: {msg}");
471        }
472    });
473}
474
475// ===========================================================================
476// Callbacks (invariant 5)
477// ===========================================================================
478
479enum CbKind {
480    OneShot,
481    Recurring,
482}
483
484struct CbShared {
485    kind: CbKind,
486    /// Registration handle returned by vpi_register_cb. One-shots remove it
487    /// from inside the trampoline, while it is valid on both supported
488    /// simulators: Icarus reaps the active callback after it returns and
489    /// Verilator releases its separately-owned handle object immediately.
490    vpi_h: Cell<sys::vpiHandle>,
491    /// True once the C-side Rc reference has been reclaimed (fired one-shot
492    /// or removed callback). Guards against double-free.
493    released: Cell<bool>,
494    once: RefCell<Option<Box<dyn FnOnce()>>>,
495    repeat: RefCell<Option<Box<dyn FnMut()>>>,
496}
497
498/// RAII callback registration. Dropping an unfired/live handle removes the
499/// simulator callback — this is what makes drop-based task cancellation
500/// (design-doc §4.6) clean up trigger registrations for free.
501pub struct CallbackHandle {
502    shared: Rc<CbShared>,
503    raw: *const CbShared,
504    detached: bool,
505}
506
507impl CallbackHandle {
508    /// Detach Rust ownership without leaking it. A detached one-shot keeps its
509    /// C-side reference until it fires and then self-cleans; a detached
510    /// recurring callback lives for the rest of the simulation.
511    pub fn forget(mut self) {
512        self.detached = true;
513    }
514}
515
516impl Drop for CallbackHandle {
517    fn drop(&mut self) {
518        if self.detached {
519            return;
520        }
521        if !self.shared.released.get() {
522            self.shared.released.set(true);
523            unsafe {
524                sys::vpi_remove_cb(self.shared.vpi_h.get());
525                // Reclaim the C-side reference.
526                drop(Rc::from_raw(self.raw));
527            }
528        }
529    }
530}
531
532extern "C" fn trampoline(cb: *mut sys::t_cb_data) -> i32 {
533    unsafe {
534        let ud = (*cb).user_data as *const CbShared;
535        if ud.is_null() {
536            return 0;
537        }
538        // Hold our own reference for the duration of the call so that
539        // closures dropping the CallbackHandle can't free us mid-flight.
540        Rc::increment_strong_count(ud);
541        let shared: Rc<CbShared> = Rc::from_raw(ud);
542        match shared.kind {
543            CbKind::OneShot => {
544                if !shared.released.get() {
545                    shared.released.set(true);
546                    let f = shared.once.borrow_mut().take();
547                    // The returned callback handle has different post-fire
548                    // ownership across simulators. Remove it while the active
549                    // callback is still valid on both: Icarus marks it for
550                    // self-reaping, while Verilator deletes the retained
551                    // VerilatedVpioReasonCb handle.
552                    sys::vpi_remove_cb(shared.vpi_h.get());
553                    // Reclaim the C-side reference before running user code.
554                    drop(Rc::from_raw(ud));
555                    if let Some(f) = f {
556                        if let Err(p) = catch_unwind(AssertUnwindSafe(f)) {
557                            report_panic(p);
558                        }
559                    }
560                }
561            }
562            CbKind::Recurring => {
563                let mut guard = shared.repeat.borrow_mut();
564                if let Some(f) = guard.as_mut() {
565                    if let Err(p) = catch_unwind(AssertUnwindSafe(|| f())) {
566                        report_panic(p);
567                    }
568                }
569            }
570        }
571        drop(shared);
572    }
573    0
574}
575
576fn register(
577    kind: CbKind,
578    once: Option<Box<dyn FnOnce()>>,
579    repeat: Option<Box<dyn FnMut()>>,
580    reason: i32,
581    obj: sys::vpiHandle,
582    time: Option<sys::t_vpi_time>,
583) -> CallbackHandle {
584    let shared = Rc::new(CbShared {
585        kind,
586        vpi_h: Cell::new(std::ptr::null_mut()),
587        released: Cell::new(false),
588        once: RefCell::new(once),
589        repeat: RefCell::new(repeat),
590    });
591    // C-side reference:
592    let raw = Rc::into_raw(shared.clone());
593
594    let mut t = time.unwrap_or(sys::t_vpi_time {
595        type_: sys::vpiSuppressTime,
596        high: 0,
597        low: 0,
598        real: 0.0,
599    });
600    // value: NULL — closures read signal values themselves; Icarus rejects
601    // vpiSuppressVal on value-change callbacks ("format 10 not supported").
602    let mut cb = sys::t_cb_data {
603        reason,
604        cb_rtn: Some(trampoline),
605        obj,
606        time: &mut t,
607        value: std::ptr::null_mut(),
608        index: 0,
609        user_data: raw as *mut _,
610    };
611    let vpi_h = unsafe { sys::vpi_register_cb(&mut cb) };
612    assert!(!vpi_h.is_null(), "vpi_register_cb failed (reason {reason})");
613    shared.vpi_h.set(vpi_h);
614    CallbackHandle { shared, raw, detached: false }
615}
616
617fn simtime(steps: u64) -> sys::t_vpi_time {
618    sys::t_vpi_time {
619        type_: sys::vpiSimTime,
620        high: (steps >> 32) as u32,
621        low: (steps & 0xFFFF_FFFF) as u32,
622        real: 0.0,
623    }
624}
625
626/// One-shot callback after `steps` precision units (cbAfterDelay).
627pub fn register_timer(steps: u64, f: Box<dyn FnOnce()>) -> CallbackHandle {
628    register(CbKind::OneShot, Some(f), None, sys::cbAfterDelay, std::ptr::null_mut(), Some(simtime(steps)))
629}
630
631/// Recurring callback on any value change of `sig` (cbValueChange). The
632/// closure reads the signal itself; edge filtering happens in rustdv-sim.
633pub fn register_value_change(sig: LogicHandle, f: Box<dyn FnMut()>) -> CallbackHandle {
634    register(
635        CbKind::Recurring,
636        None,
637        Some(f),
638        sys::cbValueChange,
639        sig.h.0,
640        Some(simtime(0)),
641    )
642}
643
644/// One-shot callback at the next ReadWrite synch point.
645pub fn register_read_write(f: Box<dyn FnOnce()>) -> CallbackHandle {
646    register(CbKind::OneShot, Some(f), None, sys::cbReadWriteSynch, std::ptr::null_mut(), Some(simtime(0)))
647}
648
649/// One-shot callback at the next ReadOnly synch point.
650pub fn register_read_only(f: Box<dyn FnOnce()>) -> CallbackHandle {
651    register(CbKind::OneShot, Some(f), None, sys::cbReadOnlySynch, std::ptr::null_mut(), Some(simtime(0)))
652}
653
654/// One-shot callback at the next simulation time step.
655pub fn register_next_sim_time(f: Box<dyn FnOnce()>) -> CallbackHandle {
656    register(CbKind::OneShot, Some(f), None, sys::cbNextSimTime, std::ptr::null_mut(), None)
657}
658
659/// One-shot callback at the end of the current simulation time step.
660pub fn register_at_end_of_sim_time(f: Box<dyn FnOnce()>) -> CallbackHandle {
661    register(
662        CbKind::OneShot,
663        Some(f),
664        None,
665        sys::cbAtEndOfSimTime,
666        std::ptr::null_mut(),
667        Some(simtime(sim_time_steps())),
668    )
669}
670
671/// One-shot callback at start of simulation (the bootstrap hook).
672pub fn register_start_of_simulation(f: Box<dyn FnOnce()>) -> CallbackHandle {
673    register(CbKind::OneShot, Some(f), None, sys::cbStartOfSimulation, std::ptr::null_mut(), None)
674}
675
676/// One-shot callback at end of simulation.
677pub fn register_end_of_simulation(f: Box<dyn FnOnce()>) -> CallbackHandle {
678    register(CbKind::OneShot, Some(f), None, sys::cbEndOfSimulation, std::ptr::null_mut(), None)
679}
680
681#[cfg(test)]
682mod callback_tests {
683    use super::*;
684    use std::cell::Cell;
685    use std::rc::Rc;
686
687    #[test]
688    fn fired_one_shot_releases_its_vpi_handle() {
689        rustdv_vpi_stubs::reset_callbacks();
690        let fired = Rc::new(Cell::new(false));
691        let fired_in_callback = fired.clone();
692        let handle = register_timer(1, Box::new(move || fired_in_callback.set(true)));
693
694        assert_eq!(rustdv_vpi_stubs::live_callback_handles(), 1);
695        rustdv_vpi_stubs::fire_next_callback();
696        assert!(fired.get());
697        drop(handle);
698
699        assert_eq!(rustdv_vpi_stubs::live_callback_handles(), 0);
700        rustdv_vpi_stubs::reset_callbacks();
701    }
702
703    #[test]
704    fn detached_one_shot_releases_shared_state_after_firing() {
705        rustdv_vpi_stubs::reset_callbacks();
706        let handle = register_timer(1, Box::new(|| {}));
707        let shared = Rc::downgrade(&handle.shared);
708
709        handle.forget();
710        assert!(shared.upgrade().is_some());
711        rustdv_vpi_stubs::fire_next_callback();
712
713        assert_eq!(rustdv_vpi_stubs::live_callback_handles(), 0);
714        assert!(shared.upgrade().is_none());
715        rustdv_vpi_stubs::reset_callbacks();
716    }
717
718    #[test]
719    fn callback_stub_refuses_to_reset_a_live_handle() {
720        rustdv_vpi_stubs::reset_callbacks();
721        let handle = register_timer(1, Box::new(|| {}));
722
723        let reset = catch_unwind(AssertUnwindSafe(rustdv_vpi_stubs::reset_callbacks));
724        assert!(reset.is_err());
725
726        drop(handle);
727        rustdv_vpi_stubs::reset_callbacks();
728    }
729}
730
731#[cfg(test)]
732mod handle_tests {
733    use super::*;
734
735    fn make_signal(width: i32, words: &[sys::t_vpi_vecval], binstr: &str) -> LogicHandle {
736        rustdv_vpi_stubs::configure_signal(width, words, binstr);
737        let raw = 1usize as sys::vpiHandle;
738        let AnyHandle::Logic(signal) = AnyHandle::classify(ObjHandle(raw)) else {
739            panic!("stub object was not classified as a signal");
740        };
741        signal
742    }
743
744    #[test]
745    fn signal_width_is_cached_when_the_handle_is_classified() {
746        rustdv_vpi_stubs::reset_property_gets();
747        let signal = make_signal(37, &[sys::t_vpi_vecval::default(); 2], &"0".repeat(37));
748
749        assert_eq!(signal.size(), 37);
750        assert_eq!(signal.size(), 37);
751        assert_eq!(rustdv_vpi_stubs::property_get_count(sys::vpiType), 1);
752        assert_eq!(rustdv_vpi_stubs::property_get_count(sys::vpiSize), 1);
753        rustdv_vpi_stubs::reset_property_gets();
754    }
755
756    #[test]
757    fn vector_read_uses_vpi_word_order_and_masks_unused_bits() {
758        let signal = make_signal(
759            37,
760            &[
761                sys::t_vpi_vecval {
762                    aval: 0x89ab_cdef,
763                    bval: 0,
764                },
765                sys::t_vpi_vecval {
766                    aval: 0xffff_fff5,
767                    bval: 0xffff_ffe0,
768                },
769            ],
770            "101011001101010111100110111101111",
771        );
772
773        assert_eq!(signal.get_u64(), Ok(0x0000_0015_89ab_cdef));
774    }
775
776    #[test]
777    fn vector_read_reports_xz_in_used_bits() {
778        let signal = make_signal(
779            8,
780            &[sys::t_vpi_vecval {
781                aval: 0x5a,
782                bval: 0x04,
783            }],
784            "01011x10",
785        );
786
787        assert_eq!(
788            signal.get_u64(),
789            Err(ValueError::FourState("01011x10".to_owned()))
790        );
791    }
792
793    #[test]
794    fn vector_write_truncates_and_zero_extends_to_signal_width() {
795        let signal = make_signal(37, &[sys::t_vpi_vecval::default(); 2], &"0".repeat(37));
796        signal.set_u64_now(0xffff_fff5_89ab_cdef);
797        let words = rustdv_vpi_stubs::last_put_vector();
798
799        assert_eq!(words.len(), 2);
800        assert_eq!(words[0].aval, 0x89ab_cdef);
801        assert_eq!(words[1].aval, 0x15);
802        assert_eq!(words[0].bval, 0);
803        assert_eq!(words[1].bval, 0);
804
805        let wide = make_signal(70, &[sys::t_vpi_vecval::default(); 3], &"0".repeat(70));
806        wide.set_u64_now(0x0123_4567_89ab_cdef);
807        let words = rustdv_vpi_stubs::last_put_vector();
808        assert_eq!(words.len(), 3);
809        assert_eq!(words[0].aval, 0x89ab_cdef);
810        assert_eq!(words[1].aval, 0x0123_4567);
811        assert_eq!(words[2].aval, 0);
812    }
813}