From 3412bfbc3c3baff3b44d8d395957d0424125c7f2 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Mon, 27 Apr 2026 03:18:14 +0200 Subject: [PATCH] feat(plugins): enforce dialog-vtable contract at plugin load time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plugin that advertises kCapabilityHasDialog (DataSource) or kToolboxCapabilityHasDialog (Toolbox) but doesn't export a usable dialog vtable is buggy. Previously the host catalog accepted such plugins silently, and downstream consumers had to discover the violation at use time — by which point the user is staring at garbled curves with no diagnostic. Move the check upstream: PluginRuntimeCatalog now calls resolveDialogVtable() during loadAndRegister{DataSource,Toolbox} when the capability bit is set, and refuses to register the plugin if resolution fails. The reason (e.g. "PJ_get_dialog_vtable returned null", "Dialog protocol version mismatch") is reported through the existing DiagnosticSink as kError, so the marketplace UI can show the plugin as broken. The PJ4 host (`pj_app/src/DialogPresenter.cpp`) keeps its runtime contract-violation path as defense-in-depth: any future ABI drift between scanner and runtime, or a hypothetical hot-loaded plugin that bypasses the catalog, still produces a sane error rather than silent garbage. Verified: clean build, 52/52 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- pj_plugins/src/plugin_runtime_catalog.cpp | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pj_plugins/src/plugin_runtime_catalog.cpp b/pj_plugins/src/plugin_runtime_catalog.cpp index a43bc7f..3283d56 100644 --- a/pj_plugins/src/plugin_runtime_catalog.cpp +++ b/pj_plugins/src/plugin_runtime_catalog.cpp @@ -6,6 +6,7 @@ #include #include "pj_base/data_source_protocol.h" +#include "pj_base/toolbox_protocol.h" namespace PJ { @@ -185,6 +186,22 @@ bool PluginRuntimeCatalog::loadAndRegisterDataSource(const PluginDescriptor& des loaded.name = descriptor.name; loaded.version = descriptor.version; loaded.capabilities = loaded.library.createHandle().capabilities(); + + // Fail-fast on plugins that lie about kCapabilityHasDialog: a misbuilt + // plugin that advertises the bit but doesn't export the dialog vtable + // would otherwise reach the host's dialog flow and silently degrade to + // "no dialog", confusing the user. Block it here so the broken plugin + // never enters the loaded set. + if ((loaded.capabilities & PJ_DATA_SOURCE_CAPABILITY_HAS_DIALOG) != 0) { + auto vt = loaded.library.resolveDialogVtable(); + if (!vt) { + report( + DiagnosticLevel::kError, descriptor.id, + descriptor.dso_path.string() + ": advertises kCapabilityHasDialog but " + vt.error()); + return false; + } + } + loaded.file_extensions.reserve(descriptor.file_extensions.size()); for (const auto& ext : descriptor.file_extensions) { loaded.file_extensions.push_back(normalizeExtension(ext)); @@ -234,6 +251,18 @@ bool PluginRuntimeCatalog::loadAndRegisterToolbox(const PluginDescriptor& descri loaded.version = descriptor.version; loaded.capabilities = loaded.library.createHandle().capabilities(); + // Same fail-fast contract as DataSource above: kToolboxCapabilityHasDialog + // requires an exported dialog vtable. + if ((loaded.capabilities & PJ_TOOLBOX_CAPABILITY_HAS_DIALOG) != 0) { + auto vt = loaded.library.resolveDialogVtable(); + if (!vt) { + report( + DiagnosticLevel::kError, descriptor.id, + descriptor.dso_path.string() + ": advertises kToolboxCapabilityHasDialog but " + vt.error()); + return false; + } + } + report(DiagnosticLevel::kInfo, loaded.id, "Loaded Toolbox " + loaded.name + " from " + loaded.path); toolbox_plugins_.push_back(std::move(loaded)); return true;