Skip to main content

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,
}
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
P: AsRef<Path>
required
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)
}

Reading ELF Headers

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"\x7FELF" {
        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

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 = 0x10000u64;
    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);
    }
}

Performance Tips

1

Use memory mapping for large files

Files larger than a few MB benefit significantly from memory mapping vs. read().
2

Sequential access is optimal

The OS prefetcher works best with sequential reads. Random access may cause page faults.
3

Keep mappings alive

Don’t drop the File object while you need access to the data. The mapping is unmapped on drop.
4

Use slices for zero-copy parsing

Pass &file[offset..offset+size] to parsers instead of copying data.

See Also

Build docs developers (and LLMs) love