mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-09 04:30:49 -04:00
Service lifecycle events
This commit is contained in:
@@ -156,6 +156,26 @@ svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disa
|
||||
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-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);
|
||||
|
||||
void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data) {
|
||||
if (event == MOD_LIFECYCLE_DETACHED) {
|
||||
drop_state_for(subject); // same ModContext* the subject passed into your service
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t watch = 0;
|
||||
svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
|
||||
```
|
||||
|
||||
`MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and
|
||||
every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead.
|
||||
|
||||
---
|
||||
|
||||
## Runtime Lifecycle
|
||||
|
||||
@@ -216,8 +216,8 @@ private:
|
||||
// Registers exports (if needed), resolves imports and runs mod_initialize.
|
||||
// Returns whether the mod ended up active; failures go through fail_mod.
|
||||
bool activate_mod(LoadedMod& mod);
|
||||
// Runs mod_shutdown (if needed), removes the mod's services, and unloads the native lib.
|
||||
// Must only run with no mod code on the stack (startup, shutdown, or top of tick).
|
||||
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
|
||||
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
|
||||
void deactivate_mod(LoadedMod& mod);
|
||||
void init_services();
|
||||
bool register_static_service_exports(LoadedMod& mod);
|
||||
|
||||
+34
-1
@@ -9,7 +9,28 @@
|
||||
|
||||
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||
#define HOST_SERVICE_MAJOR 1u
|
||||
#define HOST_SERVICE_MINOR 0u
|
||||
#define HOST_SERVICE_MINOR 1u
|
||||
|
||||
/*
|
||||
* Ignore unknown values: later service minors may add events.
|
||||
*/
|
||||
typedef enum ModLifecycleEvent {
|
||||
/*
|
||||
* The subject mod is gone: its mod_shutdown has run (when it initialized at all) and
|
||||
* every service has already dropped the state it held for it. The subject's library is
|
||||
* still mapped, so pointers into it are valid to compare against, but they must not be
|
||||
* called or dereferenced after the callback returns. Drop everything keyed to the
|
||||
* mod: callbacks it registered, its ModContext*, state indexed by it.
|
||||
*/
|
||||
MOD_LIFECYCLE_DETACHED = 0,
|
||||
} ModLifecycleEvent;
|
||||
|
||||
/*
|
||||
* ctx is the watching mod's own context; subject identifies the mod the event is about.
|
||||
* subject_id is valid only for the duration of the call.
|
||||
*/
|
||||
typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data);
|
||||
|
||||
typedef struct HostService {
|
||||
ServiceHeader header;
|
||||
@@ -50,6 +71,18 @@ typedef struct HostService {
|
||||
* and reload within a session, but the directory is wiped at game startup.
|
||||
*/
|
||||
const char* (*mod_dir)(ModContext* ctx);
|
||||
|
||||
/*
|
||||
* Observe other mods' lifecycle events. Any mod whose service hands out per-caller state
|
||||
* (registrations, callbacks, handles) should watch for MOD_LIFECYCLE_DETACHED and drop what it
|
||||
* holds for the subject.
|
||||
*
|
||||
* Callbacks fire on the game thread at a lifecycle safe point (never mid-frame), for
|
||||
* every mod but the watcher itself (use mod_shutdown for self-cleanup).
|
||||
*/
|
||||
ModResult (*watch_mod_lifecycle)(
|
||||
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
|
||||
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
|
||||
} HostService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -599,6 +599,8 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
warn_unpublished_deferred_exports(mod);
|
||||
|
||||
if (!mod.active) {
|
||||
// Failed initialization may have left hooks or other service state behind
|
||||
svc::modules_mod_detached(mod);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -624,6 +626,7 @@ void ModLoader::deactivate_mod(LoadedMod& mod) {
|
||||
svc::remove_services_for_provider(mod);
|
||||
mod.servicesRegistered = false;
|
||||
}
|
||||
svc::modules_mod_detached(mod);
|
||||
unload_native(mod);
|
||||
|
||||
mod.active = false;
|
||||
@@ -739,6 +742,8 @@ void ModLoader::init() {
|
||||
}
|
||||
}
|
||||
|
||||
svc::modules_lifecycle_applied();
|
||||
|
||||
auto active = std::ranges::count_if(mods(), [](const LoadedMod& m) { return m.active; });
|
||||
Log.info("{}/{} mod(s) active", active, m_mods.size());
|
||||
|
||||
@@ -1040,11 +1045,14 @@ void ModLoader::apply_pending_requests() {
|
||||
apply_lifecycle_change(*mod, request.kind == RequestKind::Reload);
|
||||
}
|
||||
|
||||
svc::modules_lifecycle_applied();
|
||||
|
||||
auto active = std::ranges::count_if(mods(), [](const LoadedMod& m) { return m.active; });
|
||||
Log.info("{}/{} mod(s) active", active, m_mods.size());
|
||||
}
|
||||
|
||||
void ModLoader::tick() {
|
||||
svc::modules_frame_begin();
|
||||
apply_pending_requests();
|
||||
|
||||
for (auto& mod : mods()) {
|
||||
@@ -1064,6 +1072,7 @@ void ModLoader::tick() {
|
||||
}
|
||||
}
|
||||
|
||||
svc::modules_frame_end();
|
||||
config_flush_if_dirty(false);
|
||||
}
|
||||
|
||||
@@ -1080,6 +1089,7 @@ void ModLoader::shutdown() {
|
||||
|
||||
m_mods.clear();
|
||||
drain_retired_natives();
|
||||
svc::modules_shutdown();
|
||||
clear_services();
|
||||
config_flush_if_dirty(true);
|
||||
Log.info("all mods unloaded");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
@@ -56,6 +60,63 @@ const char* host_mod_dir(ModContext* context) {
|
||||
return mod != nullptr ? mod->dir.c_str() : "";
|
||||
}
|
||||
|
||||
struct LifecycleWatcher {
|
||||
uint64_t handle = 0;
|
||||
LoadedMod* owner = nullptr;
|
||||
ModLifecycleFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
std::vector<LifecycleWatcher> s_watchers;
|
||||
uint64_t s_nextWatchHandle = 1;
|
||||
|
||||
ModResult host_watch_mod_lifecycle(
|
||||
ModContext* context, ModLifecycleFn fn, void* userData, uint64_t* outHandle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || fn == nullptr || outHandle == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto handle = s_nextWatchHandle++;
|
||||
s_watchers.push_back({handle, mod, fn, userData});
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult host_unwatch_mod_lifecycle(ModContext* context, const uint64_t handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto erased = std::erase_if(s_watchers,
|
||||
[&](const LifecycleWatcher& w) { return w.handle == handle && w.owner == mod; });
|
||||
return erased != 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
void host_mod_detached(LoadedMod& mod) {
|
||||
// The subject's own watches go first: a mod is never notified about its own teardown.
|
||||
std::erase_if(s_watchers, [&](const LifecycleWatcher& w) { return w.owner == &mod; });
|
||||
|
||||
// Iterate a snapshot: callbacks may watch/unwatch, and a failing callback erases the
|
||||
// failing mod's services.
|
||||
const auto snapshot = s_watchers;
|
||||
for (const auto& watcher : snapshot) {
|
||||
const bool alive = std::ranges::any_of(
|
||||
s_watchers, [&](const LifecycleWatcher& w) { return w.handle == watcher.handle; });
|
||||
if (!alive) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
watcher.fn(watcher.owner->context.get(), mod.context.get(), mod.metadata.id.c_str(),
|
||||
MOD_LIFECYCLE_DETACHED, watcher.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*watcher.owner, MOD_ERROR,
|
||||
fmt::format("exception in mod lifecycle callback: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*watcher.owner, MOD_ERROR, "unknown exception in mod lifecycle callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr HostService s_hostService{
|
||||
.header = SERVICE_HEADER(HostService, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR),
|
||||
.get_service = host_get_service,
|
||||
@@ -65,12 +126,18 @@ constexpr HostService s_hostService{
|
||||
.mod_name = host_mod_name,
|
||||
.mod_version = host_mod_version,
|
||||
.mod_dir = host_mod_dir,
|
||||
.watch_mod_lifecycle = host_watch_mod_lifecycle,
|
||||
.unwatch_mod_lifecycle = host_unwatch_mod_lifecycle,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const HostService& host_service() {
|
||||
return s_hostService;
|
||||
}
|
||||
constinit const ServiceModule g_hostModule{
|
||||
.id = HOST_SERVICE_ID,
|
||||
.majorVersion = HOST_SERVICE_MAJOR,
|
||||
.minorVersion = HOST_SERVICE_MINOR,
|
||||
.service = &s_hostService,
|
||||
.modDetached = host_mod_detached,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -57,8 +57,11 @@ constexpr LogService s_logService{
|
||||
|
||||
} // namespace
|
||||
|
||||
const LogService& log_service() {
|
||||
return s_logService;
|
||||
}
|
||||
constinit const ServiceModule g_logModule{
|
||||
.id = LOG_SERVICE_ID,
|
||||
.majorVersion = LOG_SERVICE_MAJOR,
|
||||
.minorVersion = LOG_SERVICE_MINOR,
|
||||
.service = &s_logService,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include <ranges>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
std::unordered_map<std::string, ServiceRecord> s_services;
|
||||
std::vector<const ServiceModule*> s_modules;
|
||||
|
||||
std::string service_key(std::string_view id, const uint16_t majorVersion) {
|
||||
std::string key{id};
|
||||
@@ -130,6 +133,60 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m
|
||||
|
||||
void clear_services() {
|
||||
s_services.clear();
|
||||
s_modules.clear();
|
||||
}
|
||||
|
||||
ModResult register_module(const ServiceModule& module) {
|
||||
const auto result = register_service(
|
||||
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
s_modules.push_back(&module);
|
||||
if (module.initialize != nullptr) {
|
||||
module.initialize();
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void modules_mod_detached(LoadedMod& mod) {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->modDetached != nullptr) {
|
||||
module->modDetached(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_lifecycle_applied() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->lifecycleApplied != nullptr) {
|
||||
module->lifecycleApplied();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_frame_begin() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->frameBegin != nullptr) {
|
||||
module->frameBegin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_frame_end() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->frameEnd != nullptr) {
|
||||
module->frameEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_shutdown() {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->shutdown != nullptr) {
|
||||
module->shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -138,10 +195,14 @@ namespace dusk::mods {
|
||||
|
||||
void ModLoader::init_services() {
|
||||
svc::clear_services();
|
||||
svc::register_service(HOST_SERVICE_ID, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR,
|
||||
&svc::host_service(), nullptr, false);
|
||||
svc::register_service(
|
||||
LOG_SERVICE_ID, LOG_SERVICE_MAJOR, LOG_SERVICE_MINOR, &svc::log_service(), nullptr, false);
|
||||
for (const auto* module :
|
||||
{
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
}
|
||||
}
|
||||
|
||||
bool ModLoader::register_static_service_exports(LoadedMod& mod) {
|
||||
|
||||
@@ -18,6 +18,31 @@ struct ServiceRecord {
|
||||
bool deferred = false;
|
||||
};
|
||||
|
||||
// A host service and its lifecycle hooks. Every hook is optional. Frame and lifecycle hooks run in
|
||||
// registration order, modDetached in reverse registration order.
|
||||
struct ServiceModule {
|
||||
const char* id = nullptr;
|
||||
uint16_t majorVersion = 0;
|
||||
uint16_t minorVersion = 0;
|
||||
const void* service = nullptr;
|
||||
|
||||
// One-time setup, at registration (ModLoader::init_services).
|
||||
void (*initialize)() = nullptr;
|
||||
// A mod is going away (deactivation or failed activation): drop all state held for it.
|
||||
// Runs after the mod's mod_shutdown and before its library unloads, so pointers into
|
||||
// the mod are still valid but must not be called.
|
||||
void (*modDetached)(LoadedMod& mod) = nullptr;
|
||||
// A batch of (de)activations finished applying: startup, and runtime enable/disable/
|
||||
// reload requests. The set of active mods is stable when this runs.
|
||||
void (*lifecycleApplied)() = nullptr;
|
||||
// Top of ModLoader::tick, before pending lifecycle requests apply.
|
||||
void (*frameBegin)() = nullptr;
|
||||
// End of ModLoader::tick, after every mod_update.
|
||||
void (*frameEnd)() = nullptr;
|
||||
// ModLoader::shutdown, after every mod has deactivated.
|
||||
void (*shutdown)() = nullptr;
|
||||
};
|
||||
|
||||
bool valid_service_id(const char* serviceId);
|
||||
ModResult register_service(const char* serviceId, uint16_t majorVersion, uint16_t minorVersion,
|
||||
const void* service, LoadedMod* provider, bool deferred);
|
||||
@@ -30,7 +55,14 @@ const ServiceRecord* find_service(
|
||||
const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion);
|
||||
void clear_services();
|
||||
|
||||
const HostService& host_service();
|
||||
const LogService& log_service();
|
||||
ModResult register_module(const ServiceModule& module);
|
||||
void modules_mod_detached(LoadedMod& mod);
|
||||
void modules_lifecycle_applied();
|
||||
void modules_frame_begin();
|
||||
void modules_frame_end();
|
||||
void modules_shutdown();
|
||||
|
||||
extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
Reference in New Issue
Block a user