Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 18 additions & 12 deletions pj_base/include/pj_base/sdk/data_source_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
10 changes: 8 additions & 2 deletions pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 { \
Expand All @@ -253,4 +258,5 @@ class ToolboxPluginBase {
manifest); \
return vt; \
}
#define PJ_TOOLBOX_PLUGIN(ClassName, manifest) PJ_TOOLBOX_PLUGIN_NAMED(ClassName, ClassName, manifest)
#endif
10 changes: 10 additions & 0 deletions pj_plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -322,8 +327,9 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept {
// dialogVtableFor<ClassName>() 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 { \
Expand All @@ -338,7 +344,9 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept {
namespace PJ { \
template <> \
[[maybe_unused]] inline const PJ_dialog_vtable_t* dialogVtableFor<ClassName>() 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
4 changes: 4 additions & 0 deletions pj_plugins/docs/data-source-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pj_plugins/docs/dialog-plugin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
4 changes: 4 additions & 0 deletions pj_plugins/docs/message-parser-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pj_plugins/docs/toolbox-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 18 additions & 12 deletions pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
70 changes: 70 additions & 0 deletions pj_plugins/tests/static_namespaced_plugin_macros_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: Apache-2.0

#include <gtest/gtest.h>

#include <pj_base/sdk/data_source_plugin_base.hpp>
#include <pj_base/sdk/toolbox_plugin_base.hpp>
#include <pj_plugins/sdk/dialog_plugin_typed.hpp>
#include <pj_plugins/sdk/message_parser_plugin_base.hpp>

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<namespaced_plugin::Dialog>(), pj_static_get_dialog_vtable_NamespacedDialog());
}
Loading