Service lifecycle events

This commit is contained in:
Luke Street
2026-07-07 11:09:44 -06:00
parent 5c9c76cc0b
commit 9b75dc5fd2
8 changed files with 241 additions and 15 deletions
+20
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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");
+70 -3
View File
@@ -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
+6 -3
View File
@@ -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
+65 -4
View File
@@ -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) {
+34 -2
View File
@@ -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