mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-22 21:22:41 -04:00
Mod API: Move dylibs to libs/{platform}; services import latest minor versions
This commit is contained in:
+63
-12
@@ -1,8 +1,9 @@
|
||||
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json>
|
||||
# [RUNTIME_LIBRARIES <file>...] [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||
# [TEXTURES_DIR <textures>] [OUTPUT_DIR <dir>] [BUNDLE])
|
||||
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into")
|
||||
|
||||
function(_mod_lib_name out_var)
|
||||
function(_mod_lib_info out_platform_var out_name_var)
|
||||
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES _count)
|
||||
@@ -23,7 +24,8 @@ function(_mod_lib_name out_var)
|
||||
else ()
|
||||
set(_ext ".so")
|
||||
endif ()
|
||||
set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE)
|
||||
set(${out_platform_var} "${_platform}-${_arch}" PARENT_SCOPE)
|
||||
set(${out_name_var} "mod${_ext}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_mod_resolve_source_path out_var path)
|
||||
@@ -45,7 +47,8 @@ function(_mod_collect_assets out_var dir)
|
||||
endfunction()
|
||||
|
||||
function(add_mod target_name)
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN})
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR"
|
||||
"SOURCES;RUNTIME_LIBRARIES" ${ARGN})
|
||||
if (NOT ARG_MOD_JSON)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON is required")
|
||||
endif ()
|
||||
@@ -55,11 +58,12 @@ function(add_mod target_name)
|
||||
endif ()
|
||||
|
||||
set(_has_lib FALSE)
|
||||
set(_lib_platform "")
|
||||
set(_lib_name "")
|
||||
if (ARG_SOURCES)
|
||||
set(_has_lib TRUE)
|
||||
add_library(${target_name} SHARED ${ARG_SOURCES})
|
||||
_mod_lib_name(_lib_name)
|
||||
_mod_lib_info(_lib_platform _lib_name)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
PREFIX ""
|
||||
C_VISIBILITY_PRESET hidden
|
||||
@@ -92,6 +96,9 @@ function(add_mod target_name)
|
||||
if (APPLE)
|
||||
# Game symbols resolve against the host executable at dlopen time.
|
||||
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||
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)
|
||||
@@ -100,8 +107,14 @@ function(add_mod target_name)
|
||||
else ()
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||
endif ()
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN")
|
||||
elseif (UNIX)
|
||||
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
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:
|
||||
@@ -134,6 +147,9 @@ function(add_mod target_name)
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
if (ARG_RUNTIME_LIBRARIES AND NOT _has_lib)
|
||||
message(FATAL_ERROR "add_mod: RUNTIME_LIBRARIES requires SOURCES")
|
||||
endif ()
|
||||
|
||||
set(_output_dir "${DUSK_MODS_OUTPUT_DIR}")
|
||||
if (ARG_OUTPUT_DIR)
|
||||
@@ -142,18 +158,39 @@ function(add_mod target_name)
|
||||
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
|
||||
set(_out "${_output_dir}/${target_name}.dusk")
|
||||
|
||||
set(_zip_args "${_lib_name}" mod.json)
|
||||
set(_zip_args mod.json)
|
||||
set(_package_deps "${_mod_json}")
|
||||
set(_package_inputs "${_mod_json}")
|
||||
set(_extra_cmds "")
|
||||
set(_lib_copy_cmd "")
|
||||
set(_target_depend "")
|
||||
if (_has_lib)
|
||||
list(APPEND _zip_args "${_lib_name}")
|
||||
set(_lib_copy_cmd COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}")
|
||||
list(APPEND _zip_args lib)
|
||||
set(_lib_copy_cmd
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}/lib/${_lib_platform}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:${target_name}>" "${_stage}/lib/${_lib_platform}/${_lib_name}")
|
||||
set(_target_depend ${target_name})
|
||||
endif ()
|
||||
string(TOLOWER "${_lib_name}" _lib_name_key)
|
||||
set(_runtime_lib_name_keys "${_lib_name_key}")
|
||||
foreach (_runtime_lib IN LISTS ARG_RUNTIME_LIBRARIES)
|
||||
_mod_resolve_source_path(_runtime_lib_path "${_runtime_lib}")
|
||||
if (NOT EXISTS "${_runtime_lib_path}" OR IS_DIRECTORY "${_runtime_lib_path}")
|
||||
message(FATAL_ERROR "add_mod: runtime library does not exist or is not a file: ${_runtime_lib_path}")
|
||||
endif ()
|
||||
get_filename_component(_runtime_lib_name "${_runtime_lib_path}" NAME)
|
||||
string(TOLOWER "${_runtime_lib_name}" _runtime_lib_name_key)
|
||||
list(FIND _runtime_lib_name_keys "${_runtime_lib_name_key}" _runtime_lib_name_index)
|
||||
if (NOT _runtime_lib_name_index EQUAL -1)
|
||||
message(FATAL_ERROR "add_mod: duplicate runtime library filename: ${_runtime_lib_name}")
|
||||
endif ()
|
||||
list(APPEND _runtime_lib_name_keys "${_runtime_lib_name_key}")
|
||||
list(APPEND _package_deps "${_runtime_lib_path}")
|
||||
list(APPEND _package_inputs "${_runtime_lib_path}")
|
||||
list(APPEND _lib_copy_cmd COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_runtime_lib_path}" "${_stage}/lib/${_lib_platform}/${_runtime_lib_name}")
|
||||
endforeach ()
|
||||
if (ARG_RES_DIR)
|
||||
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
|
||||
_mod_collect_assets(_res_deps "${_res_dir}")
|
||||
@@ -190,6 +227,7 @@ function(add_mod target_name)
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_PLATFORMS "${_lib_platform}")
|
||||
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}")
|
||||
set(_bundle_cmds
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods"
|
||||
@@ -230,7 +268,8 @@ endfunction()
|
||||
# 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> and the dylib into Frameworks/<id>.dylib.
|
||||
# - iOS/tvOS: assets into <app>/mods/<id>, the mod dylib into Frameworks/<id>.dylib,
|
||||
# and runtime libraries alongside it.
|
||||
# - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets.
|
||||
function(install_bundled_mods)
|
||||
get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS)
|
||||
@@ -239,6 +278,7 @@ function(install_bundled_mods)
|
||||
endif ()
|
||||
get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS)
|
||||
get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES)
|
||||
get_property(_lib_platforms GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_PLATFORMS)
|
||||
get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES)
|
||||
list(LENGTH _targets _count)
|
||||
math(EXPR _last "${_count} - 1")
|
||||
@@ -254,19 +294,30 @@ function(install_bundled_mods)
|
||||
list(GET _targets ${_i} _target)
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _stages ${_i} _stage)
|
||||
list(GET _lib_platforms ${_i} _lib_platform)
|
||||
list(GET _lib_names ${_i} _lib_name)
|
||||
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}"
|
||||
PATTERN "${_lib_name}" EXCLUDE)
|
||||
PATTERN "lib" EXCLUDE)
|
||||
install(PROGRAMS "$<TARGET_FILE:${_target}>"
|
||||
DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib")
|
||||
install(DIRECTORY "${_stage}/lib/${_lib_platform}/"
|
||||
DESTINATION "${_bundle_dir}/Frameworks"
|
||||
PATTERN "${_lib_name}" EXCLUDE)
|
||||
endforeach ()
|
||||
else ()
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _stages ${_i} _stage)
|
||||
list(GET _lib_platforms ${_i} _lib_platform)
|
||||
list(GET _lib_names ${_i} _lib_name)
|
||||
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
install(CODE "
|
||||
file(GLOB _mod_libs \"${_bundle_dir}/Contents/Resources/mods/${_id}/lib/${_lib_platform}/*\")
|
||||
foreach (_mod_lib IN LISTS _mod_libs)
|
||||
if (NOT IS_DIRECTORY \"\${_mod_lib}\")
|
||||
execute_process(COMMAND /usr/bin/codesign --force --sign - \"\${_mod_lib}\" COMMAND_ERROR_IS_FATAL ANY)
|
||||
endif ()
|
||||
endforeach ()")
|
||||
endforeach ()
|
||||
if (TARGET crashpad_handler)
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/MacOS/$<TARGET_FILE_NAME:crashpad_handler>\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
|
||||
+48
-13
@@ -22,7 +22,7 @@ function, read and write data fields, and hook the vast majority of game functio
|
||||
7. [Asset Overlays](#asset-overlays)
|
||||
8. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
9. [Error Handling](#error-handling)
|
||||
10. [Advanced: Exporting Services](#advanced-exporting-services)
|
||||
10. [Advanced](#advanced)
|
||||
|
||||
---
|
||||
|
||||
@@ -131,26 +131,30 @@ A service is a struct of C function pointers with a version header. You declare
|
||||
loader resolves it before your mod initializes:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(LogService, svc_log); // required, any minor version
|
||||
IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2
|
||||
IMPORT_SERVICE(LogService, svc_log); // required, latest minor version
|
||||
IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor version 0 (for backwards compatibility)
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that
|
||||
want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to
|
||||
check at **runtime** whether a specific function is available.
|
||||
functions to the end of the struct without breaking existing callers and simply bumps the minor version.
|
||||
|
||||
`IMPORT_SERVICE` and `IMPORT_OPTIONAL_SERVICE` require the latest minor version compiled against, making every field in
|
||||
the service safe to call. A mod can use `IMPORT_SERVICE_VERSION` (or its optional counterpart) with an older minor
|
||||
version to remain compatible with older Dusklight versions, then use `SERVICE_HAS` to check at runtime for fields added
|
||||
after that explicitly requested version.
|
||||
|
||||
The contract (see `include/mods/api.h` for the full version):
|
||||
|
||||
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
|
||||
error. No need to null check at call sites.
|
||||
- **Anything at or below the minor version you imported can be called unconditionally.**
|
||||
- **Anything at or below the minor version you imported can be called unconditionally.** The default macros import
|
||||
the service type's current minor version; the versioned macros explicitly override that minimum.
|
||||
- Optional imports may be null; check once in `mod_initialize`.
|
||||
- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null
|
||||
check.
|
||||
- Fields newer than your imported minor version must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a
|
||||
null check.
|
||||
|
||||
---
|
||||
|
||||
@@ -200,13 +204,13 @@ const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory
|
||||
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod
|
||||
```
|
||||
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-exporting-services).
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Exporting Services](#exporting-services).
|
||||
|
||||
**Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks,
|
||||
handles), watch other mods' lifecycle and drop what you hold for a mod when it detaches.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE_VERSION(HostService, svc_host, 1);
|
||||
IMPORT_SERVICE(HostService, svc_host);
|
||||
|
||||
void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data) {
|
||||
@@ -666,7 +670,9 @@ explicit results.
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Exporting Services
|
||||
## Advanced
|
||||
|
||||
### Exporting Services
|
||||
|
||||
Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a
|
||||
header both mods share:
|
||||
@@ -690,6 +696,7 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<MyModService> {
|
||||
static constexpr const char* id = MY_MOD_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = MY_MOD_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
```
|
||||
@@ -718,7 +725,7 @@ svc_my_mod->do_thing(mod_ctx, 42);
|
||||
The loader registers all exports before resolving any imports, so declaration order between mods doesn't matter. Note
|
||||
that the `ctx` a provider receives identifies the *calling* mod.
|
||||
|
||||
### Dependencies between mods
|
||||
#### Dependencies between mods
|
||||
|
||||
Service imports are also dependency declarations: the loader initializes mods in dependency order, so by the time your
|
||||
`mod_initialize` runs, every mod you import services from (required *or* optional) has already finished its own
|
||||
@@ -748,3 +755,31 @@ For services whose construction can't happen at static-init time, declare the ex
|
||||
and publish the pointer later via `svc_host->publish_service(...)`. Consumers can fetch services dynamically with
|
||||
`svc_host->get_service(...)`; prefer manifest imports whenever possible, since they give the loader dependency
|
||||
information and fail fast with good errors.
|
||||
|
||||
### Native Runtime Libraries
|
||||
|
||||
`RUNTIME_LIBRARIES` passed to `add_mod` are packaged beside the mod's native module in `lib/<platform>/`. Dusklight
|
||||
extracts the whole directory before loading the mod, so libraries linked by the mod resolve normally. The SDK links the
|
||||
mod itself with `$ORIGIN` on Linux and `@loader_path` on Apple platforms; runtime libraries with their own non-system
|
||||
dependencies must also be built with origin-relative lookup paths. On Windows, Dusklight uses an isolated DLL search
|
||||
rooted at this directory.
|
||||
|
||||
```cmake
|
||||
add_mod(my_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RUNTIME_LIBRARIES "${VENDOR_RUNTIME_LIBRARY}")
|
||||
```
|
||||
|
||||
SDKs that load plugins by directory can pass them the absolute runtime path from the current HostService:
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(HostService, svc_host);
|
||||
|
||||
const char* nativeDir = svc_host->native_dir(mod_ctx); // read-only
|
||||
```
|
||||
|
||||
Libraries loaded explicitly by the mod remain its responsibility: stop their threads and unload them during
|
||||
`mod_shutdown`. Do not write into `native_dir`; use `mod_dir` for writable state. Native library namespaces are
|
||||
process-wide on some platforms, so two mods cannot safely assume that incompatible libraries with the same filename
|
||||
will remain isolated.
|
||||
|
||||
@@ -176,6 +176,8 @@ struct LoadedMod {
|
||||
uint32_t cacheGeneration = 0;
|
||||
// Currently extracted native library, empty if none.
|
||||
std::string nativePath;
|
||||
// Read-only directory containing the current platform's main module and runtime libraries.
|
||||
std::string nativeDir;
|
||||
|
||||
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||
std::unique_ptr<NativeMod> native;
|
||||
@@ -225,7 +227,7 @@ private:
|
||||
// the next tick, by which point every per-frame entry into the mod should have returned.
|
||||
struct RetiredNative {
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::string path;
|
||||
std::string directory;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<LoadedMod>> m_mods;
|
||||
@@ -238,7 +240,8 @@ private:
|
||||
bool m_startupComplete = false;
|
||||
|
||||
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
|
||||
void load_native(LoadedMod& mod, const std::string& dllEntry);
|
||||
void load_native(LoadedMod& mod, const std::string& dllEntry,
|
||||
const std::vector<std::string>& runtimeEntries);
|
||||
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
|
||||
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
|
||||
void unload_native(LoadedMod& mod);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ extern "C" {
|
||||
#define MOD_EXTERN_C
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 6u
|
||||
#define MOD_ABI_VERSION 1u
|
||||
#define MOD_ERROR_MESSAGE_SIZE 512u
|
||||
|
||||
typedef struct ModContext ModContext;
|
||||
|
||||
@@ -42,7 +42,8 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
|
||||
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||
// must be checked against nullptr before use.
|
||||
// must be checked against nullptr before use. The unversioned macros use the latest minor version;
|
||||
// set an explicit version to target an older minor version for backwards compatibility.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
@@ -59,7 +60,9 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||
SERVICE_IMPORT_REQUIRED)
|
||||
|
||||
#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0)
|
||||
#define IMPORT_SERVICE(service_type, variable) \
|
||||
IMPORT_SERVICE_VERSION( \
|
||||
service_type, variable, ::dusk::mods::ServiceTraits<service_type>::minor_version)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||
@@ -67,7 +70,8 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
SERVICE_IMPORT_OPTIONAL)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE(service_type, variable) \
|
||||
IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0)
|
||||
IMPORT_OPTIONAL_SERVICE_VERSION( \
|
||||
service_type, variable, ::dusk::mods::ServiceTraits<service_type>::minor_version)
|
||||
|
||||
#define EXPORT_SERVICE_AS(instance, service_id_value) \
|
||||
MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##instance = { \
|
||||
|
||||
@@ -60,5 +60,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -103,5 +103,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -26,5 +26,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GAME_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -205,5 +205,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GFX_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -121,5 +121,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = HOOK_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
+12
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||
#define HOST_SERVICE_MAJOR 2u
|
||||
#define HOST_SERVICE_MINOR 0u
|
||||
#define HOST_SERVICE_MINOR 1u
|
||||
|
||||
/*
|
||||
* Ignore unknown values: later service minors may add events.
|
||||
@@ -91,6 +91,16 @@ typedef struct HostService {
|
||||
ModResult (*watch_mod_lifecycle)(
|
||||
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
|
||||
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
|
||||
|
||||
/*
|
||||
* Read-only directory containing this platform's packaged native runtime: the mod module
|
||||
* and any RUNTIME_LIBRARIES. The path is absolute and remains valid until mod_shutdown
|
||||
* returns. Libraries loaded dynamically from here are owned by the mod and must be unloaded
|
||||
* during mod_shutdown.
|
||||
*
|
||||
* Added in minor version 1.
|
||||
*/
|
||||
const char* (*native_dir)(ModContext* ctx);
|
||||
} HostService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -100,5 +110,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<HostService> {
|
||||
static constexpr const char* id = HOST_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = HOST_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -43,5 +43,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<LogService> {
|
||||
static constexpr const char* id = LOG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = LOG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -53,5 +53,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<OverlayService> {
|
||||
static constexpr const char* id = OVERLAY_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = OVERLAY_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -48,5 +48,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<ResourceService> {
|
||||
static constexpr const char* id = RESOURCE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = RESOURCE_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -84,5 +84,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<TextureService> {
|
||||
static constexpr const char* id = TEXTURE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = TEXTURE_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -280,5 +280,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<UiService> {
|
||||
static constexpr const char* id = UI_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = UI_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
|
||||
+148
-57
@@ -30,46 +30,51 @@ using namespace std::string_view_literals;
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if defined(_M_ARM64)
|
||||
static constexpr std::string_view k_nativeLibName = "windows-arm64.dll"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "windows-arm64"sv;
|
||||
#elif defined(_M_X64)
|
||||
static constexpr std::string_view k_nativeLibName = "windows-amd64.dll"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "windows-amd64"sv;
|
||||
#elif defined(_M_IX86)
|
||||
static constexpr std::string_view k_nativeLibName = "windows-x86.dll"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "windows-x86"sv;
|
||||
#else
|
||||
static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
static constexpr std::string_view k_nativePlatform = ""sv;
|
||||
#endif
|
||||
static constexpr std::string_view k_nativeLibName = "mod.dll"sv;
|
||||
#elif defined(__ANDROID__)
|
||||
#if defined(__aarch64__)
|
||||
static constexpr std::string_view k_nativeLibName = "android-aarch64.so"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "android-aarch64"sv;
|
||||
#elif defined(__x86_64__)
|
||||
static constexpr std::string_view k_nativeLibName = "android-x86_64.so"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "android-x86_64"sv;
|
||||
#else
|
||||
static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
static constexpr std::string_view k_nativePlatform = ""sv;
|
||||
#endif
|
||||
static constexpr std::string_view k_nativeLibName = "mod.so"sv;
|
||||
#elif defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#if TARGET_OS_IOS
|
||||
static constexpr std::string_view k_nativeLibName = "ios-arm64.dylib"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "ios-arm64"sv;
|
||||
#elif TARGET_OS_TV
|
||||
static constexpr std::string_view k_nativeLibName = "tvos-arm64.dylib"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv;
|
||||
#elif defined(__aarch64__)
|
||||
static constexpr std::string_view k_nativeLibName = "darwin-arm64.dylib"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "darwin-arm64"sv;
|
||||
#elif defined(__x86_64__)
|
||||
static constexpr std::string_view k_nativeLibName = "darwin-x86_64.dylib"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "darwin-x86_64"sv;
|
||||
#else
|
||||
static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
static constexpr std::string_view k_nativePlatform = ""sv;
|
||||
#endif
|
||||
static constexpr std::string_view k_nativeLibName = "mod.dylib"sv;
|
||||
#elif defined(__linux__)
|
||||
#if defined(__aarch64__)
|
||||
static constexpr std::string_view k_nativeLibName = "linux-aarch64.so"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv;
|
||||
#elif defined(__x86_64__)
|
||||
static constexpr std::string_view k_nativeLibName = "linux-x86_64.so"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv;
|
||||
#elif defined(__i386__)
|
||||
static constexpr std::string_view k_nativeLibName = "linux-x86.so"sv;
|
||||
static constexpr std::string_view k_nativePlatform = "linux-x86"sv;
|
||||
#else
|
||||
static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
static constexpr std::string_view k_nativePlatform = ""sv;
|
||||
#endif
|
||||
static constexpr std::string_view k_nativeLibName = "mod.so"sv;
|
||||
#else
|
||||
static constexpr std::string_view k_nativePlatform = ""sv;
|
||||
static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
#endif
|
||||
|
||||
@@ -77,6 +82,23 @@ namespace dusk::mods {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
ModLoader g_modLoader;
|
||||
constexpr std::string_view k_nativeLibDir = "lib/"sv;
|
||||
|
||||
class DirectoryRollback {
|
||||
public:
|
||||
~DirectoryRollback() {
|
||||
if (!mPath.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(mPath, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void set_path(std::filesystem::path path) { mPath = std::move(path); }
|
||||
void release() { mPath.clear(); }
|
||||
|
||||
private:
|
||||
std::filesystem::path mPath;
|
||||
};
|
||||
|
||||
std::unique_ptr<ModBundle> load_bundle(const std::filesystem::path& modPath, bool fromDir) {
|
||||
if (fromDir) {
|
||||
@@ -87,24 +109,49 @@ std::unique_ptr<ModBundle> load_bundle(const std::filesystem::path& modPath, boo
|
||||
}
|
||||
}
|
||||
|
||||
struct DllLocateResult {
|
||||
struct NativeLocateResult {
|
||||
std::string entry;
|
||||
std::vector<std::string> runtimeEntries;
|
||||
bool anyLibs = false;
|
||||
};
|
||||
|
||||
DllLocateResult locate_dll_in_bundle(ModBundle& bundle) {
|
||||
DllLocateResult result;
|
||||
NativeLocateResult locate_native_runtime(ModBundle& bundle) {
|
||||
NativeLocateResult result;
|
||||
const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform);
|
||||
const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName};
|
||||
for (const auto& name : bundle.getFileNames()) {
|
||||
if (name.find('/') != std::string::npos ||
|
||||
(!name.ends_with(".dll"sv) && !name.ends_with(".dylib"sv) && !name.ends_with(".so"sv)))
|
||||
{
|
||||
if (!name.starts_with(k_nativeLibDir)) {
|
||||
continue;
|
||||
}
|
||||
result.anyLibs = true;
|
||||
if (name == k_nativeLibName) {
|
||||
|
||||
const std::string_view libPath{
|
||||
name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()};
|
||||
const auto platformEnd = libPath.find('/');
|
||||
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))
|
||||
{
|
||||
result.anyLibs = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) {
|
||||
const std::string_view relativeName{
|
||||
name.data() + platformPrefix.size(), name.size() - platformPrefix.size()};
|
||||
if (!is_safe_resource_path(relativeName)) {
|
||||
continue;
|
||||
}
|
||||
result.runtimeEntries.push_back(name);
|
||||
}
|
||||
if (name == nativeEntry) {
|
||||
result.entry = name;
|
||||
}
|
||||
}
|
||||
std::ranges::sort(result.runtimeEntries);
|
||||
result.runtimeEntries.erase(
|
||||
std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()),
|
||||
result.runtimeEntries.end());
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
@@ -390,7 +437,7 @@ static std::string native_status_message(const NativeModStatus status) {
|
||||
case NativeModStatus::BuildDisabled:
|
||||
return "Code mods are disabled on this Dusklight build";
|
||||
case NativeModStatus::ModMissingPlatform:
|
||||
return fmt::format("Mod not supported on this platform ({})", k_nativeLibName);
|
||||
return fmt::format("Mod not supported on this platform ({})", k_nativePlatform);
|
||||
case NativeModStatus::ApiVersionMismatch:
|
||||
// TODO: differentiate whether mod or Dusklight is out of date
|
||||
return "Mod ABI version mismatch";
|
||||
@@ -425,7 +472,8 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod)
|
||||
return path;
|
||||
}
|
||||
|
||||
void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
void ModLoader::load_native(
|
||||
LoadedMod& mod, const std::string& dllEntry, const std::vector<std::string>& runtimeEntries) {
|
||||
if (!EnableCodeMods) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build");
|
||||
mod.nativeStatus = NativeModStatus::BuildDisabled;
|
||||
@@ -435,10 +483,19 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path cacheDir = m_cacheDir / mod.metadata.id;
|
||||
const fs::path scratchDir = cacheDir / "data";
|
||||
std::error_code ec;
|
||||
fs::create_directories(cacheDir, ec);
|
||||
fs::create_directories(scratchDir, ec);
|
||||
if (ec) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}",
|
||||
io::fs_path_to_string(scratchDir), ec.message());
|
||||
return;
|
||||
}
|
||||
mod.dir = io::fs_path_to_string(fs::absolute(scratchDir));
|
||||
|
||||
fs::path libPath;
|
||||
fs::path runtimeDir;
|
||||
DirectoryRollback runtimeDirRollback;
|
||||
if (mod.inPlace) {
|
||||
if (!dllEntry.empty()) {
|
||||
libPath = fs::path(mod.modPath) / dllEntry;
|
||||
@@ -450,6 +507,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
mod.nativeStatus = NativeModStatus::ModMissingPlatform;
|
||||
return;
|
||||
}
|
||||
runtimeDir = libPath.parent_path();
|
||||
} else {
|
||||
if (dllEntry.empty()) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
|
||||
@@ -458,34 +516,63 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generation-versioned filename: every dlopen gets a path it has never seen, so a reload
|
||||
// always yields a fresh image with fresh statics even if the previous dlclose did not
|
||||
// fully unmap the old one (TLS/ObjC pinning). The .cache dir is wiped on startup.
|
||||
const fs::path dllCachePath =
|
||||
cacheDir / fmt::format("{}.g{}{}", mod.metadata.id, ++mod.cacheGeneration,
|
||||
io::fs_path_to_string(fs::path(dllEntry).extension()));
|
||||
|
||||
std::vector<u8> dllData;
|
||||
try {
|
||||
dllData = mod.bundle->readFile(dllEntry);
|
||||
} catch (const std::exception& e) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}", dllEntry);
|
||||
// Every generation gets a new directory. The main module and all of its runtime
|
||||
// libraries therefore have fresh paths and can coexist with a previous generation
|
||||
// that is still unwinding after a reload.
|
||||
runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration);
|
||||
runtimeDirRollback.set_path(runtimeDir);
|
||||
fs::create_directories(runtimeDir, ec);
|
||||
if (ec) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
|
||||
"failed to create native runtime directory {}: {}",
|
||||
io::fs_path_to_string(runtimeDir), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream out(dllCachePath, std::ios::binary | std::ios::out);
|
||||
if (!out) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}",
|
||||
io::fs_path_to_string(dllCachePath));
|
||||
const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform);
|
||||
for (const auto& entry : runtimeEntries) {
|
||||
if (!entry.starts_with(platformPrefix)) {
|
||||
continue;
|
||||
}
|
||||
const std::string_view relativeName{
|
||||
entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()};
|
||||
if (!is_safe_resource_path(relativeName)) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
|
||||
"unsafe native runtime path '{}'; skipping", entry);
|
||||
return;
|
||||
}
|
||||
|
||||
out.write(reinterpret_cast<const char*>(dllData.data()),
|
||||
static_cast<std::streamsize>(dllData.size()));
|
||||
const fs::path outputPath = runtimeDir / fs::path{relativeName};
|
||||
fs::create_directories(outputPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
|
||||
"failed to create directory for {}: {}", entry, ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<u8> data;
|
||||
try {
|
||||
data = mod.bundle->readFile(entry);
|
||||
} catch (const std::exception& e) {
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
std::ofstream out(outputPath, std::ios::binary | std::ios::out);
|
||||
if (!out) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry);
|
||||
return;
|
||||
}
|
||||
out.write(reinterpret_cast<const char*>(data.data()),
|
||||
static_cast<std::streamsize>(data.size()));
|
||||
if (!out) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
libPath = dllCachePath;
|
||||
libPath = runtimeDir / fs::path{dllEntry}.filename();
|
||||
}
|
||||
|
||||
auto nativeMod = std::make_unique<NativeMod>();
|
||||
@@ -522,10 +609,11 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
}
|
||||
*nativeMod->contextSymbol = mod.context.get();
|
||||
|
||||
mod.dir = io::fs_path_to_string(fs::absolute(cacheDir));
|
||||
mod.nativePath = io::fs_path_to_string(fs::absolute(libPath));
|
||||
mod.nativeDir = io::fs_path_to_string(fs::absolute(runtimeDir));
|
||||
mod.native = std::move(nativeMod);
|
||||
mod.nativeStatus = NativeModStatus::Loaded;
|
||||
runtimeDirRollback.release();
|
||||
}
|
||||
|
||||
void ModLoader::unload_native(LoadedMod& mod) {
|
||||
@@ -533,16 +621,17 @@ void ModLoader::unload_native(LoadedMod& mod) {
|
||||
return;
|
||||
}
|
||||
// Deferred dlclose: this mod's code may still be on the stack below the current tick
|
||||
m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativePath)});
|
||||
m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)});
|
||||
mod.nativePath.clear();
|
||||
mod.nativeDir.clear();
|
||||
}
|
||||
|
||||
void ModLoader::drain_retired_natives() {
|
||||
for (auto& retired : m_retiredNatives) {
|
||||
retired.native.reset();
|
||||
if (!retired.path.empty()) {
|
||||
if (!retired.directory.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(retired.path, ec);
|
||||
std::filesystem::remove_all(retired.directory, ec);
|
||||
}
|
||||
}
|
||||
m_retiredNatives.clear();
|
||||
@@ -663,10 +752,10 @@ void ModLoader::try_load_mod(
|
||||
mod.cvarIsEnabled =
|
||||
std::make_unique<ConfigVar<bool>>(mod_enabled_cvar_name(mod.metadata.id), true);
|
||||
|
||||
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
|
||||
const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle);
|
||||
if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) {
|
||||
mod.nativeStatus = NativeModStatus::Unknown;
|
||||
load_native(mod, dllEntry);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
if (mod.nativeStatus != NativeModStatus::Loaded) {
|
||||
Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id);
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
@@ -992,14 +1081,14 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
|
||||
const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle);
|
||||
if (!anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) {
|
||||
mod.nativeStatus = NativeModStatus::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
mod.nativeStatus = NativeModStatus::Unknown;
|
||||
load_native(mod, dllEntry);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
if (mod.nativeStatus != NativeModStatus::Loaded) {
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
return false;
|
||||
@@ -1035,9 +1124,11 @@ bool ModLoader::reload_bundle(LoadedMod& mod) {
|
||||
mod.failureReason.clear();
|
||||
|
||||
ModManifestInfo newInfo;
|
||||
if (const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); anyLibs) {
|
||||
if (const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle);
|
||||
anyLibs)
|
||||
{
|
||||
mod.nativeStatus = NativeModStatus::Unknown;
|
||||
load_native(mod, dllEntry);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
if (mod.nativeStatus != NativeModStatus::Loaded) {
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
return false;
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
namespace {
|
||||
#if defined(_WIN32)
|
||||
void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return LoadLibraryW(p.wstring().c_str());
|
||||
return LoadLibraryExW(p.wstring().c_str(), nullptr,
|
||||
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
|
||||
}
|
||||
void* pl_dlsym(void* h, const char* name) {
|
||||
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
|
||||
@@ -55,11 +56,10 @@ std::string pl_dlerror() {
|
||||
return e ? e : "(unknown error)";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
NativeModule::NativeModule() noexcept : handle(nullptr) {
|
||||
}
|
||||
NativeModule::NativeModule() noexcept : handle(nullptr) {}
|
||||
|
||||
NativeModule::NativeModule(NativeModule&& other) noexcept {
|
||||
handle = other.handle;
|
||||
|
||||
@@ -63,6 +63,11 @@ const char* host_mod_dir(ModContext* context) {
|
||||
return mod != nullptr ? mod->dir.c_str() : "";
|
||||
}
|
||||
|
||||
const char* host_native_dir(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->nativeDir.c_str() : "";
|
||||
}
|
||||
|
||||
struct LifecycleWatcher {
|
||||
ModLifecycleFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
@@ -142,6 +147,7 @@ constinit HostService s_hostService{
|
||||
.mod_dir = host_mod_dir,
|
||||
.watch_mod_lifecycle = host_watch_mod_lifecycle,
|
||||
.unwatch_mod_lifecycle = host_unwatch_mod_lifecycle,
|
||||
.native_dir = host_native_dir,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user