diff --git a/Cargo.toml b/Cargo.toml
index bfd6be9..a3cb50d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ libm = { version = "0.2.7", optional = true }
 wasi = { version = "0.11.0+wasi-snapshot-preview1", default-features = false }
 
 [features]
+std = ["alloc"]
 alloc = []
 derive = ["asr-derive"]
 flags = ["bitflags"]
diff --git a/src/file_format/macho.rs b/src/file_format/macho.rs
new file mode 100644
index 0000000..0f6a7c0
--- /dev/null
+++ b/src/file_format/macho.rs
@@ -0,0 +1,123 @@
+//! Support for parsing MachO files
+
+use crate::{Address, PointerSize, Process};
+
+use core::mem;
+
+// Magic mach-o header constants from:
+// https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
+const MH_MAGIC_32: u32 = 0xfeedface;
+const MH_CIGAM_32: u32 = 0xcefaedfe;
+const MH_MAGIC_64: u32 = 0xfeedfacf;
+const MH_CIGAM_64: u32 = 0xcffaedfe;
+
+struct MachOFormatOffsets {
+    number_of_commands: usize,
+    load_commands: usize,
+    command_size: usize,
+    symbol_table_offset: usize,
+    number_of_symbols: usize,
+    string_table_offset: usize,
+    nlist_value: usize,
+    size_of_nlist_item: usize,
+}
+
+impl MachOFormatOffsets {
+    const fn new() -> Self {
+        // offsets taken from:
+        //  - https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MachOFormatOffsets.cs
+        //  - https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
+        MachOFormatOffsets {
+            number_of_commands: 0x10,
+            load_commands: 0x20,
+            command_size: 0x04,
+            symbol_table_offset: 0x08,
+            number_of_symbols: 0x0c,
+            string_table_offset: 0x10,
+            nlist_value: 0x08,
+            size_of_nlist_item: 0x10,
+        }
+    }
+}
+
+/// Scans the range for a page that begins with MachO Magic
+pub fn scan_macho_page(process: &Process, range: (Address, u64)) -> Option<Address> {
+    const PAGE_SIZE: u64 = 0x1000;
+    let (addr, len) = range;
+    // negation mod PAGE_SIZE
+    let distance_to_page = (PAGE_SIZE - (addr.value() % PAGE_SIZE)) % PAGE_SIZE;
+    // round up to the next multiple of PAGE_SIZE
+    let first_page = addr + distance_to_page;
+    for i in 0..((len - distance_to_page) / PAGE_SIZE) {
+        let a = first_page + (i * PAGE_SIZE);
+        match process.read::<u32>(a) {
+            Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) => {
+                return Some(a);
+            }
+            _ => ()
+        }
+    }
+    None
+}
+
+/// Determines whether a MachO header at the address is 64-bit or 32-bit
+pub fn pointer_size(process: &Process, address: Address) -> Option<PointerSize> {
+    let magic: u32 = process.read(address).ok()?;
+    match magic {
+        MH_MAGIC_64 | MH_CIGAM_64 => Some(PointerSize::Bit64),
+        MH_MAGIC_32 | MH_CIGAM_32 => Some(PointerSize::Bit32),
+        _ => None
+    }
+}
+
+/// Finds the address of a function from a MachO module range and file contents.
+pub fn get_function_address(process: &Process, range: (Address, u64), macho_bytes: &[u8], function_name: &[u8]) -> Option<Address> {
+    let function_offset: u32 = get_function_offset(&macho_bytes, function_name)?;
+    let function_address = scan_macho_page(process, range)? + function_offset;
+    let actual: [u8; 0x100] = process.read(function_address).ok()?;
+    let expected: [u8; 0x100] = slice_read(&macho_bytes, function_offset as usize).ok()?;
+    if actual != expected { return None; }
+    Some(function_address)
+}
+
+/// Finds the offset of a function in the bytes of a MachO file.
+pub fn get_function_offset(macho_bytes: &[u8], function_name: &[u8]) -> Option<u32> {
+    let macho_offsets = MachOFormatOffsets::new();
+    let number_of_commands: u32 = slice_read(macho_bytes, macho_offsets.number_of_commands).ok()?;
+    let function_name_len = function_name.len();
+
+    let mut offset_to_next_command: usize = macho_offsets.load_commands as usize;
+    for _i in 0..number_of_commands {
+        // Check if load command is LC_SYMTAB
+        let next_command: i32 = slice_read(macho_bytes, offset_to_next_command).ok()?;
+        if next_command == 2 {
+            let symbol_table_offset: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.symbol_table_offset).ok()?;
+            let number_of_symbols: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.number_of_symbols).ok()?;
+            let string_table_offset: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.string_table_offset).ok()?;
+
+            for j in 0..(number_of_symbols as usize) {
+                let symbol_name_offset: u32 = slice_read(macho_bytes, symbol_table_offset as usize + (j * macho_offsets.size_of_nlist_item)).ok()?;
+                let string_offset = string_table_offset as usize + symbol_name_offset as usize;
+                let symbol_name: &[u8] = &macho_bytes[string_offset..(string_offset + function_name_len + 1)];
+
+                if symbol_name[function_name_len] == 0 && symbol_name.starts_with(function_name) {
+                    return Some(slice_read(macho_bytes, symbol_table_offset as usize + (j * macho_offsets.size_of_nlist_item) + macho_offsets.nlist_value).ok()?);
+                }
+            }
+
+            break;
+        } else {
+            let command_size: u32 = slice_read(macho_bytes, offset_to_next_command + macho_offsets.command_size).ok()?;
+            offset_to_next_command += command_size as usize;
+        }
+    }
+    None
+}
+
+/// Reads a value of the type specified from the slice at the address
+/// given.
+pub fn slice_read<T: bytemuck::CheckedBitPattern>(slice: &[u8], address: usize) -> Result<T, bytemuck::checked::CheckedCastError> {
+    let size = mem::size_of::<T>();
+    let slice_src = &slice[address..(address + size)];
+    bytemuck::checked::try_from_bytes(slice_src).cloned()
+}
diff --git a/src/file_format/mod.rs b/src/file_format/mod.rs
index 14b8a83..f26597d 100644
--- a/src/file_format/mod.rs
+++ b/src/file_format/mod.rs
@@ -2,3 +2,4 @@
 
 pub mod elf;
 pub mod pe;
