wolfram_library_link/managed.rs
1//! Managed expressions.
2//!
3//! Managed expressions are Wolfram Language expressions created using
4//! [`CreateManagedLibraryExpression`][ref/CreateManagedLibraryExpression]<sub>WL</sub>,
5//! which are associated with a unique [`Id`] number that is shared with a loaded library.
6//!
7//! Using [`register_library_expression_manager()`], a library can register a callback
8//! function, which will recieve a [`ManagedExpressionEvent`] each time a new managed
9//! expression is created or deallocated.
10//!
11//! The managed expression [`Create(Id)`][ManagedExpressionEvent::Create] event is
12//! typically handled by the library to create an instance of some library data type that
13//! is associated with the managed expression. When the managed expression is finally
14//! deallocated, a [`Drop(Id)`][ManagedExpressionEvent::Drop] event is generated, and
15//! the library knows it is safe to free the associated data object.
16//!
17//! In this way, managed expressions allow memory-management of Rust objects to be
18//! performed indirectly based on the lifetime of a Wolfram Language expression.
19//!
20// TODO: Expand and polish this section: # Alternatives
21//
22// * Canonical WL expression representation
23// * MyStruct[<some raw pointer value>] and manual memory management from WL
24//
25// Managed expressions are a way for objects that cannot be easily or efficiently
26// represented as a Wolfram Language expression to still be associated with the lifetime
27// of a Wolfram Language expression.
28//
29// If an object can be represented as a Wolfram expression, then that is the most
30// straightforward thing to do. the
31// simplest possible [[ToExpr, FromExpr]].
32//
33// The simplest alternative to managed expressions to simply convert the
34//
35//! # Related links
36//!
37//! * [Managed Library Expressions] section of the LibraryLink documentation.
38//! * The [wolfram-library-link managed expressions example](https://github.com/WolframResearch/wolfram-library-link-rs#example-programs).
39//!
40//! [Managed Library Expressions]: https://reference.wolfram.com/language/LibraryLink/tutorial/InteractionWithWolframLanguage.html#353220453
41//! [ref/CreateManagedLibraryExpression]: https://reference.wolfram.com/language/ref/CreateManagedLibraryExpression.html
42
43use std::{ffi::CString, sync::Mutex};
44
45use once_cell::sync::Lazy;
46
47use crate::{rtl, sys};
48
49/// Lifecycle events triggered by the creation and deallocation of managed expressions.
50pub enum ManagedExpressionEvent {
51 /// Instruction that the library should create a new instance of a managed expression
52 /// with the specified [`Id`].
53 ///
54 /// This event occurs when
55 /// [CreateManagedLibraryExpression][ref/CreateManagedLibraryExpression] is called.
56 ///
57 /// [ref/CreateManagedLibraryExpression]: https://reference.wolfram.com/language/ref/CreateManagedLibraryExpression.html
58 Create(Id),
59 /// Instruction that the library should drop any data associated with the managed
60 /// expression identified by this [`Id`].
61 ///
62 /// This event occurs when the managed expression is no longer used by the Wolfram
63 /// Language.
64 Drop(Id),
65}
66
67impl ManagedExpressionEvent {
68 /// Get the managed expression [`Id`] targeted by this action.
69 pub fn id(&self) -> Id {
70 match *self {
71 ManagedExpressionEvent::Create(id) => id,
72 ManagedExpressionEvent::Drop(id) => id,
73 }
74 }
75}
76
77/// Unique identifier associated with an instance of a managed library expression.
78pub type Id = u32;
79
80/// Register a new callback function for handling managed expression events.
81pub fn register_library_expression_manager(
82 name: &str,
83 manage_instance: fn(ManagedExpressionEvent),
84) {
85 register_using_next_slot(name, manage_instance)
86}
87
88//======================================
89// C wrapper functions
90//======================================
91
92/// # Implementation note on the reason for this static / "slot" system.
93///
94/// Having this static is not a direct requirement of the library expression
95/// C API, however it is necessary as a workaround for the problem described below.
96///
97/// `registerLibraryExpressionManager()` expects a callback function of the type:
98///
99/// ```ignore
100/// unsafe extern "C" fn(WolframLibraryData, mbool, mint)
101/// ```
102///
103/// however, for the purpose of providing a more ergonomic and safe wrapper to the user,
104/// we want the user to be able to pass `register_library_expression_manager()` a callback
105/// function with the type:
106///
107/// ```ignore
108/// fn(ManagedExpressionAction)
109/// ```
110///
111/// This specific problem is an instance of the more general problem of how to expose a
112/// user-provided function/closure (non-`extern "C"`) as-if it actually were an
113/// `extern "C"` function.
114///
115/// There are two common ways we could concievably do this:
116///
117/// 1. Use a macro to generate an `extern "C"` function that calls the user-provided
118/// function.
119///
120/// 2. Use a "trampoline" function (e.g. like async_task_thread_trampoline()) which has
121/// the correct `extern "C"` signature, and wraps the user function. This only works
122/// if the `extern "C"` function has a parameter that we can control and use to pass
123/// in a function pointer to the user-provided function.
124///
125/// The (1.) strategy is easy to implement, but is undesirable because:
126///
127/// a) it requires the user to use a macro -- and for subtle reasons (see: this comment)).
128/// b) it exposes the underlying `unsafe extern "C" fn(...)` type to the user.
129///
130/// The (2.) strategy is often a good choice, but cannot be used in this particular
131/// case, because their is no way to pass a custom argument to the callback expected by
132/// registerLibraryExpressionManager().
133///
134/// In both the (1.) and (2.) strategies, the solution is to create a single wrapper
135/// function for each user function, which is hard-coded to call the user function that
136/// it wraps.
137///
138/// The technique used here is a third strategy:
139///
140/// 3. Store the user-provided function pointer into a static array, and, instead of
141/// having a single `extern "C"` wrapper function, have multiple `extern "C"` wrapper
142/// functions, each of which statically access a different index in the static array.
143///
144/// By using different `extern "C"` functions that access different static data, we
145/// can essentially "fake" having an extra function argument that we control.
146///
147/// This depends on the observation that the callback function pointer is itself a
148/// value we control.
149///
150/// This technique is limited by the fact that the static function pointers must be
151/// declared ahead of time (see `def_slot_fn!` below), and so practically there is a
152/// somewhat arbitrary limit on how many callbacks can be registered at a time.
153///
154/// In our case, the *only* data we are able pass through the C API is the static function
155/// pointer we are registering; so strategy (3.) is the way to go.
156///
157/// `SLOTS` has 8 elements, and we define 8 `extern "C" fn slot_<X>(..)` functions that
158/// access only the corresponding element in `SLOTS`.
159///
160/// 8 was picked arbitrarily, on the assumption that 8 different registered types should
161/// be sufficient for the vast majority of libraries. Libraries that want to register more
162/// than 8 types can use `rtl::registerLibraryExpressionManager` directly as a workaround.
163///
164/// TODO: Also store the "name" of this manager, and pass it to the user function?
165static SLOTS: Lazy<Mutex<[Option<fn(ManagedExpressionEvent)>; 8]>> =
166 Lazy::new(|| Mutex::new([None; 8]));
167
168fn register_using_next_slot(name: &str, manage_instance: fn(ManagedExpressionEvent)) {
169 let name_cstr = CString::new(name).expect("failed to allocate C string");
170
171 let mut slots = SLOTS.lock().unwrap();
172
173 let available_slot: Option<(usize, &mut Option<_>)> = slots
174 .iter_mut()
175 .enumerate()
176 .filter(|(_, slot)| slot.is_none())
177 .next();
178
179 let result = if let Some((index, slot)) = available_slot {
180 *slot = Some(manage_instance);
181 register_using_slot(name_cstr, index)
182 } else {
183 // Drop `slots` to avoid poisoning SLOTS when we panic.
184 drop(slots);
185 panic!("maxiumum number of library expression managers have been registered");
186 };
187
188 drop(slots);
189
190 if let Err(()) = result {
191 panic!(
192 "library expression manager with name '{}' has already been registered",
193 name
194 );
195 }
196}
197
198fn register_using_slot(name_cstr: CString, index: usize) -> Result<(), ()> {
199 let static_slot_fn: unsafe extern "C" fn(_, _, _) = match index {
200 0 => slot_0,
201 1 => slot_1,
202 2 => slot_2,
203 3 => slot_3,
204 4 => slot_4,
205 5 => slot_5,
206 6 => slot_6,
207 7 => slot_7,
208 8 => slot_8,
209 _ => unreachable!(),
210 };
211
212 let err_code: i32 = unsafe {
213 rtl::registerLibraryExpressionManager(name_cstr.as_ptr(), Some(static_slot_fn))
214 };
215
216 if err_code != 0 {
217 Err(())
218 } else {
219 Ok(())
220 }
221}
222
223//--------------------------
224// Static slot_<X> functions
225//--------------------------
226
227fn call_callback_in_slot(slot: usize, mode: sys::mbool, id: sys::mint) {
228 let slots = SLOTS.lock().unwrap();
229
230 let user_fn: fn(ManagedExpressionEvent) = match slots[slot] {
231 Some(func) => func,
232 // TODO: Set something like "RustLink`$LibraryLastError" with a descriptive error?
233 None => return,
234 };
235
236 // Ensure we're not holding a lock on `slots`, to avoid poisoning SLOTS in the case
237 // `user_fn` panics.
238 drop(slots);
239
240 let id: u32 = match u32::try_from(id) {
241 Ok(id) => id,
242 // TODO: Set something like "RustLink`$LibraryLastError" with a descriptive error?
243 Err(_) => return,
244 };
245
246 let action = match mode {
247 0 => ManagedExpressionEvent::Create(id),
248 1 => ManagedExpressionEvent::Drop(id),
249 _ => panic!("unknown managed expression 'mode' value: {}", mode),
250 };
251
252 user_fn(action)
253}
254
255macro_rules! def_slot_fn {
256 ($name:ident, $index:literal) => {
257 unsafe extern "C" fn $name(
258 // Assume this library is already initialized.
259 _: sys::WolframLibraryData,
260 mode: sys::mbool,
261 id: sys::mint,
262 ) {
263 let result = crate::catch_panic::call_and_catch_panic(|| {
264 call_callback_in_slot($index, mode, id)
265 });
266
267 if let Err(_) = result {
268 // Do nothing.
269 // TODO: Set something like "RustLink`$LibraryLastError" with this panic?
270 }
271 }
272 };
273}
274
275def_slot_fn!(slot_0, 0);
276def_slot_fn!(slot_1, 1);
277def_slot_fn!(slot_2, 2);
278def_slot_fn!(slot_3, 3);
279def_slot_fn!(slot_4, 4);
280def_slot_fn!(slot_5, 5);
281def_slot_fn!(slot_6, 6);
282def_slot_fn!(slot_7, 7);
283def_slot_fn!(slot_8, 8);