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);