Skip to content

Commit 3ef471e

Browse files
facontidavideDavide Faconticlaude
authored
Plugin hardening (#85)
* fix: retain plugin library lifetime * fix: harden plugin host boundaries * docs: clarify plugin author guidance * fix: avoid dialog instantiation during catalog scans Add a static manifest tail slot to dialog vtables and teach catalog discovery to use it when present, while preserving the legacy create/get_manifest fallback for existing plugins. Update SDK examples and docs for the two-argument dialog export, cover the static-manifest path in tests, and simplify redundant unexpected(std::string(...)) error construction. * fix: fallback for null dialog static manifests Use the dialog static manifest tail slot only when it is present and non-null. New-header plugins using the legacy one-argument dialog macro now correctly fall back to runtime manifest discovery. * refactor: move plugin vtable validation out of header Keep the loader required-slot checks in a private implementation unit and wire them through an internal detail library. Also remove the dialog protocol subproject's global C++ standard override so it no longer downgrades the root build setting. * formatting * feat: add parser arrow stream writes * fix: harden dialog plugin loading * fix: enable MSVC conformant preprocessor PJ_DIALOG_PLUGIN's overload-by-arg-count idiom relies on __VA_ARGS__ splitting on commas inside nested macro calls. MSVC's traditional preprocessor passes __VA_ARGS__ as a single token, causing the 2-argument form to dispatch to the legacy 1-argument macro and generate a bogus dialogVtableFor<Class, kManifest> specialisation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update missing slot fixture for parser tail slot * fix: export MSVC preprocessor mode for dialog SDK --------- Co-authored-by: Davide Faconti <dfaconti@aurynrobotics.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e936b85 commit 3ef471e

70 files changed

Lines changed: 2167 additions & 639 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ option(PJ_ENABLE_ABI_CHECK "Enable abidiff-based ABI drift gate (requires libabi
3232
# ---------------------------------------------------------------------------
3333

3434
if(MSVC)
35-
set(PJ_WARNING_FLAGS /W4 /WX /permissive-)
35+
# /Zc:preprocessor: conformant preprocessor — required so __VA_ARGS__ inside
36+
# nested macro calls (e.g. PJ_DIALOG_PLUGIN's overload-by-arg-count idiom)
37+
# splits on commas instead of being passed as a single token.
38+
set(PJ_WARNING_FLAGS /W4 /WX /permissive- /Zc:preprocessor)
3639
else()
3740
set(PJ_WARNING_FLAGS
3841
-Wall -Wextra -Werror -Wshadow -Wnon-virtual-dtor -Wold-style-cast

docs/dialog-sdk-reference.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
5353
| Method | Description |
5454
|--------|-------------|
5555
| `setButtonText(name, text)` | Set button label |
56-
| `setShortcut(name, key_sequence)` | Assign keyboard shortcut (e.g. `"Ctrl+A"`) **NEW** |
56+
| `setButtonIcon(name, svg_data)` | Set an inline SVG icon |
57+
| `setShortcut(name, key_sequence)` | Assign keyboard shortcut (e.g. `"Ctrl+A"`) |
5758
| `setFilePicker(name, text, filter, title)` | Turn into file picker |
58-
| `setFolderPicker(name, text, title)` | Turn into folder picker **NEW** |
59+
| `setFolderPicker(name, text, title)` | Turn into folder picker |
5960

6061
### QListWidget
6162

@@ -71,13 +72,23 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
7172
| `setTableHeaders(name, vector<string>)` | Set column headers |
7273
| `setTableRows(name, vector<vector<string>>)` | Set row data |
7374
| `setSelectedRows(name, vector<int>)` | Set selected row indices |
74-
| `setDisabledRows(name, vector<int>)` | Grey out rows (non-selectable) **NEW** |
75+
| `setDisabledRows(name, vector<int>)` | Grey out rows (non-selectable) |
76+
77+
### QFrame Chart Container
78+
79+
| Method | Description |
80+
|--------|-------------|
81+
| `setChartSeries(name, vector<ChartSeries>)` | Create/update chart series inside a QFrame |
82+
| `clearChart(name)` | Remove chart series |
83+
| `setChartZoomEnabled(name, bool)` | Enable chart zoom/pan events |
7584

7685
### QPlainTextEdit
7786

7887
| Method | Description |
7988
|--------|-------------|
80-
| `setPlainText(name, text)` | Set plain text content **NEW** |
89+
| `setPlainText(name, text)` | Set plain text content |
90+
| `setCodeContent(name, code)` | Set editable code content |
91+
| `setCodeLanguage(name, lang)` | Set syntax highlighting language such as `"lua"` or `"python"` |
8192

8293
### QTabWidget
8394

@@ -98,6 +109,7 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
98109
|--------|-------------|
99110
| `setEnabled(name, bool)` | Enable/disable widget |
100111
| `setVisible(name, bool)` | Show/hide widget |
112+
| `setDropTarget(name, bool)` | Accept dropped item labels and emit `onItemsDropped` |
101113

102114
### Dialog-level Commands
103115

@@ -121,9 +133,12 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch
121133
| `onValueChanged(name, double)` | QDoubleSpinBox | New double value |
122134
| `onClicked(name)` | QPushButton | (no payload) |
123135
| `onFileSelected(name, path)` | QPushButton (file picker) | Selected file path |
124-
| `onFolderSelected(name, path)` | QPushButton (folder picker) | Selected folder path **NEW** |
136+
| `onFolderSelected(name, path)` | QPushButton (folder picker) | Selected folder path |
125137
| `onSelectionChanged(name, items)` | QListWidget, QTableWidget | Vector of selected item texts |
126138
| `onItemDoubleClicked(name, index)` | QListWidget, QTableWidget | Row index of double-clicked item |
139+
| `onCodeChanged(name, code)` | QPlainTextEdit code editor | Edited code |
140+
| `onItemsDropped(name, items)` | Any widget with `setDropTarget` | Dropped item labels |
141+
| `onChartViewChanged(name, x_min, x_max, y_min, y_max)` | QFrame chart container | Visible chart range |
127142
| `onTabChanged(name, index)` | QTabWidget | New tab index |
128143

129144
---
@@ -137,7 +152,6 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch
137152
| `onRejected()` | User clicked Cancel | void |
138153
| `saveConfig()` | Host persisting state | JSON string |
139154
| `loadConfig(json)` | Host restoring state | `true` if state changed |
140-
| `lastError()` | Host checking for errors | Error string or empty |
141155

142156
---
143157

docs/toolbox-porting-gap-analysis.md

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# Toolbox Porting — SDK Gap Analysis
22

3-
This document provides an exhaustive comparison of PlotJuggler 3.x toolbox plugin
4-
features against the current PJ 4.x SDK capabilities. It identifies what Dialog SDK
5-
extensions are required to port the existing toolboxes with full feature parity.
3+
This historical document compares PlotJuggler 3.x toolbox plugin features
4+
against the PJ 4.x SDK capabilities available when the porting work began.
5+
Some gaps listed below have since been closed in the Dialog SDK.
66

77
**Scope:** `plotjuggler_core` (Dialog SDK, `ToolboxPluginBase`) + `pj-official-plugins`
88
(Quaternion to RPY port as the reference implementation).
99

10-
**Summary:** The SDK is sufficient for simple, headless processing toolboxes (Quaternion
11-
reaches ~80% feature parity). FFT drops to ~60%. The Lua Reactive Script Editor cannot
12-
be ported without major SDK extensions. Six gaps are blocking and require new
13-
infrastructure: embedded chart widget, zoom event, drag-drop on chart, editable code
14-
editor, reactive time-tick, and ScatterXY output type.
10+
**Current status:** Dialog SDK support now exists for chart containers,
11+
chart zoom/pan events, drop targets, editable `QPlainTextEdit` code editors,
12+
and periodic ticks. Treat the original gap analysis below as historical
13+
context for porting priorities, not as a current reference. Remaining
14+
porting risks should be revalidated against the current SDK and datastore
15+
surface; ScatterXY-style output remains a separate datastore concern.
1516

1617
---
1718

@@ -51,7 +52,10 @@ In the current port:
5152
- No preview — the transform runs directly on OK
5253
- Modal dialog that opens and closes
5354

54-
This is not a failure of the port — it is a limitation of the current SDK: `DialogPluginTyped` does not support preview widgets (charts) or drag-and-drop. Those features require Dialog SDK extensions.
55+
At the time of the original port, `DialogPluginTyped` did not support preview
56+
widgets or drag-and-drop. Current Dialog SDKs provide chart containers,
57+
chart range events, and generic drop targets; any toolbox port should be
58+
revalidated against those APIs before treating this section as blocking.
5559

5660
The end-to-end wiring is not yet confirmed at 100%. The plugin loads, the dialog opens with combo boxes, but we have not verified that the transform produces data visible in the plots. There may be a mismatch in catalog field names.
5761

@@ -74,11 +78,11 @@ Three toolbox plugins exist in `plotjuggler/plotjuggler_plugins/`:
7478
| Aspect | PJ 3.x | New SDK |
7579
|--------|--------|---------|
7680
| Plugin owns its UI | Yes — full `QWidget` with arbitrary children | No — `.ui` file + host-rendered dialog runtime |
77-
| Data access | Direct reference to `PlotDataMapRef` | Via handles: `catalogSnapshot`, `readSeries`, `appendRecord` |
81+
| Data access | Direct reference to `PlotDataMapRef` | Via handles: `catalogSnapshot`, `readSeriesArrow`, `appendRecord` |
7882
| Communication | Qt signals/slots | C ABI vtables + JSON config |
7983
| Qt dependency | Required | None in core/plugin SDK; GUI hosts supply their toolkit runtime |
80-
| Embedded chart preview | Integrated (`PlotWidgetBase`) | Not available in the SDK |
81-
| Drag-and-drop | Via `eventFilter` in the plugin | Not supported by the dialog protocol |
84+
| Embedded chart preview | Integrated (`PlotWidgetBase`) | Available through chart data on QFrame containers |
85+
| Drag-and-drop | Via `eventFilter` in the plugin | Available through dialog drop targets and `onItemsDropped` |
8286
| Output type | `PlotData` (time series) **or** `PlotDataXY` (scatter) | Time-indexed series only |
8387
| Transform registry | Yes — re-applied on layout reload | No — outputs are static data |
8488
| Reactive execution | `ReactiveLuaFunction` re-runs on every slider tick | `onTick()` exists but cannot write to datastore or access current timestamp |
@@ -250,7 +254,10 @@ The Lua editor uses `QCodeEditor` (external library) with:
250254
| Instances | 3: global code, function body, library |
251255
| Font size | Ctrl+wheel: 8–14 pt range, persisted to `QSettings` |
252256

253-
The current SDK has `setPlainText()` for **read-only** text display only. The dialog docs explicitly state that `QTextEdit` and `QPlainTextEdit` are **not supported** by the widget binding system. Without an editable code widget, the Lua editor cannot exist.
257+
The current Dialog SDK has `setPlainText()`, editable `setCodeContent()`,
258+
`setCodeLanguage()`, and `onCodeChanged()` for `QPlainTextEdit`-based code
259+
editing. The Lua editor port still needs product-level validation, but an
260+
editable code widget is no longer a missing SDK primitive.
254261

255262
**SDK equivalent needed:**
256263
```cpp
@@ -299,7 +306,7 @@ The sol2 Lua API exposed to scripts:
299306
| `CreatedSeriesTime` | `at(i)`, `clear()`, `push_back(x,y)`, `size()` |
300307
| `CreatedSeriesXY` | `at(i)`, `clear()`, `push_back(x,y)`, `size()` |
301308

302-
The current SDK `onTick()` in `DialogPluginTyped` fires periodically while the dialog is open, but:
309+
Dialog-level `onTick()` in `DialogPluginTyped` fires periodically while the dialog is open, but:
303310
- Has no access to the current time slider value
304311
- Cannot call `toolboxHost().appendRecord()` — the toolbox host is separate from the dialog plugin
305312
- Cannot be used to implement time-reactive series generation
@@ -388,13 +395,13 @@ The Lua editor persists several settings directly via `QSettings` outside of `sa
388395
| 4.2 | Transform registry (re-apply on reload) | MEDIUM | MEDIUM | HIGH | MEDIUM |
389396
| 4.3 | `QSettings` fine-grained persistence | LOW | LOW | MEDIUM | LOW |
390397
391-
**Feature parity estimate without SDK changes:**
398+
**Original feature parity estimate from the first gap analysis:**
392399
393400
| Toolbox | Parity | Blocking gaps |
394401
|---------|--------|---------------|
395-
| Quaternion | ~80% | No chart preview; no drag-drop auto-fill |
396-
| FFT | ~60% | No chart preview; no drag-drop; no zoom-aware range; no ScatterXY output |
397-
| Lua Editor | ~10% | No editable code widget; no reactive execution |
402+
| Quaternion | ~80% | Originally blocked by chart preview and drag-drop auto-fill; revalidate against current chart/drop APIs |
403+
| FFT | ~60% | Revalidate chart, drag-drop, and zoom support; ScatterXY output remains a datastore question |
404+
| Lua Editor | ~10% | Editable code widget now exists; reactive execution still needs product-level validation |
398405
399406
---
400407
@@ -478,4 +485,6 @@ void appendArrowScatterIpc(TopicHandle topic,
478485
479486
---
480487
481-
*Gaps 1.1, 1.4, 2.1, 3.1, 3.3, and 4.1 are blocking for their respective toolboxes and require new SDK infrastructure. The remaining gaps represent UX degradation that can be partially mitigated with workarounds within the current SDK.*
488+
*Historical note: several gaps called blocking in this document have since
489+
received SDK support. Before using this as a porting checklist, re-run the
490+
gap analysis against the current Dialog SDK and datastore APIs.*

pj_base/include/pj_base/data_source_protocol.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @file data_source_protocol.h
33
* @brief C ABI protocol for DataSource plugins (version 4).
44
*
5-
* v4 summary of changes vs v3:
5+
* v4 summary:
66
* - Arrow C Data Interface at the write boundary: bulk loaders use
77
* SourceWriteHost::append_arrow_stream instead of per-row appends.
88
* See pj_base/plugin_data_api.h. append_arrow_ipc is removed.
@@ -49,7 +49,7 @@ extern "C" {
4949
* Reads of any slot added after v4.0 must be gated with PJ_HAS_TAIL_SLOT.
5050
*
5151
* Computed as `offsetof(last v4.0 slot) + sizeof(its function pointer)`.
52-
* Last v4.0 slot is `get_plugin_extension` (promoted from v3.1 tail).
52+
* Last v4.0 slot is `get_plugin_extension`.
5353
*/
5454
#define PJ_DATA_SOURCE_MIN_VTABLE_SIZE \
5555
(offsetof(PJ_data_source_vtable_t, get_plugin_extension) + sizeof(const void* (*)(void*, PJ_string_view_t)))

pj_base/include/pj_base/message_parser_protocol.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
* @file message_parser_protocol.h
33
* @brief C ABI protocol for MessageParser plugins (version 4).
44
*
5-
* v4 summary of changes vs v3:
5+
* v4 summary:
66
* - Every vtable slot is PJ_NOEXCEPT and carries a thread-class tag.
77
* - Parser write host (pj.parser_write.v1) no longer has
8-
* append_arrow_ipc — see plugin_data_api.h. Parsers stay per-record;
9-
* the host coalesces into Arrow batches internally.
8+
* append_arrow_ipc — see plugin_data_api.h. Parsers normally write
9+
* per-record, with an optional append_arrow_stream tail slot for
10+
* parser-shaped formats that naturally decode batches.
1011
*
1112
* Pure-functional production (scalars by value, canonical objects by
1213
* value with BufferAnchor) is a C++ SDK contract: parsers inheriting from
@@ -46,7 +47,7 @@ extern "C" {
4647
* MUST NOT GROW when new tail slots are appended. See PJ_ABI_VERSION comment
4748
* in plugin_data_api.h for the rationale.
4849
*
49-
* Last v4.0 slot is `get_plugin_extension` (promoted from v3 tail).
50+
* Last v4.0 slot is `get_plugin_extension`.
5051
*/
5152
#define PJ_MESSAGE_PARSER_MIN_VTABLE_SIZE \
5253
(offsetof(PJ_message_parser_vtable_t, get_plugin_extension) + sizeof(const void* (*)(void*, PJ_string_view_t)))
@@ -82,8 +83,8 @@ typedef struct PJ_message_parser_vtable_t {
8283
* and the marketplace; must be unique per plugin.
8384
* "name" — human-readable plugin name (string).
8485
* "version" — semver version string (string).
85-
* "encoding" — encoding this parser handles (string). The host uses
86-
* this to match binding requests to parsers.
86+
* "encoding" — encodings this parser handles (array of strings). The host
87+
* uses this to match binding requests to parsers.
8788
*/
8889
const char* manifest_json;
8990

pj_base/include/pj_base/plugin_data_api.h

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extern "C" {
4545
* (e.g. DataSource + Dialog in one .so) work without any extra ceremony.
4646
* Do not redefine it manually.
4747
*
48-
* v4 plugins advertise version 4. Breaking v3→v4 changes:
48+
* v4 plugins advertise version 4. Data-plane changes from the pre-v4 design:
4949
* - Arrow C Data Interface replaces Arrow IPC bytes at the boundary
5050
* (append_arrow_stream + read_series_arrow).
5151
* - append_arrow_ipc removed from all write hosts.
@@ -388,9 +388,10 @@ typedef struct {
388388
* Parser write host: single-topic writes. The bound topic is set at
389389
* service-creation time; the parser plugin never names it.
390390
*
391-
* No append_arrow_stream: parsers are inherently per-message. The host
392-
* internally coalesces per-record appends into Arrow batches before
393-
* committing to storage — plugin authors never see the batch grain. */
391+
* append_arrow_stream is an optional tail slot for parser-shaped formats
392+
* that naturally decode a batch in one parse() call. Ownership matches
393+
* PJ_source_write_host_vtable_t::append_arrow_stream. Plugins must gate
394+
* this slot with PJ_HAS_TAIL_SLOT when calling through the C ABI directly. */
394395
typedef struct PJ_parser_write_host_vtable_t {
395396
uint32_t abi_version;
396397
uint32_t struct_size;
@@ -409,8 +410,22 @@ typedef struct PJ_parser_write_host_vtable_t {
409410
bool (*append_bound_record)(
410411
void* ctx, int64_t timestamp, const PJ_bound_field_value_t* fields, size_t field_count,
411412
PJ_error_t* out_error) PJ_NOEXCEPT;
413+
414+
/* [stream-thread] Optional batch path. Plugin hands ownership of an Arrow
415+
* C Data Interface stream for the bound topic. The timestamp column rule
416+
* and success/failure ownership contract are identical to
417+
* PJ_source_write_host_vtable_t::append_arrow_stream. */
418+
bool (*append_arrow_stream)(
419+
void* ctx, struct ArrowArrayStream* stream, PJ_string_view_t timestamp_column, PJ_error_t* out_error) PJ_NOEXCEPT;
412420
} PJ_parser_write_host_vtable_t;
413421

422+
/*
423+
* Parser write-host v4.0 floor, before append_arrow_stream was added as an
424+
* optional tail slot. Hosts/plugins that care about the batch path must use
425+
* PJ_HAS_TAIL_SLOT(PJ_parser_write_host_vtable_t, vtable, append_arrow_stream).
426+
*/
427+
#define PJ_PARSER_WRITE_HOST_MIN_VTABLE_SIZE (offsetof(PJ_parser_write_host_vtable_t, append_arrow_stream))
428+
414429
typedef struct {
415430
void* ctx;
416431
const PJ_parser_write_host_vtable_t* vtable;

pj_base/include/pj_base/sdk/arrow.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ using ArrowArrayHolder = detail::ArrowHolder<::ArrowArray>;
133133
///
134134
/// Recommended usage: hand the holder by rvalue reference to the
135135
/// `appendArrowStream(ArrowStreamHolder&&, ...)` overload on
136-
/// `SourceWriteHostView` / `ToolboxHostView`, which disarms the holder on
137-
/// success:
136+
/// `SourceWriteHostView`, `ParserWriteHostView`, or `ToolboxHostView`,
137+
/// which disarms the holder on success:
138138
///
139139
/// ArrowStreamHolder stream(buildMyStream());
140140
/// auto status = writeHost.appendArrowStream(topic, std::move(stream), "timestamp");
141141
/// // on success, holder is inert; on failure, destructor releases the stream.
142142
///
143+
/// Parser write hosts omit the `topic` argument because the host binds the
144+
/// parser to one topic before parsing begins.
145+
///
143146
/// The raw-pointer overload of `appendArrowStream` remains as an ABI escape
144147
/// hatch for callers that own the stream through some other mechanism.
145148
using ArrowStreamHolder = detail::ArrowHolder<::ArrowArrayStream>;

0 commit comments

Comments
 (0)