From 6bd1f210de9141d5210a637825b299edbdc2d1d8 Mon Sep 17 00:00:00 2001 From: Toni Alatalo Date: Thu, 2 Apr 2026 12:37:49 +0300 Subject: [PATCH] feat: add macOS support - Fix PlatformExtensionName() to return "dylib" on macOS instead of "so" - Replace hardcoded Windows paths in the Rust loader with platform-aware logic: derive root_dir from the plugin path, use UNREAL_RUST_TARGET_DIR env var or relative fallback for the target library path - Use cfg-gated constants for platform-specific library extensions (.dll/.so/.dylib) and library names (with lib prefix on Unix) - Guard .pdb debug symbol copy with #[cfg(target_os = "windows")] - Add aarch64-apple-darwin and x86_64-apple-darwin Cargo targets Closes #20 Co-Authored-By: Claude Opus 4.6 --- .cargo/config.toml | 5 + RustPlugin/Source/RustPlugin/RustPlugin.cpp | 19 +-- unreal-rust-loader/Cargo.toml | 3 + unreal-rust-loader/src/lib.rs | 179 +++++++++++++++----- 4 files changed, 154 insertions(+), 52 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 8e50250e..58691995 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,3 +5,8 @@ linker = "rust-lld.exe" # ] [target.x86_64-pc-windows-gnu] rustflags = ["-C", "link-arg=-fuse-ld=lld"] + +# macOS targets use the system linker (no special config needed) +[target.aarch64-apple-darwin] + +[target.x86_64-apple-darwin] diff --git a/RustPlugin/Source/RustPlugin/RustPlugin.cpp b/RustPlugin/Source/RustPlugin/RustPlugin.cpp index 3721a9a3..2fe870d2 100644 --- a/RustPlugin/Source/RustPlugin/RustPlugin.cpp +++ b/RustPlugin/Source/RustPlugin/RustPlugin.cpp @@ -231,7 +231,9 @@ namespace FString PlatformExtensionName() { -#if PLATFORM_LINUX || PLATFORM_MAC +#if PLATFORM_MAC + return FString(TEXT("dylib")); +#elif PLATFORM_LINUX return FString(TEXT("so")); #elif PLATFORM_WINDOWS return FString(TEXT("dll")); @@ -313,18 +315,11 @@ bool FRustLoader::SetupLoader() this->Handle = LocalHandle; - void* LocalBindings = FPlatformProcess::GetDllExport(LocalHandle, TEXT("register_unreal_bindings\0")); - // void* LocalEditorTick = FPlatformProcess::GetDllExport(LocalHandle, TEXT("editor_tick\0")); - this->TryLoadFunction = static_cast(FPlatformProcess::GetDllExport(LocalHandle, TEXT("try_load\0"))); - this->IsOutOfDateFunction = static_cast(FPlatformProcess::GetDllExport( - LocalHandle, TEXT("is_out_of_date\0"))); - ensure(LocalBindings); + this->Bindings = reinterpret_cast(FPlatformProcess::GetDllExport(LocalHandle, TEXT("register_unreal_bindings\0"))); + this->TryLoadFunction = reinterpret_cast(FPlatformProcess::GetDllExport(LocalHandle, TEXT("try_load\0"))); + this->IsOutOfDateFunction = reinterpret_cast(FPlatformProcess::GetDllExport(LocalHandle, TEXT("is_out_of_date\0"))); + ensure(this->Bindings); ensure(this->TryLoadFunction); - // ensure(LocalEditorTick); - - // this->EditorTick = static_cast(LocalEditorTick); - - this->Bindings = static_cast(LocalBindings); this->TargetPath = LocalTargetDllPath; NeedsInit = true; diff --git a/unreal-rust-loader/Cargo.toml b/unreal-rust-loader/Cargo.toml index 78549df6..4b60d442 100644 --- a/unreal-rust-loader/Cargo.toml +++ b/unreal-rust-loader/Cargo.toml @@ -7,5 +7,8 @@ edition.workspace = true unreal-ffi = { path = "../unreal-ffi" } libloading = "0.9" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [lib] crate-type = ["cdylib"] diff --git a/unreal-rust-loader/src/lib.rs b/unreal-rust-loader/src/lib.rs index 2900832a..bc5ade33 100644 --- a/unreal-rust-loader/src/lib.rs +++ b/unreal-rust-loader/src/lib.rs @@ -1,13 +1,82 @@ -use std::collections::btree_map::Entry; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::OnceLock; use std::time::SystemTime; -use libloading::{Library, Symbol}; -use unreal_ffi::{self as ffi, TickFn}; +use libloading::Library; -use unreal_ffi::{PluginBindings, RegisterUnrealBindings, RustBindings, UnrealBindings}; +use unreal_ffi::{RegisterUnrealBindings, RustBindings, UnrealBindings}; + +#[cfg(target_os = "windows")] +const PLUGIN_EXTENSION: &str = "dll"; +#[cfg(target_os = "linux")] +const PLUGIN_EXTENSION: &str = "so"; +#[cfg(target_os = "macos")] +const PLUGIN_EXTENSION: &str = "dylib"; + +#[cfg(target_os = "windows")] +const PLUGIN_LIB_NAME: &str = "unreal_rust_example.dll"; +#[cfg(target_os = "linux")] +const PLUGIN_LIB_NAME: &str = "libunreal_rust_example.so"; +#[cfg(target_os = "macos")] +const PLUGIN_LIB_NAME: &str = "libunreal_rust_example.dylib"; + +/// Find the loader dylib's own filesystem path at runtime using platform APIs. +/// This lets us locate other files relative to the UE project Binaries directory. +fn find_own_dylib_path() -> Option { + #[cfg(unix)] + { + unsafe extern "C" { + fn dladdr(addr: *const u8, info: *mut libc::Dl_info) -> libc::c_int; + } + let mut info: libc::Dl_info = unsafe { std::mem::zeroed() }; + let result = + unsafe { dladdr(find_own_dylib_path as *const u8, &mut info) }; + if result != 0 && !info.dli_fname.is_null() { + let path = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; + path.to_str().ok().map(PathBuf::from) + } else { + None + } + } + #[cfg(windows)] + { + None // Windows uses hardcoded paths in the original code + } +} + +/// Resolve the path to the example plugin library. +/// +/// Search order: +/// 1. UNREAL_RUST_TARGET_DIR env var (absolute path to cargo target dir) +/// 2. Relative to the loader dylib: traverse up from Binaries/ to find +/// the workspace target/ directory (handles the symlinked plugin layout) +/// 3. Fallback to cargo default target/release/ +fn resolve_plugin_path() -> PathBuf { + // 1. Explicit env var + if let Ok(dir) = std::env::var("UNREAL_RUST_TARGET_DIR") { + return PathBuf::from(dir).join(PLUGIN_LIB_NAME); + } + + // 2. Derive from loader's own location + // Loader lives at: /Binaries/unreal_rust_loader.dylib + // Workspace root is: /../../ (through the Plugins/RustPlugin symlink) + // Cargo output is: /target/release/ + if let Some(own_path) = find_own_dylib_path() { + if let Some(binaries_dir) = own_path.parent() { + // Try: /../../../target/release/ (project -> workspace via setup.sh layout) + let workspace_root = binaries_dir.join("../../.."); + for profile in ["release", "debug", "development"] { + let candidate = workspace_root.join("target").join(profile).join(PLUGIN_LIB_NAME); + if candidate.exists() { + return candidate; + } + } + } + } + + // 3. Fallback + PathBuf::from("target/release").join(PLUGIN_LIB_NAME) +} pub struct Plugin { library: Library, @@ -19,24 +88,27 @@ impl Plugin { unreal_bindings: &UnrealBindings, rust_bindings: &mut RustBindings, path: &Path, - ) -> Self { - let library = unsafe { Library::new(path).unwrap() }; + ) -> Result { + let library = unsafe { + Library::new(path).map_err(|e| format!("Failed to load {:?}: {}", path, e))? + }; - let metadata = std::fs::metadata(path).unwrap(); + let metadata = std::fs::metadata(path) + .map_err(|e| format!("Failed to read metadata for {:?}: {}", path, e))?; let register_unreal_bindings: RegisterUnrealBindings = unsafe { *library .get::("register_unreal_bindings\0") - .unwrap() + .map_err(|e| format!("Failed to find register_unreal_bindings in {:?}: {}", path, e))? }; (register_unreal_bindings)(unreal_bindings.clone(), rust_bindings); - Plugin { + Ok(Plugin { library, register_unreal_bindings, timestamp: metadata.modified().unwrap(), - } + }) } } @@ -73,21 +145,30 @@ impl Loader { timestamp > plugin.timestamp } - /// Safety pub fn load(&mut self, rust_bindings: &mut RustBindings) { - // TODO: Maybe add some unloading logic here - // unload the currently loaded plugin + // Unload the currently loaded plugin self.loaded_plugin = None; - let root_dir = - PathBuf::from("D:\\projects\\unreal-rust\\example\\RustExample\\Binaries\\rust"); + if !self.path.exists() { + eprintln!( + "[unreal-rust] Plugin not found at {:?}, skipping load", + self.path + ); + return; + } + + let root_dir = self + .path + .parent() + .map(|p| p.join("rust")) + .unwrap_or_else(|| PathBuf::from("Binaries/rust")); - for entry in root_dir.read_dir().unwrap().flatten() { + for entry in root_dir.read_dir().into_iter().flatten().flatten() { if entry.path().is_dir() && let Some(file_name) = entry.file_name().to_str() && file_name.starts_with("rust-plugin") { - // try to delete every hot reload folder. Some we can't delete because the debugger + // Try to delete every hot reload folder. Some we can't delete because the debugger // might keep them open let _ = fs::remove_dir_all(entry.path()); } @@ -99,10 +180,31 @@ impl Loader { if hot_reload_dir.exists() { let _ = fs::remove_dir_all(&hot_reload_dir); } - let hotreload_dll_path = hot_reload_dir.join("rust_plugin.dll"); + let hotreload_lib_name = format!("rust_plugin.{}", PLUGIN_EXTENSION); + let hotreload_lib_path = hot_reload_dir.join(&hotreload_lib_name); let _ = fs::create_dir_all(&hot_reload_dir); - let _ = fs::copy(&self.path, &hotreload_dll_path); + if let Err(e) = fs::copy(&self.path, &hotreload_lib_path) { + eprintln!( + "[unreal-rust] Failed to copy {:?} to {:?}: {}", + self.path, hotreload_lib_path, e + ); + return; + } + + // On macOS, copied dylibs lose their code signature and the OS will + // kill the process with SIGKILL (Code Signature Invalid). Re-sign + // with an ad-hoc signature so the hot-reloaded copy can be loaded. + #[cfg(target_os = "macos")] + { + let _ = std::process::Command::new("codesign") + .args(["--force", "--sign", "-"]) + .arg(&hotreload_lib_path) + .output(); + } + + // Copy debug symbols on platforms that use sidecar files + #[cfg(target_os = "windows")] if let Some(pdb_dir) = self.path.parent() { let pdb_path = pdb_dir.join("unreal_rust_example.pdb"); if pdb_path.exists() { @@ -110,11 +212,16 @@ impl Loader { } } - let plugin = Plugin::new(&self.unreal_bindings, rust_bindings, &hotreload_dll_path); - - self.loaded_plugin = Some(plugin); - - self.hotreload_id += 1; + match Plugin::new(&self.unreal_bindings, rust_bindings, &hotreload_lib_path) { + Ok(plugin) => { + eprintln!("[unreal-rust] Loaded plugin from {:?}", self.path); + self.loaded_plugin = Some(plugin); + self.hotreload_id += 1; + } + Err(e) => { + eprintln!("[unreal-rust] {}", e); + } + } } } @@ -122,26 +229,18 @@ impl Loader { extern "C" fn register_unreal_bindings(bindings: UnrealBindings) -> u32 { unsafe { if LOADER.is_null() { - LOADER = Box::leak(Box::new(Loader::new( - bindings, - PathBuf::from( - "D:\\projects\\unreal-rust\\target\\development\\unreal_rust_example.dll", - ), - ))) as *mut _; + let plugin_path = resolve_plugin_path(); + eprintln!( + "[unreal-rust] Resolved plugin path: {:?} (exists: {})", + plugin_path, + plugin_path.exists() + ); + LOADER = Box::leak(Box::new(Loader::new(bindings, plugin_path))) as *mut _; } } 1 } -// #[unsafe(no_mangle)] -// unsafe extern "C" fn initialize(plugin_bindings: *mut PluginBindings) -> u32 { -// unsafe { -// let bindings = PluginBindings { tick, begin_play }; -// *plugin_bindings = bindings; -// } -// 1 -// } - #[unsafe(no_mangle)] unsafe extern "C" fn is_out_of_date() -> u32 { unsafe {