Skip to content

Commit 2d85541

Browse files
facontidavideclaude
andcommitted
feat(sdk): static plugin registry for WASM / no-dlopen builds
Add a static-linking path alongside the dlopen one, for Qt-for-WebAssembly (and any single-binary build) where many plugins are linked into the host: - PJ_{DATA_SOURCE,TOOLBOX,MESSAGE_PARSER}_PLUGIN, under #ifdef PJ_STATIC_PLUGINS, emit a uniquely class-keyed `pj_static_get_*_vtable_<Class>()` instead of the fixed `extern "C" PJ_get_*_vtable` symbol (which would collide across plugins in one binary). The C-ABI vtable contract is unchanged. - {DataSource,MessageParser,Toolbox}Library::loadStatic(vtable): wrap a statically-linked vtable with a sentinel shared_ptr owner (no DSO), so valid()/createHandle() work unchanged. - PluginRuntimeCatalog::registerStatic{DataSource,MessageParser,Toolbox}(vtable): read the embedded manifest JSON and register without a disk scan. - CMakeLists: append -Wno-error under EMSCRIPTEN (wasm32 size_t is 32-bit). Pure additions (+243/-0); the dlopen path and desktop behavior are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 18b8c03 commit 2d85541

12 files changed

Lines changed: 243 additions & 0 deletions

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ else()
3535
-Wall -Wextra -Werror -Wshadow -Wnon-virtual-dtor -Wold-style-cast
3636
-Wcast-qual -Wconversion -Woverloaded-virtual -Wpedantic
3737
)
38+
# On wasm32 size_t is 32-bit, so many uint64_t->size_t conversions trip
39+
# -Wshorten-64-to-32 / -Wsign-conversion that never fire on 64-bit desktop
40+
# (see SUSTAINABILITY.md T0-3). Keep the warnings visible but don't fail the
41+
# build for the WASM prototype. TODO: make the ABI/size types 32-bit-clean.
42+
if(EMSCRIPTEN)
43+
list(APPEND PJ_WARNING_FLAGS -Wno-error)
44+
endif()
3845
endif()
3946

4047
# ---------------------------------------------------------------------------

pj_base/include/pj_base/sdk/data_source_plugin_base.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,26 @@ class DataSourcePluginBase {
250250
manifest); \
251251
return vt; \
252252
}
253+
254+
// --- Static-link variant (WASM / no dlopen) ----------------------------------
255+
// With PJ_STATIC_PLUGINS the plugin is linked into the host binary. The fixed
256+
// `extern "C" PJ_get_data_source_vtable` symbol would collide across plugins in
257+
// one binary, so emit a uniquely-named (class-keyed) C++ function instead. The
258+
// host's static registry declares these extern and passes them to
259+
// PluginRuntimeCatalog::registerStaticDataSource().
260+
#ifdef PJ_STATIC_PLUGINS
261+
#undef PJ_DATA_SOURCE_PLUGIN
262+
#define PJ_DATA_SOURCE_PLUGIN(ClassName, manifest) \
263+
const PJ_data_source_vtable_t* pj_static_get_data_source_vtable_##ClassName() noexcept { \
264+
static const PJ_data_source_vtable_t* vt = PJ::DataSourcePluginBase::vtableWithCreate( \
265+
[]() noexcept -> void* { \
266+
try { \
267+
return new ClassName(); \
268+
} catch (...) { \
269+
return nullptr; \
270+
} \
271+
}, \
272+
manifest); \
273+
return vt; \
274+
}
275+
#endif

pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,21 @@ class ToolboxPluginBase {
250250
manifest); \
251251
return vt; \
252252
}
253+
254+
// --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp
255+
#ifdef PJ_STATIC_PLUGINS
256+
#undef PJ_TOOLBOX_PLUGIN
257+
#define PJ_TOOLBOX_PLUGIN(ClassName, manifest) \
258+
const PJ_toolbox_vtable_t* pj_static_get_toolbox_vtable_##ClassName() noexcept { \
259+
static const PJ_toolbox_vtable_t* vt = PJ::ToolboxPluginBase::vtableWithCreate( \
260+
[]() noexcept -> void* { \
261+
try { \
262+
return new ClassName(); \
263+
} catch (...) { \
264+
return nullptr; \
265+
} \
266+
}, \
267+
manifest); \
268+
return vt; \
269+
}
270+
#endif

