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.
This commit is contained in:
Luke Street
2026-07-13 18:14:09 -06:00
parent 700bbf0a5a
commit d2cdbf0a83
6 changed files with 218 additions and 32 deletions
+70
View File
@@ -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 "$<TARGET_OBJECTS:${target}>")
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
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 "$<TARGET_FILE:${_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 "$<TARGET_FILE:dawn::webgpu_dawn>")
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 "$<TARGET_FILE:${target}>" -o "${_stub}"
--soname "$<TARGET_FILE_NAME:${target}>" --machine "${_machine}"
BYPRODUCTS "${_stub}"
COMMENT "Generating dusklight link stub"
VERBATIM)
install(FILES "${_stub}" DESTINATION sdk)
endfunction()
+91
View File
@@ -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 "$<CONFIG>/")
endif ()
set(_rsp_lines "$<TARGET_OBJECTS:${target}>")
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
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 "$<TARGET_FILE:${_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 "$<TARGET_FILE:dawn::webgpu_dawn>")
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()
+48 -20
View File
@@ -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 "$<TARGET_FILE:dusklight>")
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-<arch>.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 "$<TARGET_FILE:dusklight>")
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 <install>/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 <app>/mods/<id>, the mod dylib into Frameworks/<id>.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 <app>/mods/<id>, the mod library into Frameworks/<id>.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 "$<TARGET_FILE:${_target}>"
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)
+5 -6
View File
@@ -7,8 +7,7 @@
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
# add_mod(my_mod SOURCES ... MOD_JSON mod.json)
#
# On Windows, pass -DDUSK_GAME_IMPLIB=<path to sdk/dusklight.lib> 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 ()
+4 -4
View File
@@ -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;
}
-2
View File
@@ -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