Skip to main content

rustdv_sim/
simulator_trace.rs

1//! Optional runtime waveform capture supplied by the active simulator host.
2//!
3//! The API describes lifecycle and capability, not a waveform file format.
4//! A simulator may implement the host ABI using its own recording mechanism.
5//! Callers should treat [`TraceError::Unavailable`] as a normal result when
6//! the current simulator or executable has no runtime capture support.
7//!
8//! With a trace host present, operations must run on the simulator thread at
9//! a settled ReadOnly point, normally inside [`crate::service_read_only`].
10//! Host availability and path validation may be reported before phase errors.
11
12use std::ffi::CString;
13use std::fmt;
14use std::os::raw::{c_char, c_int};
15use std::path::Path;
16
17const ABI_VERSION: u32 = 1;
18const COMMAND_STATUS: u32 = 0;
19const COMMAND_START: u32 = 1;
20const COMMAND_STOP: u32 = 2;
21const COMMAND_FLUSH: u32 = 3;
22const ERROR_CAPACITY: usize = 512;
23
24#[repr(C)]
25#[derive(Default)]
26struct RawTraceStatus {
27    abi_version: u32,
28    capability: u32,
29    state: u32,
30    reserved: u32,
31    start_time_steps: u64,
32    end_time_steps: u64,
33    dump_count: u64,
34}
35
36type TraceControl = unsafe extern "C" fn(
37    command: u32,
38    path: *const c_char,
39    status: *mut RawTraceStatus,
40    error: *mut c_char,
41    error_capacity: usize,
42) -> c_int;
43
44#[cfg(unix)]
45type TraceFormatProbe = unsafe extern "C" fn() -> u32;
46
47/// Output format advertised by an optional runtime trace host.
48///
49/// A host with an unrecognized format can still provide the generic control
50/// ABI, but consumers must not guess how to decode its output.
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum TraceFormat {
53    Fst,
54    Other(u32),
55}
56
57/// State reported by the active simulator's trace host.
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum TraceState {
60    /// The executable has no runtime waveform capture capability.
61    Unavailable,
62    /// Capture support is present but no capture is active.
63    Idle,
64    /// A capture is currently accepting settled simulation values.
65    Active,
66}
67
68/// Runtime trace status returned by the active simulator host.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct TraceStatus {
71    pub state: TraceState,
72    pub start_time_steps: u64,
73    pub end_time_steps: u64,
74    pub dump_count: u64,
75}
76
77/// Errors from the optional simulator trace host.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum TraceError {
80    Unavailable(String),
81    InvalidPath(String),
82    WrongState(String),
83    Host(String),
84}
85
86impl fmt::Display for TraceError {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::Unavailable(message) => write!(formatter, "{message}"),
90            Self::InvalidPath(message) => write!(formatter, "{message}"),
91            Self::WrongState(message) => write!(formatter, "{message}"),
92            Self::Host(message) => write!(formatter, "{message}"),
93        }
94    }
95}
96
97impl std::error::Error for TraceError {}
98
99/// Return the current runtime trace capability and state.
100pub fn status() -> Result<TraceStatus, TraceError> {
101    control(COMMAND_STATUS, None)
102}
103
104/// Whether the process exports the optional simulator trace-control ABI.
105///
106/// This is a host-presence probe, not a promise that the current model was
107/// compiled with tracing. Unlike [`status`], it is safe before ReadOnly and
108/// lets a debug transport choose its backend during testbench startup.
109pub fn host_present() -> bool {
110    load_control().is_ok()
111}
112
113/// Identify the host's trace format without entering a simulator phase.
114///
115/// Older Verilator hosts export only the legacy control symbol; they are
116/// known to write FST and are recognized for mixed-version testbenches.
117/// Unknown hosts return `None` rather than being mistaken for FST.
118pub fn host_format() -> Option<TraceFormat> {
119    #[cfg(unix)]
120    {
121        let library = libloading::os::unix::Library::this();
122        // SAFETY: both symbols are supplied by the simulator executable and
123        // remain loaded for the lifetime of the testbench library.
124        unsafe {
125            if let Ok(probe) = library.get::<TraceFormatProbe>(b"rustdv_simulator_trace_format\0") {
126                return Some(match probe() {
127                    1 => TraceFormat::Fst,
128                    other => TraceFormat::Other(other),
129                });
130            }
131            if library
132                .get::<TraceControl>(b"rustdv_verilator_trace_control\0")
133                .is_ok()
134            {
135                return Some(TraceFormat::Fst);
136            }
137        }
138    }
139    None
140}
141
142/// Start capturing the signals retained in the trace-capable model.
143/// The backend determines the output format and required build instrumentation.
144pub fn start(path: &Path) -> Result<TraceStatus, TraceError> {
145    let path = path_to_c_string(path)?;
146    control(COMMAND_START, Some(&path))
147}
148
149/// Flush the active capture so its bounded on-disk state can be inspected.
150pub fn flush() -> Result<TraceStatus, TraceError> {
151    control(COMMAND_FLUSH, None)
152}
153
154/// Stop and close the active capture at the current settled simulation time.
155pub fn stop() -> Result<TraceStatus, TraceError> {
156    control(COMMAND_STOP, None)
157}
158
159fn control(command: u32, path: Option<&CString>) -> Result<TraceStatus, TraceError> {
160    let function = load_control()?;
161    if crate::phase::current_phase_if_initialized() != Some(crate::phase::SimPhase::ReadOnly) {
162        return Err(TraceError::WrongState(
163            "simulator trace control must run on the simulator thread at settled ReadOnly"
164                .to_owned(),
165        ));
166    }
167    let mut raw = RawTraceStatus::default();
168    let mut error = [0_u8; ERROR_CAPACITY];
169    let path = path.map_or(std::ptr::null(), |value| value.as_ptr());
170
171    // SAFETY: the dynamically loaded function is checked against the stable
172    // C ABI declared above.  All pointers remain valid for the duration of
173    // the call and the host is required to NUL-terminate its error buffer.
174    let result = unsafe {
175        function(
176            command,
177            path,
178            &mut raw,
179            error.as_mut_ptr().cast(),
180            error.len(),
181        )
182    };
183    let message = error_message(&error);
184
185    match result {
186        0 => decode_status(raw),
187        1 => Err(TraceError::Unavailable(message)),
188        3 => Err(TraceError::InvalidPath(message)),
189        4 => Err(TraceError::WrongState(message)),
190        _ => Err(TraceError::Host(message)),
191    }
192}
193
194fn decode_status(raw: RawTraceStatus) -> Result<TraceStatus, TraceError> {
195    if raw.abi_version != ABI_VERSION {
196        return Err(TraceError::Host(format!(
197            "unsupported simulator trace ABI {} (expected {ABI_VERSION})",
198            raw.abi_version
199        )));
200    }
201    let state = match (raw.capability, raw.state) {
202        (0, _) => TraceState::Unavailable,
203        (1, 0) => TraceState::Idle,
204        (1, 1) => TraceState::Active,
205        (capability, state) => {
206            return Err(TraceError::Host(format!(
207                "invalid simulator trace status capability={capability} state={state}"
208            )));
209        }
210    };
211    Ok(TraceStatus {
212        state,
213        start_time_steps: raw.start_time_steps,
214        end_time_steps: raw.end_time_steps,
215        dump_count: raw.dump_count,
216    })
217}
218
219fn error_message(buffer: &[u8]) -> String {
220    let terminator = buffer
221        .iter()
222        .position(|byte| *byte == 0)
223        .unwrap_or(buffer.len());
224    let message = String::from_utf8_lossy(&buffer[..terminator]).into_owned();
225    if message.is_empty() {
226        "simulator trace host returned an unspecified error".to_owned()
227    } else {
228        message
229    }
230}
231
232#[cfg(unix)]
233fn path_to_c_string(path: &Path) -> Result<CString, TraceError> {
234    use std::os::unix::ffi::OsStrExt;
235    CString::new(path.as_os_str().as_bytes())
236        .map_err(|_| TraceError::InvalidPath("trace path contains an interior NUL byte".to_owned()))
237}
238
239#[cfg(not(unix))]
240fn path_to_c_string(path: &Path) -> Result<CString, TraceError> {
241    CString::new(path.to_string_lossy().as_bytes())
242        .map_err(|_| TraceError::InvalidPath("trace path contains an interior NUL byte".to_owned()))
243}
244
245#[cfg(unix)]
246fn load_control() -> Result<TraceControl, TraceError> {
247    let library = libloading::os::unix::Library::this();
248    // SAFETY: copying the function pointer detaches it from the short-lived
249    // symbol guard.  The symbol belongs to the simulator executable, which
250    // necessarily outlives the loaded VPI testbench.
251    resolve_control(|name| unsafe { library.get::<TraceControl>(name).ok().map(|symbol| *symbol) })
252        .ok_or_else(|| {
253            TraceError::Unavailable(
254                "the current simulator host does not provide runtime waveform capture".to_owned(),
255            )
256        })
257}
258
259#[cfg(unix)]
260fn resolve_control(mut lookup: impl FnMut(&[u8]) -> Option<TraceControl>) -> Option<TraceControl> {
261    lookup(b"rustdv_simulator_trace_control\0")
262        .or_else(|| lookup(b"rustdv_verilator_trace_control\0"))
263}
264
265#[cfg(not(unix))]
266fn load_control() -> Result<TraceControl, TraceError> {
267    Err(TraceError::Unavailable(
268        "runtime simulator trace control is currently supported on Unix hosts only".to_owned(),
269    ))
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[cfg(unix)]
277    unsafe extern "C" fn legacy_control(
278        _: u32,
279        _: *const c_char,
280        _: *mut RawTraceStatus,
281        _: *mut c_char,
282        _: usize,
283    ) -> c_int {
284        0
285    }
286
287    #[cfg(unix)]
288    #[test]
289    fn legacy_verilator_host_symbol_remains_supported() {
290        let mut looked_up = Vec::new();
291        let control = resolve_control(|name| {
292            looked_up.push(name.to_vec());
293            (name == b"rustdv_verilator_trace_control\0").then_some(legacy_control as TraceControl)
294        });
295        assert!(std::ptr::fn_addr_eq(
296            control.expect("legacy symbol must resolve"),
297            legacy_control as TraceControl
298        ));
299        assert_eq!(
300            looked_up,
301            [
302                b"rustdv_simulator_trace_control\0".to_vec(),
303                b"rustdv_verilator_trace_control\0".to_vec(),
304            ]
305        );
306    }
307
308    #[test]
309    fn ordinary_rust_test_reports_missing_trace_host() {
310        assert!(!host_present());
311        assert_eq!(host_format(), None);
312        let error = status().unwrap_err();
313        assert!(matches!(error, TraceError::Unavailable(_)));
314    }
315}