Skip to content
Merged
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
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ else()
-Wall -Wextra -Werror -Wshadow -Wnon-virtual-dtor -Wold-style-cast
-Wcast-qual -Wconversion -Woverloaded-virtual -Wpedantic
)
# On wasm32 size_t is 32-bit, so many uint64_t->size_t conversions trip
# -Wshorten-64-to-32 / -Wsign-conversion that never fire on 64-bit desktop
# (see SUSTAINABILITY.md T0-3). Keep the warnings visible but don't fail the
# build for the WASM prototype. TODO: make the ABI/size types 32-bit-clean.
if(EMSCRIPTEN)
list(APPEND PJ_WARNING_FLAGS -Wno-error)
endif()
endif()

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -111,7 +118,7 @@ endif()
if(PJ_INSTALL_SDK)
include(CMakePackageConfigHelpers)

set(PJ_PACKAGE_VERSION "0.10.0")
set(PJ_PACKAGE_VERSION "0.11.0")
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk)

install(EXPORT plotjuggler_sdkTargets
Expand Down
4 changes: 2 additions & 2 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)

A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.10.0` and then:
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.11.0` and then:

find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk)
target_link_libraries(my_plugin PRIVATE plotjuggler_sdk::plugin_sdk)
Expand All @@ -30,7 +30,7 @@

class PlotjugglerSdkConan(ConanFile):
name = "plotjuggler_sdk"
version = "0.10.0"
version = "0.11.0"
# Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE.
license = "Apache-2.0"
url = "https://github.com/PlotJuggler/plotjuggler_sdk"
Expand Down
23 changes: 23 additions & 0 deletions pj_base/include/pj_base/sdk/data_source_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,26 @@ class DataSourcePluginBase {
manifest); \
return vt; \
}

// --- 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
// one binary, so emit a uniquely-named (class-keyed) C++ function instead. The
// host's static registry declares these extern and passes them to
// PluginRuntimeCatalog::registerStaticDataSource().
#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; \
}
#endif
18 changes: 18 additions & 0 deletions pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,21 @@ class ToolboxPluginBase {
manifest); \
return vt; \
}

// --- 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 { \
static const PJ_toolbox_vtable_t* vt = PJ::ToolboxPluginBase::vtableWithCreate( \
[]() noexcept -> void* { \
try { \
return new ClassName(); \
} catch (...) { \
return nullptr; \
} \
}, \
manifest); \
return vt; \
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,33 @@ PJ_borrowed_dialog_t borrowDialog(DialogT& dialog) noexcept {
return PJ_get_dialog_vtable(); \
} \
}

