From d2cdbf0a83a1febed171335b5af0d16ff0f8288f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 13 Jul 2026 18:14:09 -0600 Subject: [PATCH] Mods: Rework mod linking Apple: mods are now MH_BUNDLE (.so) linked with -bundle_loader, replacing the deprecated (on iOS) -undefined dynamic_lookup. Windows: clang-cl mods link dusklight.exe directly; lld's mingw driver synthesizes imports from the export table. cl still requires the import library. --- cmake/AndroidExports.cmake | 70 ++++++++++++++++++++ cmake/AppleExports.cmake | 91 ++++++++++++++++++++++++++ cmake/ModSDK.cmake | 68 +++++++++++++------ sdk/CMakeLists.txt | 11 ++-- src/dusk/mods/loader/loader.cpp | 8 +-- src/dusk/mods/loader/native_module.hpp | 2 - 6 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 cmake/AndroidExports.cmake create mode 100644 cmake/AppleExports.cmake diff --git a/cmake/AndroidExports.cmake b/cmake/AndroidExports.cmake new file mode 100644 index 0000000000..6bc94b61dc --- /dev/null +++ b/cmake/AndroidExports.cmake @@ -0,0 +1,70 @@ +include_guard(GLOBAL) + +get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +# Android mod linking: symgen scans and filters the game's exports, generating a version script used for the executable +# link step, and generates a shared object stub that mods can link against without having to build the whole game. +function(setup_android_exports target) + include("${_dir}/SymbolManifest.cmake") + ensure_symgen(TRUE) + add_dependencies(${target} symgen) + + set(_rsp_lines "$") + foreach (_lib IN LISTS JSYSTEM_LIBRARIES) + list(APPEND _rsp_lines "$") + endforeach () + list(JOIN _rsp_lines "\n" _rsp_content) + set(_rsp "${CMAKE_BINARY_DIR}/dusklight_exports_input.rsp") + file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}") + + set(_sdk_args) + foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx + aurora_os aurora_pad aurora_si aurora_vi) + if (TARGET ${_lib}) + list(APPEND _sdk_args --sdk-lib "$") + endif () + endforeach () + if (TARGET dawn::webgpu_dawn) + get_target_property(_dawn_type dawn::webgpu_dawn TYPE) + if (_dawn_type STREQUAL "STATIC_LIBRARY") + list(APPEND _sdk_args --sdk-lib "$") + endif () + endif () + + set(_vscript "${CMAKE_BINARY_DIR}/dusklight_exports.ver") + add_custom_command(TARGET ${target} PRE_LINK + # TODO: src/dusk/ is NOT excluded: inline code in game headers + # currently call into it (e.g. dusk::frame_interp::lookup_replacement). + COMMAND "${SYMGEN_EXE}" exports + --rsp "${_rsp}" + --out "${_vscript}" + --format version-script + --exclude cmake_pch + --exclude miniz + --exclude asan_options + # Resolved from the Java side; the SDL ones live in the statically-linked + # SDL archive, outside the provenance scan. + --extra-sym JNI_OnLoad + --extra-sym SDL_main + --extra-sym "Java_*" + ${_sdk_args} + COMMENT "Generating dusklight exports" + VERBATIM) + target_link_options(${target} PRIVATE "-Wl,--version-script=${_vscript}") + + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) + if (_arch STREQUAL "aarch64") + set(_machine "arm64") + else () + set(_machine "amd64") + endif () + + set(_stub "${CMAKE_BINARY_DIR}/stub-android-${_arch}.so") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${SYMGEN_EXE}" stub -f elf "$" -o "${_stub}" + --soname "$" --machine "${_machine}" + BYPRODUCTS "${_stub}" + COMMENT "Generating dusklight link stub" + VERBATIM) + install(FILES "${_stub}" DESTINATION sdk) +endfunction() diff --git a/cmake/AppleExports.cmake b/cmake/AppleExports.cmake new file mode 100644 index 0000000000..47f45a9718 --- /dev/null +++ b/cmake/AppleExports.cmake @@ -0,0 +1,91 @@ +include_guard(GLOBAL) + +get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +# Apple mod linking: symgen scans and filters the game's exports, generating an exports list used for the executable +# link step, and generates a MH_EXECUTE Mach-O stub that mods can link against without having to build the whole game. +function(setup_apple_exports target) + include("${_dir}/SymbolManifest.cmake") + ensure_symgen(TRUE) + set(_symgen "${SYMGEN_EXE}") + add_dependencies(${target} symgen) + + set(_config_subdir "") + if (CMAKE_CONFIGURATION_TYPES) + set(_config_subdir "$/") + endif () + + set(_rsp_lines "$") + foreach (_lib IN LISTS JSYSTEM_LIBRARIES) + list(APPEND _rsp_lines "$") + endforeach () + list(JOIN _rsp_lines "\n" _rsp_content) + set(_rsp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports_input.rsp") + file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}") + + set(_sdk_args) + foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx + aurora_os aurora_pad aurora_si aurora_vi) + if (TARGET ${_lib}) + list(APPEND _sdk_args --sdk-lib "$") + endif () + endforeach () + + # Dawn is linked statically on Apple; mods reach wgpu* through the executable. + if (TARGET dawn::webgpu_dawn) + get_target_property(_dawn_type dawn::webgpu_dawn TYPE) + if (_dawn_type STREQUAL "STATIC_LIBRARY") + list(APPEND _sdk_args --sdk-lib "$") + endif () + endif () + + set(_exp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.exp") + add_custom_command(TARGET ${target} PRE_LINK + # TODO: src/dusk/ is NOT excluded: inline code in game headers + # currently call into it (e.g. dusk::frame_interp::lookup_replacement). + COMMAND "${_symgen}" exports + --rsp "${_rsp}" + --out "${_exp}" + --exclude cmake_pch + --exclude miniz + --exclude asan_options + ${_sdk_args} + COMMENT "Generating dusklight exports" + VERBATIM) + target_link_options(${target} PRIVATE -Xlinker -exported_symbols_list -Xlinker "${_exp}") + + # Generate the stub executable mods link against via -bundle_loader. + set(_stub_args) + if (IOS) + set(_stub_platform "ios") + list(APPEND _stub_args --platform ios) + elseif (TVOS) + set(_stub_platform "tvos") + list(APPEND _stub_args --platform tvos) + else () + set(_stub_platform "macos") + endif () + if (CMAKE_OSX_DEPLOYMENT_TARGET) + list(APPEND _stub_args --min-os "${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif () + if (CMAKE_OSX_ARCHITECTURES) + set(_archs "${CMAKE_OSX_ARCHITECTURES}") + else () + set(_archs "${CMAKE_SYSTEM_PROCESSOR}") + endif () + set(_arch_names "") + foreach (_arch IN LISTS _archs) + string(TOLOWER "${_arch}" _arch) + list(APPEND _stub_args --arch "${_arch}") + list(APPEND _arch_names "${_arch}") + endforeach () + list(JOIN _arch_names "_" _arch_name) + + set(_stub "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight-stub") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${_symgen}" stub -f macho "${_exp}" -o "${_stub}" ${_stub_args} + BYPRODUCTS "${_stub}" + COMMENT "Generating dusklight link stub" + VERBATIM) + install(FILES "${_stub}" DESTINATION sdk RENAME "stub-${_stub_platform}-${_arch_name}") +endfunction() diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index 81d3c81509..3bf80035e6 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -13,14 +13,15 @@ function(_mod_lib_info out_platform_var out_name_var) set(_arch "${CMAKE_OSX_ARCHITECTURES}") endif () string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform) + if (_platform STREQUAL "darwin") + set(_platform "macos") + endif () string(TOLOWER "${_arch}" _arch) if (_arch MATCHES "^(i[3-6]86|x86)$") set(_arch "x86") endif () if (WIN32) set(_ext ".dll") - elseif (APPLE) - set(_ext ".dylib") else () set(_ext ".so") endif () @@ -62,7 +63,7 @@ function(add_mod target_name) set(_lib_name "") if (ARG_SOURCES) set(_has_lib TRUE) - add_library(${target_name} SHARED ${ARG_SOURCES}) + add_library(${target_name} MODULE ${ARG_SOURCES}) _mod_lib_info(_lib_platform _lib_name) set_target_properties(${target_name} PROPERTIES PREFIX "" @@ -94,18 +95,28 @@ function(add_mod target_name) endif () if (APPLE) - # Game symbols resolve against the host executable at dlopen time. - target_link_options(${target_name} PRIVATE -undefined dynamic_lookup) + if (TARGET dusklight) + set(_game_exe "$") + add_dependencies(${target_name} dusklight) + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_exe "${DUSK_GAME_EXE}") + else () + message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (game executable)") + endif () + target_link_options(${target_name} PRIVATE + -Xlinker -bundle_loader -Xlinker "${_game_exe}") + set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_game_exe}") set_target_properties(${target_name} PROPERTIES BUILD_RPATH "@loader_path" INSTALL_RPATH "@loader_path") elseif (ANDROID) if (TARGET dusklight) target_link_libraries(${target_name} PRIVATE dusklight) - elseif (DUSK_GAME_SOLIB) - target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}") + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") + target_link_libraries(${target_name} PRIVATE "${_game_lib}") else () - message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)") + message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (libmain.so or stub)") endif () set_target_properties(${target_name} PROPERTIES BUILD_RPATH "$ORIGIN" @@ -116,16 +127,33 @@ function(add_mod target_name) BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN") elseif (WIN32) - # Link against the generated import library (game ABI surface). Function calls - # resolve through import thunks. Data is toolchain dependent: - # - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by - # the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp). - # - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated + # Function calls resolve through import thunks. Data is toolchain dependent: + # - clang-cl: lld's mingw driver synthesizes imports straight from the game + # executable's export table (no import library needed) and auto-imports data + # references, fixed up at load by the mod SDK's pseudo-relocation runtime + # (pseudo_reloc.cpp). + # - cl (MSVC): link.exe only imports through an import library, so + # DUSK_GAME_EXE must point at one (sdk/windows-.lib or `symgen stub` + # output); only DUSK_GAME_DATA-annotated data is reachable. Un-annotated # references fail to link. - if (NOT DUSK_GAME_IMPLIB) - message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.") + if (TARGET dusklight) + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(_game_lib "$") + add_dependencies(${target_name} dusklight) + elseif (DUSK_GAME_IMPLIB) + set(_game_lib "${DUSK_GAME_IMPLIB}") + else () + message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set (see setup_windows_exports)") + endif () + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") + if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT _game_lib MATCHES "\\.lib$") + message(FATAL_ERROR "add_mod: cl must use an import library for DUSK_GAME_EXE") + endif () + else () + message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (game executable or import library)") endif () - target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}") + target_link_libraries(${target_name} PRIVATE "${_game_lib}") set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") @@ -266,9 +294,9 @@ endfunction() # user cache). # - Linux: pre-extracted stage dirs into /mods so native libs dlopen in place from # read-only installs. -# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs -# ad-hoc signed in place, then the whole bundle re-signed. -# - iOS/tvOS: assets into /mods/, the mod dylib into Frameworks/.dylib, +# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, native +# libs ad-hoc signed in place, then the whole bundle re-signed. +# - iOS/tvOS: assets into /mods/, the mod library into Frameworks/.so, # and runtime libraries alongside it. # - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets. function(install_bundled_mods) @@ -299,7 +327,7 @@ function(install_bundled_mods) install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}" PATTERN "lib" EXCLUDE) install(PROGRAMS "$" - DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib") + DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.so") install(DIRECTORY "${_stage}/lib/${_lib_platform}/" DESTINATION "${_bundle_dir}/Frameworks" PATTERN "${_lib_name}" EXCLUDE) diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 8670603866..7d1d17a8c2 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -7,8 +7,7 @@ # add_subdirectory(/sdk dusk-sdk EXCLUDE_FROM_ALL) # add_mod(my_mod SOURCES ... MOD_JSON mod.json) # -# On Windows, pass -DDUSK_GAME_IMPLIB= from the -# matching game release. TODO: auto-download from tag +# TODO: auto-download link targets from tag cmake_minimum_required(VERSION 3.25) @@ -38,10 +37,10 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDawnProvider.c # Game ABI headers & compile definitions include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake") -if (WIN32) - set(DUSK_GAME_IMPLIB "" CACHE FILEPATH "Path to the dusklight import library (sdk/dusklight.lib)") - if (DUSK_GAME_IMPLIB AND NOT EXISTS "${DUSK_GAME_IMPLIB}") - message(FATAL_ERROR "Mod SDK: DUSK_GAME_IMPLIB does not exist: ${DUSK_GAME_IMPLIB}") +if (WIN32 OR APPLE OR ANDROID) + set(DUSK_GAME_EXE "" CACHE FILEPATH "Game binary or link stub mods link against") + if (DUSK_GAME_EXE AND NOT EXISTS "${DUSK_GAME_EXE}") + message(FATAL_ERROR "Mod SDK: DUSK_GAME_EXE does not exist: ${DUSK_GAME_EXE}") endif () endif () diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 008ace157d..adf465e367 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -55,13 +55,13 @@ static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; #elif TARGET_OS_TV static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; #elif defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "darwin-arm64"sv; +static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; #elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "darwin-x86_64"sv; +static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; #endif -static constexpr std::string_view k_nativeLibName = "mod.dylib"sv; +static constexpr std::string_view k_nativeLibName = "mod.so"sv; #elif defined(__linux__) #if defined(__aarch64__) static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; @@ -130,7 +130,7 @@ NativeLocateResult locate_native_runtime(ModBundle& bundle) { if (platformEnd != std::string_view::npos) { const auto entryName = libPath.substr(platformEnd + 1); if (entryName.find('/') == std::string_view::npos && - (entryName == "mod.dll"sv || entryName == "mod.dylib"sv || entryName == "mod.so"sv)) + (entryName == "mod.dll"sv || entryName == "mod.so"sv)) { result.anyLibs = true; } diff --git a/src/dusk/mods/loader/native_module.hpp b/src/dusk/mods/loader/native_module.hpp index ff2acf9632..2632496f96 100644 --- a/src/dusk/mods/loader/native_module.hpp +++ b/src/dusk/mods/loader/native_module.hpp @@ -22,8 +22,6 @@ public: #if defined(_WIN32) static constexpr auto LibraryExtension = ".dll"; -#elif defined(__APPLE__) - static constexpr auto LibraryExtension = ".dylib"; #else static constexpr auto LibraryExtension = ".so"; #endif