diff --git a/CHANGELOG.md b/CHANGELOG.md index d87205a..09eb0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to `plotjuggler_sdk` are recorded here. Versioning policy is ## [0.19.0] +### Fix: static plugin exports support namespaced classes (PATCH-level) + +The static variants of the plugin export macros formed their getter symbol by +token-pasting the C++ class argument. Qualified names such as +`mosaico::MosaicoToolbox` therefore compiled as dynamic plugins but failed when +`PJ_STATIC_PLUGINS` was enabled. Each plugin family now has a backward-compatible +`*_PLUGIN_NAMED(ClassName, SymbolName, manifest)` form: `ClassName` may be +qualified, while `SymbolName` is the unqualified token used for the static getter. +Existing macros and getter names are unchanged for unqualified classes. + ### Feature: exported manifest decoder — one validation policy for DSO and static plugins (MINOR) `decodeManifest(source_path, family, manifest_json)` is now part of the diff --git a/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp b/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp index 83f6005..2da8041 100644 --- a/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/data_source_plugin_base.hpp @@ -229,6 +229,10 @@ class DataSourcePluginBase { return vt; \ } +// Variant for namespaced plugin classes. SymbolName must be an unqualified +// identifier and is used only to form the unique static getter name. +#define PJ_DATA_SOURCE_PLUGIN_NAMED(ClassName, SymbolName, manifest) PJ_DATA_SOURCE_PLUGIN(ClassName, manifest) + // --- Static-link variant (WASM / no dlopen) ---------------------------------- // With PJ_STATIC_PLUGINS the plugin is linked into the host binary. The fixed // `extern "C" PJ_get_data_source_vtable` symbol would collide across plugins in @@ -237,17 +241,19 @@ class DataSourcePluginBase { // runtime catalog's registerStaticDataSource() (host-side, in the app). #ifdef PJ_STATIC_PLUGINS #undef PJ_DATA_SOURCE_PLUGIN -#define PJ_DATA_SOURCE_PLUGIN(ClassName, manifest) \ - const PJ_data_source_vtable_t* pj_static_get_data_source_vtable_##ClassName() noexcept { \ - static const PJ_data_source_vtable_t* vt = PJ::DataSourcePluginBase::vtableWithCreate( \ - []() noexcept -> void* { \ - try { \ - return new ClassName(); \ - } catch (...) { \ - return nullptr; \ - } \ - }, \ - manifest); \ - return vt; \ +#undef PJ_DATA_SOURCE_PLUGIN_NAMED +#define PJ_DATA_SOURCE_PLUGIN_NAMED(ClassName, SymbolName, manifest) \ + const PJ_data_source_vtable_t* pj_static_get_data_source_vtable_##SymbolName() noexcept { \ + static const PJ_data_source_vtable_t* vt = PJ::DataSourcePluginBase::vtableWithCreate( \ + []() noexcept -> void* { \ + try { \ + return new ClassName(); \ + } catch (...) { \ + return nullptr; \ + } \ + }, \ + manifest); \ + return vt; \ } +#define PJ_DATA_SOURCE_PLUGIN(ClassName, manifest) PJ_DATA_SOURCE_PLUGIN_NAMED(ClassName, ClassName, manifest) #endif diff --git a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp index 7876bbc..734b195 100644 --- a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp @@ -237,11 +237,16 @@ class ToolboxPluginBase { return vt; \ } +// Variant for namespaced plugin classes. SymbolName must be an unqualified +// identifier and is used only to form the unique static getter name. +#define PJ_TOOLBOX_PLUGIN_NAMED(ClassName, SymbolName, manifest) PJ_TOOLBOX_PLUGIN(ClassName, manifest) + // --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp #ifdef PJ_STATIC_PLUGINS #undef PJ_TOOLBOX_PLUGIN -#define PJ_TOOLBOX_PLUGIN(ClassName, manifest) \ - const PJ_toolbox_vtable_t* pj_static_get_toolbox_vtable_##ClassName() noexcept { \ +#undef PJ_TOOLBOX_PLUGIN_NAMED +#define PJ_TOOLBOX_PLUGIN_NAMED(ClassName, SymbolName, manifest) \ + const PJ_toolbox_vtable_t* pj_static_get_toolbox_vtable_##SymbolName() noexcept { \ static const PJ_toolbox_vtable_t* vt = PJ::ToolboxPluginBase::vtableWithCreate( \ []() noexcept -> void* { \ try { \ @@ -253,4 +258,5 @@ class ToolboxPluginBase { manifest); \ return vt; \ } +#define PJ_TOOLBOX_PLUGIN(ClassName, manifest) PJ_TOOLBOX_PLUGIN_NAMED(ClassName, ClassName, manifest) #endif diff --git a/pj_plugins/CMakeLists.txt b/pj_plugins/CMakeLists.txt index 2f3a6f8..b30c368 100644 --- a/pj_plugins/CMakeLists.txt +++ b/pj_plugins/CMakeLists.txt @@ -337,6 +337,16 @@ target_link_libraries(toolbox_plugin_test PRIVATE ) add_test(NAME toolbox_plugin_test COMMAND toolbox_plugin_test) +# Static export macros need an explicit symbol token when the C++ class is +# namespaced; compile and call every plugin family's named variant together. +add_executable(static_namespaced_plugin_macros_test tests/static_namespaced_plugin_macros_test.cpp) +target_compile_definitions(static_namespaced_plugin_macros_test PRIVATE PJ_STATIC_PLUGINS) +target_compile_options(static_namespaced_plugin_macros_test PRIVATE ${PJ_WARNING_FLAGS}) +target_link_libraries(static_namespaced_plugin_macros_test PRIVATE + pj_plugin_sdk GTest::gtest_main +) +add_test(NAME static_namespaced_plugin_macros_test COMMAND static_namespaced_plugin_macros_test) + # --------------------------------------------------------------------------- # Plugin catalog DSO scanner test. # --------------------------------------------------------------------------- diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_base.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_base.hpp index 72ff4ea..0843ae0 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_base.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_base.hpp @@ -312,6 +312,11 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept { } \ } +// Variant for namespaced plugin classes. SymbolName must be an unqualified +// identifier and is used only to form the unique static getter name. +#define PJ_DIALOG_PLUGIN_NAMED(ClassName, SymbolName, ManifestJson) \ + PJ_DIALOG_PLUGIN_WITH_MANIFEST(ClassName, ManifestJson) + // --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp. // Many statically-linked plugins each carry a PJ_DIALOG_PLUGIN, so the fixed // `extern "C" PJ_get_dialog_vtable` symbol (and the ABI-version export) would @@ -322,8 +327,9 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept { // dialogVtableFor() also uses it inside the plugin. #ifdef PJ_STATIC_PLUGINS #undef PJ_DIALOG_PLUGIN_WITH_MANIFEST -#define PJ_DIALOG_PLUGIN_WITH_MANIFEST(ClassName, ManifestJson) \ - const PJ_dialog_vtable_t* pj_static_get_dialog_vtable_##ClassName() noexcept { \ +#undef PJ_DIALOG_PLUGIN_NAMED +#define PJ_DIALOG_PLUGIN_NAMED(ClassName, SymbolName, ManifestJson) \ + const PJ_dialog_vtable_t* pj_static_get_dialog_vtable_##SymbolName() noexcept { \ static const PJ_dialog_vtable_t* vt = PJ::DialogPluginBase::vtableWithCreate( \ []() noexcept -> void* { \ try { \ @@ -338,7 +344,9 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept { namespace PJ { \ template <> \ [[maybe_unused]] inline const PJ_dialog_vtable_t* dialogVtableFor() noexcept { \ - return pj_static_get_dialog_vtable_##ClassName(); \ + return pj_static_get_dialog_vtable_##SymbolName(); \ } \ } +#define PJ_DIALOG_PLUGIN_WITH_MANIFEST(ClassName, ManifestJson) \ + PJ_DIALOG_PLUGIN_NAMED(ClassName, ClassName, ManifestJson) #endif // PJ_STATIC_PLUGINS diff --git a/pj_plugins/docs/data-source-guide.md b/pj_plugins/docs/data-source-guide.md index 961cab5..19a7587 100644 --- a/pj_plugins/docs/data-source-guide.md +++ b/pj_plugins/docs/data-source-guide.md @@ -44,6 +44,10 @@ directly only when the supplied state machines genuinely don't fit. 3. Export with `PJ_DATA_SOURCE_PLUGIN(YourClass, R"({"id":"...","name":"...","version":"..."})")` 4. Build as a shared library linking `pj_base` +For a namespaced class in a static build, provide the getter-symbol token +separately: `PJ_DATA_SOURCE_PLUGIN_NAMED(my::Source, MySource, kManifest)`. +Dynamic builds may use either form. + A complete example lives at `pj_plugins/examples/mock_data_source.cpp`. ## Plugin Contract diff --git a/pj_plugins/docs/dialog-plugin-guide.md b/pj_plugins/docs/dialog-plugin-guide.md index c09aaf9..4177cd5 100644 --- a/pj_plugins/docs/dialog-plugin-guide.md +++ b/pj_plugins/docs/dialog-plugin-guide.md @@ -44,6 +44,10 @@ renders the widgets, and relays events to the plugin over the C vtable. 6. Export with `PJ_DIALOG_PLUGIN(YourClass, kManifestJson)`. 7. Build as a shared library linking `pj_dialog_sdk`. +For a namespaced class in a static build, provide the getter-symbol token +separately: `PJ_DIALOG_PLUGIN_NAMED(my::Dialog, MyDialog, kManifestJson)`. +Dynamic builds may use either form. + A complete example lives at `pj_plugins/dialog_protocol/examples/mock_dialog.cpp`. diff --git a/pj_plugins/docs/message-parser-guide.md b/pj_plugins/docs/message-parser-guide.md index 0b7a869..be639d9 100644 --- a/pj_plugins/docs/message-parser-guide.md +++ b/pj_plugins/docs/message-parser-guide.md @@ -52,6 +52,10 @@ the host, which routes them to the appropriate parser based on encoding name. 3. Export with `PJ_MESSAGE_PARSER_PLUGIN(YourClass, R"({"id":"...","name":"...","version":"...","encoding":["..."]})")` 4. Build as a shared library linking `pj_base` +For a namespaced class in a static build, provide the getter-symbol token +separately: `PJ_MESSAGE_PARSER_PLUGIN_NAMED(my::Parser, MyParser, kManifest)`. +Dynamic builds may use either form. + A complete example lives at `pj_plugins/examples/mock_json_parser.cpp`. ## Plugin Contract diff --git a/pj_plugins/docs/toolbox-guide.md b/pj_plugins/docs/toolbox-guide.md index 6b1b91c..95bd5de 100644 --- a/pj_plugins/docs/toolbox-guide.md +++ b/pj_plugins/docs/toolbox-guide.md @@ -45,6 +45,11 @@ editor, custom data transforms. 5. Build as a shared library linking `pj_base` (+ `pj_dialog_sdk` if you have a dialog) +For namespaced classes in a static build, provide each getter-symbol token +separately: `PJ_TOOLBOX_PLUGIN_NAMED(my::Toolbox, MyToolbox, kManifest)` and +`PJ_DIALOG_PLUGIN_NAMED(my::Dialog, MyDialog, kManifest)`. Dynamic builds may +use either form. + A complete example lives at `pj_plugins/examples/mock_toolbox.cpp`. ## Plugin Contract diff --git a/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp b/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp index 041bf59..0c06b98 100644 --- a/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp +++ b/pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp @@ -377,20 +377,26 @@ class MessageParserPluginBase { return vt; \ } +// Variant for namespaced plugin classes. SymbolName must be an unqualified +// identifier and is used only to form the unique static getter name. +#define PJ_MESSAGE_PARSER_PLUGIN_NAMED(ClassName, SymbolName, manifest) PJ_MESSAGE_PARSER_PLUGIN(ClassName, manifest) + // --- Static-link variant (WASM / no dlopen) --- see data_source_plugin_base.hpp #ifdef PJ_STATIC_PLUGINS #undef PJ_MESSAGE_PARSER_PLUGIN -#define PJ_MESSAGE_PARSER_PLUGIN(ClassName, manifest) \ - const PJ_message_parser_vtable_t* pj_static_get_message_parser_vtable_##ClassName() noexcept { \ - static const PJ_message_parser_vtable_t* vt = PJ::MessageParserPluginBase::vtableWithCreate( \ - []() noexcept -> void* { \ - try { \ - return new ClassName(); \ - } catch (...) { \ - return nullptr; \ - } \ - }, \ - manifest); \ - return vt; \ +#undef PJ_MESSAGE_PARSER_PLUGIN_NAMED +#define PJ_MESSAGE_PARSER_PLUGIN_NAMED(ClassName, SymbolName, manifest) \ + const PJ_message_parser_vtable_t* pj_static_get_message_parser_vtable_##SymbolName() noexcept { \ + static const PJ_message_parser_vtable_t* vt = PJ::MessageParserPluginBase::vtableWithCreate( \ + []() noexcept -> void* { \ + try { \ + return new ClassName(); \ + } catch (...) { \ + return nullptr; \ + } \ + }, \ + manifest); \ + return vt; \ } +#define PJ_MESSAGE_PARSER_PLUGIN(ClassName, manifest) PJ_MESSAGE_PARSER_PLUGIN_NAMED(ClassName, ClassName, manifest) #endif diff --git a/pj_plugins/tests/static_namespaced_plugin_macros_test.cpp b/pj_plugins/tests/static_namespaced_plugin_macros_test.cpp new file mode 100644 index 0000000..1263ba4 --- /dev/null +++ b/pj_plugins/tests/static_namespaced_plugin_macros_test.cpp @@ -0,0 +1,70 @@ +// Copyright 2026 Davide Faconti +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include + +namespace namespaced_plugin { + +class Source : public PJ::DataSourcePluginBase { + public: + uint64_t capabilities() const override { + return 0; + } + PJ::Status start() override { + return PJ::okStatus(); + } + void stop() override {} + PJ::DataSourceState currentState() const override { + return PJ::DataSourceState::kStopped; + } +}; + +class Parser : public PJ::MessageParserPluginBase {}; + +class Toolbox : public PJ::ToolboxPluginBase { + public: + uint64_t capabilities() const override { + return 0; + } +}; + +class Dialog : public PJ::DialogPluginTyped { + public: + std::string manifest() const override { + return kManifest; + } + std::string ui_content() const override { + return ""; + } + std::string widget_data() override { + return "{}"; + } + + static constexpr const char* kManifest = R"({"id":"namespaced-dialog","name":"Namespaced Dialog","version":"1.0.0"})"; +}; + +} // namespace namespaced_plugin + +PJ_DATA_SOURCE_PLUGIN_NAMED( + namespaced_plugin::Source, NamespacedSource, + R"({"id":"namespaced-source","name":"Namespaced Source","version":"1.0.0"})") +PJ_MESSAGE_PARSER_PLUGIN_NAMED( + namespaced_plugin::Parser, NamespacedParser, + R"({"id":"namespaced-parser","name":"Namespaced Parser","version":"1.0.0","encoding":["test"]})") +PJ_TOOLBOX_PLUGIN_NAMED( + namespaced_plugin::Toolbox, NamespacedToolbox, + R"({"id":"namespaced-toolbox","name":"Namespaced Toolbox","version":"1.0.0"})") +PJ_DIALOG_PLUGIN_NAMED(namespaced_plugin::Dialog, NamespacedDialog, namespaced_plugin::Dialog::kManifest) + +TEST(StaticNamespacedPluginMacrosTest, EmitCallableNamedGetters) { + EXPECT_NE(pj_static_get_data_source_vtable_NamespacedSource(), nullptr); + EXPECT_NE(pj_static_get_message_parser_vtable_NamespacedParser(), nullptr); + EXPECT_NE(pj_static_get_toolbox_vtable_NamespacedToolbox(), nullptr); + EXPECT_NE(pj_static_get_dialog_vtable_NamespacedDialog(), nullptr); + EXPECT_EQ(PJ::dialogVtableFor(), pj_static_get_dialog_vtable_NamespacedDialog()); +}