Skip to content

Commit 6e91268

Browse files
feat(sdk): static plugin registry + static dialog for no-dlopen (WASM) builds (#132)
* 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> * feat(sdk): static PJ_DIALOG_PLUGIN variant (no-collision, WASM) Under PJ_STATIC_PLUGINS many plugins link into one binary; the fixed `extern "C" PJ_get_dialog_vtable` symbol (+ the ABI-version export) PJ_DIALOG_PLUGIN emits would collide across them at link. Emit a uniquely class-keyed pj_static_get_dialog_vtable_<Class>() instead (keeping the dialogVtableFor<Class> specialization), completing the static-export pattern the other three family macros already follow. The static registry resolves dialogs by this getter, not the dlsym entry point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(release): bump version to 0.11.0 MINOR: backward-compatible API additions for static (no-dlopen) plugin linking — the static plugin registry + static dialog variant used by the Qt-for-WebAssembly build. All already-built plugins keep working unchanged. Also normalize clang-format (backslash-continuation alignment) on the static-plugin macro headers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(sdk): collapse the three registerStatic* bodies into one helper registerStaticDataSource/MessageParser/Toolbox were near-verbatim copies differing only in library type, runtime struct, family label, and the one family-specific field. Extract a `registerStaticPlugin<>` helper that does the shared load-static + parse-manifest + fill-common-fields + push body; each method now passes only the family-specific fill lambda. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 18b8c03 commit 6e91268

15 files changed

Lines changed: 262 additions & 4 deletions

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
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
# ---------------------------------------------------------------------------
@@ -111,7 +118,7 @@ endif()
111118
if(PJ_INSTALL_SDK)
112119
include(CMakePackageConfigHelpers)
113120

114-
set(PJ_PACKAGE_VERSION "0.10.0")
121+
set(PJ_PACKAGE_VERSION "0.11.0")
115122
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk)
116123

117124
install(EXPORT plotjuggler_sdkTargets

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
77
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
88
9-
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.10.0` and then:
9+
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.11.0` and then:
1010
1111
find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk)
1212
target_link_libraries(my_plugin PRIVATE plotjuggler_sdk::plugin_sdk)
@@ -30,7 +30,7 @@
3030

3131
class PlotjugglerSdkConan(ConanFile):
3232
name = "plotjuggler_sdk"
33-
version = "0.10.0"
33+
version = "0.11.0"
3434
# Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE.
3535
license = "Apache-2.0"
3636
url = "https://github.com/PlotJuggler/plotjuggler_sdk"

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/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_base.hpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,33 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept {
303303
return PJ_get_dialog_vtable(); \
304304
} \
305305
}
306+
307+
// --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp.
308+
// Many statically-linked plugins each carry a PJ_DIALOG_PLUGIN, so the fixed
309+
// `extern "C" PJ_get_dialog_vtable` symbol (and the ABI-version export) would
310+
// collide at link. The static registry never resolves a dialog via dlsym (the
311+
// host short-circuits the dialog path on WASM), so emit only a uniquely
312+
// class-keyed getter — kept so dialogVtableFor<ClassName>() still resolves for
313+
// any in-binary caller.
314+
#ifdef PJ_STATIC_PLUGINS
315+
#undef PJ_DIALOG_PLUGIN_WITH_MANIFEST
316+
#define PJ_DIALOG_PLUGIN_WITH_MANIFEST(ClassName, ManifestJson) \
317+
inline const PJ_dialog_vtable_t* pj_static_get_dialog_vtable_##ClassName() noexcept { \
318+
static const PJ_dialog_vtable_t* vt = PJ::DialogPluginBase::vtableWithCreate( \
319+
[]() noexcept -> void* { \
320+
try { \
321+
return static_cast<PJ::DialogPluginBase*>(new ClassName()); \
322+
} catch (...) { \
323+
return nullptr; \
324+
} \
325+
}, \
326+
ManifestJson); \
327+
return vt; \
328+
} \
329+
namespace PJ { \
330+
template <> \
331+
[[maybe_unused]] inline const PJ_dialog_vtable_t* dialogVtableFor<ClassName>() noexcept { \
332+
return pj_static_get_dialog_vtable_##ClassName(); \
333+
} \
334+
}
335+
#endif // PJ_STATIC_PLUGINS

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

0 commit comments

Comments
 (0)