Skip to main content

mlx_c_rust/runtime/
io.rs

1//! The MLX input/output domain: file loading and saving, safetensors maps,
2//! GGUF containers, and the reader/writer transport types.
3
4use std::os::raw::c_void;
5
6use crate::error::check_status;
7use crate::raw;
8use crate::types::maps::{MlxMapStringToArray, MlxMapStringToString};
9use crate::types::vectors::MlxVectorString;
10use crate::{MlxArray, MlxCError, MlxStream, MlxString};
11
12/// A host path argument translated into a C string.
13fn path_argument(path: &str, operation: &'static str) -> Result<std::ffi::CString, MlxCError> {
14    std::ffi::CString::new(path).map_err(|_| MlxCError {
15        operation,
16        description: "path contains an interior null byte".to_owned(),
17    })
18}
19
20impl MlxArray {
21    /// Loads one array from a file through MLX-C `mlx_load`.
22    ///
23    /// # Errors
24    /// Returns the captured MLX-C description when loading fails.
25    pub fn load_from_file(path: &str, stream: &MlxStream) -> Result<Self, MlxCError> {
26        let operation: &'static str = "load an MLX array from a file";
27        let path_value = path_argument(path, operation)?;
28        let mut output = Self::empty();
29        // SAFETY: The path remains valid for the call, the stream handle is
30        // live, and the output pointer is valid writable storage.
31        let status = unsafe { raw::mlx_load(output.raw_mut(), path_value.as_ptr(), stream.raw()) };
32        check_status(status, operation)?;
33        output.require_populated(operation)?;
34        Ok(output)
35    }
36
37    /// Saves this array to a file through MLX-C `mlx_save`.
38    ///
39    /// # Errors
40    /// Returns the captured MLX-C description when saving fails.
41    pub fn save_to_file(&self, path: &str) -> Result<(), MlxCError> {
42        let operation: &'static str = "save an MLX array to a file";
43        let path_value = path_argument(path, operation)?;
44        // SAFETY: The path remains valid for the call and the array handle
45        // is live.
46        let status = unsafe { raw::mlx_save(path_value.as_ptr(), self.raw()) };
47        check_status(status, operation)
48    }
49}
50
51/// Loads the string-to-array and string-to-string safetensors maps from a
52/// file.
53///
54/// # Errors
55/// Returns the captured MLX-C description when loading fails.
56pub fn load_safetensors(
57    path: &str,
58    stream: &MlxStream,
59) -> Result<(MlxMapStringToArray, MlxMapStringToString), MlxCError> {
60    let operation: &'static str = "load MLX safetensors maps from a file";
61    let path_value = path_argument(path, operation)?;
62    let mut arrays = MlxMapStringToArray::new()?;
63    let mut metadata = MlxMapStringToString::new()?;
64    // SAFETY: The path remains valid for the call and both output pointers
65    // are valid writable storage.
66    let status = unsafe {
67        raw::mlx_load_safetensors(
68            arrays.raw_mut(),
69            metadata.raw_mut(),
70            path_value.as_ptr(),
71            stream.raw(),
72        )
73    };
74    check_status(status, operation)?;
75    Ok((arrays, metadata))
76}
77
78/// Saves the parameter and metadata maps to a safetensors file.
79///
80/// # Errors
81/// Returns the captured MLX-C description when saving fails.
82pub fn save_safetensors(
83    path: &str,
84    arrays: &MlxMapStringToArray,
85    metadata: &MlxMapStringToString,
86) -> Result<(), MlxCError> {
87    let operation: &'static str = "save MLX safetensors maps to a file";
88    let path_value = path_argument(path, operation)?;
89    // SAFETY: The path remains valid for the call and both handles are live.
90    let status =
91        unsafe { raw::mlx_save_safetensors(path_value.as_ptr(), arrays.raw(), metadata.raw()) };
92    check_status(status, operation)
93}
94
95/// Owned MLX reader transport released exactly once.
96///
97/// The descriptor and vtable follow MLX-C's `mlx_io_reader` contract; the
98/// safe surface keeps the pair together so the transport stays valid for its
99/// whole lifetime.
100#[derive(Debug)]
101pub struct MlxIoReader {
102    raw_reader: raw::mlx_io_reader,
103}
104
105impl MlxIoReader {
106    /// Builds an owned reader from a descriptor and vtable following the
107    /// MLX-C io contract.
108    ///
109    /// # Safety
110    /// The descriptor must satisfy the vtable's expectations for the
111    /// reader's whole lifetime, and every vtable slot must be a valid C
112    /// ABI function.
113    pub unsafe fn from_parts(
114        descriptor: *mut c_void,
115        vtable: raw::mlx_io_vtable,
116    ) -> Result<Self, MlxCError> {
117        let operation: &'static str = "create an MLX reader transport";
118        // SAFETY: The caller upholds the descriptor/vtable contract and the
119        // returned handle enters RAII ownership.
120        let raw_reader = unsafe { raw::mlx_io_reader_new(descriptor, vtable) };
121        if raw_reader.ctx.is_null() {
122            return Err(MlxCError {
123                operation,
124                description: "MLX returned an empty reader handle".to_owned(),
125            });
126        }
127        Ok(Self { raw_reader })
128    }
129
130    /// The descriptor this transport was built with.
131    pub fn descriptor(&self) -> Result<*mut c_void, MlxCError> {
132        let operation: &'static str = "read the MLX reader descriptor";
133        let mut descriptor: *mut c_void = std::ptr::null_mut();
134        // SAFETY: `self` owns a live reader and the output pointer is valid
135        // writable storage.
136        let status = unsafe { raw::mlx_io_reader_descriptor(&mut descriptor, self.raw_reader) };
137        check_status(status, operation)?;
138        Ok(descriptor)
139    }
140
141    /// Renders the reader description text.
142    pub fn to_string_lossy(&self) -> Result<String, MlxCError> {
143        let operation: &'static str = "describe an MLX reader transport";
144        let mut output = MlxString::empty();
145        // SAFETY: `self` owns a live reader and the output pointer is valid
146        // writable storage.
147        let status = unsafe { raw::mlx_io_reader_tostring(output.raw_mut(), self.raw_reader) };
148        check_status(status, operation)?;
149        output.require_populated(operation)?;
150        Ok(output.to_string_lossy())
151    }
152
153    pub(crate) const fn raw(&self) -> raw::mlx_io_reader {
154        self.raw_reader
155    }
156}
157
158impl Drop for MlxIoReader {
159    fn drop(&mut self) {
160        // SAFETY: This owner releases its live reader exactly once and never
161        // accesses the handle afterward.
162        unsafe {
163            raw::mlx_io_reader_free(self.raw_reader);
164        }
165    }
166}
167
168/// Owned MLX writer transport released exactly once.
169#[derive(Debug)]
170pub struct MlxIoWriter {
171    raw_writer: raw::mlx_io_writer,
172}
173
174impl MlxIoWriter {
175    /// Builds an owned writer from a descriptor and vtable following the
176    /// MLX-C io contract.
177    ///
178    /// # Safety
179    /// The descriptor must satisfy the vtable's expectations for the
180    /// writer's whole lifetime, and every vtable slot must be a valid C
181    /// ABI function.
182    pub unsafe fn from_parts(
183        descriptor: *mut c_void,
184        vtable: raw::mlx_io_vtable,
185    ) -> Result<Self, MlxCError> {
186        let operation: &'static str = "create an MLX writer transport";
187        // SAFETY: The caller upholds the descriptor/vtable contract and the
188        // returned handle enters RAII ownership.
189        let raw_writer = unsafe { raw::mlx_io_writer_new(descriptor, vtable) };
190        if raw_writer.ctx.is_null() {
191            return Err(MlxCError {
192                operation,
193                description: "MLX returned an empty writer handle".to_owned(),
194            });
195        }
196        Ok(Self { raw_writer })
197    }
198
199    /// The descriptor this transport was built with.
200    pub fn descriptor(&self) -> Result<*mut c_void, MlxCError> {
201        let operation: &'static str = "read the MLX writer descriptor";
202        let mut descriptor: *mut c_void = std::ptr::null_mut();
203        // SAFETY: `self` owns a live writer and the output pointer is valid
204        // writable storage.
205        let status = unsafe { raw::mlx_io_writer_descriptor(&mut descriptor, self.raw_writer) };
206        check_status(status, operation)?;
207        Ok(descriptor)
208    }
209
210    /// Renders the writer description text.
211    pub fn to_string_lossy(&self) -> Result<String, MlxCError> {
212        let operation: &'static str = "describe an MLX writer transport";
213        let mut output = MlxString::empty();
214        // SAFETY: `self` owns a live writer and the output pointer is valid
215        // writable storage.
216        let status = unsafe { raw::mlx_io_writer_tostring(output.raw_mut(), self.raw_writer) };
217        check_status(status, operation)?;
218        output.require_populated(operation)?;
219        Ok(output.to_string_lossy())
220    }
221
222    pub(crate) const fn raw(&self) -> raw::mlx_io_writer {
223        self.raw_writer
224    }
225}
226
227impl Drop for MlxIoWriter {
228    fn drop(&mut self) {
229        // SAFETY: This owner releases its live writer exactly once and never
230        // accesses the handle afterward.
231        unsafe {
232            raw::mlx_io_writer_free(self.raw_writer);
233        }
234    }
235}
236
237/// Loads one array from an open reader transport.
238///
239/// # Errors
240/// Returns the captured MLX-C description when loading fails.
241pub fn load_array_from_reader(
242    reader: &MlxIoReader,
243    stream: &MlxStream,
244) -> Result<MlxArray, MlxCError> {
245    let operation: &'static str = "load an MLX array from a reader";
246    let mut output = MlxArray::empty();
247    // SAFETY: The reader handle is live and the output pointer is valid
248    // writable storage.
249    let status = unsafe { raw::mlx_load_reader(output.raw_mut(), reader.raw(), stream.raw()) };
250    check_status(status, operation)?;
251    output.require_populated(operation)?;
252    Ok(output)
253}
254
255/// Loads the safetensors maps from an open reader transport.
256///
257/// # Errors
258/// Returns the captured MLX-C description when loading fails.
259pub fn load_safetensors_from_reader(
260    reader: &MlxIoReader,
261    stream: &MlxStream,
262) -> Result<(MlxMapStringToArray, MlxMapStringToString), MlxCError> {
263    let operation: &'static str = "load MLX safetensors maps from a reader";
264    let mut arrays = MlxMapStringToArray::new()?;
265    let mut metadata = MlxMapStringToString::new()?;
266    // SAFETY: The reader handle is live and both output pointers are valid
267    // writable storage.
268    let status = unsafe {
269        raw::mlx_load_safetensors_reader(
270            arrays.raw_mut(),
271            metadata.raw_mut(),
272            reader.raw(),
273            stream.raw(),
274        )
275    };
276    check_status(status, operation)?;
277    Ok((arrays, metadata))
278}
279
280/// Saves one array through an open writer transport.
281///
282/// # Errors
283/// Returns the captured MLX-C description when saving fails.
284pub fn save_array_to_writer(writer: &MlxIoWriter, array: &MlxArray) -> Result<(), MlxCError> {
285    let operation: &'static str = "save an MLX array through a writer";
286    // SAFETY: Both handles are live for the duration of the call.
287    let status = unsafe { raw::mlx_save_writer(writer.raw(), array.raw()) };
288    check_status(status, operation)
289}
290
291/// Saves the parameter and metadata maps through an open writer transport.
292///
293/// # Errors
294/// Returns the captured MLX-C description when saving fails.
295pub fn save_safetensors_to_writer(
296    writer: &MlxIoWriter,
297    arrays: &MlxMapStringToArray,
298    metadata: &MlxMapStringToString,
299) -> Result<(), MlxCError> {
300    let operation: &'static str = "save MLX safetensors maps through a writer";
301    // SAFETY: All handles are live for the duration of the call.
302    let status =
303        unsafe { raw::mlx_save_safetensors_writer(writer.raw(), arrays.raw(), metadata.raw()) };
304    check_status(status, operation)
305}
306
307/// Owned MLX GGUF container handle released exactly once.
308#[derive(Debug)]
309pub struct MlxIoGguf(raw::mlx_io_gguf);
310
311impl MlxIoGguf {
312    /// Creates an empty GGUF container.
313    pub fn new() -> Result<Self, MlxCError> {
314        let operation: &'static str = "create an MLX GGUF container";
315        // SAFETY: The returned handle enters RAII ownership immediately.
316        let raw_gguf = unsafe { raw::mlx_io_gguf_new() };
317        if raw_gguf.ctx.is_null() {
318            return Err(MlxCError {
319                operation,
320                description: "MLX returned an empty GGUF handle".to_owned(),
321            });
322        }
323        Ok(Self(raw_gguf))
324    }
325
326    pub(crate) fn raw_mut(&mut self) -> *mut raw::mlx_io_gguf {
327        &mut self.0
328    }
329
330    /// Loads a GGUF container from a file.
331    ///
332    /// # Errors
333    /// Returns the captured MLX-C description when loading fails.
334    pub fn load_from_file(path: &str, stream: &MlxStream) -> Result<Self, MlxCError> {
335        let operation: &'static str = "load an MLX GGUF container from a file";
336        let path_value = path_argument(path, operation)?;
337        let mut output = Self::new()?;
338        // SAFETY: The path remains valid for the call, the stream handle is
339        // live, and the output pointer is valid writable storage.
340        let status =
341            unsafe { raw::mlx_load_gguf(output.raw_mut(), path_value.as_ptr(), stream.raw()) };
342        check_status(status, operation)?;
343        Ok(output)
344    }
345
346    /// Saves this GGUF container to a file.
347    ///
348    /// # Errors
349    /// Returns the captured MLX-C description when saving fails.
350    pub fn save_to_file(&self, path: &str) -> Result<(), MlxCError> {
351        let operation: &'static str = "save an MLX GGUF container to a file";
352        let path_value = path_argument(path, operation)?;
353        // SAFETY: The path remains valid for the call and the handle is live.
354        let status = unsafe { raw::mlx_save_gguf(path_value.as_ptr(), self.0) };
355        check_status(status, operation)
356    }
357
358    /// Every array key in the container.
359    pub fn array_keys(&self) -> Result<MlxVectorString, MlxCError> {
360        let operation: &'static str = "list MLX GGUF array keys";
361        let mut output = MlxVectorString::empty();
362        // SAFETY: `self` owns a live container and the output pointer is
363        // valid writable storage.
364        let status = unsafe { raw::mlx_io_gguf_get_keys(output.raw_mut(), self.0) };
365        check_status(status, operation)?;
366        Ok(output)
367    }
368
369    /// Copies the array stored under a key.
370    ///
371    /// # Errors
372    /// Returns the captured MLX-C description when the key is missing.
373    pub fn array_at(&self, key: &str) -> Result<MlxArray, MlxCError> {
374        let operation: &'static str = "read an MLX GGUF array";
375        let key_argument = path_argument(key, operation)?;
376        let mut output = MlxArray::empty();
377        // SAFETY: `self` owns a live container, the key outlives the call,
378        // and the output pointer is valid writable storage.
379        let status =
380            unsafe { raw::mlx_io_gguf_get_array(output.raw_mut(), self.0, key_argument.as_ptr()) };
381        check_status(status, operation)?;
382        output.require_populated(operation)?;
383        Ok(output)
384    }
385
386    /// Copies the metadata array stored under a key.
387    ///
388    /// # Errors
389    /// Returns the captured MLX-C description when the key is missing.
390    pub fn metadata_array_at(&self, key: &str) -> Result<MlxArray, MlxCError> {
391        let operation: &'static str = "read an MLX GGUF metadata array";
392        let key_argument = path_argument(key, operation)?;
393        let mut output = MlxArray::empty();
394        // SAFETY: `self` owns a live container, the key outlives the call,
395        // and the output pointer is valid writable storage.
396        let status = unsafe {
397            raw::mlx_io_gguf_get_metadata_array(output.raw_mut(), self.0, key_argument.as_ptr())
398        };
399        check_status(status, operation)?;
400        output.require_populated(operation)?;
401        Ok(output)
402    }
403
404    /// Copies the metadata string stored under a key.
405    ///
406    /// # Errors
407    /// Returns the captured MLX-C description when the key is missing.
408    pub fn metadata_string_at(&self, key: &str) -> Result<String, MlxCError> {
409        let operation: &'static str = "read an MLX GGUF metadata string";
410        let key_argument = path_argument(key, operation)?;
411        let mut output = MlxString::empty();
412        // SAFETY: `self` owns a live container, the key outlives the call,
413        // and the output pointer is valid writable storage.
414        let status = unsafe {
415            raw::mlx_io_gguf_get_metadata_string(output.raw_mut(), self.0, key_argument.as_ptr())
416        };
417        check_status(status, operation)?;
418        output.require_populated(operation)?;
419        Ok(output.to_string_lossy())
420    }
421
422    /// Copies the metadata string vector stored under a key.
423    ///
424    /// # Errors
425    /// Returns the captured MLX-C description when the key is missing.
426    pub fn metadata_string_vector_at(&self, key: &str) -> Result<MlxVectorString, MlxCError> {
427        let operation: &'static str = "read an MLX GGUF metadata string vector";
428        let key_argument = path_argument(key, operation)?;
429        let mut output = MlxVectorString::empty();
430        // SAFETY: `self` owns a live container, the key outlives the call,
431        // and the output pointer is valid writable storage.
432        let status = unsafe {
433            raw::mlx_io_gguf_get_metadata_vector_string(
434                output.raw_mut(),
435                self.0,
436                key_argument.as_ptr(),
437            )
438        };
439        check_status(status, operation)?;
440        Ok(output)
441    }
442
443    /// Whether metadata of the requested kind exists under the key.
444    fn has_metadata(
445        &self,
446        key: &str,
447        probe: unsafe extern "C" fn(
448            *mut bool,
449            raw::mlx_io_gguf,
450            *const std::os::raw::c_char,
451        ) -> i32,
452        operation: &'static str,
453    ) -> Result<bool, MlxCError> {
454        let key_argument = path_argument(key, operation)?;
455        let mut exists = false;
456        // SAFETY: `self` owns a live container, the key outlives the call,
457        // and the output pointer is valid writable storage.
458        let status = unsafe { probe(&mut exists, self.0, key_argument.as_ptr()) };
459        check_status(status, operation)?;
460        Ok(exists)
461    }
462
463    /// Whether array metadata exists under the key.
464    pub fn has_metadata_array(&self, key: &str) -> Result<bool, MlxCError> {
465        self.has_metadata(
466            key,
467            raw::mlx_io_gguf_has_metadata_array,
468            "probe MLX GGUF array metadata",
469        )
470    }
471
472    /// Whether string metadata exists under the key.
473    pub fn has_metadata_string(&self, key: &str) -> Result<bool, MlxCError> {
474        self.has_metadata(
475            key,
476            raw::mlx_io_gguf_has_metadata_string,
477            "probe MLX GGUF string metadata",
478        )
479    }
480
481    /// Whether string-vector metadata exists under the key.
482    pub fn has_metadata_string_vector(&self, key: &str) -> Result<bool, MlxCError> {
483        self.has_metadata(
484            key,
485            raw::mlx_io_gguf_has_metadata_vector_string,
486            "probe MLX GGUF string-vector metadata",
487        )
488    }
489
490    /// Stores one array under the key.
491    pub fn set_array(&mut self, key: &str, value: &MlxArray) -> Result<(), MlxCError> {
492        let operation: &'static str = "store an MLX GGUF array";
493        let key_argument = path_argument(key, operation)?;
494        // SAFETY: `self` owns a live container, the key outlives the call,
495        // and the array handle is live.
496        let status =
497            unsafe { raw::mlx_io_gguf_set_array(self.0, key_argument.as_ptr(), value.raw()) };
498        check_status(status, operation)
499    }
500
501    /// Stores one metadata array under the key.
502    pub fn set_metadata_array(&mut self, key: &str, value: &MlxArray) -> Result<(), MlxCError> {
503        let operation: &'static str = "store MLX GGUF metadata array";
504        let key_argument = path_argument(key, operation)?;
505        // SAFETY: `self` owns a live container, the key outlives the call,
506        // and the array handle is live.
507        let status = unsafe {
508            raw::mlx_io_gguf_set_metadata_array(self.0, key_argument.as_ptr(), value.raw())
509        };
510        check_status(status, operation)
511    }
512
513    /// Stores one metadata string under the key.
514    pub fn set_metadata_string(&mut self, key: &str, value: &str) -> Result<(), MlxCError> {
515        let operation: &'static str = "store MLX GGUF metadata string";
516        let key_argument = path_argument(key, operation)?;
517        let value_argument = path_argument(value, operation)?;
518        // SAFETY: `self` owns a live container and both strings outlive the
519        // call.
520        let status = unsafe {
521            raw::mlx_io_gguf_set_metadata_string(
522                self.0,
523                key_argument.as_ptr(),
524                value_argument.as_ptr(),
525            )
526        };
527        check_status(status, operation)
528    }
529
530    /// Stores one metadata string vector under the key.
531    pub fn set_metadata_string_vector(
532        &mut self,
533        key: &str,
534        values: &MlxVectorString,
535    ) -> Result<(), MlxCError> {
536        let operation: &'static str = "store MLX GGUF metadata string vector";
537        let key_argument = path_argument(key, operation)?;
538        // SAFETY: `self` owns a live container, the key outlives the call,
539        // and the vector handle is live.
540        let status = unsafe {
541            raw::mlx_io_gguf_set_metadata_vector_string(self.0, key_argument.as_ptr(), values.raw())
542        };
543        check_status(status, operation)
544    }
545}
546
547impl Drop for MlxIoGguf {
548    fn drop(&mut self) {
549        // SAFETY: This owner releases its live container exactly once and
550        // never accesses the handle afterward.
551        unsafe {
552            raw::mlx_io_gguf_free(self.0);
553        }
554    }
555}