diff --git a/CMakeLists.txt b/CMakeLists.txt index ae2b7ce6..0cf071ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() # --------------------------------------------------------------------------- @@ -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 diff --git a/conanfile.py b/conanfile.py index b1ebcfe2..dce74dfe 100644 --- a/conanfile.py +++ b/conanfile.py @@ -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) @@ -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" 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 55cfe513..23f29100 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 @@ -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 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 c6e3e0db..b4fd47f1 100644 --- a/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp +++ b/pj_base/include/pj_base/sdk/toolbox_plugin_base.hpp @@ -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 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 872ff1b1..e04e7ceb 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 @@ -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() 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(new ClassName()); \ + } catch (...) { \ + return nullptr; \ + } \ + }, \ + ManifestJson); \ + return vt; \ + } \ + namespace PJ { \ + template <> \ + [[maybe_unused]] inline const PJ_dialog_vtable_t* dialogVtableFor() noexcept { \ + return pj_static_get_dialog_vtable_##ClassName(); \ + } \ + } +#endif // PJ_STATIC_PLUGINS diff --git a/pj_plugins/include/pj_plugins/host/data_source_library.hpp b/pj_plugins/include/pj_plugins/host/data_source_library.hpp index 4a2e9496..ec58e278 100644 --- a/pj_plugins/include/pj_plugins/host/data_source_library.hpp +++ b/pj_plugins/include/pj_plugins/host/data_source_library.hpp @@ -51,6 +51,10 @@ class DataSourceLibrary { /// Load a plugin from @p path. Returns an error string on failure. [[nodiscard]] static Expected 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 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; diff --git a/pj_plugins/include/pj_plugins/host/message_parser_library.hpp b/pj_plugins/include/pj_plugins/host/message_parser_library.hpp index 211d6bc9..50a57d3b 100644 --- a/pj_plugins/include/pj_plugins/host/message_parser_library.hpp +++ b/pj_plugins/include/pj_plugins/host/message_parser_library.hpp @@ -51,6 +51,10 @@ class MessageParserLibrary { /// Load a plugin from @p path. Returns an error string on failure. [[nodiscard]] static Expected 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 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; diff --git a/pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp b/pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp index 09ce0e2e..5129a335 100644 --- a/pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp +++ b/pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp @@ -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& dataSources() const { return data_sources_; diff --git a/pj_plugins/include/pj_plugins/host/toolbox_library.hpp b/pj_plugins/include/pj_plugins/host/toolbox_library.hpp index 091efb1a..e4bc72e7 100644 --- a/pj_plugins/include/pj_plugins/host/toolbox_library.hpp +++ b/pj_plugins/include/pj_plugins/host/toolbox_library.hpp @@ -51,6 +51,10 @@ class ToolboxLibrary { /// Load a plugin from @p path. Returns an error string on failure. [[nodiscard]] static Expected 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 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; 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 c08306e8..041bf591 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 @@ -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 diff --git a/pj_plugins/src/data_source_library.cpp b/pj_plugins/src/data_source_library.cpp index 7d2bd0ac..2ce00625 100644 --- a/pj_plugins/src/data_source_library.cpp +++ b/pj_plugins/src/data_source_library.cpp @@ -70,6 +70,26 @@ Expected DataSourceLibrary::load(std::string_view path) { return DataSourceLibrary(std::move(handle), vtable, std::string(path)); } +Expected 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 handle(&anchor, [](void*) {}); + return DataSourceLibrary(std::move(handle), vtable, "static://"); +} + Expected DataSourceLibrary::resolveDialogVtable() const { auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable"); if (!sym) { diff --git a/pj_plugins/src/message_parser_library.cpp b/pj_plugins/src/message_parser_library.cpp index 4912cae5..63db8d4e 100644 --- a/pj_plugins/src/message_parser_library.cpp +++ b/pj_plugins/src/message_parser_library.cpp @@ -68,6 +68,24 @@ Expected MessageParserLibrary::load(std::string_view path) return MessageParserLibrary(std::move(handle), vtable, std::string(path)); } +Expected 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 handle(&anchor, [](void*) {}); + return MessageParserLibrary(std::move(handle), vtable, "static://"); +} + Expected MessageParserLibrary::resolveDialogVtable() const { auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable"); if (!sym) { diff --git a/pj_plugins/src/plugin_runtime_catalog.cpp b/pj_plugins/src/plugin_runtime_catalog.cpp index 3c1d0db2..c6e045b2 100644 --- a/pj_plugins/src/plugin_runtime_catalog.cpp +++ b/pj_plugins/src/plugin_runtime_catalog.cpp @@ -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 readManifestStringArray(const nlohmann::json& j, const char* key) { + std::vector 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()); + } + } + } + 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 +bool registerStaticPlugin( + const auto* vtable, std::vector& 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( + 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( + 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( + 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: diff --git a/pj_plugins/src/toolbox_library.cpp b/pj_plugins/src/toolbox_library.cpp index 47ae8c9c..a38a32cd 100644 --- a/pj_plugins/src/toolbox_library.cpp +++ b/pj_plugins/src/toolbox_library.cpp @@ -67,6 +67,24 @@ Expected ToolboxLibrary::load(std::string_view path) { return ToolboxLibrary(std::move(handle), vtable, std::string(path)); } +Expected 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 handle(&anchor, [](void*) {}); + return ToolboxLibrary(std::move(handle), vtable, "static://"); +} + Expected ToolboxLibrary::resolveDialogVtable() const { auto sym = detail::resolveSymbol(handle_.get(), "PJ_get_dialog_vtable"); if (!sym) { diff --git a/recipe.yaml b/recipe.yaml index 66bccae8..2439291f 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -1,7 +1,7 @@ schema_version: 1 context: - version: "0.10.0" + version: "0.11.0" package: name: plotjuggler_sdk