Skip to main content

russimp_ng/
fs.rs

1//! The `fs` module contains functionality for interfacing custom resource loading.
2//!
3//! Implement the FileSystem trait for your custom resource loading, with its open() method returning
4//! objects satisfying the FileOperations trait.
5use russimp_sys_ng::{aiFile, aiFileIO, aiOrigin, aiReturn};
6use std::ffi::CStr;
7use std::io::SeekFrom;
8use std::os::raw::c_char;
9
10/// Implement FileSystem to use custom resource loading using `Scene::from_filesystem()`.
11///
12/// Rusty version of the underlying aiFileIO type.
13pub trait FileSystem {
14    fn open(&self, file_path: &str, mode: &str) -> Option<Box<dyn FileOperations>>;
15}
16
17/// Implement this for a given resource to support custom resource loading.
18///
19/// This trait class is the rusty version of the underlying aiFile type.
20pub trait FileOperations {
21    /// Should return the number of bytes read, or Err if read unsuccessful.
22    fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()>;
23    /// Should return the number of bytes written, or Err if write unsuccessful.
24    fn write(&mut self, buf: &[u8]) -> Result<usize, ()>;
25    fn tell(&mut self) -> usize;
26    fn size(&mut self) -> usize;
27    fn seek(&mut self, seek_from: SeekFrom) -> Result<(), ()>;
28    fn flush(&mut self);
29    fn close(&mut self);
30}
31
32/// This type allows us to generate C stubs for whatever trait object the user supplies.
33pub(crate) struct FileOperationsWrapper<T: FileSystem> {
34    ai_file: aiFileIO,
35    _phantom: std::marker::PhantomData<T>,
36}
37
38impl<T: FileSystem> FileOperationsWrapper<T> {
39    /// Returns a wrapper that can create an aiFileIO to be used with the assimp C-API.
40    pub fn new(file_system: &T) -> FileOperationsWrapper<T> {
41        let trait_obj: &dyn FileSystem = file_system;
42        let managed_box = Box::new(trait_obj);
43        let user_data = Box::into_raw(managed_box);
44        let user_data = user_data as *mut c_char;
45        FileOperationsWrapper {
46            ai_file: aiFileIO {
47                OpenProc: Some(FileOperationsWrapper::<T>::io_open),
48                CloseProc: Some(FileOperationsWrapper::<T>::io_close),
49                UserData: user_data,
50            },
51            _phantom: Default::default(),
52        }
53    }
54    /// Get the aiFileIO to pass to the C-interface.
55    pub fn ai_file(&mut self) -> &mut aiFileIO {
56        &mut self.ai_file
57    }
58    /// Implementation for aiFileIO::OpenProc.
59    unsafe extern "C" fn io_open(
60        ai_file_io: *mut aiFileIO,
61        file_path: *const ::std::os::raw::c_char,
62        mode: *const ::std::os::raw::c_char,
63    ) -> *mut aiFile {
64        let file_system = Box::leak(Box::from_raw(
65            (*ai_file_io).UserData as *mut &dyn FileSystem,
66        ));
67
68        let file_path = CStr::from_ptr(file_path)
69            .to_str()
70            .unwrap_or("Invalid UTF-8 Filename");
71        let mode = CStr::from_ptr(mode)
72            .to_str()
73            .unwrap_or("Invalid UTF-8 Mode");
74        let file = match file_system.open(file_path, mode) {
75            None => return std::ptr::null_mut(),
76            Some(file) => file,
77        };
78
79        // Take the returned file, and double box it here so that it can be converted to a single
80        // raw pointer that can be stuffed in the UserData.
81        let double_box = Box::new(file);
82        let managed_box = Box::into_raw(double_box); // Cleaned up in io_close.
83        let user_data = managed_box as *mut c_char;
84        let ai_file = aiFile {
85            ReadProc: Some(Self::io_read),
86            WriteProc: Some(Self::io_write),
87            TellProc: Some(Self::io_tell),
88            FileSizeProc: Some(Self::io_size),
89            SeekProc: Some(Self::io_seek),
90            FlushProc: Some(Self::io_flush),
91            UserData: user_data,
92        };
93        // Lifetime of ai_file is managed by backend assimp library, cleaned up in io_close().
94        Box::into_raw(Box::new(ai_file))
95    }
96
97    /// Implementation for aiFileIO::CloseProc.
98    unsafe extern "C" fn io_close(_ai_file_io: *mut aiFileIO, ai_file: *mut aiFile) {
99        // Given that this is close, we are careful to not leak, but instead drop the file when we
100        // exit this scope.
101        let ai_file = Box::from_raw(ai_file);
102        let mut file: Box<Box<dyn FileOperations>> =
103            Box::from_raw(ai_file.UserData as *mut Box<dyn FileOperations>);
104        file.close();
105    }
106    /// Turn an aiFile pointer into a the "self" object.
107    ///
108    /// Safety: Only safe to call once from within each of the io_* callbacks. This assumes that
109    /// the loading library has ownership of the aiFile object that was returned by the FileSystem.
110    /// It is expected to only be called serially on a single thread for the lifetype 'a, which
111    /// *should* keep access scoped to within the io_* callback.
112    unsafe fn get_file<'a>(ai_file: *mut aiFile) -> &'a mut Box<dyn FileOperations> {
113        // We return a "leaked" pointer here, using the saved off double box pointer that we
114        // stuffed in the UserData. We "leak" as we don't acutally want to return ownership, only
115        // the mutable reference. The box is manually cleaned up as part of io_close.
116        Box::leak(Box::from_raw(
117            (*ai_file).UserData as *mut Box<dyn FileOperations>,
118        ))
119    }
120    // Implementation for aiFile::ReadProc.
121    unsafe extern "C" fn io_read(
122        ai_file: *mut aiFile,
123        buffer: *mut c_char,
124        size: usize,
125        count: usize,
126    ) -> usize {
127        let file = Self::get_file(ai_file);
128        let mut buffer =
129            std::slice::from_raw_parts_mut(buffer as *mut u8, size * count);
130        if size == 0 {
131            panic!("Size 0 is invalid");
132        }
133        if count == 0 {
134            panic!("Count 0 is invalid");
135        }
136        if size == usize::MAX {
137            panic!("huge read size not supported");
138        }
139        if size == 1 {
140            // This looks like a memcpy.
141            if count == usize::MAX {
142                panic!("huge read not supported");
143            }
144
145            let (buffer, _) = buffer.split_at_mut(count);
146            match file.read(buffer) {
147                Ok(size) => size,
148                Err(_) => usize::MAX,
149            }
150        } else {
151            // We have to copy in strides. Implement this by looping for each object and tally the
152            // count of full objects read.
153            let mut total: usize = 0;
154            for _ in 0..count {
155                let split = buffer.split_at_mut(size);
156                buffer = split.1;
157                let bytes_read = match file.read(split.0) {
158                    Err(_) => break,
159                    Ok(bytes_read) => bytes_read,
160                };
161                if bytes_read != size {
162                    break;
163                }
164                total += 1;
165            }
166            total
167        }
168    }
169    // Implementation for aiFile::WriteProc.
170    unsafe extern "C" fn io_write(
171        ai_file: *mut aiFile,
172        buffer: *const std::os::raw::c_char,
173        size: usize,
174        count: usize,
175    ) -> usize {
176        let file = Self::get_file(ai_file);
177        let mut buffer =
178            std::slice::from_raw_parts(buffer as *mut u8, size * count);
179        if size == 0 {
180            panic!("Write of size 0");
181        }
182        if count == 0 {
183            panic!("Write of count 0");
184        }
185        if size == usize::MAX {
186            panic!("huge write size not supported");
187        }
188        if size == 1 {
189            if count == usize::MAX {
190                panic!("huge write not supported");
191            }
192            let (buffer, _) = buffer.split_at(count);
193            match file.write(buffer) {
194                Ok(size) => size,
195                Err(_) => usize::MAX,
196            }
197        } else {
198            // Write in strides. Implement this by looping for each object and tally the
199            // count of full objects written.
200            let mut total: usize = 0;
201            for _ in 0..count {
202                let split = buffer.split_at(size);
203                buffer = split.1;
204                let bytes_written = match file.write(split.0) {
205                    Err(_) => break,
206                    Ok(bytes_written) => bytes_written,
207                };
208                if bytes_written != size {
209                    break;
210                }
211                total += 1;
212            }
213            total
214        }
215    }
216    // Implementation for aiFile::TellProc.
217    unsafe extern "C" fn io_tell(ai_file: *mut aiFile) -> usize {
218        let file = Self::get_file(ai_file);
219        file.tell()
220    }
221    // Implementation for aiFile::FileSizeProc.
222    unsafe extern "C" fn io_size(ai_file: *mut aiFile) -> usize {
223        let file = Self::get_file(ai_file);
224        file.size()
225    }
226    // Implementation for aiFile::SeekProc.
227    unsafe extern "C" fn io_seek(ai_file: *mut aiFile, pos: usize, origin: aiOrigin) -> aiReturn {
228        let file = Self::get_file(ai_file);
229        let seek_from = match origin {
230            russimp_sys_ng::aiOrigin_aiOrigin_SET => SeekFrom::Start(pos as u64),
231            russimp_sys_ng::aiOrigin_aiOrigin_CUR => SeekFrom::Current(pos as i64),
232            russimp_sys_ng::aiOrigin_aiOrigin_END => SeekFrom::End(pos as i64),
233            _ => panic!("Assimp passed invalid origin"),
234        };
235        match file.seek(seek_from) {
236            Ok(()) => 0,
237            Err(()) => russimp_sys_ng::aiReturn_aiReturn_FAILURE,
238        }
239    }
240    // Implementation for aiFile::FlushProc.
241    unsafe extern "C" fn io_flush(ai_file: *mut aiFile) {
242        let file = Self::get_file(ai_file);
243        file.flush();
244    }
245}
246
247impl<T: FileSystem> Drop for FileOperationsWrapper<T> {
248    fn drop(&mut self) {
249        // Re-construct and drop the box that was used for the C-API.
250        let _managed_box: Box<&dyn FileSystem> =
251            unsafe { Box::from_raw(self.ai_file.UserData as *mut &dyn FileSystem) };
252    }
253}
254
255#[cfg(test)]
256mod test {
257    use crate::scene::PostProcess;
258    use crate::scene::Scene;
259    use crate::utils;
260    use std::fs::File;
261    use std::io::{prelude::*, SeekFrom};
262
263    struct MyFileOperations {
264        file: File,
265    }
266
267    impl super::FileOperations for MyFileOperations {
268        fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()> {
269            self.file.read(buf).map_err(|_| ())
270        }
271
272        fn write(&mut self, _buf: &[u8]) -> Result<usize, ()> {
273            unimplemented!("write support");
274        }
275
276        fn tell(&mut self) -> usize {
277            self.file
278                .stream_position()
279                .unwrap_or(0)
280                .try_into()
281                .unwrap_or(0)
282        }
283
284        fn size(&mut self) -> usize {
285            self.file
286                .metadata()
287                .expect("Missing metadata")
288                .len()
289                .try_into()
290                .unwrap_or(0)
291        }
292
293        fn seek(&mut self, seek_from: SeekFrom) -> Result<(), ()> {
294            match self.file.seek(seek_from) {
295                Ok(_) => Ok(()),
296                Err(_) => Err(()),
297            }
298        }
299
300        fn flush(&mut self) {
301            // write suppot not implemented.
302        }
303
304        fn close(&mut self) {
305            // Nothing to do.
306        }
307    }
308
309    struct MyFS {}
310
311    impl super::FileSystem for MyFS {
312        fn open(&self, file_path: &str, mode: &str) -> Option<Box<dyn super::FileOperations>> {
313            // We only support reading for this test.
314            assert_eq!(mode, "rb");
315            let file = File::open(file_path).expect("Couldn't open {file_path}");
316            Some(Box::new(MyFileOperations { file }))
317        }
318    }
319
320    #[test]
321    fn test_file_operations() {
322        // Load the cube.obj as it also has to load the cube.mtl through the filesystem to get the
323        // materials right.
324        let model_path = utils::get_model("models/OBJ/cube.obj");
325        let mut myfs = MyFS {};
326        let scene = Scene::from_file_system(
327            model_path.as_str(),
328            vec![
329                PostProcess::CalculateTangentSpace,
330                PostProcess::Triangulate,
331                PostProcess::JoinIdenticalVertices,
332                PostProcess::SortByPrimitiveType,
333            ],
334            &mut myfs,
335        )
336        .unwrap();
337
338        assert_eq!(scene.meshes[0].texture_coords.len(), 8);
339        assert_eq!(scene.materials.len(), 2);
340    }
341}