Skip to main content

mlx_c_rust/types/
closures.rs

1//! The MLX closure domain: owned closures built from Rust callables through
2//! payload trampolines, plus the kwargs and value-and-gradient variants.
3//! The custom-transform family lives in `crate::types::closure_custom` and
4//! shares this module's trampoline machinery.
5//!
6//! Every payload is a `RefCell` of the Rust callable boxed into C memory; the
7//! matching destructor reclaims it. Callback panics are caught at the FFI
8//! boundary, converted to typed failures, and parked thread-locally for the
9//! caller's status translation so nothing unwinds across the C ABI.
10
11use std::cell::RefCell;
12use std::os::raw::c_void;
13
14use crate::error::check_status;
15use crate::raw;
16use crate::types::maps::MlxMapStringToArray;
17use crate::{MlxArray, MlxArrayVector, MlxCError};
18
19/// Reads every array of a live input vector into owned handles.
20///
21/// # Errors
22/// Returns the captured MLX-C description when an element read fails.
23pub(crate) fn owned_input_arrays(input: raw::mlx_vector_array) -> Result<Vec<MlxArray>, MlxCError> {
24    let element_count = unsafe { raw::mlx_vector_array_size(input) };
25    let mut elements = Vec::with_capacity(element_count);
26    for element_index in 0..element_count {
27        let mut element = MlxArray::empty();
28        // SAFETY: The input vector is live for this trampoline's duration and
29        // the output pointer is valid writable storage.
30        let status = unsafe { raw::mlx_vector_array_get(element.raw_mut(), input, element_index) };
31        check_status(status, "read a closure input array")?;
32        elements.push(element);
33    }
34    Ok(elements)
35}
36
37/// Writes owned output arrays into the caller's output vector handle.
38///
39/// # Errors
40/// Returns the captured MLX-C description when the write fails.
41pub(crate) fn write_output_arrays(
42    output: *mut raw::mlx_vector_array,
43    outputs: Vec<MlxArray>,
44) -> Result<(), MlxCError> {
45    let raw_outputs: Vec<raw::mlx_array> = outputs.iter().map(MlxArray::raw).collect();
46    // SAFETY: The output pointer is valid writable storage and the handle
47    // storage remains valid for this copying call.
48    let status =
49        unsafe { raw::mlx_vector_array_set_data(output, raw_outputs.as_ptr(), raw_outputs.len()) };
50    check_status(status, "write closure output arrays")
51}
52
53/// Runs the payload callable, catching panics at the FFI boundary.
54pub(crate) fn run_payload_closure<F, R>(
55    payload: *mut c_void,
56    operation: &'static str,
57    invoke: impl FnOnce(&mut F) -> R,
58) -> Option<R> {
59    let payload_cell = unsafe { &*(payload.cast::<RefCell<F>>()) };
60    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
61        let mut callback = payload_cell
62            .try_borrow_mut()
63            .expect("closure payload is not reentrant");
64        invoke(&mut callback)
65    }));
66    match outcome {
67        Ok(value) => Some(value),
68        Err(panic_payload) => {
69            crate::error::set_closure_error(MlxCError {
70                operation,
71                description: panic_message(panic_payload),
72            });
73            None
74        }
75    }
76}
77
78fn panic_message(panic_payload: Box<dyn std::any::Any + Send>) -> String {
79    panic_payload
80        .downcast_ref::<&str>()
81        .map(|message| (*message).to_owned())
82        .or_else(|| panic_payload.downcast_ref::<String>().cloned())
83        .unwrap_or_else(|| "closure panicked".to_owned())
84}
85
86/// Owned MLX closure handle released exactly once through the C API.
87#[derive(Debug)]
88pub struct MlxClosure(pub(crate) raw::mlx_closure);
89
90impl MlxClosure {
91    /// Builds an owned closure from a Rust callable mapping input arrays to
92    /// output arrays.
93    pub fn from_fn<F>(callback: F) -> Result<Self, MlxCError>
94    where
95        F: FnMut(&[MlxArray]) -> Result<Vec<MlxArray>, MlxCError> + 'static,
96    {
97        let operation: &'static str = "create an MLX closure from a Rust callable";
98        let payload = Box::into_raw(Box::new(RefCell::new(callback)));
99        // SAFETY: The trampoline and destructor follow the exact C ABI, the
100        // payload stays alive until the destructor runs, and the returned
101        // handle enters RAII ownership.
102        let raw_closure = unsafe {
103            raw::mlx_closure_new_func_payload(
104                Some(vector_callback_trampoline::<F>),
105                payload.cast(),
106                Some(drop_payload::<F>),
107            )
108        };
109        let closure = Self(raw_closure);
110        closure.require_populated(operation)?;
111        Ok(closure)
112    }
113
114    /// Creates an empty closure handle.
115    pub fn empty() -> Self {
116        // SAFETY: The returned handle enters RAII ownership immediately.
117        let raw_closure = unsafe { raw::mlx_closure_new() };
118        Self(raw_closure)
119    }
120
121    /// Copies the source closure handle into this handle.
122    pub fn set(&mut self, source: &Self) -> Result<(), MlxCError> {
123        // SAFETY: Both handles are live and this call only replaces the
124        // owned closure value.
125        let status = unsafe { raw::mlx_closure_set(&mut self.0, source.0) };
126        check_status(status, "copy an MLX closure")
127    }
128
129    /// Applies the closure to the input arrays on the caller's behalf.
130    ///
131    /// # Errors
132    /// Returns the captured MLX-C description when application fails.
133    pub fn apply(&self, input: &MlxArrayVector) -> Result<MlxArrayVector, MlxCError> {
134        let operation: &'static str = "apply an MLX closure";
135        let mut output = MlxArrayVector::empty(operation)?;
136        // SAFETY: Both handles are live and the output pointer is valid
137        // writable storage.
138        let status = unsafe { raw::mlx_closure_apply(output.raw_mut(), self.0, input.raw()) };
139        check_status(status, operation)?;
140        Ok(output)
141    }
142
143    pub(crate) const fn raw(&self) -> raw::mlx_closure {
144        self.0
145    }
146
147    pub(crate) fn raw_mut(&mut self) -> *mut raw::mlx_closure {
148        &mut self.0
149    }
150
151    pub(crate) fn require_populated(&self, operation: &'static str) -> Result<(), MlxCError> {
152        if self.0.ctx.is_null() {
153            return Err(MlxCError {
154                operation,
155                description: "MLX returned an empty closure handle".to_owned(),
156            });
157        }
158        Ok(())
159    }
160}
161
162impl Drop for MlxClosure {
163    fn drop(&mut self) {
164        // SAFETY: This owner releases its live closure exactly once and
165        // never accesses the handle afterward.
166        unsafe {
167            raw::mlx_closure_free(self.0);
168        }
169    }
170}
171
172unsafe extern "C" fn vector_callback_trampoline<F>(
173    output: *mut raw::mlx_vector_array,
174    input: raw::mlx_vector_array,
175    payload: *mut c_void,
176) -> i32
177where
178    F: FnMut(&[MlxArray]) -> Result<Vec<MlxArray>, MlxCError> + 'static,
179{
180    let Some(outputs) = run_payload_closure::<F, Result<Vec<MlxArray>, MlxCError>>(
181        payload,
182        "apply a Rust closure",
183        |callback| owned_input_arrays(input).and_then(|inputs| callback(&inputs)),
184    ) else {
185        return 1;
186    };
187    match outputs.and_then(|owned_outputs| write_output_arrays(output, owned_outputs)) {
188        Ok(()) => 0,
189        Err(failure) => {
190            crate::error::set_closure_error(failure);
191            1
192        }
193    }
194}
195
196pub(crate) unsafe extern "C" fn drop_payload<F>(payload: *mut c_void) {
197    if payload.is_null() {
198        return;
199    }
200    // SAFETY: The payload was created by `Box::into_raw` of the matching
201    // type and the destructor runs exactly once per closure.
202    drop(unsafe { Box::from_raw(payload.cast::<RefCell<F>>()) });
203}
204
205/// Owned MLX closure with keyword arguments, released exactly once.
206#[derive(Debug)]
207pub struct MlxClosureKwargs(raw::mlx_closure_kwargs);
208
209impl MlxClosureKwargs {
210    /// Builds an owned kwargs closure from a Rust callable mapping input
211    /// arrays and a keyword map to output arrays.
212    pub fn from_fn<F>(callback: F) -> Result<Self, MlxCError>
213    where
214        F: FnMut(&[MlxArray], &MlxMapStringToArray) -> Result<Vec<MlxArray>, MlxCError> + 'static,
215    {
216        let operation: &'static str = "create an MLX kwargs closure from a Rust callable";
217        let payload = Box::into_raw(Box::new(RefCell::new(callback)));
218        // SAFETY: The trampoline and destructor follow the exact C ABI and
219        // the returned handle enters RAII ownership.
220        let raw_closure = unsafe {
221            raw::mlx_closure_kwargs_new_func_payload(
222                Some(kwargs_callback_trampoline::<F>),
223                payload.cast(),
224                Some(drop_payload::<F>),
225            )
226        };
227        let closure = Self(raw_closure);
228        if closure.0.ctx.is_null() {
229            return Err(MlxCError {
230                operation,
231                description: "MLX returned an empty closure handle".to_owned(),
232            });
233        }
234        Ok(closure)
235    }
236
237    /// Creates an empty kwargs closure handle.
238    pub fn empty() -> Self {
239        // SAFETY: The returned handle enters RAII ownership immediately.
240        let raw_closure = unsafe { raw::mlx_closure_kwargs_new() };
241        Self(raw_closure)
242    }
243
244    /// Copies the source closure handle into this handle.
245    pub fn set(&mut self, source: &Self) -> Result<(), MlxCError> {
246        // SAFETY: Both handles are live and this call only replaces the
247        // owned closure value.
248        let status = unsafe { raw::mlx_closure_kwargs_set(&mut self.0, source.0) };
249        check_status(status, "copy an MLX kwargs closure")
250    }
251
252    /// Applies the closure to the inputs and keyword map.
253    ///
254    /// # Errors
255    /// Returns the captured MLX-C description when application fails.
256    pub fn apply(
257        &self,
258        input: &MlxArrayVector,
259        keyword_arguments: &MlxMapStringToArray,
260    ) -> Result<MlxArrayVector, MlxCError> {
261        let operation: &'static str = "apply an MLX kwargs closure";
262        let mut output = MlxArrayVector::empty(operation)?;
263        // SAFETY: All handles are live and the output pointer is valid
264        // writable storage.
265        let status = unsafe {
266            raw::mlx_closure_kwargs_apply(
267                output.raw_mut(),
268                self.0,
269                input.raw(),
270                keyword_arguments.raw(),
271            )
272        };
273        check_status(status, operation)?;
274        Ok(output)
275    }
276}
277
278impl MlxClosureKwargs {
279    pub(crate) const fn raw(&self) -> raw::mlx_closure_kwargs {
280        self.0
281    }
282}
283
284impl Drop for MlxClosureKwargs {
285    fn drop(&mut self) {
286        // SAFETY: This owner releases its live closure exactly once and
287        // never accesses the handle afterward.
288        unsafe {
289            raw::mlx_closure_kwargs_free(self.0);
290        }
291    }
292}
293
294unsafe extern "C" fn kwargs_callback_trampoline<F>(
295    output: *mut raw::mlx_vector_array,
296    input: raw::mlx_vector_array,
297    keyword_arguments: raw::mlx_map_string_to_array,
298    payload: *mut c_void,
299) -> i32
300where
301    F: FnMut(&[MlxArray], &MlxMapStringToArray) -> Result<Vec<MlxArray>, MlxCError> + 'static,
302{
303    let Some(outputs) = run_payload_closure::<F, Result<Vec<MlxArray>, MlxCError>>(
304        payload,
305        "apply a Rust kwargs closure",
306        |callback| {
307            owned_input_arrays(input).and_then(|inputs| {
308                let borrowed_keywords = MlxMapStringToArray::from_borrowed_raw(keyword_arguments);
309                callback(&inputs, &borrowed_keywords)
310            })
311        },
312    ) else {
313        return 1;
314    };
315    match outputs.and_then(|owned_outputs| write_output_arrays(output, owned_outputs)) {
316        Ok(()) => 0,
317        Err(failure) => {
318            crate::error::set_closure_error(failure);
319            1
320        }
321    }
322}
323
324/// Owned MLX value-and-gradient closure, released exactly once.
325#[derive(Debug)]
326pub struct MlxClosureValueAndGrad(raw::mlx_closure_value_and_grad);
327
328impl MlxClosureValueAndGrad {
329    /// Builds an owned value-and-gradient closure from a Rust callable
330    /// returning both outputs and gradients.
331    pub fn from_fn<F>(callback: F) -> Result<Self, MlxCError>
332    where
333        F: FnMut(&[MlxArray]) -> Result<(Vec<MlxArray>, Vec<MlxArray>), MlxCError> + 'static,
334    {
335        let operation: &'static str = "create an MLX value-and-gradient closure";
336        let payload = Box::into_raw(Box::new(RefCell::new(callback)));
337        // SAFETY: The trampoline and destructor follow the exact C ABI and
338        // the returned handle enters RAII ownership.
339        let raw_closure = unsafe {
340            raw::mlx_closure_value_and_grad_new_func_payload(
341                Some(value_and_grad_callback_trampoline::<F>),
342                payload.cast(),
343                Some(drop_payload::<F>),
344            )
345        };
346        let closure = Self(raw_closure);
347        if closure.0.ctx.is_null() {
348            return Err(MlxCError {
349                operation,
350                description: "MLX returned an empty closure handle".to_owned(),
351            });
352        }
353        Ok(closure)
354    }
355
356    /// Creates an empty value-and-gradient closure handle.
357    pub fn empty() -> Self {
358        // SAFETY: The returned handle enters RAII ownership immediately.
359        let raw_closure = unsafe { raw::mlx_closure_value_and_grad_new() };
360        Self(raw_closure)
361    }
362
363    /// Copies the source closure handle into this handle.
364    pub fn set(&mut self, source: &Self) -> Result<(), MlxCError> {
365        // SAFETY: Both handles are live and this call only replaces the
366        // owned closure value.
367        let status = unsafe { raw::mlx_closure_value_and_grad_set(&mut self.0, source.0) };
368        check_status(status, "copy an MLX value-and-gradient closure")
369    }
370
371    /// Applies the closure, producing both the outputs and the gradients.
372    ///
373    /// # Errors
374    /// Returns the captured MLX-C description when application fails.
375    pub fn apply(
376        &self,
377        input: &MlxArrayVector,
378    ) -> Result<(MlxArrayVector, MlxArrayVector), MlxCError> {
379        let operation: &'static str = "apply an MLX value-and-gradient closure";
380        let mut outputs = MlxArrayVector::empty(operation)?;
381        let mut gradients = MlxArrayVector::empty(operation)?;
382        // SAFETY: All handles are live and both output pointers are valid
383        // writable storage.
384        let status = unsafe {
385            raw::mlx_closure_value_and_grad_apply(
386                outputs.raw_mut(),
387                gradients.raw_mut(),
388                self.0,
389                input.raw(),
390            )
391        };
392        check_status(status, operation)?;
393        Ok((outputs, gradients))
394    }
395}
396
397impl MlxClosureValueAndGrad {
398    pub(crate) fn raw_mut(&mut self) -> *mut raw::mlx_closure_value_and_grad {
399        &mut self.0
400    }
401}
402
403impl Drop for MlxClosureValueAndGrad {
404    fn drop(&mut self) {
405        // SAFETY: This owner releases its live closure exactly once and
406        // never accesses the handle afterward.
407        unsafe {
408            raw::mlx_closure_value_and_grad_free(self.0);
409        }
410    }
411}
412
413unsafe extern "C" fn value_and_grad_callback_trampoline<F>(
414    outputs: *mut raw::mlx_vector_array,
415    gradients: *mut raw::mlx_vector_array,
416    input: raw::mlx_vector_array,
417    payload: *mut c_void,
418) -> i32
419where
420    F: FnMut(&[MlxArray]) -> Result<(Vec<MlxArray>, Vec<MlxArray>), MlxCError> + 'static,
421{
422    let Some(result) = run_payload_closure::<F, Result<(Vec<MlxArray>, Vec<MlxArray>), MlxCError>>(
423        payload,
424        "apply a Rust value-and-gradient closure",
425        |callback| owned_input_arrays(input).and_then(|inputs| callback(&inputs)),
426    ) else {
427        return 1;
428    };
429    let outcome = result.and_then(|(owned_outputs, owned_gradients)| {
430        write_output_arrays(outputs, owned_outputs)?;
431        write_output_arrays(gradients, owned_gradients)
432    });
433    match outcome {
434        Ok(()) => 0,
435        Err(failure) => {
436            crate::error::set_closure_error(failure);
437            1
438        }
439    }
440}