Skip to content

Commit 5ee9eee

Browse files
feat(pj_plugins): expose DataSourceHandle::libraryOwner() for lazy-payload DSO lifetime (#116)
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) <noreply@anthropic.com>
1 parent a90a969 commit 5ee9eee

3 files changed

Lines changed: 17 additions & 4 deletions

File tree

pj_plugins/CLAUDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ submodule-internal modules; `pj_base` carries none).
3535
only `MessageParserPluginBase` + `object_ingest_policy.hpp` live here under
3636
`pj_plugins/sdk/`. (The `docs/ARCHITECTURE.md` §2 diagram is stale on this.)
3737
- **Handles keep the DSO mapped.** Every handle holds a `shared_ptr<void>`
38-
library token, so destroying/hot-reloading the loader cannot `dlclose` a live
39-
plugin. Dialog handles add a non-owning `borrowed()` form for source/toolbox
40-
embedded dialogs — those must not outlive the owning handle.
38+
library token (exposed via `libraryOwner()`), so destroying/hot-reloading the
39+
loader cannot `dlclose` a live plugin — and a lazy ObjectStore payload anchor,
40+
whose `release` fn is plugin code, can capture that token to stay safe past the
41+
handle's own lifetime. Dialog handles add a non-owning `borrowed()` form for
42+
source/toolbox embedded dialogs — those must not outlive the owning handle.
4143

4244
## Read deeper
4345
| For | Read |

pj_plugins/docs/ARCHITECTURE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,10 @@ Each family has a move-only RAII handle:
359359
- Destructor calls `vt->destroy(ctx)`.
360360
- Handles created by a loader retain a shared DSO owner; destroying or
361361
hot-reloading the loader/catalog entry cannot `dlclose` the plugin while
362-
live handles still call its vtable.
362+
live handles still call its vtable. `DataSourceHandle` exposes this token via
363+
`libraryOwner()` so it can be captured anywhere plugin code may outlive the
364+
handle — e.g. a lazy `ObjectStore` payload anchor whose `release` fn lives in
365+
the plugin `.so` — keeping the DSO mapped until that captor is gone too.
363366
- No copy, move-only semantics.
364367
- Methods delegate to vtable functions with the stored context pointer.
365368

pj_plugins/include/pj_plugins/host/data_source_handle.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ class DataSourceHandle {
7373
return vt_ != nullptr && ctx_ != nullptr;
7474
}
7575

76+
// The shared library token that keeps this plugin's DSO mapped. Capture a copy
77+
// anywhere a callback or payload anchor PRODUCED BY the plugin may outlive this
78+
// handle (e.g. lazy ObjectStore payloads): the .so must not be dlclosed while
79+
// plugin code (a payload anchor's release fn) can still run.
80+
[[nodiscard]] std::shared_ptr<void> libraryOwner() const {
81+
return library_owner_;
82+
}
83+
7684
[[nodiscard]] std::string manifest() const {
7785
return vt_->manifest_json != nullptr ? std::string(vt_->manifest_json) : std::string();
7886
}

0 commit comments

Comments
 (0)