+pub mod macho;
diff --git a/src/game_engine/unity/mono.rs b/src/game_engine/unity/mono.rs
index 3e63dc8..e7d0309 100644
--- a/src/game_engine/unity/mono.rs
+++ b/src/game_engine/unity/mono.rs
@@ -5,11 +5,19 @@ use crate::{
     deep_pointer::DeepPointer, file_format::pe, future::retry, signature::Signature,
     string::ArrayCString, Address, Address32, Address64, Error, PointerSize, Process,
 };
+#[cfg(feature = "std")]
+use crate::file_format::macho;
 use core::{array, cell::RefCell, iter};
 
+#[cfg(all(debug_assertions, feature = "alloc"))]
+use alloc::collections::BTreeSet;
+#[cfg(feature = "std")]
+use alloc::vec::Vec;
 #[cfg(feature = "derive")]
 pub use asr_derive::MonoClass as Class;
 use bytemuck::CheckedBitPattern;
+#[cfg(feature = "std")]
+use std::{path::Path, fs::File, io, io::Read};
 
 const CSTR: usize = 128;
 
@@ -36,39 +44,72 @@ impl Module {
     /// correct for this function to work. If you don't know the version in
     /// advance, use [`attach_auto_detect`](Self::attach_auto_detect) instead.
     pub fn attach(process: &Process, version: Version) -> Option<Self> {
-        let module = ["mono.dll", "mono-2.0-bdwgc.dll"]
-            .iter()
-            .find_map(|&name| process.get_module_address(name).ok())?;
-
-        let pointer_size = match pe::MachineType::read(process, module)? {
-            pe::MachineType::X86_64 => PointerSize::Bit64,
-            _ => PointerSize::Bit32,
+        #[allow(unused)]
+        let (module_name, module_range, format) = [
+            ("mono.dll", BinaryFormat::PE),
+            ("mono-2.0-bdwgc.dll", BinaryFormat::PE),
+            #[cfg(feature = "std")]
+            ("libmono.0.dylib", BinaryFormat::MachO),
+            #[cfg(feature = "std")]
+            ("libmonobdwgc-2.0.dylib", BinaryFormat::MachO)
+        ].into_iter()
+            .find_map(|(name, format)| Some((name, process.get_module_range(name).ok()?, format)))?;
+        
+        let module = module_range.0;
+
+        let pointer_size = match format {
+            BinaryFormat::PE => {
+                match pe::MachineType::read(process, module)? {
+                    pe::MachineType::X86_64 => PointerSize::Bit64,
+                    _ => PointerSize::Bit32,
+                }
+            }
+            #[cfg(feature = "std")]
+            BinaryFormat::MachO => macho::pointer_size(process, macho::scan_macho_page(process, module_range)?)?,
+        };
+        let offsets = Offsets::new(version, pointer_size, format)?;
+
+        let mono_assembly_foreach_address = match format {
+            BinaryFormat::PE => {
+                pe::symbols(process, module)
+                    .find(|symbol| {
+                        symbol
+                            .get_name::<25>(process)
+                            .is_ok_and(|name| name.matches("mono_assembly_foreach"))
+                    })?
+                    .address
+            },
+            #[cfg(feature = "std")]
+            BinaryFormat::MachO => {
+                let mono_module_path = process.get_module_path(module_name).ok()?;
+                let mono_module_bytes = file_read_all_bytes(mono_module_path).ok()?;
+                macho::get_function_address(process, module_range, &mono_module_bytes, b"_mono_assembly_foreach")?
+            }
         };
 
-        let offsets = Offsets::new(version, pointer_size)?;
-
-        let root_domain_function_address = pe::symbols(process, module)
-            .find(|symbol| {
-                symbol
-                    .get_name::<25>(process)
-                    .is_ok_and(|name| name.matches("mono_assembly_foreach"))
-            })?
-            .address;
-
-        let assemblies_pointer: Address = match pointer_size {
-            PointerSize::Bit64 => {
-                const SIG_MONO_64: Signature<3> = Signature::new("48 8B 0D");
-                let scan_address: Address = SIG_MONO_64
-                    .scan_process_range(process, (root_domain_function_address, 0x100))?
+        let assemblies_pointer: Address = match (pointer_size, format) {
+            (PointerSize::Bit64, BinaryFormat::PE) => {
+                const SIG_MONO_64_PE: Signature<3> = Signature::new("48 8B 0D");
+                let scan_address: Address = SIG_MONO_64_PE
+                    .scan_process_range(process, (mono_assembly_foreach_address, 0x100))?
                     + 3;
                 scan_address + 0x4 + process.read::<i32>(scan_address).ok()?
-            }
-            PointerSize::Bit32 => {
+            },
+            #[cfg(feature = "std")]
+            (PointerSize::Bit64, BinaryFormat::MachO) => {
+                const SIG_MONO_64_MACHO: Signature<3> = Signature::new("48 8B 3D");
+                // RIP-relative addressing
+                // 3 is the offset to the next thing after the signature
+                let scan_address = SIG_MONO_64_MACHO.scan_process_range(process, (mono_assembly_foreach_address, 0x100))? + 3;
+                // 4 is the offset to the next instruction after relative
+                scan_address + 0x4 + process.read::<i32>(scan_address).ok()?
+            },
+            (PointerSize::Bit32, BinaryFormat::PE) => {
                 const SIG_32_1: Signature<2> = Signature::new("FF 35");
                 const SIG_32_2: Signature<2> = Signature::new("8B 0D");
 
                 let ptr = [SIG_32_1, SIG_32_2].iter().find_map(|sig| {
-                    sig.scan_process_range(process, (root_domain_function_address, 0x100))
+                    sig.scan_process_range(process, (mono_assembly_foreach_address, 0x100))
                 })? + 2;
 
                 process.read::<Address32>(ptr).ok()?.into()
@@ -266,6 +307,8 @@ impl Image {
         };
 
         (0..class_cache_size.unwrap_or_default()).flat_map(move |i| {
+            #[cfg(all(debug_assertions, feature = "alloc"))]
+            let mut seen = BTreeSet::new();
             let mut table = match table_addr {
                 Ok(table_addr) => process
                     .read_pointer(
@@ -277,6 +320,8 @@ impl Image {
             };
 
             iter::from_fn(move || {
+                #[cfg(all(debug_assertions, feature = "alloc"))]
+                if seen.replace(table?).is_some() { panic!("Image classes cycle detected"); }
                 let class = process.read_pointer(table?, module.pointer_size).ok()?;
 
                 table = process
@@ -788,6 +833,13 @@ impl<const CAP: usize> UnityPointer<CAP> {
     }
 }
 
+#[derive(Copy, Clone, PartialEq, Hash, Debug)]
+enum BinaryFormat {
+    PE,
+    #[cfg(feature = "std")]
+    MachO,
+}
+
 struct Offsets {
     monoassembly_aname: u8,
     monoassembly_image: u8,
@@ -811,9 +863,9 @@ struct Offsets {
 }
 
 impl Offsets {
-    const fn new(version: Version, pointer_size: PointerSize) -> Option<&'static Self> {
-        match pointer_size {
-            PointerSize::Bit64 => match version {
+    const fn new(version: Version, pointer_size: PointerSize, format: BinaryFormat) -> Option<&'static Self> {
+        match (pointer_size, format) {
+            (PointerSize::Bit64, BinaryFormat::PE) => match version {
                 Version::V1 => Some(&Self {
                     monoassembly_aname: 0x10,
                     monoassembly_image: 0x58,
@@ -835,25 +887,27 @@ impl Offsets {
                     monovtable_vtable: 0x48,
                     monoclassfieldalignment: 0x20,
                 }),
+                // 64-bit PE V2 matches Unity2019_4_2020_3_x64_PE_Offsets from
+                // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L49
                 Version::V2 => Some(&Self {
                     monoassembly_aname: 0x10,
-                    monoassembly_image: 0x60,
-                    monoimage_class_cache: 0x4C0,
-                    monointernalhashtable_table: 0x20,
-                    monointernalhashtable_size: 0x18,
-                    monoclassdef_next_class_cache: 0x108,
+                    monoassembly_image: 0x60, // AssemblyImage = 0x44 + 0x1c
+                    monoimage_class_cache: 0x4C0, // ImageClassCache = 0x354 + 0x16c
+                    monointernalhashtable_table: 0x20, // HashTableTable = 0x14 + 0xc
+                    monointernalhashtable_size: 0x18, // HashTableSize = 0xc + 0xc
+                    monoclassdef_next_class_cache: 0x108, // TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4
                     monoclassdef_klass: 0x0,
-                    monoclass_name: 0x48,
-                    monoclass_name_space: 0x50,
-                    monoclass_fields: 0x98,
-                    monoclassdef_field_count: 0x100,
-                    monoclass_runtime_info: 0xD0,
-                    monoclass_vtable_size: 0x5C,
-                    monoclass_parent: 0x30,
+                    monoclass_name: 0x48, // TypeDefinitionName = 0x2c + 0x1c
+                    monoclass_name_space: 0x50, // TypeDefinitionNamespace = 0x30 + 0x20
+                    monoclass_fields: 0x98, // TypeDefinitionFields = 0x60 + 0x20 + 0x18
+                    monoclassdef_field_count: 0x100, // TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18
+                    monoclass_runtime_info: 0xD0, // TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18
+                    monoclass_vtable_size: 0x5C, // TypeDefinitionVTableSize = 0x38 + 0x24
+                    monoclass_parent: 0x30, // TypeDefinitionParent = 0x20 + 0x10
                     monoclassfield_name: 0x8,
                     monoclassfield_offset: 0x18,
-                    monoclassruntimeinfo_domain_vtables: 0x8,
-                    monovtable_vtable: 0x40,
+                    monoclassruntimeinfo_domain_vtables: 0x8, // TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4
+                    monovtable_vtable: 0x40, // VTable = 0x28 + 0x18
                     monoclassfieldalignment: 0x20,
                 }),
                 Version::V3 => Some(&Self {
@@ -878,7 +932,7 @@ impl Offsets {
                     monoclassfieldalignment: 0x20,
                 }),
             },
-            PointerSize::Bit32 => match version {
+            (PointerSize::Bit32, BinaryFormat::PE) => match version {
                 Version::V1 => Some(&Self {
                     monoassembly_aname: 0x8,
                     monoassembly_image: 0x40,
@@ -899,26 +953,28 @@ impl Offsets {
                     monoclassruntimeinfo_domain_vtables: 0x4,
                     monovtable_vtable: 0x28,
                     monoclassfieldalignment: 0x10,
+                // 32-bit PE V2 matches Unity2018_4_10_x86_PE_Offsets from
+                // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L12
                 }),
                 Version::V2 => Some(&Self {
                     monoassembly_aname: 0x8,
-                    monoassembly_image: 0x44,
-                    monoimage_class_cache: 0x354,
-                    monointernalhashtable_table: 0x14,
-                    monointernalhashtable_size: 0xC,
-                    monoclassdef_next_class_cache: 0xA8,
+                    monoassembly_image: 0x44, // AssemblyImage
+                    monoimage_class_cache: 0x354, // ImageClassCache
+                    monointernalhashtable_table: 0x14, // HashTableTable
+                    monointernalhashtable_size: 0xC, // HashTableSize
+                    monoclassdef_next_class_cache: 0xA8, // TypeDefinitionNextClassCache
                     monoclassdef_klass: 0x0,
-                    monoclass_name: 0x2C,
-                    monoclass_name_space: 0x30,
-                    monoclass_fields: 0x60,
-                    monoclassdef_field_count: 0xA4,
-                    monoclass_runtime_info: 0x84,
-                    monoclass_vtable_size: 0x38,
-                    monoclass_parent: 0x20,
+                    monoclass_name: 0x2C, // TypeDefinitionName
+                    monoclass_name_space: 0x30, // TypeDefinitionNamespace
+                    monoclass_fields: 0x60, // TypeDefinitionFields
+                    monoclassdef_field_count: 0xA4, // TypeDefinitionFieldCount
+                    monoclass_runtime_info: 0x84, // TypeDefinitionRuntimeInfo
+                    monoclass_vtable_size: 0x38, // TypeDefinitionVTableSize
+                    monoclass_parent: 0x20, // TypeDefinitionParent
                     monoclassfield_name: 0x4,
                     monoclassfield_offset: 0xC,
-                    monoclassruntimeinfo_domain_vtables: 0x4,
-                    monovtable_vtable: 0x28,
+                    monoclassruntimeinfo_domain_vtables: 0x4, // TypeDefinitionRuntimeInfoDomainVTables
+                    monovtable_vtable: 0x28, // VTable
                     monoclassfieldalignment: 0x10,
                 }),
                 Version::V3 => Some(&Self {
@@ -943,6 +999,54 @@ impl Offsets {
                     monoclassfieldalignment: 0x10,
                 }),
             },
+            #[cfg(feature = "std")]
+            (PointerSize::Bit64, BinaryFormat::MachO) => match version {
+                Version::V1 => Some(&Self {
+                    monoassembly_aname: 0x10,
+                    monoassembly_image: 0x58, // matches 64-bit PE V1
+                    monoimage_class_cache: 0x3D0, // matches 64-bit PE V1
+                    monointernalhashtable_table: 0x20,
+                    monointernalhashtable_size: 0x18,
+                    monoclassdef_next_class_cache: 0xF8, // 0x8 less than 64-bit PE V1
+                    monoclassdef_klass: 0x0,
+                    monoclass_name: 0x40, // 0x8 less than 64-bit PE V1
+                    monoclass_name_space: 0x48, // 0x8 less than 64-bit PE V1
+                    monoclass_fields: 0xA0, // 0x8 less than 64-bit PE V1
+                    monoclassdef_field_count: 0x8C, // 0x8 less than 64-bit PE V1
+                    monoclass_runtime_info: 0xF0, // 0x8 less than 64-bit PE V1
+                    monoclass_vtable_size: 0x18, // MonoVtable.data
+                    monoclass_parent: 0x28, // 0x8 less than 64-bit PE V1
+                    monoclassfield_name: 0x8,
+                    monoclassfield_offset: 0x18,
+                    monoclassruntimeinfo_domain_vtables: 0x8,
+                    monovtable_vtable: 0x0, // UNUSED for V1
+                    monoclassfieldalignment: 0x20,
+                }),
+                // 64-bit MachO V2 matches Unity2019_4_2020_3_x64_MachO_Offsets from
+                // https://github.com/hackf5/unityspy/blob/master/src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs#L86
+                Version::V2 => Some(&Self {
+                    monoassembly_aname: 0x10,
+                    monoassembly_image: 0x60, // AssemblyImage = 0x44 + 0x1c
+                    monoimage_class_cache: 0x4C0, // ImageClassCache = 0x354 + 0x16c
+                    monointernalhashtable_table: 0x20, // HashTableTable = 0x14 + 0xc
+                    monointernalhashtable_size: 0x18, // HashTableSize = 0xc + 0xc
+                    monoclassdef_next_class_cache: 0x100, // TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4 - 0x8
+                    monoclassdef_klass: 0x0,
+                    monoclass_name: 0x40, // TypeDefinitionName = 0x2c + 0x1c - 0x8
+                    monoclass_name_space: 0x48, // TypeDefinitionNamespace = 0x30 + 0x20 - 0x8
+                    monoclass_fields: 0x90, // TypeDefinitionFields = 0x60 + 0x20 + 0x18 - 0x8
+                    monoclassdef_field_count: 0xF8, // TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18 - 0x8
+                    monoclass_runtime_info: 0xC8, // TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18 - 0x8
+                    monoclass_vtable_size: 0x54, // TypeDefinitionVTableSize = 0x38 + 0x24 - 0x8
+                    monoclass_parent: 0x28, // TypeDefinitionParent = 0x20 + 0x10 - 0x8
+                    monoclassfield_name: 0x8,
+                    monoclassfield_offset: 0x18,
+                    monoclassruntimeinfo_domain_vtables: 0x8, // TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4
+                    monovtable_vtable: 0x40, // VTable = 0x28 + 0x18
+                    monoclassfieldalignment: 0x20,
+                }),
+                Version::V3 => None,
+            },
             _ => None,
         }
     }
@@ -964,41 +1068,40 @@ fn detect_version(process: &Process) -> Option<Version> {
     if process.get_module_address("mono.dll").is_ok() {
         return Some(Version::V1);
     }
+    if process.get_module_address("libmono.0.dylib").is_ok() {
+        return Some(Version::V1);
+    }
 
-    let unity_module = {
-        let address = process.get_module_address("UnityPlayer.dll").ok()?;
-        let range = pe::read_size_of_image(process, address)? as u64;
-        (address, range)
-    };
+    let unity_module = [
+        ("UnityPlayer.dll", BinaryFormat::PE),
+        #[cfg(feature = "std")]
+        ("UnityPlayer.dylib", BinaryFormat::MachO)
+    ].into_iter().find_map(|(name, format)| {
+        match format {
+            BinaryFormat::PE => {
+                let address = process.get_module_address(name).ok()?;
+                let range = pe::read_size_of_image(process, address)? as u64;
+                Some((address, range))
+            },
+            #[cfg(feature = "std")]
+            BinaryFormat::MachO => process.get_module_range(name).ok()
+        }
+    })?;
 
+    // null "202" wildcard "."
     const SIG_202X: Signature<6> = Signature::new("00 32 30 32 ?? 2E");
 
     let Some(addr) = SIG_202X.scan_process_range(process, unity_module) else {
         return Some(Version::V2);
     };
 
-    const ZERO: u8 = b'0';
-    const NINE: u8 = b'9';
-
     let version_string = process.read::<[u8; 6]>(addr + 1).ok()?;
 
     let (before, after) = version_string.split_at(version_string.iter().position(|&x| x == b'.')?);
 
-    let mut unity: u32 = 0;
-    for &val in before {
-        match val {
-            ZERO..=NINE => unity = unity * 10 + (val - ZERO) as u32,
-            _ => break,
-        }
-    }
+    let unity: u32 = ascii_read_u32(before);
 
-    let mut unity_minor: u32 = 0;
-    for &val in &after[1..] {
-        match val {
-            ZERO..=NINE => unity_minor = unity_minor * 10 + (val - ZERO) as u32,
-            _ => break,
-        }
-    }
+    let unity_minor: u32 = ascii_read_u32(&after[1..]);
 
     Some(if (unity == 2021 && unity_minor >= 2) || (unity > 2021) {
         Version::V3
@@ -1006,3 +1109,27 @@ fn detect_version(process: &Process) -> Option<Version> {
         Version::V2
     })
 }
+
+fn ascii_read_u32(slice: &[u8]) -> u32 {
+    const ZERO: u8 = b'0';
+    const NINE: u8 = b'9';
+
+    let mut result: u32 = 0;
+    for &val in slice {
+        match val {
+            ZERO..=NINE => result = result * 10 + (val - ZERO) as u32,
+            _ => break,
+        }
+    }
+    result
+}
+
+// --------------------------------------------------------
+
+#[cfg(feature = "std")]
+fn file_read_all_bytes<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
+    let mut f = File::open(path)?;
+    let mut buffer: Vec<u8> = Vec::new();
+    f.read_to_end(&mut buffer)?;
+    Ok(buffer)
+}
diff --git a/src/game_engine/unity/scene.rs b/src/game_engine/unity/scene.rs
index 51c3214..7474d25 100644
--- a/src/game_engine/unity/scene.rs
+++ b/src/game_engine/unity/scene.rs
@@ -11,6 +11,7 @@ use core::{array, iter, mem::MaybeUninit};
 use crate::{
     file_format::pe, future::retry, signature::Signature, string::ArrayCString, Address, Address32,
     Address64, Error, PointerSize, Process,
+    file_format::macho,
 };
 
 const CSTR: usize = 128;
@@ -30,33 +31,58 @@ pub struct SceneManager {
 impl SceneManager {
     /// Attaches to the scene manager in the given process.
     pub fn attach(process: &Process) -> Option<Self> {
-        const SIG_64_BIT: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ???????? 33 F6");
+        const SIG_64_BIT_PE: Signature<13> = Signature::new("48 83 EC 20 4C 8B ?5 ???????? 33 F6");
+        const SIG_64_BIT_MACHO: Signature<13> = Signature::new("41 54 53 50 4C 8B ?5 ???????? 41 83");
         const SIG_32_1: Signature<12> = Signature::new("55 8B EC 51 A1 ???????? 53 33 DB");
         const SIG_32_2: Signature<6> = Signature::new("53 8D 41 ?? 33 DB");
         const SIG_32_3: Signature<14> = Signature::new("55 8B EC 83 EC 18 A1 ???????? 33 C9 53");
 
-        let unity_player = process.get_module_range("UnityPlayer.dll").ok()?;
+        let (unity_player, format) = [("UnityPlayer.dll", BinaryFormat::PE), ("UnityPlayer.dylib", BinaryFormat::MachO)]
+            .into_iter()
+            .find_map(|(name, format)| Some((process.get_module_range(name).ok()?, format)))?;
 
-        let pointer_size = match pe::MachineType::read(process, unity_player.0)? {
-            pe::MachineType::X86_64 => PointerSize::Bit64,
-            _ => PointerSize::Bit32,
+        let pointer_size = match format {
+            BinaryFormat::PE => {
+                match pe::MachineType::read(process, unity_player.0)? {
+                    pe::MachineType::X86_64 => PointerSize::Bit64,
+                    _ => PointerSize::Bit32,
+                }
+            }
+            BinaryFormat::MachO => macho::pointer_size(process, macho::scan_macho_page(process, unity_player)?)?,
         };
-
         let is_il2cpp = process.get_module_address("GameAssembly.dll").is_ok();
 
         // There are multiple signatures that can be used, depending on the version of Unity
         // used in the target game.
-        let base_address: Address = if pointer_size == PointerSize::Bit64 {
-            let addr = SIG_64_BIT.scan_process_range(process, unity_player)? + 7;
-            addr + 0x4 + process.read::<i32>(addr).ok()?
-        } else if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) {
-            process.read::<Address32>(addr + 5).ok()?.into()
-        } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) {
-            process.read::<Address32>(addr.add_signed(-4)).ok()?.into()
-        } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) {
-            process.read::<Address32>(addr + 7).ok()?.into()
-        } else {
-            return None;
+        let base_address: Address = match (pointer_size, format) {
+            (PointerSize::Bit64, BinaryFormat::PE) => {
+                let addr = SIG_64_BIT_PE.scan_process_range(process, unity_player)? + 7;
+                addr + 0x4 + process.read::<i32>(addr).ok()?
+            },
+            (PointerSize::Bit64, BinaryFormat::MachO) => {
+                // RIP-relative addressing
+                // 7 is the offset to the ???????? question marks in the signature
+                let addr = SIG_64_BIT_MACHO.scan_process_range(process, unity_player)? + 7;
+                // 4 is the offset to the next instruction after the question marks
+                addr + 0x4 + process.read::<i32>(addr).ok()?
+            },
+            (PointerSize::Bit32, BinaryFormat::PE) => {
+                if let Some(addr) = SIG_32_1.scan_process_range(process, unity_player) {
+                    process.read::<Address32>(addr + 5).ok()?.into()
+                } else if let Some(addr) = SIG_32_2.scan_process_range(process, unity_player) {
+                    process.read::<Address32>(addr.add_signed(-4)).ok()?.into()
+                } else if let Some(addr) = SIG_32_3.scan_process_range(process, unity_player) {
+                    process.read::<Address32>(addr + 7).ok()?.into()
+                } else {
+                    return None;
+                }
+            },
+            (PointerSize::Bit32, BinaryFormat::MachO) => {
+                return None;
+            },
+            (PointerSize::Bit16, _) => {
+                return None;
+            },
         };
 
         let offsets = Offsets::new(pointer_size);
@@ -429,6 +455,12 @@ impl Transform {
     }
 }
 
+#[derive(Copy, Clone, PartialEq, Hash, Debug)]
+enum BinaryFormat {
+    PE,
+    MachO,
+}
+
 struct Offsets {
     scene_count: u8,
     active_scene: u8,
diff --git a/src/lib.rs b/src/lib.rs
index d22d79b..8f1d560 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-#![no_std]
+#![cfg_attr(not(feature = "std"), no_std)]
 #![warn(
     clippy::complexity,
     clippy::correctness,