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 handles invalidate on removal/fire — modeled by RAII
9//!    ([`CallbackHandle`]): dropping an unfired handle removes the callback.
10//! 3. Strings are copied at the boundary, every call.
11//! 4. No unwinding across FFI: every trampoline wraps the closure in
12//!    `catch_unwind`; panics are routed to the panic sink.
13//! 5. Callback user-data ownership: an `Rc` whose C-side reference is
14//!    reclaimed exactly once (on fire for one-shots, on removal otherwise).
15//!
16//! Thread affinity (§3.4): all types here hold raw pointers and are
17//! therefore `!Send`/`!Sync` — the compiler rejects moving them off the
18//! simulator thread.
19
20use std::cell::{Cell, RefCell};
21use std::ffi::{CStr, CString};
22use std::fmt;
23use std::panic::{catch_unwind, AssertUnwindSafe};
24use std::rc::Rc;
25
26use rustdv_gpi_sys as sys;
27
28// Test executables need vpi_* symbol definitions (the simulator provides
29// them for the real cdylib) — see rustdv-vpi-stubs.
30#[cfg(test)]
31use rustdv_vpi_stubs as _;
32
33pub mod value;
34pub use value::{Logic, LogicArray};
35
36// ===========================================================================
37// Errors
38// ===========================================================================
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum HandleError {
42    /// Name did not resolve (cocotb raises AttributeError here; rustdv
43    /// returns this — design-doc §0.6).
44    NotFound { name: String, scope: String },
45    /// Handle exists but is not the requested kind.
46    WrongKind { name: String, expected: &'static str, actual: String },
47    NoTopModule,
48}
49
50impl fmt::Display for HandleError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            HandleError::NotFound { name, scope } => {
54                write!(f, "no object named '{name}' in scope '{scope}'")
55            }
56            HandleError::WrongKind { name, expected, actual } => {
57                write!(f, "'{name}' is a {actual}, expected {expected}")
58            }
59            HandleError::NoTopModule => write!(f, "no top-level module found"),
60        }
61    }
62}
63impl std::error::Error for HandleError {}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ValueError {
67    /// Value contains x/z bits and was asked for as an integer.
68    FourState(String),
69    Width { want: u32, have: usize },
70}
71
72impl fmt::Display for ValueError {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            ValueError::FourState(s) => write!(f, "value '{s}' has x/z bits"),
76            ValueError::Width { want, have } => write!(f, "width mismatch: want {want}, have {have}"),
77        }
78    }
79}
80impl std::error::Error for ValueError {}
81
82// ===========================================================================
83// Handles
84// ===========================================================================
85
86/// Raw non-null simulator object handle. Valid for the whole simulation
87/// (invariant 2), hence `Copy`. `!Send` because it wraps a raw pointer.
88#[derive(Copy, Clone, PartialEq, Eq)]
89pub struct ObjHandle(sys::vpiHandle);
90
91impl ObjHandle {
92    fn new(h: sys::vpiHandle) -> Option<Self> {
93        if h.is_null() { None } else { Some(ObjHandle(h)) }
94    }
95    fn get(self, prop: i32) -> i32 {
96        unsafe { sys::vpi_get(prop, self.0) }
97    }
98    fn get_str(self, prop: i32) -> String {
99        // Invariant 3: copy immediately.
100        unsafe {
101            let p = sys::vpi_get_str(prop, self.0);
102            if p.is_null() {
103                String::new()
104            } else {
105                CStr::from_ptr(p).to_string_lossy().into_owned()
106            }
107        }
108    }
109}
110
111/// Any simulator object, classified by type (design-doc §3.3 downcasting).
112#[derive(Copy, Clone)]
113pub enum AnyHandle {
114    Hierarchy(HierarchyHandle),
115    Logic(LogicHandle),
116    Other(ObjHandle),
117}
118
119impl AnyHandle {
120    pub fn classify(h: ObjHandle) -> AnyHandle {
121        match h.get(sys::vpiType) {
122            sys::vpiModule => AnyHandle::Hierarchy(HierarchyHandle { h }),
123            sys::vpiNet | sys::vpiReg | sys::vpiIntegerVar | sys::vpiPort | sys::vpiMemory
124            | sys::vpiLongIntVar | sys::vpiShortIntVar | sys::vpiIntVar | sys::vpiByteVar
125            | sys::vpiEnumVar | sys::vpiBitVar => {
126                AnyHandle::Logic(LogicHandle { h })
127            }
128            _ => AnyHandle::Other(h),
129        }
130    }
131
132    pub fn as_logic(self) -> Result<LogicHandle, HandleError> {
133        match self {
134            AnyHandle::Logic(l) => Ok(l),
135            AnyHandle::Hierarchy(h) => Err(HandleError::WrongKind {
136                name: h.full_name(),
137                expected: "signal",
138                actual: "module".into(),
139            }),
140            AnyHandle::Other(o) => Err(HandleError::WrongKind {
141                name: o.get_str(sys::vpiFullName),
142                expected: "signal",
143                actual: format!("vpiType {}", o.get(sys::vpiType)),
144            }),
145        }
146    }
147
148    pub fn as_hierarchy(self) -> Result<HierarchyHandle, HandleError> {
149        match self {
150            AnyHandle::Hierarchy(h) => Ok(h),
151            AnyHandle::Logic(l) => Err(HandleError::WrongKind {
152                name: l.full_name(),
153                expected: "module",
154                actual: "signal".into(),
155            }),
156            AnyHandle::Other(o) => Err(HandleError::WrongKind {
157                name: o.get_str(sys::vpiFullName),
158                expected: "module",
159                actual: format!("vpiType {}", o.get(sys::vpiType)),
160            }),
161        }
162    }
163}
164
165/// A module/scope handle. Port of cocotb's HierarchyObject
166/// (cocotb: handle.py), with `Result`-returning lookup (mapping row 19).
167#[derive(Copy, Clone)]
168pub struct HierarchyHandle {
169    h: ObjHandle,
170}
171
172impl HierarchyHandle {
173    /// A handle to nothing, for unit tests that need a `RustdvCtx` but never
174    /// touch the DUT. Any VPI call through it goes to `rustdv-vpi-stubs`,
175    /// which panics — so a test that *does* touch the DUT fails loudly
176    /// instead of reading garbage.
177    pub fn null_for_test() -> HierarchyHandle {
178        HierarchyHandle { h: ObjHandle(std::ptr::null_mut()) }
179    }
180
181    /// Dynamic child lookup: `dut.child("clk")?` (design-doc OQ-6 lean).
182    pub fn child(&self, name: &str) -> Result<AnyHandle, HandleError> {
183        let cname = CString::new(name).expect("NUL in signal name");
184        let h = unsafe { sys::vpi_handle_by_name(cname.as_ptr(), self.h.0) };
185        match ObjHandle::new(h) {
186            Some(h) => Ok(AnyHandle::classify(h)),
187            None => Err(HandleError::NotFound { name: name.into(), scope: self.full_name() }),
188        }
189    }
190
191    /// Shorthand: child that must be a signal.
192    pub fn signal(&self, name: &str) -> Result<LogicHandle, HandleError> {
193        self.child(name)?.as_logic()
194    }
195
196    pub fn name(&self) -> String {
197        self.h.get_str(sys::vpiName)
198    }
199    pub fn full_name(&self) -> String {
200        self.h.get_str(sys::vpiFullName)
201    }
202
203    /// Iterate child objects (modules, nets, regs) — serves the
204    /// `visit_children` debug-print role at the DUT level.
205    pub fn children(&self) -> Vec<AnyHandle> {
206        let mut out = Vec::new();
207        for t in [sys::vpiModule, sys::vpiNet, sys::vpiReg] {
208            unsafe {
209                let it = sys::vpi_iterate(t, self.h.0);
210                if it.is_null() {
211                    continue;
212                }
213                loop {
214                    let c = sys::vpi_scan(it);
215                    if c.is_null() {
216                        break; // scan returning NULL frees the iterator
217                    }
218                    if let Some(h) = ObjHandle::new(c) {
219                        out.push(AnyHandle::classify(h));
220                    }
221                }
222            }
223        }
224        out
225    }
226}
227
228/// A value-bearing signal handle (net/reg/var). Port of cocotb's
229/// LogicObject surface: explicit `get()`/`set()` (mapping row 20).
230/// Buffered-write semantics live a layer up in rustdv-sim; the methods
231/// here apply values immediately.
232#[derive(Copy, Clone, PartialEq, Eq)]
233pub struct LogicHandle {
234    h: ObjHandle,
235}
236
237impl LogicHandle {
238    pub fn name(&self) -> String {
239        self.h.get_str(sys::vpiName)
240    }
241    pub fn full_name(&self) -> String {
242        self.h.get_str(sys::vpiFullName)
243    }
244    pub fn size(&self) -> u32 {
245        self.h.get(sys::vpiSize).max(0) as u32
246    }
247
248    /// Current value as a binary string, e.g. "0101", "xxxx".
249    pub fn get_binstr(&self) -> String {
250        let mut val = sys::t_vpi_value {
251            format: sys::vpiBinStrVal,
252            value: sys::u_vpi_value_union { integer: 0 },
253        };
254        unsafe {
255            sys::vpi_get_value(self.h.0, &mut val);
256            let p = val.value.str_;
257            if p.is_null() {
258                String::new()
259            } else {
260                CStr::from_ptr(p).to_string_lossy().into_owned()
261            }
262        }
263    }
264
265    /// Current value as a LogicArray (4-state).
266    pub fn get(&self) -> LogicArray {
267        LogicArray::from_binstr(&self.get_binstr())
268    }
269
270    /// Current value as u64; `Err` if any bit is x/z (design-doc §0.6:
271    /// conversion failures are Results, not exceptions).
272    pub fn get_u64(&self) -> Result<u64, ValueError> {
273        let s = self.get_binstr();
274        let mut v: u64 = 0;
275        for c in s.chars() {
276            match c {
277                '0' => v <<= 1,
278                '1' => v = (v << 1) | 1,
279                _ => return Err(ValueError::FourState(s)),
280            }
281        }
282        Ok(v)
283    }
284
285    fn put_binstr_flags(&self, bin: &str, flags: i32) {
286        let c = CString::new(bin).expect("NUL in binstr");
287        let mut val = sys::t_vpi_value {
288            format: sys::vpiBinStrVal,
289            value: sys::u_vpi_value_union { str_: c.as_ptr() as *mut _ },
290        };
291        unsafe {
292            sys::vpi_put_value(self.h.0, &mut val, std::ptr::null_mut(), flags);
293        }
294    }
295
296    /// Immediate (NoDelay) write of an integer value, zero-extended /
297    /// truncated to the signal width. This is the "setimmediatevalue"
298    /// analog; scheduled writes are layered above (design-doc §4.1(4)).
299    pub fn set_u64_now(&self, v: u64) {
300        let w = self.size().max(1) as usize;
301        let mut s = String::with_capacity(w);
302        for i in (0..w).rev() {
303            s.push(if (v >> i) & 1 == 1 { '1' } else { '0' });
304        }
305        self.put_binstr_flags(&s, sys::vpiNoDelay);
306    }
307
308    /// Immediate write of a 4-state value.
309    pub fn set_now(&self, v: &LogicArray) {
310        self.put_binstr_flags(&v.to_binstr(), sys::vpiNoDelay);
311    }
312}
313
314/// All top-level modules in the design.
315pub fn top_modules() -> Vec<HierarchyHandle> {
316    let mut out = Vec::new();
317    unsafe {
318        let it = sys::vpi_iterate(sys::vpiModule, std::ptr::null_mut());
319        if it.is_null() {
320            return out;
321        }
322        loop {
323            let m = sys::vpi_scan(it);
324            if m.is_null() {
325                break;
326            }
327            if let Some(h) = ObjHandle::new(m) {
328                out.push(HierarchyHandle { h });
329            }
330        }
331    }
332    out
333}
334
335/// The first top-level module (the DUT in single-top designs).
336pub fn top_module() -> Result<HierarchyHandle, HandleError> {
337    top_modules().into_iter().next().ok_or(HandleError::NoTopModule)
338}
339
340// ===========================================================================
341// Time
342// ===========================================================================
343
344/// Current simulation time in simulator precision steps.
345pub fn sim_time_steps() -> u64 {
346    let mut t = sys::t_vpi_time { type_: sys::vpiSimTime, high: 0, low: 0, real: 0.0 };
347    unsafe { sys::vpi_get_time(std::ptr::null_mut(), &mut t) };
348    ((t.high as u64) << 32) | (t.low as u64)
349}
350
351/// Simulator time precision as a power of ten (e.g. -9 = 1 ns).
352pub fn time_precision() -> i32 {
353    thread_local! {
354        static PREC: Cell<Option<i32>> = const { Cell::new(None) };
355    }
356    PREC.with(|p| match p.get() {
357        Some(v) => v,
358        None => {
359            let v = unsafe { sys::vpi_get(sys::vpiTimePrecision, std::ptr::null_mut()) };
360            p.set(Some(v));
361            v
362        }
363    })
364}
365
366/// End the simulation (vpi_control(vpiFinish)).
367pub fn finish() {
368    unsafe {
369        sys::vpi_control(sys::vpiFinish, 0i32);
370    }
371}
372
373// ===========================================================================
374// Panic sink (invariant 4)
375// ===========================================================================
376
377thread_local! {
378    static PANIC_SINK: RefCell<Option<Box<dyn Fn(String)>>> = const { RefCell::new(None) };
379}
380
381/// Install the handler invoked when a callback closure panics (the runner
382/// routes this to "fail the current test", mirroring cocotb catching
383/// BaseException per task).
384pub fn set_panic_sink(f: Box<dyn Fn(String)>) {
385    PANIC_SINK.with(|s| *s.borrow_mut() = Some(f));
386}
387
388fn report_panic(payload: Box<dyn std::any::Any + Send>) {
389    let msg = if let Some(s) = payload.downcast_ref::<&str>() {
390        s.to_string()
391    } else if let Some(s) = payload.downcast_ref::<String>() {
392        s.clone()
393    } else {
394        "panic (non-string payload)".to_string()
395    };
396    PANIC_SINK.with(|s| {
397        if let Some(f) = s.borrow().as_ref() {
398            f(msg.clone());
399        } else {
400            eprintln!("rustdv: panic in simulator callback: {msg}");
401        }
402    });
403}
404
405// ===========================================================================
406// Callbacks (invariant 5)
407// ===========================================================================
408
409enum CbKind {
410    OneShot,
411    Recurring,
412}
413
414struct CbShared {
415    kind: CbKind,
416    /// True once the C-side Rc reference has been reclaimed (fired one-shot
417    /// or removed callback). Guards against double-free.
418    released: Cell<bool>,
419    once: RefCell<Option<Box<dyn FnOnce()>>>,
420    repeat: RefCell<Option<Box<dyn FnMut()>>>,
421}
422
423/// RAII callback registration. Dropping an unfired/live handle removes the
424/// simulator callback — this is what makes drop-based task cancellation
425/// (design-doc §4.6) clean up trigger registrations for free.
426pub struct CallbackHandle {
427    shared: Rc<CbShared>,
428    raw: *const CbShared,
429    vpi_h: sys::vpiHandle,
430}
431
432impl CallbackHandle {
433    /// Detach: let the callback live for the rest of the simulation
434    /// (recurring singletons like the phase hub).
435    pub fn forget(self) {
436        std::mem::forget(self);
437    }
438}
439
440impl Drop for CallbackHandle {
441    fn drop(&mut self) {
442        if !self.shared.released.get() {
443            self.shared.released.set(true);
444            unsafe {
445                sys::vpi_remove_cb(self.vpi_h);
446                // Reclaim the C-side reference.
447                drop(Rc::from_raw(self.raw));
448            }
449        }
450    }
451}
452
453extern "C" fn trampoline(cb: *mut sys::t_cb_data) -> i32 {
454    unsafe {
455        let ud = (*cb).user_data as *const CbShared;
456        if ud.is_null() {
457            return 0;
458        }
459        // Hold our own reference for the duration of the call so that
460        // closures dropping the CallbackHandle can't free us mid-flight.
461        Rc::increment_strong_count(ud);
462        let shared: Rc<CbShared> = Rc::from_raw(ud);
463        match shared.kind {
464            CbKind::OneShot => {
465                if !shared.released.get() {
466                    shared.released.set(true);
467                    let f = shared.once.borrow_mut().take();
468                    // Reclaim the C-side reference before running user code.
469                    drop(Rc::from_raw(ud));
470                    if let Some(f) = f {
471                        if let Err(p) = catch_unwind(AssertUnwindSafe(f)) {
472                            report_panic(p);
473                        }
474                    }
475                }
476            }
477            CbKind::Recurring => {
478                let mut guard = shared.repeat.borrow_mut();
479                if let Some(f) = guard.as_mut() {
480                    if let Err(p) = catch_unwind(AssertUnwindSafe(|| f())) {
481                        report_panic(p);
482                    }
483                }
484            }
485        }
486        drop(shared);
487    }
488    0
489}
490
491fn register(
492    kind: CbKind,
493    once: Option<Box<dyn FnOnce()>>,
494    repeat: Option<Box<dyn FnMut()>>,
495    reason: i32,
496    obj: sys::vpiHandle,
497    time: Option<sys::t_vpi_time>,
498) -> CallbackHandle {
499    let shared = Rc::new(CbShared {
500        kind,
501        released: Cell::new(false),
502        once: RefCell::new(once),
503        repeat: RefCell::new(repeat),
504    });
505    // C-side reference:
506    let raw = Rc::into_raw(shared.clone());
507
508    let mut t = time.unwrap_or(sys::t_vpi_time {
509        type_: sys::vpiSuppressTime,
510        high: 0,
511        low: 0,
512        real: 0.0,
513    });
514    // value: NULL — closures read signal values themselves; Icarus rejects
515    // vpiSuppressVal on value-change callbacks ("format 10 not supported").
516    let mut cb = sys::t_cb_data {
517        reason,
518        cb_rtn: Some(trampoline),
519        obj,
520        time: &mut t,
521        value: std::ptr::null_mut(),
522        index: 0,
523        user_data: raw as *mut _,
524    };
525    let vpi_h = unsafe { sys::vpi_register_cb(&mut cb) };
526    assert!(!vpi_h.is_null(), "vpi_register_cb failed (reason {reason})");
527    CallbackHandle { shared, raw, vpi_h }
528}
529
530fn simtime(steps: u64) -> sys::t_vpi_time {
531    sys::t_vpi_time {
532        type_: sys::vpiSimTime,
533        high: (steps >> 32) as u32,
534        low: (steps & 0xFFFF_FFFF) as u32,
535        real: 0.0,
536    }
537}
538
539/// One-shot callback after `steps` precision units (cbAfterDelay).
540pub fn register_timer(steps: u64, f: Box<dyn FnOnce()>) -> CallbackHandle {
541    register(CbKind::OneShot, Some(f), None, sys::cbAfterDelay, std::ptr::null_mut(), Some(simtime(steps)))
542}
543
544/// Recurring callback on any value change of `sig` (cbValueChange). The
545/// closure reads the signal itself; edge filtering happens in rustdv-sim.
546pub fn register_value_change(sig: LogicHandle, f: Box<dyn FnMut()>) -> CallbackHandle {
547    register(
548        CbKind::Recurring,
549        None,
550        Some(f),
551        sys::cbValueChange,
552        sig.h.0,
553        Some(simtime(0)),
554    )
555}
556
557/// One-shot callback at the next ReadWrite synch point.
558pub fn register_read_write(f: Box<dyn FnOnce()>) -> CallbackHandle {
559    register(CbKind::OneShot, Some(f), None, sys::cbReadWriteSynch, std::ptr::null_mut(), Some(simtime(0)))
560}
561
562/// One-shot callback at the next ReadOnly synch point.
563pub fn register_read_only(f: Box<dyn FnOnce()>) -> CallbackHandle {
564    register(CbKind::OneShot, Some(f), None, sys::cbReadOnlySynch, std::ptr::null_mut(), Some(simtime(0)))
565}
566
567/// One-shot callback at the next simulation time step.
568pub fn register_next_sim_time(f: Box<dyn FnOnce()>) -> CallbackHandle {
569    register(CbKind::OneShot, Some(f), None, sys::cbNextSimTime, std::ptr::null_mut(), None)
570}
571
572/// One-shot callback at start of simulation (the bootstrap hook).
573pub fn register_start_of_simulation(f: Box<dyn FnOnce()>) -> CallbackHandle {
574    register(CbKind::OneShot, Some(f), None, sys::cbStartOfSimulation, std::ptr::null_mut(), None)
575}
576
577/// One-shot callback at end of simulation.
578pub fn register_end_of_simulation(f: Box<dyn FnOnce()>) -> CallbackHandle {
579    register(CbKind::OneShot, Some(f), None, sys::cbEndOfSimulation, std::ptr::null_mut(), None)
580}