Skip to main content

mlx_c_rust/runtime/
export.rs

1//! The MLX export and graph-utilities domain: function export/import and
2//! graph rendering through the node namer.
3
4use std::os::raw::c_void;
5
6use crate::error::check_status;
7use crate::raw;
8use crate::types::closures::MlxClosure;
9use crate::types::closures::MlxClosureKwargs;
10use crate::types::maps::MlxMapStringToArray;
11use crate::{MlxArray, MlxArrayVector, MlxCError};
12
13/// A host path argument translated into a C string.
14fn path_argument(path: &str, operation: &'static str) -> Result<std::ffi::CString, MlxCError> {
15    std::ffi::CString::new(path).map_err(|_| MlxCError {
16        operation,
17        description: "path contains an interior null byte".to_owned(),
18    })
19}
20
21/// Exports a closure and example arguments to an MLX function file.
22///
23/// # Errors
24/// Returns the captured MLX-C description when export fails.
25pub fn export_function(
26    path: &str,
27    function: &MlxClosure,
28    example_arguments: &MlxArrayVector,
29    shapeless: bool,
30) -> Result<(), MlxCError> {
31    let operation: &'static str = "export an MLX function";
32    let path_value = path_argument(path, operation)?;
33    // SAFETY: The path remains valid for the call and both handles are live.
34    let status = unsafe {
35        raw::mlx_export_function(
36            path_value.as_ptr(),
37            function.raw(),
38            example_arguments.raw(),
39            shapeless,
40        )
41    };
42    check_status(status, operation)
43}
44
45/// Exports a kwargs closure and example arguments to an MLX function file.
46///
47/// # Errors
48/// Returns the captured MLX-C description when export fails.
49pub fn export_function_with_keywords(
50    path: &str,
51    function: &MlxClosureKwargs,
52    example_arguments: &MlxArrayVector,
53    keyword_arguments: &MlxMapStringToArray,
54    shapeless: bool,
55) -> Result<(), MlxCError> {
56    let operation: &'static str = "export an MLX function with keywords";
57    let path_value = path_argument(path, operation)?;
58    // SAFETY: The path remains valid for the call and all handles are live.
59    let status = unsafe {
60        raw::mlx_export_function_kwargs(
61            path_value.as_ptr(),
62            function.raw(),
63            example_arguments.raw(),
64            keyword_arguments.raw(),
65            shapeless,
66        )
67    };
68    check_status(status, operation)
69}
70
71/// Owned MLX function exporter released exactly once.
72#[derive(Debug)]
73pub struct MlxFunctionExporter(raw::mlx_function_exporter);
74
75impl MlxFunctionExporter {
76    /// Builds an exporter that writes new function versions into the file.
77    ///
78    /// # Errors
79    /// Returns the captured MLX-C description when creation fails.
80    pub fn new(path: &str, function: &MlxClosure, shapeless: bool) -> Result<Self, MlxCError> {
81        let operation: &'static str = "create an MLX function exporter";
82        let path_value = path_argument(path, operation)?;
83        // SAFETY: The path remains valid for the call, the closure handle is
84        // live, and the returned handle enters RAII ownership.
85        let raw_exporter = unsafe {
86            raw::mlx_function_exporter_new(path_value.as_ptr(), function.raw(), shapeless)
87        };
88        if raw_exporter.ctx.is_null() {
89            return Err(MlxCError {
90                operation,
91                description: "MLX returned an empty function exporter handle".to_owned(),
92            });
93        }
94        Ok(Self(raw_exporter))
95    }
96
97    /// Writes one compiled specialization for the arguments.
98    ///
99    /// # Errors
100    /// Returns the captured MLX-C description when the specialization fails.
101    pub fn apply(&self, arguments: &MlxArrayVector) -> Result<(), MlxCError> {
102        let operation: &'static str = "specialize an MLX function exporter";
103        // SAFETY: Both handles are live for the duration of the call.
104        let status = unsafe { raw::mlx_function_exporter_apply(self.0, arguments.raw()) };
105        check_status(status, operation)
106    }
107
108    /// Writes one compiled specialization for the arguments and keywords.
109    ///
110    /// # Errors
111    /// Returns the captured MLX-C description when the specialization fails.
112    pub fn apply_with_keywords(
113        &self,
114        arguments: &MlxArrayVector,
115        keyword_arguments: &MlxMapStringToArray,
116    ) -> Result<(), MlxCError> {
117        let operation: &'static str = "specialize an MLX function exporter with keywords";
118        // SAFETY: All handles are live for the duration of the call.
119        let status = unsafe {
120            raw::mlx_function_exporter_apply_kwargs(
121                self.0,
122                arguments.raw(),
123                keyword_arguments.raw(),
124            )
125        };
126        check_status(status, operation)
127    }
128}
129
130impl Drop for MlxFunctionExporter {
131    fn drop(&mut self) {
132        // SAFETY: This owner releases its live exporter exactly once and
133        // never accesses the handle afterward.
134        unsafe {
135            raw::mlx_function_exporter_free(self.0);
136        }
137    }
138}
139
140/// Owned MLX imported function released exactly once.
141#[derive(Debug)]
142pub struct MlxImportedFunction(raw::mlx_imported_function);
143
144impl MlxImportedFunction {
145    /// Loads a previously exported MLX function file.
146    ///
147    /// # Errors
148    /// Returns the captured MLX-C description when loading fails.
149    pub fn load(path: &str) -> Result<Self, MlxCError> {
150        let operation: &'static str = "import an MLX function";
151        let path_value = path_argument(path, operation)?;
152        // SAFETY: The path remains valid for the call and the returned
153        // handle enters RAII ownership.
154        let raw_function = unsafe { raw::mlx_imported_function_new(path_value.as_ptr()) };
155        if raw_function.ctx.is_null() {
156            return Err(MlxCError {
157                operation,
158                description: "MLX returned an empty imported function handle".to_owned(),
159            });
160        }
161        Ok(Self(raw_function))
162    }
163
164    /// Applies the imported function to the arguments.
165    ///
166    /// # Errors
167    /// Returns the captured MLX-C description when application fails.
168    pub fn apply(&self, arguments: &MlxArrayVector) -> Result<MlxArrayVector, MlxCError> {
169        let operation: &'static str = "apply an imported MLX function";
170        let mut output = MlxArrayVector::empty(operation)?;
171        // SAFETY: Both handles are live and the output pointer is valid
172        // writable storage.
173        let status =
174            unsafe { raw::mlx_imported_function_apply(output.raw_mut(), self.0, arguments.raw()) };
175        check_status(status, operation)?;
176        Ok(output)
177    }
178
179    /// Applies the imported function to the arguments and keywords.
180    ///
181    /// # Errors
182    /// Returns the captured MLX-C description when application fails.
183    pub fn apply_with_keywords(
184        &self,
185        arguments: &MlxArrayVector,
186        keyword_arguments: &MlxMapStringToArray,
187    ) -> Result<MlxArrayVector, MlxCError> {
188        let operation: &'static str = "apply an imported MLX function with keywords";
189        let mut output = MlxArrayVector::empty(operation)?;
190        // SAFETY: All handles are live and the output pointer is valid
191        // writable storage.
192        let status = unsafe {
193            raw::mlx_imported_function_apply_kwargs(
194                output.raw_mut(),
195                self.0,
196                arguments.raw(),
197                keyword_arguments.raw(),
198            )
199        };
200        check_status(status, operation)?;
201        Ok(output)
202    }
203}
204
205impl Drop for MlxImportedFunction {
206    fn drop(&mut self) {
207        // SAFETY: This owner releases its live imported function exactly
208        // once and never accesses the handle afterward.
209        unsafe {
210            raw::mlx_imported_function_free(self.0);
211        }
212    }
213}
214
215/// Owned MLX node namer released exactly once.
216#[derive(Debug)]
217pub struct MlxNodeNamer(raw::mlx_node_namer);
218
219impl MlxNodeNamer {
220    /// Creates an empty namer.
221    pub fn new() -> Result<Self, MlxCError> {
222        let operation: &'static str = "create an MLX node namer";
223        // SAFETY: The returned handle enters RAII ownership immediately.
224        let raw_namer = unsafe { raw::mlx_node_namer_new() };
225        if raw_namer.ctx.is_null() {
226            return Err(MlxCError {
227                operation,
228                description: "MLX returned an empty node namer handle".to_owned(),
229            });
230        }
231        Ok(Self(raw_namer))
232    }
233
234    /// Names one array node.
235    pub fn set_name(&mut self, array: &MlxArray, name: &str) -> Result<(), MlxCError> {
236        let operation: &'static str = "name an MLX graph node";
237        let name_argument = path_argument(name, operation)?;
238        // SAFETY: The namer and array handles are live and the name outlives
239        // the call.
240        let status =
241            unsafe { raw::mlx_node_namer_set_name(self.0, array.raw(), name_argument.as_ptr()) };
242        check_status(status, operation)
243    }
244
245    pub(crate) const fn raw(&self) -> raw::mlx_node_namer {
246        self.0
247    }
248
249    /// Copies the name recorded for the array node.
250    pub fn name_of(&self, array: &MlxArray) -> Result<String, MlxCError> {
251        let operation: &'static str = "read an MLX graph node name";
252        let mut name_pointer: *const std::os::raw::c_char = std::ptr::null();
253        // SAFETY: The namer and array handles are live and the output
254        // pointer is valid writable storage.
255        let status =
256            unsafe { raw::mlx_node_namer_get_name(&mut name_pointer, self.0, array.raw()) };
257        check_status(status, operation)?;
258        if name_pointer.is_null() {
259            return Err(MlxCError {
260                operation,
261                description: "MLX returned a null node name".to_owned(),
262            });
263        }
264        // SAFETY: MLX loans the null-terminated name from the live namer;
265        // the text is copied before this call returns.
266        let borrowed = unsafe { std::ffi::CStr::from_ptr(name_pointer) };
267        Ok(borrowed.to_string_lossy().into_owned())
268    }
269}
270
271impl Drop for MlxNodeNamer {
272    fn drop(&mut self) {
273        // SAFETY: This owner releases its live namer exactly once and never
274        // accesses the handle afterward.
275        unsafe {
276            raw::mlx_node_namer_free(self.0);
277        }
278    }
279}
280
281/// A rendered-graph destination backed by a C `FILE*` the caller owns for
282/// the duration of one rendering call.
283///
284/// # Safety
285/// The pointer must be a valid open `FILE` for the duration of the render
286/// call and must not be closed while rendering.
287pub struct GraphOutputFile(pub *mut c_void);
288
289/// Writes the graph behind the outputs to a Graphviz DOT file.
290///
291/// # Safety
292/// The output file must be a valid open `FILE` for the duration of the call.
293///
294/// # Errors
295/// Returns the captured MLX-C description when rendering fails.
296pub unsafe fn export_graph_to_dot(
297    output_file: GraphOutputFile,
298    namer: &MlxNodeNamer,
299    outputs: &MlxArrayVector,
300) -> Result<(), MlxCError> {
301    let operation: &'static str = "export an MLX graph to DOT";
302    // SAFETY: The caller upholds the FILE lifetime contract for the call.
303    let status =
304        unsafe { raw::mlx_export_to_dot(output_file.0.cast(), namer.raw(), outputs.raw()) };
305    check_status(status, operation)
306}
307
308/// Prints the graph behind the outputs as text.
309///
310/// # Safety
311/// The output file must be a valid open `FILE` for the duration of the call.
312///
313/// # Errors
314/// Returns the captured MLX-C description when printing fails.
315pub unsafe fn print_graph(
316    output_file: GraphOutputFile,
317    namer: &MlxNodeNamer,
318    outputs: &MlxArrayVector,
319) -> Result<(), MlxCError> {
320    let operation: &'static str = "print an MLX graph";
321    // SAFETY: The caller upholds the FILE lifetime contract for the call.
322    let status = unsafe { raw::mlx_print_graph(output_file.0.cast(), namer.raw(), outputs.raw()) };
323    check_status(status, operation)
324}