Skip to main content

vj_sim/
dpi_task.rs

1//! Blocking DPI import-task calls (LRM 1800-2023 35.5.3, 35.7, 35.9).
2//!
3//! A [`TaskCall`] runs one C import-task call on a [`Fiber`]. When that C
4//! code calls an exported SV task which consumes time, the export's thunk
5//! calls [`sv_rt_dpi_export_task_call`], which parks the fiber and hands the
6//! owner a [`ExportRequest`]: "run export `export` with the argument block
7//! at `args`". The owner (the calling process's kernel) spawns the export's
8//! task as a child process, copies `args` into its frame, waits for it, copies
9//! the `output`/`inout` formals back into `args`, then polls again; the thunk
10//! returns and the C code continues. [`TaskCall::poll`] reports
11//! [`CallPoll::Finished`] with the import's `int` return (35.9: 0, or 1 when
12//! the call was disabled) once the C function returns.
13//!
14//! `args` points into the parked fiber's own stack, so it stays valid exactly
15//! as long as the request is outstanding.
16
17use crate::fiber::{self, Fiber, FiberState};
18use std::cell::{Cell, RefCell};
19use std::rc::Rc;
20use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
21
22/// What a parked call is asking the owner to run.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ExportRequest {
25    /// The export's index in the program's timed-export table.
26    pub export: u32,
27    /// The argument block, laid out by the thunk (opaque here).
28    pub args: *mut u8,
29    /// The `SimHandle` of the instance of the C code's current `svScope`:
30    /// the instance the export task runs in.
31    pub handle: *mut core::ffi::c_void,
32}
33
34/// The outcome of one [`TaskCall::poll`].
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum CallPoll {
37    /// The C function returned this `int` (the disable status, 35.9).
38    Finished(i32),
39    /// The C code is blocked in an export task; see [`ExportRequest`].
40    Request(ExportRequest),
41}
42
43/// Status [`sv_rt_dpi_export_task_call`] returns when the call was disabled.
44pub const EXPORT_CALL_DISABLED: i32 = 1;
45/// Status it returns when it ran off a fiber (a task export called from an
46/// import *function*, or from C code outside any import call): illegal,
47/// LRM 35.5.3 -- only an import task may call a task that consumes time.
48pub const EXPORT_CALL_NOT_ON_FIBER: i32 = -1;
49
50#[derive(Default)]
51struct Shared {
52    request: Cell<Option<ExportRequest>>,
53    result: Cell<i32>,
54    disabled: Cell<bool>,
55    /// Whether the C code called `svAckDisabledState()` in the disabled state.
56    acked: Cell<bool>,
57    /// Whether the C code is inside an import *task* call (set by the call
58    /// site, [`sv_rt_dpi_task_scope`]); an import function is not.
59    in_task: Cell<bool>,
60    /// The SV name of the import task being called ([`sv_rt_dpi_task_scope`]):
61    /// what a `disable` of that task finds the call by (35.9).
62    name: RefCell<String>,
63}
64
65thread_local! {
66    /// The call being resumed on this thread, for the thunk to post to.
67    static ACTIVE: RefCell<Option<Rc<Shared>>> = const { RefCell::new(None) };
68}
69
70/// One import-task call in flight.
71pub struct TaskCall {
72    fiber: Fiber,
73    shared: Rc<Shared>,
74}
75
76// SAFETY: a call is created, polled and dropped by the one simulation thread
77// that owns its process; the `Rc` is never shared with another thread (the
78// fiber machinery itself is thread-affine for the same reason).
79unsafe impl Send for TaskCall {}
80
81impl TaskCall {
82    /// Prepares `call` (the C import, returning its `int`) to run on its own
83    /// stack at the first [`Self::poll`]. `None` when no stack can be mapped.
84    pub fn new(call: impl FnOnce() -> i32 + 'static) -> Option<TaskCall> {
85        let shared = Rc::new(Shared::default());
86        let s = shared.clone();
87        let fiber = Fiber::new(move || s.result.set(call()))?;
88        Some(TaskCall { fiber, shared })
89    }
90
91    /// Runs the call until it finishes or blocks in an export task.
92    pub fn poll(&mut self) -> CallPoll {
93        let prev = ACTIVE.with(|a| a.replace(Some(self.shared.clone())));
94        let state = self.fiber.resume();
95        ACTIVE.with(|a| *a.borrow_mut() = prev);
96        match state {
97            FiberState::Finished => {
98                let unacked = self.shared.disabled.get()
99                    && !(self.shared.acked.get() || mirror_state() == MIRROR_ACKED);
100                mirror(MIRROR_OFF);
101                if unacked {
102                    undisabled_return_error(&self.shared.name.borrow());
103                }
104                CallPoll::Finished(self.shared.result.get())
105            }
106            FiberState::Yielded => CallPoll::Request(
107                self.shared
108                    .request
109                    .take()
110                    .expect("a parked DPI call posted no export request"),
111            ),
112        }
113    }
114
115    /// Marks the call disabled (35.9): the blocked export call returns
116    /// [`EXPORT_CALL_DISABLED`] on the next [`Self::poll`], and every export
117    /// the C code calls after that returns it at once.
118    pub fn disable(&mut self) {
119        self.shared.disabled.set(true);
120    }
121
122    /// The SV name of the import task this call runs (set while it is inside
123    /// the call, empty for a call outside any import task).
124    pub fn import_name(&self) -> String {
125        self.shared.name.borrow().clone()
126    }
127
128    /// Whether the C function has returned.
129    pub fn is_finished(&self) -> bool {
130        self.fiber.is_finished()
131    }
132}
133
134/// A flag outside this copy of the crate that mirrors the disabled state: the
135/// `vj run-aot` driver answers `svIsDisabledState()` itself, from a
136/// library with a copy of this module of its own ([`set_disabled_flag`]).
137static MIRROR: AtomicPtr<AtomicU8> = AtomicPtr::new(std::ptr::null_mut());
138
139/// [`MIRROR`] values: no disable, disabled (35.9), disabled and acknowledged
140/// by `svAckDisabledState()`.
141pub const MIRROR_OFF: u8 = 0;
142/// See [`MIRROR_OFF`].
143pub const MIRROR_DISABLED: u8 = 1;
144/// See [`MIRROR_OFF`].
145pub const MIRROR_ACKED: u8 = 2;
146
147/// Mirrors the disabled state of the running call into `flag` from now on.
148///
149/// # Safety
150/// `flag` stays valid for the life of the process.
151pub unsafe fn set_disabled_flag(flag: *const AtomicU8) {
152    MIRROR.store(flag as *mut AtomicU8, Ordering::Relaxed);
153}
154
155fn mirror(state: u8) {
156    let p = MIRROR.load(Ordering::Relaxed);
157    if !p.is_null() {
158        // SAFETY: `set_disabled_flag`'s contract.
159        unsafe { (*p).store(state, Ordering::Relaxed) };
160    }
161}
162
163fn mirror_state() -> u8 {
164    let p = MIRROR.load(Ordering::Relaxed);
165    if p.is_null() {
166        MIRROR_OFF
167    } else {
168        // SAFETY: `set_disabled_flag`'s contract.
169        unsafe { (*p).load(Ordering::Relaxed) }
170    }
171}
172
173/// `svAckDisabledState()` (35.9) for the call running on this thread.
174pub fn ack_disabled_state() {
175    FN_DISABLE.with(|f| f.borrow_mut().acked = true);
176    ACTIVE.with(|a| {
177        if let Some(s) = a.borrow().as_ref() {
178            s.acked.set(true);
179        }
180    });
181}
182
183/// 35.9: an import task whose call was disabled must call
184/// `svAckDisabledState()` before it returns. Aborts the simulation.
185fn undisabled_return_error(name: &str) -> ! {
186    eprintln!(
187        "error: DPI import task `{name}` was disabled but returned without calling \
188         svAckDisabledState() (IEEE 1800-2023 35.9)"
189    );
190    std::process::abort();
191}
192
193/// The `disable` of a block outside the exported function that runs it, while
194/// C code is in an import *function* call (LRM 35.9, 9.6.2).
195#[derive(Default)]
196struct FnDisable {
197    /// The block the function disabled; the call site leaves it once the C
198    /// call has returned ([`sv_rt_dpi_fn_disable_index`]).
199    pending: Option<String>,
200    /// Import-function calls in flight ([`sv_rt_dpi_fn_scope`]).
201    in_c: u32,
202    /// The disable came while C code was running: C must acknowledge it.
203    from_c: bool,
204    /// The C code called `svAckDisabledState()` in the disabled state.
205    acked: bool,
206}
207
208thread_local! {
209    static FN_DISABLE: RefCell<FnDisable> = RefCell::new(FnDisable::default());
210}
211
212/// `void sv_rt_dpi_fn_scope(int on)`: brackets the C call of an import
213/// *function* in a design where an exported function disables a block
214/// (35.9: the C code then sees the disabled state).
215#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_dpi_fn_scope"))]
216pub extern "C" fn sv_rt_dpi_fn_scope(on: i32) {
217    FN_DISABLE.with(|f| {
218        let mut f = f.borrow_mut();
219        f.in_c = if on != 0 {
220            f.in_c + 1
221        } else {
222            f.in_c.saturating_sub(1)
223        };
224    });
225}
226
227/// `void sv_rt_dpi_fn_disable_set(const char* label)`: `disable label;`
228/// executed in a function while the block is not one of its own (LRM 9.6.2):
229/// the function goes on to its end and the call site that reaches the block
230/// leaves it ([`sv_rt_dpi_fn_disable_index`]). Inside an import-function call
231/// this is the disabled state of 35.9 for the C code.
232///
233/// # Safety
234/// `label` is a NUL-terminated string.
235#[cfg_attr(
236    feature = "aot-exports",
237    unsafe(export_name = "sv_rt_dpi_fn_disable_set")
238)]
239pub unsafe extern "C" fn sv_rt_dpi_fn_disable_set(label: *const core::ffi::c_char) {
240    // SAFETY: the caller's contract.
241    let label = unsafe { std::ffi::CStr::from_ptr(label) }
242        .to_string_lossy()
243        .into_owned();
244    FN_DISABLE.with(|f| {
245        let mut f = f.borrow_mut();
246        f.pending = Some(label);
247        f.from_c = f.in_c > 0;
248        f.acked = false;
249        if f.from_c {
250            mirror(MIRROR_DISABLED);
251        }
252    });
253}
254
255/// `int64_t sv_rt_dpi_fn_disable_index(const char* labels)`: after a
256/// statement that called a subroutine. `labels` are the blocks enclosing the
257/// statement, separated by `\n`. Returns the index of the one the call
258/// disabled (the statement then leaves it), or -1. A C call that was disabled
259/// and returned without `svAckDisabledState()` aborts (35.9).
260///
261/// # Safety
262/// `labels` is a NUL-terminated string.
263#[cfg_attr(
264    feature = "aot-exports",
265    unsafe(export_name = "sv_rt_dpi_fn_disable_index")
266)]
267pub unsafe extern "C" fn sv_rt_dpi_fn_disable_index(labels: *const core::ffi::c_char) -> i64 {
268    let (pending, from_c, acked) = FN_DISABLE.with(|f| {
269        let mut f = f.borrow_mut();
270        let r = (f.pending.take(), f.from_c, f.acked);
271        f.from_c = false;
272        f.acked = false;
273        r
274    });
275    let Some(label) = pending else {
276        return -1;
277    };
278    let mirror_acked = mirror_state() == MIRROR_ACKED;
279    mirror(MIRROR_OFF);
280    if from_c && !(acked || mirror_acked) {
281        eprintln!(
282            "error: DPI import function was disabled (`disable {label};`) but returned without \
283             calling svAckDisabledState() (IEEE 1800-2023 35.9)"
284        );
285        std::process::abort();
286    }
287    // SAFETY: the caller's contract.
288    let labels = unsafe { std::ffi::CStr::from_ptr(labels) }.to_string_lossy();
289    labels
290        .split('\n')
291        .position(|l| l == label)
292        .map_or(-1, |i| i as i64)
293}
294
295/// Whether the import-task call running on this thread is in the disabled
296/// state (35.9): what `svIsDisabledState()` reports.
297pub fn active_call_disabled() -> bool {
298    ACTIVE.with(|a| a.borrow().as_ref().is_some_and(|s| s.disabled.get()))
299        || FN_DISABLE.with(|f| {
300            let f = f.borrow();
301            f.from_c && f.pending.is_some()
302        })
303}
304
305/// `void sv_rt_dpi_task_scope(int on, const char* name)`: brackets the C call of an import
306/// *task* (LRM 35.5.3). Only inside such a call may C code call an export
307/// task which consumes time; the call site of an import function does not set
308/// it, so the same call there is the runtime error of
309/// [`EXPORT_CALL_NOT_ON_FIBER`]. `name` is the import task's SV name (null
310/// when `on` is 0), which `disable <task>;` of another process matches (35.9).
311///
312/// # Safety
313/// `name` is null or a NUL-terminated string valid for the call.
314#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_dpi_task_scope"))]
315pub unsafe extern "C" fn sv_rt_dpi_task_scope(on: i32, name: *const core::ffi::c_char) {
316    if let Some(shared) = ACTIVE.with(|a| a.borrow().clone()) {
317        shared.in_task.set(on != 0);
318        // The name stays after the call's scope closes, for the 35.9 error.
319        if on != 0 && !name.is_null() {
320            // SAFETY: the call site passes a NUL-terminated constant, or null.
321            *shared.name.borrow_mut() = unsafe { std::ffi::CStr::from_ptr(name) }
322                .to_string_lossy()
323                .into_owned();
324        }
325    }
326}
327
328/// `void sv_rt_dpi_export_task_error(const char* name)`: the runtime error of
329/// a call of the timed export task `name` from outside an import task
330/// (LRM 35.5.3: only an import task may wait, an import function may not).
331/// Aborts the simulation.
332///
333/// # Safety
334/// `name` is a NUL-terminated string.
335#[cfg_attr(
336    feature = "aot-exports",
337    unsafe(export_name = "sv_rt_dpi_export_task_error")
338)]
339pub unsafe extern "C" fn sv_rt_dpi_export_task_error(name: *const core::ffi::c_char) {
340    // SAFETY: the caller's contract.
341    let name = unsafe { std::ffi::CStr::from_ptr(name) }.to_string_lossy();
342    eprintln!(
343        "error: DPI export task `{name}` consumes time, so it can only be called from an \
344         import task (not from an import function): the call would block (IEEE 1800-2023 35.5.3)"
345    );
346    std::process::abort();
347}
348
349/// `int sv_rt_dpi_export_task_call(uint32_t export, void* args, void* handle)`: the call an
350/// export thunk of a task that consumes time makes. Blocks the calling
351/// import task until the owner has run the export; returns 0, or
352/// [`EXPORT_CALL_DISABLED`], or [`EXPORT_CALL_NOT_ON_FIBER`].
353///
354/// # Safety
355/// `args` must stay valid until this call returns (it normally points into
356/// the caller's own, parked, stack frame).
357#[cfg_attr(
358    feature = "aot-exports",
359    unsafe(export_name = "sv_rt_dpi_export_task_call")
360)]
361pub unsafe extern "C" fn sv_rt_dpi_export_task_call(
362    export: u32,
363    args: *mut u8,
364    handle: *mut core::ffi::c_void,
365) -> i32 {
366    let Some(shared) = ACTIVE.with(|a| a.borrow().clone()) else {
367        return EXPORT_CALL_NOT_ON_FIBER;
368    };
369    if !shared.in_task.get() {
370        return EXPORT_CALL_NOT_ON_FIBER;
371    }
372    if shared.disabled.get() {
373        mirror(MIRROR_DISABLED);
374        return EXPORT_CALL_DISABLED;
375    }
376    shared.request.set(Some(ExportRequest {
377        export,
378        args,
379        handle,
380    }));
381    if !fiber::yield_now() {
382        shared.request.set(None);
383        return EXPORT_CALL_NOT_ON_FIBER;
384    }
385    if shared.disabled.get() {
386        mirror(MIRROR_DISABLED);
387        EXPORT_CALL_DISABLED
388    } else {
389        0
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    fn blocking(export: u32, slot: *mut u8) -> i32 {
398        unsafe { sv_rt_dpi_task_scope(1, std::ptr::null()) };
399        // SAFETY: `slot` outlives the call in every test below.
400        unsafe { sv_rt_dpi_export_task_call(export, slot, std::ptr::null_mut()) }
401    }
402
403    #[test]
404    fn a_call_that_never_blocks_finishes_on_the_first_poll() {
405        let mut c = TaskCall::new(|| 7).unwrap();
406        assert_eq!(c.poll(), CallPoll::Finished(7));
407        assert!(c.is_finished());
408    }
409
410    #[test]
411    fn a_blocked_call_posts_its_request_and_continues_after_the_next_poll() {
412        let mut c = TaskCall::new(|| {
413            let mut buf = [1u8, 2, 3, 4];
414            let st = blocking(3, buf.as_mut_ptr());
415            // The owner writes an output through the block while we are parked.
416            assert_eq!(st, 0);
417            i32::from(buf[0]) + 100
418        })
419        .unwrap();
420        let CallPoll::Request(r) = c.poll() else {
421            panic!("expected a request")
422        };
423        assert_eq!(r.export, 3);
424        // SAFETY: the fiber is parked with the block alive on its stack.
425        unsafe { *r.args = 9 };
426        assert_eq!(c.poll(), CallPoll::Finished(109));
427    }
428
429    #[test]
430    fn two_sequential_exports_are_two_requests() {
431        let mut c = TaskCall::new(|| {
432            let mut b = [0u8; 1];
433            blocking(1, b.as_mut_ptr());
434            blocking(2, b.as_mut_ptr());
435            0
436        })
437        .unwrap();
438        let mut seen = vec![];
439        loop {
440            match c.poll() {
441                CallPoll::Request(r) => seen.push(r.export),
442                CallPoll::Finished(v) => {
443                    assert_eq!(v, 0);
444                    break;
445                }
446            }
447        }
448        assert_eq!(seen, [1, 2]);
449    }
450
451    #[test]
452    fn disable_makes_the_blocked_export_return_the_disabled_status() {
453        let mut c = TaskCall::new(|| {
454            let mut b = [0u8; 1];
455            if blocking(5, b.as_mut_ptr()) == EXPORT_CALL_DISABLED {
456                // 35.9: the import returns 1 once it sees the disable.
457                // A further export call returns disabled at once.
458                assert_eq!(blocking(6, b.as_mut_ptr()), EXPORT_CALL_DISABLED);
459                ack_disabled_state();
460                return 1;
461            }
462            0
463        })
464        .unwrap();
465        assert!(matches!(c.poll(), CallPoll::Request(_)));
466        c.disable();
467        assert_eq!(c.poll(), CallPoll::Finished(1));
468    }
469
470    #[test]
471    fn two_concurrent_blocked_calls_resume_independently() {
472        let mk = |tag: i32| {
473            TaskCall::new(move || {
474                let mut b = [0u8; 1];
475                blocking(tag as u32, b.as_mut_ptr());
476                tag * 10
477            })
478            .unwrap()
479        };
480        let (mut a, mut b) = (mk(1), mk(2));
481        assert!(matches!(a.poll(), CallPoll::Request(r) if r.export == 1));
482        assert!(matches!(b.poll(), CallPoll::Request(r) if r.export == 2));
483        assert_eq!(b.poll(), CallPoll::Finished(20));
484        assert_eq!(a.poll(), CallPoll::Finished(10));
485    }
486
487    #[test]
488    fn an_export_task_called_from_an_import_function_is_refused() {
489        // On a fiber, but outside an import task's scope bracket (an import
490        // function): the thunk gets the not-on-fiber status, no request.
491        let mut c = TaskCall::new(|| {
492            let mut b = [0u8; 1];
493            // SAFETY: `b` outlives the call.
494            unsafe { sv_rt_dpi_export_task_call(0, b.as_mut_ptr(), std::ptr::null_mut()) }
495        })
496        .unwrap();
497        assert_eq!(c.poll(), CallPoll::Finished(EXPORT_CALL_NOT_ON_FIBER));
498    }
499
500    #[test]
501    fn an_export_task_called_off_a_fiber_is_refused() {
502        let mut b = [0u8; 1];
503        assert_eq!(blocking(0, b.as_mut_ptr()), EXPORT_CALL_NOT_ON_FIBER);
504    }
505}
506
507#[cfg(test)]
508mod ack_tests {
509    use super::*;
510
511    #[test]
512    fn ack_before_returning_disabled_is_recorded() {
513        let mut c = TaskCall::new(|| {
514            let mut b = [0u8; 1];
515            // SAFETY: `b` outlives the call.
516            unsafe { sv_rt_dpi_task_scope(1, std::ptr::null()) };
517            let st = unsafe { sv_rt_dpi_export_task_call(1, b.as_mut_ptr(), std::ptr::null_mut()) };
518            assert_eq!(st, EXPORT_CALL_DISABLED);
519            assert!(active_call_disabled());
520            ack_disabled_state();
521            1
522        })
523        .unwrap();
524        assert!(matches!(c.poll(), CallPoll::Request(_)));
525        c.disable();
526        assert_eq!(c.poll(), CallPoll::Finished(1));
527    }
528}