// --- 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
// collide at link. The static registry never resolves a dialog via dlsym (the
// host short-circuits the dialog path on WASM), so emit only a uniquely
// class-keyed getter — kept so dialogVtableFor<ClassName>() still resolves for
// any in-binary caller.
#ifdef PJ_STATIC_PLUGINS
#undef PJ_DIALOG_PLUGIN_WITH_MANIFEST
#define PJ_DIALOG_PLUGIN_WITH_MANIFEST(ClassName, ManifestJson) \
inline const PJ_dialog_vtable_t* pj_static_get_dialog_vtable_##ClassName() noexcept { \
static const PJ_dialog_vtable_t* vt = PJ::DialogPluginBase::vtableWithCreate( \
[]() noexcept -> void* { \
try { \
return static_cast<PJ::DialogPluginBase*>(new ClassName()); \
} catch (...) { \
return nullptr; \
} \
}, \
ManifestJson); \
return vt; \
} \
namespace PJ { \
template <> \
[[maybe_unused]] inline const PJ_dialog_vtable_t* dialogVtableFor<ClassName>() noexcept { \
return pj_static_get_dialog_vtable_##ClassName(); \
} \
}
#endif // PJ_STATIC_PLUGINS
4 changes: 4 additions & 0 deletions pj_plugins/include/pj_plugins/host/data_source_library.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class DataSourceLibrary {
/// Load a plugin from @p path. Returns an error string on failure.
[[nodiscard]] static Expected<DataSourceLibrary> load(std::string_view path);

/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
/// @p vtable must have static storage duration (valid for the program lifetime).
[[nodiscard]] static Expected<DataSourceLibrary> loadStatic(const PJ_data_source_vtable_t* vtable);

/// True if the library was loaded and the vtable resolved successfully.
[[nodiscard]] bool valid() const {
return handle_ != nullptr && vtable_ != nullptr;
Expand Down
4 changes: 4 additions & 0 deletions pj_plugins/include/pj_plugins/host/message_parser_library.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class MessageParserLibrary {
/// Load a plugin from @p path. Returns an error string on failure.
[[nodiscard]] static Expected<MessageParserLibrary> load(std::string_view path);

/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
/// @p vtable must have static storage duration (valid for the program lifetime).
[[nodiscard]] static Expected<MessageParserLibrary> loadStatic(const PJ_message_parser_vtable_t* vtable);

/// True if the library was loaded and the vtable resolved successfully.
[[nodiscard]] bool valid() const {
return handle_ != nullptr && vtable_ != nullptr;
Expand Down
8 changes: 8 additions & 0 deletions pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ class PluginRuntimeCatalog {
// Reconciles loaded state with disk and returns true if it changed.
[[nodiscard]] bool reload();

// --- Static plugin registration (WASM/static builds; no dlopen) -------------
// Register a statically-linked plugin by its vtable. The vtable must outlive
// this catalog (static storage duration); the manifest is read from
// vtable->manifest_json. Returns false (and reports a diagnostic) on failure.
bool registerStaticDataSource(const PJ_data_source_vtable_t* vtable);
bool registerStaticMessageParser(const PJ_message_parser_vtable_t* vtable);
bool registerStaticToolbox(const PJ_toolbox_vtable_t* vtable);

// Returns loaded DataSource plugins.
[[nodiscard]] const std::vector<RuntimeDataSourcePlugin>& dataSources() const {
return data_sources_;
Expand Down
4 changes: 4 additions & 0 deletions pj_plugins/include/pj_plugins/host/toolbox_library.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class ToolboxLibrary {
/// Load a plugin from @p path. Returns an error string on failure.
[[nodiscard]] static Expected<ToolboxLibrary> load(std::string_view path);

/// Wrap a statically-linked plugin vtable (no dlopen; for WASM/static builds).
/// @p vtable must have static storage duration (valid for the program lifetime).
[[nodiscard]] static Expected<ToolboxLibrary> loadStatic(const PJ_toolbox_vtable_t* vtable);

/// True if the library was loaded and the vtable resolved successfully.
[[nodiscard]] bool valid() const {
return handle_ != nullptr && vtable_ != nullptr;
Expand Down
18 changes: 18 additions & 0 deletions pj_plugins/include/pj_plugins/sdk/message_parser_plugin_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,21 @@ class MessageParserPluginBase {
manifest); \
return vt; \
}

// --- 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; \
}
#endif
20 changes: 20 additions & 0 deletions pj_plugins/src/data_source_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ Expected<DataSourceLibrary> DataSourceLibrary::load(std::string_view path) {
return DataSourceLibrary(std::move(handle), vtable, std::string(path));
}

Expected<DataSourceLibrary> DataSourceLibrary::loadStatic(const PJ_data_source_vtable_t* vtable) {
if (vtable == nullptr) {
return unexpected("static DataSource vtable is null");
}
if (vtable->protocol_version != PJ_DATA_SOURCE_PROTOCOL_VERSION) {
return unexpected("DataSource protocol version mismatch");
}
if (vtable->struct_size < PJ_DATA_SOURCE_MIN_VTABLE_SIZE) {
return unexpected("DataSource vtable smaller than v4.0 baseline");
}
if (auto status = detail::validateRequiredSlots(vtable); !status) {
return unexpected(status.error());
}
// Statically linked: no DSO to keep alive, but valid()/createHandle() expect a
// non-null owner. Use a sentinel shared_ptr with a no-op deleter.
static char anchor = 0;
std::shared_ptr<void> handle(&anchor, [](void*) {});
return DataSourceLibrary(std::move(handle), vtable, "static://");
}

Expected<const PJ_dialog_vtable_t*> DataSourceLibrary::resolveDialogVtable() const {
auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable");
if (!sym) {
Expand Down
18 changes: 18 additions & 0 deletions pj_plugins/src/message_parser_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ Expected<MessageParserLibrary> MessageParserLibrary::load(std::string_view path)
return MessageParserLibrary(std::move(handle), vtable, std::string(path));
}

Expected<MessageParserLibrary> MessageParserLibrary::loadStatic(const PJ_message_parser_vtable_t* vtable) {
if (vtable == nullptr) {
return unexpected("static MessageParser vtable is null");
}
if (vtable->protocol_version != PJ_MESSAGE_PARSER_PROTOCOL_VERSION) {
return unexpected("MessageParser protocol version mismatch");
}
if (vtable->struct_size < PJ_MESSAGE_PARSER_MIN_VTABLE_SIZE) {
return unexpected("MessageParser vtable smaller than v4.0 baseline");
}
if (auto status = detail::validateRequiredSlots(vtable); !status) {
return unexpected(status.error());
}
static char anchor = 0;
std::shared_ptr<void> handle(&anchor, [](void*) {});
return MessageParserLibrary(std::move(handle), vtable, "static://");
}

Expected<const PJ_dialog_vtable_t*> MessageParserLibrary::resolveDialogVtable() const {
auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable");
if (!sym) {
Expand Down
86 changes: 86 additions & 0 deletions pj_plugins/src/plugin_runtime_catalog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,92 @@ bool PluginRuntimeCatalog::reload() {
return changed;
}

namespace {

// Parse a plugin's embedded manifest JSON. Returns false on invalid JSON.
bool parseStaticManifest(const char* manifest_json, nlohmann::json& out) {
try {
out = nlohmann::json::parse(manifest_json ? manifest_json : "");
return out.is_object();
} catch (const std::exception&) {
return false;
}
}

std::vector<std::string> readManifestStringArray(const nlohmann::json& j, const char* key) {
std::vector<std::string> values;
if (auto it = j.find(key); it != j.end() && it->is_array()) {
for (const auto& v : *it) {
if (v.is_string()) {
values.push_back(v.get<std::string>());
}
}
}
return values;
}

// Shared body for the three registerStatic* methods: load the static vtable,
// parse the embedded manifest, fill the common Runtime*Plugin fields, and push.
// `fill` adds the family-specific fields (capabilities / extensions / encodings);
// `report` bridges to the catalog's private diagnostic sink.
template <typename LibraryT, typename RuntimeT, typename ReportFn, typename FillFn>
bool registerStaticPlugin(
const auto* vtable, std::vector<RuntimeT>& out, const char* family, const ReportFn& report, const FillFn& fill) {
auto result = LibraryT::loadStatic(vtable);
if (!result) {
report(DiagnosticLevel::kError, std::string{}, "static " + std::string(family) + ": " + result.error());
return false;
}
nlohmann::json j;
if (!parseStaticManifest(vtable->manifest_json, j)) {
report(DiagnosticLevel::kError, std::string{}, "static " + std::string(family) + ": invalid manifest JSON");
return false;
}
RuntimeT loaded;
loaded.library = std::move(*result);
loaded.id = j.value("id", std::string{});
loaded.name = j.value("name", std::string{});
loaded.version = j.value("version", std::string{});
loaded.path = "static://" + loaded.id;
loaded.loaded_mtime = std::filesystem::file_time_type{};
fill(loaded, j);
report(DiagnosticLevel::kInfo, loaded.id, "Registered static " + std::string(family) + " " + loaded.name);
out.push_back(std::move(loaded));
return true;
}

} // namespace

bool PluginRuntimeCatalog::registerStaticDataSource(const PJ_data_source_vtable_t* vtable) {
return registerStaticPlugin<DataSourceLibrary>(
vtable, data_sources_, "DataSource",
[this](DiagnosticLevel level, const std::string& id, std::string msg) { report(level, id, std::move(msg)); },
[](RuntimeDataSourcePlugin& loaded, const nlohmann::json& j) {
loaded.capabilities = loaded.library.createHandle().capabilities();
for (auto& ext : readManifestStringArray(j, "file_extensions")) {
loaded.file_extensions.push_back(normalizeExtension(std::move(ext)));
}
});
}

bool PluginRuntimeCatalog::registerStaticMessageParser(const PJ_message_parser_vtable_t* vtable) {
return registerStaticPlugin<MessageParserLibrary>(
vtable, message_parsers_, "MessageParser",
[this](DiagnosticLevel level, const std::string& id, std::string msg) { report(level, id, std::move(msg)); },
[](RuntimeMessageParserPlugin& loaded, const nlohmann::json& j) {
loaded.encodings = readManifestStringArray(j, "encoding");
});
}

bool PluginRuntimeCatalog::registerStaticToolbox(const PJ_toolbox_vtable_t* vtable) {
return registerStaticPlugin<ToolboxLibrary>(
vtable, toolbox_plugins_, "Toolbox",
[this](DiagnosticLevel level, const std::string& id, std::string msg) { report(level, id, std::move(msg)); },
[](RuntimeToolboxPlugin& loaded, const nlohmann::json& /*j*/) {
loaded.capabilities = loaded.library.createHandle().capabilities();
});
}

bool PluginRuntimeCatalog::loadAndRegister(const PluginDescriptor& descriptor) {
switch (descriptor.family) {
case PluginFamily::kDataSource:
Expand Down
18 changes: 18 additions & 0 deletions pj_plugins/src/toolbox_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ Expected<ToolboxLibrary> ToolboxLibrary::load(std::string_view path) {
return ToolboxLibrary(std::move(handle), vtable, std::string(path));
}

Expected<ToolboxLibrary> ToolboxLibrary::loadStatic(const PJ_toolbox_vtable_t* vtable) {
if (vtable == nullptr) {
return unexpected("static Toolbox vtable is null");
}
if (vtable->protocol_version != PJ_TOOLBOX_PLUGIN_PROTOCOL_VERSION) {
return unexpected("Toolbox protocol version mismatch");
}
if (vtable->struct_size < PJ_TOOLBOX_MIN_VTABLE_SIZE) {
return unexpected("Toolbox vtable smaller than v4.0 baseline");
}
if (auto status = detail::validateRequiredSlots(vtable); !status) {
return unexpected(status.error());
}
static char anchor = 0;
std::shared_ptr<void> handle(&anchor, [](void*) {});
return ToolboxLibrary(std::move(handle), vtable, "static://");
}

Expected<const PJ_dialog_vtable_t*> ToolboxLibrary::resolveDialogVtable() const {
auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable");
if (!sym) {
Expand Down
2 changes: 1 addition & 1 deletion recipe.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
schema_version: 1

context:
version: "0.10.0"
version: "0.11.0"

package:
name: plotjuggler_sdk
Expand Down
Loading