From 34f2d8c23f2d9104e621ab756835be1541f9b9af Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Mon, 8 Jun 2026 19:19:21 +0200 Subject: [PATCH] feat(pj_plugins): expose DataSourceHandle::libraryOwner() for lazy-payload DSO lifetime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lazy ObjectStore payload anchor carries the producing plugin's release fn (plugin .so code) and can outlive the DataSourceHandle that loaded the plugin — a cached entry surviving a mid-session catalog reload or app close. The host captures the handle's DSO keepalive token into each such anchor so the .so is not dlclosed while plugin code can still run. Expose the existing library_owner_ token via a libraryOwner() accessor for that purpose. Host-side header only (pj_plugins/host/), invisible to plugins — no plugin ABI or pj_base baseline change, so no version bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- pj_plugins/CLAUDE.md | 8 +++++--- pj_plugins/docs/ARCHITECTURE.md | 5 ++++- pj_plugins/include/pj_plugins/host/data_source_handle.hpp | 8 ++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pj_plugins/CLAUDE.md b/pj_plugins/CLAUDE.md index af0f495e..77ce188c 100644 --- a/pj_plugins/CLAUDE.md +++ b/pj_plugins/CLAUDE.md @@ -35,9 +35,11 @@ submodule-internal modules; `pj_base` carries none). only `MessageParserPluginBase` + `object_ingest_policy.hpp` live here under `pj_plugins/sdk/`. (The `docs/ARCHITECTURE.md` §2 diagram is stale on this.) - **Handles keep the DSO mapped.** Every handle holds a `shared_ptr` - library token, so destroying/hot-reloading the loader cannot `dlclose` a live - plugin. Dialog handles add a non-owning `borrowed()` form for source/toolbox - embedded dialogs — those must not outlive the owning handle. + library token (exposed via `libraryOwner()`), so destroying/hot-reloading the + loader cannot `dlclose` a live plugin — and a lazy ObjectStore payload anchor, + whose `release` fn is plugin code, can capture that token to stay safe past the + handle's own lifetime. Dialog handles add a non-owning `borrowed()` form for + source/toolbox embedded dialogs — those must not outlive the owning handle. ## Read deeper | For | Read | diff --git a/pj_plugins/docs/ARCHITECTURE.md b/pj_plugins/docs/ARCHITECTURE.md index 2099963e..b0964175 100644 --- a/pj_plugins/docs/ARCHITECTURE.md +++ b/pj_plugins/docs/ARCHITECTURE.md @@ -359,7 +359,10 @@ Each family has a move-only RAII handle: - Destructor calls `vt->destroy(ctx)`. - Handles created by a loader retain a shared DSO owner; destroying or hot-reloading the loader/catalog entry cannot `dlclose` the plugin while - live handles still call its vtable. + live handles still call its vtable. `DataSourceHandle` exposes this token via + `libraryOwner()` so it can be captured anywhere plugin code may outlive the + handle — e.g. a lazy `ObjectStore` payload anchor whose `release` fn lives in + the plugin `.so` — keeping the DSO mapped until that captor is gone too. - No copy, move-only semantics. - Methods delegate to vtable functions with the stored context pointer. diff --git a/pj_plugins/include/pj_plugins/host/data_source_handle.hpp b/pj_plugins/include/pj_plugins/host/data_source_handle.hpp index 02d5c860..feb7dcc7 100644 --- a/pj_plugins/include/pj_plugins/host/data_source_handle.hpp +++ b/pj_plugins/include/pj_plugins/host/data_source_handle.hpp @@ -73,6 +73,14 @@ class DataSourceHandle { return vt_ != nullptr && ctx_ != nullptr; } + // The shared library token that keeps this plugin's DSO mapped. Capture a copy + // anywhere a callback or payload anchor PRODUCED BY the plugin may outlive this + // handle (e.g. lazy ObjectStore payloads): the .so must not be dlclosed while + // plugin code (a payload anchor's release fn) can still run. + [[nodiscard]] std::shared_ptr libraryOwner() const { + return library_owner_; + } + [[nodiscard]] std::string manifest() const { return vt_->manifest_json != nullptr ? std::string(vt_->manifest_json) : std::string(); }