1use crate::fiber::{self, Fiber, FiberState};
18use std::cell::{Cell, RefCell};
19use std::rc::Rc;
20use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ExportRequest {
25 pub export: u32,
27 pub args: *mut u8,
29 pub handle: *mut core::ffi::c_void,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum CallPoll {
37 Finished(i32),
39 Request(ExportRequest),
41}
42
43pub const EXPORT_CALL_DISABLED: i32 = 1;
45pub 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 acked: Cell<bool>,
57 in_task: Cell<bool>,
60 name: RefCell<String>,
63}
64
65thread_local! {
66 static ACTIVE: RefCell<Option<Rc<Shared>>> = const { RefCell::new(None) };
68}
69
70pub struct TaskCall {
72 fiber: Fiber,
73 shared: Rc<Shared>,
74}
75
76unsafe impl Send for TaskCall {}
80
81impl TaskCall {
82 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 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 pub fn disable(&mut self) {
119 self.shared.disabled.set(true);
120 }
121
122 pub fn import_name(&self) -> String {
125 self.shared.name.borrow().clone()
126 }
127
128 pub fn is_finished(&self) -> bool {
130 self.fiber.is_finished()
131 }
132}
133
134static MIRROR: AtomicPtr<AtomicU8> = AtomicPtr::new(std::ptr::null_mut());
138
139pub const MIRROR_OFF: u8 = 0;
142pub const MIRROR_DISABLED: u8 = 1;
144pub const MIRROR_ACKED: u8 = 2;
146
147pub 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 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 unsafe { (*p).load(Ordering::Relaxed) }
170 }
171}
172
173pub 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
183fn 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#[derive(Default)]
196struct FnDisable {
197 pending: Option<String>,
200 in_c: u32,
202 from_c: bool,
204 acked: bool,
206}
207
208thread_local! {
209 static FN_DISABLE: RefCell<FnDisable> = RefCell::new(FnDisable::default());
210}
211
212#[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#[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 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#[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 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
295pub 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#[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 if on != 0 && !name.is_null() {
320 *shared.name.borrow_mut() = unsafe { std::ffi::CStr::from_ptr(name) }
322 .to_string_lossy()
323 .into_owned();
324 }
325 }
326}
327
328#[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 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#[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 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 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 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 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 let mut c = TaskCall::new(|| {
492 let mut b = [0u8; 1];
493 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 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}