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
+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