wolfram_library_link/async_tasks.rs
1//! Support for Wolfram Language asynchronous tasks.
2//!
3//! # Credits
4//!
5//! The implementations of this module and the associated examples are based on the path
6//! laid out by [this StackOverflow answer](https://mathematica.stackexchange.com/a/138433).
7
8use std::{
9 ffi::{c_void, CString},
10 panic,
11};
12
13use static_assertions::assert_not_impl_any;
14
15use crate::{rtl, sys, DataStore};
16
17
18/// Handle to a Wolfram Language [`AsynchronousTaskObject`][ref/AsynchronousTaskObject]<sub>WL</sub>
19/// instance.
20///
21/// Use [`spawn_with_thread()`][AsyncTaskObject::spawn_with_thread] to spawn a new
22/// asynchronous task.
23///
24/// [ref/AsynchronousTaskObject]: https://reference.wolfram.com/language/ref/AsynchronousTaskObject.html
25#[derive(Debug)]
26pub struct AsyncTaskObject(sys::mint);
27
28// TODO: Determine if it would be safe for this type to implement Copy/Clone.
29assert_not_impl_any!(AsyncTaskObject: Copy, Clone);
30
31
32//======================================
33// Impls
34//======================================
35
36impl AsyncTaskObject {
37 /// Spawn a new Wolfram Language asynchronous task.
38 ///
39 /// This method can be used within a LibraryLink function that was called via
40 ///
41 /// ```wolfram
42 /// Internal`CreateAsynchronousTask[
43 /// _LibraryFunction,
44 /// args_List,
45 /// handler
46 /// ]
47 /// ```
48 ///
49 /// to create a new [`AsynchronousTaskObject`][ref/AsynchronousTaskObject]<sub>WL</sub>
50 /// that uses a background thread that can generate events that will be processed
51 /// asynchronously by the Wolfram Language.
52 ///
53 /// The background thread is given an `AsyncTaskObject` that has the same id as
54 /// the `AsyncTaskObject` returned from this function. Events generated by the
55 /// background thread using [`raise_async_event()`][AsyncTaskObject::raise_async_event]
56 /// will result in an asynchronous call to the Wolfram Language `handler` function
57 /// specified in the call to `` Internal`CreateAsynchronousEvent ``.
58 ///
59 /// [ref/AsynchronousTaskObject]: https://reference.wolfram.com/language/ref/AsynchronousTaskObject.html
60 pub fn spawn_with_thread<F>(f: F) -> Self
61 where
62 F: FnMut(AsyncTaskObject) + Send + panic::UnwindSafe + 'static,
63 {
64 spawn_async_task_with_thread(f)
65 }
66
67 /// Returns the numeric ID which identifies this async object.
68 pub fn id(&self) -> sys::mint {
69 let AsyncTaskObject(id) = *self;
70 id
71 }
72
73 /// Returns whether this async task is still alive.
74 ///
75 /// *LibraryLink C Function:* [`asynchronousTaskAliveQ`][sys::st_WolframIOLibrary_Functions::asynchronousTaskAliveQ].
76 pub fn is_alive(&self) -> bool {
77 let is_alive: sys::mbool = unsafe { rtl::asynchronousTaskAliveQ(self.id()) };
78
79 crate::bool_from_mbool(is_alive)
80 }
81
82 /// Returns whether this async task has been started.
83 ///
84 /// *LibraryLink C Function:* [`asynchronousTaskStartedQ`][sys::st_WolframIOLibrary_Functions::asynchronousTaskStartedQ].
85 pub fn is_started(&self) -> bool {
86 let is_started: sys::mbool = unsafe { rtl::asynchronousTaskStartedQ(self.id()) };
87
88 crate::bool_from_mbool(is_started)
89 }
90
91 /// Raise a new named asynchronous event associated with the current async task.
92 ///
93 /// # Example
94 ///
95 /// Raise a new asynchronous event with no associated data:
96 ///
97 /// This will cause the Wolfram Language event handler associated with this task to
98 /// be run.
99 ///
100 /// *LibraryLink C Function:* [`raiseAsyncEvent`][sys::st_WolframIOLibrary_Functions::raiseAsyncEvent].
101 ///
102 /// ```no_run
103 /// use wolfram_library_link::{AsyncTaskObject, DataStore};
104 ///
105 /// let task_object: AsyncTaskObject = todo!();
106 ///
107 /// task_object.raise_async_event("change", DataStore::new());
108 /// ```
109 pub fn raise_async_event(&self, name: &str, data: DataStore) {
110 let AsyncTaskObject(id) = *self;
111
112 let name = CString::new(name)
113 .expect("unable to convert raised async event name to CString");
114
115 unsafe {
116 // raise_async_event(id, name.as_ptr() as *mut c_char, data.into_ptr());
117 rtl::raiseAsyncEvent(id, name.into_raw(), data.into_raw());
118 }
119 }
120}
121
122fn spawn_async_task_with_thread<F>(task: F) -> AsyncTaskObject
123where
124 // Note: Ensure that the bound on async_task_thread_trampoline() is kept up-to-date
125 // with this bound.
126 F: FnMut(AsyncTaskObject) + Send + 'static + panic::UnwindSafe,
127{
128 // FIXME: This box is being leaked. Where is an appropriate place to drop it?
129 let boxed_closure = Box::into_raw(Box::new(task));
130
131 // Spawn a background thread using the user closure.
132 let task_id: sys::mint = unsafe {
133 rtl::createAsynchronousTaskWithThread(
134 Some(async_task_thread_trampoline::<F>),
135 boxed_closure as *mut c_void,
136 )
137 };
138
139 AsyncTaskObject(task_id)
140}
141
142unsafe extern "C" fn async_task_thread_trampoline<F>(
143 async_object_id: sys::mint,
144 boxed_closure: *mut c_void,
145) where
146 F: FnMut(AsyncTaskObject) + Send + 'static + panic::UnwindSafe,
147{
148 let boxed_closure: &mut F = &mut *(boxed_closure as *mut F);
149
150 // static_assertions::assert_impl_all!(F: panic::UnwindSafe);
151
152 // Catch any panics which occur.
153 //
154 // Use AssertUnwindSafe because:
155 // 1) `F` is already required to implement UnwindSafe by the definition of AsyncTask.
156 // 2) We don't introduce any new potential unwind safety with our minimal closure
157 // here.
158 match panic::catch_unwind(panic::AssertUnwindSafe(|| {
159 boxed_closure(AsyncTaskObject(async_object_id))
160 })) {
161 Ok(()) => (),
162 Err(_) => (),
163 }
164}