use crate::fiber::{self, Fiber, FiberState};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExportRequest {
pub export: u32,
pub args: *mut u8,
pub handle: *mut core::ffi::c_void,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CallPoll {
Finished(i32),
Request(ExportRequest),
}
pub const EXPORT_CALL_DISABLED: i32 = 1;
pub const EXPORT_CALL_NOT_ON_FIBER: i32 = -1;
#[derive(Default)]
struct Shared {
request: Cell<Option<ExportRequest>>,
result: Cell<i32>,
disabled: Cell<bool>,
acked: Cell<bool>,
in_task: Cell<bool>,
name: RefCell<String>,
}
thread_local! {
static ACTIVE: RefCell<Option<Rc<Shared>>> = const { RefCell::new(None) };
}
pub struct TaskCall {
fiber: Fiber,
shared: Rc<Shared>,
}
unsafe impl Send for TaskCall {}
impl TaskCall {
pub fn new(call: impl FnOnce() -> i32 + 'static) -> Option<TaskCall> {
let shared = Rc::new(Shared::default());
let s = shared.clone();
let fiber = Fiber::new(move || s.result.set(call()))?;
Some(TaskCall { fiber, shared })
}
pub fn poll(&mut self) -> CallPoll {
let prev = ACTIVE.with(|a| a.replace(Some(self.shared.clone())));
let state = self.fiber.resume();
ACTIVE.with(|a| *a.borrow_mut() = prev);
match state {
FiberState::Finished => {
let unacked = self.shared.disabled.get()
&& !(self.shared.acked.get() || mirror_state() == MIRROR_ACKED);
mirror(MIRROR_OFF);
if unacked {
undisabled_return_error(&self.shared.name.borrow());
}
CallPoll::Finished(self.shared.result.get())
}
FiberState::Yielded => CallPoll::Request(
self.shared
.request
.take()
.expect("a parked DPI call posted no export request"),
),
}
}
pub fn disable(&mut self) {
self.shared.disabled.set(true);
}
pub fn import_name(&self) -> String {
self.shared.name.borrow().clone()
}
pub fn is_finished(&self) -> bool {
self.fiber.is_finished()
}
}
static MIRROR: AtomicPtr<AtomicU8> = AtomicPtr::new(std::ptr::null_mut());
pub const MIRROR_OFF: u8 = 0;
pub const MIRROR_DISABLED: u8 = 1;
pub const MIRROR_ACKED: u8 = 2;
pub unsafe fn set_disabled_flag(flag: *const AtomicU8) {
MIRROR.store(flag as *mut AtomicU8, Ordering::Relaxed);
}
fn mirror(state: u8) {
let p = MIRROR.load(Ordering::Relaxed);
if !p.is_null() {
unsafe { (*p).store(state, Ordering::Relaxed) };
}
}
fn mirror_state() -> u8 {
let p = MIRROR.load(Ordering::Relaxed);
if p.is_null() {
MIRROR_OFF
} else {
unsafe { (*p).load(Ordering::Relaxed) }
}
}
pub fn ack_disabled_state() {
FN_DISABLE.with(|f| f.borrow_mut().acked = true);
ACTIVE.with(|a| {
if let Some(s) = a.borrow().as_ref() {
s.acked.set(true);
}
});
}
fn undisabled_return_error(name: &str) -> ! {
eprintln!(
"error: DPI import task `{name}` was disabled but returned without calling \
svAckDisabledState() (IEEE 1800-2023 35.9)"
);
std::process::abort();
}
#[derive(Default)]
struct FnDisable {
pending: Option<String>,
in_c: u32,
from_c: bool,
acked: bool,
}
thread_local! {
static FN_DISABLE: RefCell<FnDisable> = RefCell::new(FnDisable::default());
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_dpi_fn_scope"))]
pub extern "C" fn sv_rt_dpi_fn_scope(on: i32) {
FN_DISABLE.with(|f| {
let mut f = f.borrow_mut();
f.in_c = if on != 0 {
f.in_c + 1
} else {
f.in_c.saturating_sub(1)
};
});
}
#[cfg_attr(
feature = "aot-exports",
unsafe(export_name = "sv_rt_dpi_fn_disable_set")
)]
pub unsafe extern "C" fn sv_rt_dpi_fn_disable_set(label: *const core::ffi::c_char) {
let label = unsafe { std::ffi::CStr::from_ptr(label) }
.to_string_lossy()
.into_owned();
FN_DISABLE.with(|f| {
let mut f = f.borrow_mut();
f.pending = Some(label);
f.from_c = f.in_c > 0;
f.acked = false;
if f.from_c {
mirror(MIRROR_DISABLED);
}
});
}
#[cfg_attr(
feature = "aot-exports",
unsafe(export_name = "sv_rt_dpi_fn_disable_index")
)]
pub unsafe extern "C" fn sv_rt_dpi_fn_disable_index(labels: *const core::ffi::c_char) -> i64 {
let (pending, from_c, acked) = FN_DISABLE.with(|f| {
let mut f = f.borrow_mut();
let r = (f.pending.take(), f.from_c, f.acked);
f.from_c = false;
f.acked = false;
r
});
let Some(label) = pending else {
return -1;
};
let mirror_acked = mirror_state() == MIRROR_ACKED;
mirror(MIRROR_OFF);
if from_c && !(acked || mirror_acked) {
eprintln!(
"error: DPI import function was disabled (`disable {label};`) but returned without \
calling svAckDisabledState() (IEEE 1800-2023 35.9)"
);
std::process::abort();
}
let labels = unsafe { std::ffi::CStr::from_ptr(labels) }.to_string_lossy();
labels
.split('\n')
.position(|l| l == label)
.map_or(-1, |i| i as i64)
}
pub fn active_call_disabled() -> bool {
ACTIVE.with(|a| a.borrow().as_ref().is_some_and(|s| s.disabled.get()))
|| FN_DISABLE.with(|f| {
let f = f.borrow();
f.from_c && f.pending.is_some()
})
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_dpi_task_scope"))]
pub unsafe extern "C" fn sv_rt_dpi_task_scope(on: i32, name: *const core::ffi::c_char) {
if let Some(shared) = ACTIVE.with(|a| a.borrow().clone()) {
shared.in_task.set(on != 0);
if on != 0 && !name.is_null() {
*shared.name.borrow_mut() = unsafe { std::ffi::CStr::from_ptr(name) }
.to_string_lossy()
.into_owned();
}
}
}
#[cfg_attr(
feature = "aot-exports",
unsafe(export_name = "sv_rt_dpi_export_task_error")
)]
pub unsafe extern "C" fn sv_rt_dpi_export_task_error(name: *const core::ffi::c_char) {
let name = unsafe { std::ffi::CStr::from_ptr(name) }.to_string_lossy();
eprintln!(
"error: DPI export task `{name}` consumes time, so it can only be called from an \
import task (not from an import function): the call would block (IEEE 1800-2023 35.5.3)"
);
std::process::abort();
}
#[cfg_attr(
feature = "aot-exports",
unsafe(export_name = "sv_rt_dpi_export_task_call")
)]
pub unsafe extern "C" fn sv_rt_dpi_export_task_call(
export: u32,
args: *mut u8,
handle: *mut core::ffi::c_void,
) -> i32 {
let Some(shared) = ACTIVE.with(|a| a.borrow().clone()) else {
return EXPORT_CALL_NOT_ON_FIBER;
};
if !shared.in_task.get() {
return EXPORT_CALL_NOT_ON_FIBER;
}
if shared.disabled.get() {
mirror(MIRROR_DISABLED);
return EXPORT_CALL_DISABLED;
}
shared.request.set(Some(ExportRequest {
export,
args,
handle,
}));
if !fiber::yield_now() {
shared.request.set(None);
return EXPORT_CALL_NOT_ON_FIBER;
}
if shared.disabled.get() {
mirror(MIRROR_DISABLED);
EXPORT_CALL_DISABLED
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn blocking(export: u32, slot: *mut u8) -> i32 {
unsafe { sv_rt_dpi_task_scope(1, std::ptr::null()) };
unsafe { sv_rt_dpi_export_task_call(export, slot, std::ptr::null_mut()) }
}
#[test]
fn a_call_that_never_blocks_finishes_on_the_first_poll() {
let mut c = TaskCall::new(|| 7).unwrap();
assert_eq!(c.poll(), CallPoll::Finished(7));
assert!(c.is_finished());
}
#[test]
fn a_blocked_call_posts_its_request_and_continues_after_the_next_poll() {
let mut c = TaskCall::new(|| {
let mut buf = [1u8, 2, 3, 4];
let st = blocking(3, buf.as_mut_ptr());
assert_eq!(st, 0);
i32::from(buf[0]) + 100
})
.unwrap();
let CallPoll::Request(r) = c.poll() else {
panic!("expected a request")
};
assert_eq!(r.export, 3);
unsafe { *r.args = 9 };
assert_eq!(c.poll(), CallPoll::Finished(109));
}
#[test]
fn two_sequential_exports_are_two_requests() {
let mut c = TaskCall::new(|| {
let mut b = [0u8; 1];
blocking(1, b.as_mut_ptr());
blocking(2, b.as_mut_ptr());
0
})
.unwrap();
let mut seen = vec![];
loop {
match c.poll() {
CallPoll::Request(r) => seen.push(r.export),
CallPoll::Finished(v) => {
assert_eq!(v, 0);
break;
}
}
}
assert_eq!(seen, [1, 2]);
}
#[test]
fn disable_makes_the_blocked_export_return_the_disabled_status() {
let mut c = TaskCall::new(|| {
let mut b = [0u8; 1];
if blocking(5, b.as_mut_ptr()) == EXPORT_CALL_DISABLED {
assert_eq!(blocking(6, b.as_mut_ptr()), EXPORT_CALL_DISABLED);
ack_disabled_state();
return 1;
}
0
})
.unwrap();
assert!(matches!(c.poll(), CallPoll::Request(_)));
c.disable();
assert_eq!(c.poll(), CallPoll::Finished(1));
}
#[test]
fn two_concurrent_blocked_calls_resume_independently() {
let mk = |tag: i32| {
TaskCall::new(move || {
let mut b = [0u8; 1];
blocking(tag as u32, b.as_mut_ptr());
tag * 10
})
.unwrap()
};
let (mut a, mut b) = (mk(1), mk(2));
assert!(matches!(a.poll(), CallPoll::Request(r) if r.export == 1));
assert!(matches!(b.poll(), CallPoll::Request(r) if r.export == 2));
assert_eq!(b.poll(), CallPoll::Finished(20));
assert_eq!(a.poll(), CallPoll::Finished(10));
}
#[test]
fn an_export_task_called_from_an_import_function_is_refused() {
let mut c = TaskCall::new(|| {
let mut b = [0u8; 1];
unsafe { sv_rt_dpi_export_task_call(0, b.as_mut_ptr(), std::ptr::null_mut()) }
})
.unwrap();
assert_eq!(c.poll(), CallPoll::Finished(EXPORT_CALL_NOT_ON_FIBER));
}
#[test]
fn an_export_task_called_off_a_fiber_is_refused() {
let mut b = [0u8; 1];
assert_eq!(blocking(0, b.as_mut_ptr()), EXPORT_CALL_NOT_ON_FIBER);
}
}
#[cfg(test)]
mod ack_tests {
use super::*;
#[test]
fn ack_before_returning_disabled_is_recorded() {
let mut c = TaskCall::new(|| {
let mut b = [0u8; 1];
unsafe { sv_rt_dpi_task_scope(1, std::ptr::null()) };
let st = unsafe { sv_rt_dpi_export_task_call(1, b.as_mut_ptr(), std::ptr::null_mut()) };
assert_eq!(st, EXPORT_CALL_DISABLED);
assert!(active_call_disabled());
ack_disabled_state();
1
})
.unwrap();
assert!(matches!(c.poll(), CallPoll::Request(_)));
c.disable();
assert_eq!(c.poll(), CallPoll::Finished(1));
}
}