Overview
The filesystem module provides high-performance file access using memory-mapped I/O. This is essential for loading game ROMs, executable code, and assets efficiently.
File
Memory-mapped file wrapper that provides zero-copy file access.
Structure
pub struct File {
map : Mmap ,
}
Memory-mapped file handle (from memmap2 crate)
Methods
open()
Open a file and create a memory mapping.
pub fn open < P : AsRef < Path >>( path : P ) -> Result < Self , std :: io :: Error >
Path to the file to open (can be a string, Path, or PathBuf)
return
Result<File, std::io::Error>
Returns Ok(File) on success, or an io::Error if the file cannot be opened or mapped
Example:
use oboromi_core :: fs :: File ;
let file = File :: open ( "game.nro" )
. expect ( "Failed to open ROM file" );
println! ( "File size: {} bytes" , file . len ());
Deref Implementation
The File struct implements Deref to [u8], allowing direct access to file contents as a byte slice.
impl Deref for File {
type Target = [ u8 ];
fn deref ( & self ) -> & Self :: Target {
& self . map
}
}
This means you can treat a File like a byte slice:
Example:
let file = File :: open ( "shader.bin" ) ? ;
// Access file contents directly
let first_byte = file [ 0 ];
let first_four_bytes = & file [ 0 .. 4 ];
let file_size = file . len ();
// Iterate over bytes
for byte in file . iter () {
// Process byte
}
// Read structured data
if file . len () >= 4 {
let magic = u32 :: from_le_bytes ([ file [ 0 ], file [ 1 ], file [ 2 ], file [ 3 ]]);
println! ( "Magic: {:#010x}" , magic );
}
Memory Mapping Benefits
Zero-Copy I/O Files are mapped directly into process memory. No explicit read operations or buffers needed.
Lazy Loading The OS loads pages on-demand. Only accessed portions of the file consume physical memory.
Kernel Optimization The OS can cache and optimize memory-mapped files across multiple processes.
Large File Support Can handle files larger than available RAM efficiently.
Safety Considerations
The File::open method uses unsafe internally to create the memory mapping: let file = fs :: File :: open ( path ) ? ;
let map = unsafe { Mmap :: map ( & file ) ? };
This is safe because:
The file is opened read-only
The memory mapping is immutable
The file handle is kept alive while the mapping exists
Common Use Cases
Loading Game ROMs
use oboromi_core :: fs :: File ;
fn load_rom ( path : & str ) -> Result < File , std :: io :: Error > {
let rom = File :: open ( path ) ? ;
// Verify ROM header
if rom . len () < 0x100 {
return Err ( std :: io :: Error :: new (
std :: io :: ErrorKind :: InvalidData ,
"ROM file too small"
));
}
// Check magic bytes (NRO format)
let magic = u32 :: from_le_bytes ([ rom [ 0x10 ], rom [ 0x11 ], rom [ 0x12 ], rom [ 0x13 ]]);
if magic != 0x304F524E { // "NRO0"
return Err ( std :: io :: Error :: new (
std :: io :: ErrorKind :: InvalidData ,
"Invalid NRO magic"
));
}
Ok ( rom )
}
use oboromi_core :: fs :: File ;
fn parse_elf_header ( file : & File ) -> Result <(), Box < dyn std :: error :: Error >> {
if file . len () < 64 {
return Err ( "File too small for ELF header" . into ());
}
// ELF magic: 0x7F, 'E', 'L', 'F'
if & file [ 0 .. 4 ] != b" \x7F ELF" {
return Err ( "Not an ELF file" . into ());
}
let class = file [ 4 ]; // 1 = 32-bit, 2 = 64-bit
let endian = file [ 5 ]; // 1 = little, 2 = big
println! ( "ELF Class: {}" , if class == 2 { "64-bit" } else { "32-bit" });
println! ( "Endianness: {}" , if endian == 1 { "little" } else { "big" });
Ok (())
}
Loading Shader Binaries
use oboromi_core :: fs :: File ;
use oboromi_core :: gpu :: State as GpuState ;
fn load_shader ( path : & str , gpu : & mut GpuState ) -> Result <(), Box < dyn std :: error :: Error >> {
let shader_file = File :: open ( path ) ? ;
// Shader is memory-mapped, can be used directly
println! ( "Loaded shader: {} bytes" , shader_file . len ());
// Parse shader instructions
let mut offset = 0 ;
while offset + 8 <= shader_file . len () {
let instruction = u64 :: from_le_bytes ([
shader_file [ offset ],
shader_file [ offset + 1 ],
shader_file [ offset + 2 ],
shader_file [ offset + 3 ],
shader_file [ offset + 4 ],
shader_file [ offset + 5 ],
shader_file [ offset + 6 ],
shader_file [ offset + 7 ],
]);
// Decode SM86 instruction
// ...
offset += 8 ;
}
Ok (())
}
Complete Example
ROM Loader with Memory-Mapped I/O
use oboromi_core :: fs :: File ;
use oboromi_core :: cpu :: UnicornCPU ;
struct NroHeader {
text_offset : u32 ,
text_size : u32 ,
ro_offset : u32 ,
ro_size : u32 ,
data_offset : u32 ,
data_size : u32 ,
bss_size : u32 ,
}
impl NroHeader {
fn parse ( data : & [ u8 ]) -> Result < Self , Box < dyn std :: error :: Error >> {
if data . len () < 0x80 {
return Err ( "File too small for NRO header" . into ());
}
// Parse header fields (simplified)
Ok ( Self {
text_offset : u32 :: from_le_bytes ([ data [ 0x20 ], data [ 0x21 ], data [ 0x22 ], data [ 0x23 ]]),
text_size : u32 :: from_le_bytes ([ data [ 0x24 ], data [ 0x25 ], data [ 0x26 ], data [ 0x27 ]]),
ro_offset : u32 :: from_le_bytes ([ data [ 0x28 ], data [ 0x29 ], data [ 0x2A ], data [ 0x2B ]]),
ro_size : u32 :: from_le_bytes ([ data [ 0x2C ], data [ 0x2D ], data [ 0x2E ], data [ 0x2F ]]),
data_offset : u32 :: from_le_bytes ([ data [ 0x30 ], data [ 0x31 ], data [ 0x32 ], data [ 0x33 ]]),
data_size : u32 :: from_le_bytes ([ data [ 0x34 ], data [ 0x35 ], data [ 0x36 ], data [ 0x37 ]]),
bss_size : u32 :: from_le_bytes ([ data [ 0x38 ], data [ 0x39 ], data [ 0x3A ], data [ 0x3B ]]),
})
}
}
fn load_and_execute_nro ( path : & str ) -> Result <(), Box < dyn std :: error :: Error >> {
// Load ROM with memory mapping
let rom = File :: open ( path ) ? ;
println! ( "Loaded ROM: {} bytes" , rom . len ());
// Parse header
let header = NroHeader :: parse ( & rom ) ? ;
println! ( "Text segment: offset={:#x}, size={:#x}" , header . text_offset, header . text_size);
// Create CPU
let cpu = UnicornCPU :: new () . ok_or ( "Failed to create CPU" ) ? ;
// Load text segment into CPU memory
let text_start = header . text_offset as usize ;
let text_end = text_start + header . text_size as usize ;
let text_data = & rom [ text_start .. text_end ];
let load_addr = 0x10000 u64 ;
for ( i , chunk ) in text_data . chunks ( 4 ) . enumerate () {
if chunk . len () == 4 {
let instruction = u32 :: from_le_bytes ([ chunk [ 0 ], chunk [ 1 ], chunk [ 2 ], chunk [ 3 ]]);
cpu . write_u32 ( load_addr + ( i as u64 * 4 ), instruction );
}
}
// Set up CPU state
cpu . set_pc ( load_addr );
cpu . set_sp ( 0x80000 );
// Execute
println! ( "Starting execution at {:#x}" , load_addr );
let result = cpu . run ();
if result == 1 {
println! ( "Execution completed successfully" );
} else {
println! ( "Execution failed" );
}
Ok (())
}
fn main () {
if let Err ( e ) = load_and_execute_nro ( "homebrew.nro" ) {
eprintln! ( "Error: {}" , e );
}
}
Use memory mapping for large files
Files larger than a few MB benefit significantly from memory mapping vs. read().
Sequential access is optimal
The OS prefetcher works best with sequential reads. Random access may cause page faults.
Keep mappings alive
Don’t drop the File object while you need access to the data. The mapping is unmapped on drop.
Use slices for zero-copy parsing
Pass &file[offset..offset+size] to parsers instead of copying data.
See Also