pj_plugins/include/pj_plugins/host/data_source_library.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class DataSourceLibrary {
5151
/// Load a plugin from @p path. Returns an error string on failure.
5252
[[nodiscard]] static Expected<DataSourceLibrary> load(std::string_view path);
5353

54+
/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
55+
/// @p vtable must have static storage duration (valid for the program lifetime).
56+
[[nodiscard]] static Expected<DataSourceLibrary> loadStatic(const PJ_data_source_vtable_t* vtable);
57+
5458
/// True if the library was loaded and the vtable resolved successfully.
5559
[[nodiscard]] bool valid() const {
5660
return handle_ != nullptr && vtable_ != nullptr;

pj_plugins/include/pj_plugins/host/message_parser_library.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class MessageParserLibrary {
5151
/// Load a plugin from @p path. Returns an error string on failure.
5252
[[nodiscard]] static Expected<MessageParserLibrary> load(std::string_view path);
5353

54+
/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
55+
/// @p vtable must have static storage duration (valid for the program lifetime).
56+
[[nodiscard]] static Expected<MessageParserLibrary> loadStatic(const PJ_message_parser_vtable_t* vtable);
57+
5458
/// True if the library was loaded and the vtable resolved successfully.
5559
[[nodiscard]] bool valid() const {
5660
return handle_ != nullptr && vtable_ != nullptr;

pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ class PluginRuntimeCatalog {
8181
// Reconciles loaded state with disk and returns true if it changed.
8282
[[nodiscard]] bool reload();
8383

84+
// --- Static plugin registration (WASM/static builds; no dlopen) -------------
85+
// Register a statically-linked plugin by its vtable. The vtable must outlive
86+
// this catalog (static storage duration); the manifest is read from
87+
// vtable->manifest_json. Returns false (and reports a diagnostic) on failure.
88+
bool registerStaticDataSource(const PJ_data_source_vtable_t* vtable);
89+
bool registerStaticMessageParser(const PJ_message_parser_vtable_t* vtable);
90+
bool registerStaticToolbox(const PJ_toolbox_vtable_t* vtable);
91+
8492
// Returns loaded DataSource plugins.
8593
[[nodiscard]] const std::vector<RuntimeDataSourcePlugin>& dataSources() const {
8694
return data_sources_;

pj_plugins/include/pj_plugins/host/toolbox_library.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class ToolboxLibrary {
5151
/// Load a plugin from @p path. Returns an error string on failure.
5252
[[nodiscard]] static Expected<ToolboxLibrary> load(std::string_view path);
5353

54+
/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
55+
/// @p vtable must have static storage duration (valid for the program lifetime).
56+
[[nodiscard]] static Expected<ToolboxLibrary> loadStatic(const PJ_toolbox_vtable_t* vtable);
57+
5458
/// True if the library was loaded and the vtable resolved successfully.
5559
[[nodiscard]] bool valid() const {
5660
return handle_ != nullptr && vtable_ != nullptr;

pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,21 @@ class MessageParserPluginBase {
376376
manifest); \
377377
return vt; \
378378
}
379+
380+
// --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp
381+
#ifdef PJ_STATIC_PLUGINS
382+
#undef PJ_MESSAGE_PARSER_PLUGIN
383+
#define PJ_MESSAGE_PARSER_PLUGIN(ClassName, manifest) \
384+
const PJ_message_parser_vtable_t* pj_static_get_message_parser_vtable_##ClassName() noexcept {\
385+
static const PJ_message_parser_vtable_t* vt = PJ::MessageParserPluginBase::vtableWithCreate(\
386+
[]() noexcept -> void* { \
387+
try { \
388+
return new ClassName(); \
389+
} catch (...) { \
390+
return nullptr; \
391+
} \
392+
}, \
393+
manifest); \
394+
return vt; \
395+
}
396+
#endif

pj_plugins/src/data_source_library.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@ Expected<DataSourceLibrary> DataSourceLibrary::load(std::string_view path) {
7070
return DataSourceLibrary(std::move(handle), vtable, std::string(path));
7171
}
7272

73+
Expected<DataSourceLibrary> DataSourceLibrary::loadStatic(const PJ_data_source_vtable_t* vtable) {
74+
if (vtable == nullptr) {
75+
return unexpected("static DataSource vtable is null");
76+
}
77+
if (vtable->protocol_version != PJ_DATA_SOURCE_PROTOCOL_VERSION) {
78+
return unexpected("DataSource protocol version mismatch");
79+
}
80+
if (vtable->struct_size < PJ_DATA_SOURCE_MIN_VTABLE_SIZE) {
81+
return unexpected("DataSource vtable smaller than v4.0 baseline");
82+
}
83+
if (auto status = detail::validateRequiredSlots(vtable); !status) {
84+
return unexpected(status.error());
85+
}
86+
// Statically linked: no DSO to keep alive, but valid()/createHandle() expect a
87+
// non-null owner. Use a sentinel shared_ptr with a no-op deleter.
88+
static char anchor = 0;
89+
std::shared_ptr<void> handle(&anchor, [](void*) {});
90+
return DataSourceLibrary(std::move(handle), vtable, "static://");
91+
}
92+
7393
Expected<const PJ_dialog_vtable_t*> DataSourceLibrary::resolveDialogVtable() const {
7494
auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable");
7595
if (!sym) {

pj_plugins/src/message_parser_library.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ Expected<MessageParserLibrary> MessageParserLibrary::load(std::string_view path)
6868
return MessageParserLibrary(std::move(handle), vtable, std::string(path));
6969
}
7070

71+
Expected<MessageParserLibrary> MessageParserLibrary::loadStatic(const PJ_message_parser_vtable_t* vtable) {
72+
if (vtable == nullptr) {
73+
return unexpected("static MessageParser vtable is null");
74+
}
75+
if (vtable->protocol_version != PJ_MESSAGE_PARSER_PROTOCOL_VERSION) {
76+
return unexpected("MessageParser protocol version mismatch");
77+
}
78+
if (vtable->struct_size < PJ_MESSAGE_PARSER_MIN_VTABLE_SIZE) {
79+
return unexpected("MessageParser vtable smaller than v4.0 baseline");
80+
}
81+
if (auto status = detail::validateRequiredSlots(vtable); !status) {
82+
return unexpected(status.error());
83+
}
84+
static char anchor = 0;
85+
std::shared_ptr<void> handle(&anchor, [](void*) {});
86+
return MessageParserLibrary(std::move(handle), vtable, "static://");
87+
}
88+
7189
Expected<const PJ_dialog_vtable_t*> MessageParserLibrary::resolveDialogVtable() const {
7290
auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable");
7391
if (!sym) {

0 commit comments

Comments
 (0)