Skip to content

Commit a98f458

Browse files
pabloinigoblascoDavide Faconti
andauthored
feat(sdk): add ScalarRecord/ObjectRecord for parser-controlled timestamps (#92)
* feat(sdk): add ScalarRecord/ObjectRecord for parser-controlled timestamps Introduce ScalarRecord {optional<Timestamp> ts, vector<NamedFieldValue>} and ObjectRecord {optional<Timestamp> ts, BuiltinObject} as the new return types of the SchemaHandler parse_scalars / parse_object callables. When ts is nullopt the host uses the message receive time as before; when set, the parser-provided timestamp is used instead — restoring the ability to align rows with timestamps embedded in the payload (e.g. ROS Header.stamp, JSON "timestamp" field, sensor capture time). The default parse() now iterates the returned records and respects each record's timestamp, also enabling multi-row output from a single payload (useful for batch messages such as JSON arrays). * refactor(sdk): drop multi-row vector on parse_scalars to keep one alloc per call Review feedback: returning std::vector<ScalarRecord> meant every call ended up doing two heap allocations in the common case (outer vector + the fields vector inside the only record), even for single-row decoding on a hot streaming path. The parser-controlled timestamp was the actual ask from the client; multi-row batch was added speculatively for JSON arrays and is not a shipping requirement. Drop the outer vector and return a single ScalarRecord; one allocation total, same shape as the pre-change API plus the optional<Timestamp>. Parsers that need to emit multiple rows per payload can stage a batch API later if/when a concrete use case shows up — the SchemaHandler shape leaves room for that without breaking ScalarRecord. * chore: bump conan package version to 0.3.0 * chore: bump plugin ABI to v5 --------- Co-authored-by: Davide Faconti <dfaconti@aurynrobotics.com>
1 parent fbb4241 commit a98f458

14 files changed

Lines changed: 105 additions & 72 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ endif()
183183
if(PJ_INSTALL_SDK)
184184
include(CMakePackageConfigHelpers)
185185

186-
set(PJ_PACKAGE_VERSION "0.2.1")
186+
set(PJ_PACKAGE_VERSION "0.3.0")
187187
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_core)
188188

189189
install(EXPORT plotjuggler_coreTargets

cmake/PjAbiCheck.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# bumping PJ_ABI_VERSION for a major break).
1616
#
1717
# Baseline location: pj_base/abi/baseline.abi — the single source of
18-
# truth for what the v4 ABI looks like. The baseline is generated from
18+
# truth for what the current ABI looks like. The baseline is generated from
1919
# a canary plugin DSO (mock_data_source_plugin) whose symbol surface
2020
# exercises the full ABI header set via the SDK.
2121
#

cmake/PjPluginManifest.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function(pj_emit_plugin_manifest TARGET)
8383

8484
if(NOT ARG_ABI_MAJOR)
8585
# Matches PJ_ABI_VERSION in pj_base/plugin_data_api.h. Bump in lockstep.
86-
set(ARG_ABI_MAJOR 4)
86+
set(ARG_ABI_MAJOR 5)
8787
endif()
8888

8989
# Track manifest edits so CMake reconfigures when the source changes.

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
88
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
99
10-
A consuming Conan recipe declares e.g. `plotjuggler_core/0.1.0` and then:
10+
A consuming Conan recipe declares e.g. `plotjuggler_core/0.3.0` and then:
1111
1212
find_package(plotjuggler_core REQUIRED COMPONENTS plugin_sdk)
1313
target_link_libraries(my_plugin PRIVATE plotjuggler_core::plugin_sdk)
@@ -27,7 +27,7 @@
2727

2828
class PlotjugglerCoreConan(ConanFile):
2929
name = "plotjuggler_core"
30-
version = "0.2.1"
30+
version = "0.3.0"
3131
license = "MIT"
3232
url = "https://github.com/PlotJuggler/plotjuggler_core"
3333
description = "C++20 foundation libraries for PlotJuggler: storage engine, plugin SDK, plugin host loaders."

pj_base/include/pj_base/builtin_object_abi.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
*
1010
* Canonical-object production (any concrete sdk::* type listed in
1111
* BuiltinObjectType — see pj_base/builtin/builtin_object.hpp) and the
12-
* pure-functional scalar production (Expected<vector<NamedFieldValue>>)
13-
* are C++ SDK contracts: plugins inheriting from MessageParserPluginBase
14-
* register handlers in SchemaHandler, and the in-process host consumes
15-
* them via MessageParserPluginBase::parseObject() and parseScalars()
16-
* called directly on the C++ pointer. Pure-C plugins emit scalars via
17-
* the parse() slot (writing to writeHost).
12+
* pure-functional scalar production (Expected<ObjectRecord> /
13+
* Expected<ScalarRecord>) are C++ SDK contracts: plugins inheriting from
14+
* MessageParserPluginBase register handlers in SchemaHandler, and the
15+
* in-process host consumes them via MessageParserPluginBase::parseObject()
16+
* and parseScalars() called directly on the C++ pointer. Pure-C plugins
17+
* emit scalars via the parse() slot (writing to writeHost).
1818
*/
1919
// Copyright 2026 Davide Faconti
2020
// SPDX-License-Identifier: MIT

pj_base/include/pj_base/plugin_data_api.h

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,20 @@ 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. Data-plane changes from the pre-v4 design:
48+
* v5 plugins advertise version 5. Relative to v4, this bump rejects
49+
* pre-v5 parser DSOs because the C++ MessageParserPluginBase
50+
* pure-functional contract changed parseScalars()/parseObject() return
51+
* types to ScalarRecord/ObjectRecord.
52+
*
53+
* The C data-plane layout remains the v4 layout:
4954
* - Arrow C Data Interface replaces Arrow IPC bytes at the boundary
5055
* (append_arrow_stream + read_series_arrow).
5156
* - append_arrow_ipc removed from all write hosts.
5257
* - read_series (PJ_materialized_series_t) removed from toolbox host.
5358
* - Every vtable slot is PJ_NOEXCEPT.
5459
* - Every slot carries a thread-class tag (// [main-thread], etc.).
5560
*/
56-
#define PJ_ABI_VERSION 4
61+
#define PJ_ABI_VERSION 5
5762

5863
/**
5964
* Convention for plugin-loaders:
@@ -90,13 +95,13 @@ typedef enum {
9095
PJ_PRIMITIVE_TYPE_UNSPECIFIED = 0xFF,
9196
} PJ_primitive_type_t;
9297

93-
/* ABI-FROZEN: layout permanent; changes = v4 break. */
98+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
9499
typedef struct {
95100
const char* data;
96101
size_t size;
97102
} PJ_string_view_t;
98103

99-
/* ABI-FROZEN: layout permanent; changes = v4 break. */
104+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
100105
typedef struct {
101106
const uint8_t* data;
102107
size_t size;
@@ -162,17 +167,17 @@ struct ArrowArrayStream {
162167

163168
#endif /* ARROW_C_DATA_INTERFACE */
164169

165-
/* ABI-FROZEN: layout permanent; changes = v4 break. */
170+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
166171
typedef struct {
167172
uint32_t id;
168173
} PJ_data_source_handle_t;
169174

170-
/* ABI-FROZEN: layout permanent; changes = v4 break. */
175+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
171176
typedef struct {
172177
uint32_t id;
173178
} PJ_topic_handle_t;
174179

175-
/* ABI-FROZEN: layout permanent; changes = v4 break. */
180+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
176181
typedef struct {
177182
PJ_topic_handle_t topic;
178183
uint32_t id;
@@ -506,7 +511,7 @@ typedef struct {
506511
* `pj.parser_object_write.v1` in addition to `pj.parser_write.v1`.
507512
* ========================================================================== */
508513

509-
/* ABI-FROZEN: layout permanent; changes = v5 break. */
514+
/* ABI-FROZEN: layout permanent; changes = ABI break. */
510515
typedef struct {
511516
uint32_t id; /* 0 == invalid handle */
512517
} PJ_object_topic_handle_t;

pj_base/include/pj_base/sdk/plugin_data_api.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <variant>
1515
#include <vector>
1616

17+
#include "pj_base/builtin/builtin_object.hpp"
18+
1719
#include "pj_base/expected.hpp"
1820
#include "pj_base/plugin_data_api.h"
1921
#include "pj_base/sdk/arrow.hpp"
@@ -96,6 +98,25 @@ struct BoundFieldValue {
9698
ValueRef value;
9799
};
98100

101+
/// One row of scalar data with an optional parser-controlled timestamp.
102+
/// When `ts` is nullopt the host uses the message's own timestamp
103+
/// (the value passed to parse_scalars). Set `ts` to override — e.g. to
104+
/// use a timestamp field embedded inside the payload rather than the
105+
/// transport-level receive time.
106+
struct ScalarRecord {
107+
std::optional<Timestamp> ts;
108+
std::vector<NamedFieldValue> fields;
109+
};
110+
111+
/// A builtin object (image, point cloud, …) with an optional parser-controlled
112+
/// timestamp. Mirrors ScalarRecord for the object route: when `ts` is nullopt
113+
/// the host uses the message's own timestamp; set it to use the sensor time
114+
/// embedded in the payload (e.g. ROS Header.stamp).
115+
struct ObjectRecord {
116+
std::optional<Timestamp> ts;
117+
BuiltinObject object;
118+
};
119+
99120
class CatalogSnapshot {
100121
public:
101122
CatalogSnapshot() = default;

pj_base/tests/abi_layout_sentinels_test.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
22
* @file abi_layout_sentinels_test.cpp
3-
* @brief Compile-time sentinels that pin the v4 plugin ABI layout.
3+
* @brief Compile-time sentinels that pin the plugin C ABI layout.
44
*
55
* Every assertion here is a static_assert. A failure at compile time means
66
* a struct defined in the ABI-visible headers has shifted in a way that
7-
* would silently break binary compatibility with existing v4 plugins.
7+
* would silently break binary compatibility with existing plugins.
88
*
99
* Maintenance rule:
1010
* - Sizes and alignments are allowed to GROW at the tail (new slots
@@ -16,7 +16,8 @@
1616
* break.
1717
* - MIN-size constants (PJ_*_MIN_VTABLE_SIZE) MUST NEVER INCREASE
1818
* within a major version. They are pinned at v4.0 release and are
19-
* the floor that forward compatibility relies on within the v4 series.
19+
* the floor that forward compatibility relies on within the current
20+
* major series.
2021
*
2122
* Pinning target: x86-64 System V (Linux/macOS on Intel/AMD). For other
2223
* ABIs (ARM64, MSVC), either confirm identical layout during initial
@@ -63,7 +64,7 @@ static_assert(sizeof(PJ_borrowed_dialog_t) == 16, "PJ_borrowed_dialog_t fat poin
6364

6465
// --- DataSource vtable (ABI-APPENDABLE within v4) ----------------------------
6566
// Offsets of v4.0 slots: PINNED. sizeof and MIN_VTABLE_SIZE are allowed to
66-
// grow at the tail via future appends within the v4 series.
67+
// grow at the tail via future appends within the current major series.
6768
static_assert(offsetof(PJ_data_source_vtable_t, protocol_version) == 0, "v4 prefix pinned");
6869
static_assert(offsetof(PJ_data_source_vtable_t, struct_size) == 4, "v4 prefix pinned");
6970
static_assert(offsetof(PJ_data_source_vtable_t, bind) == 40, "v4 bind slot pinned");
@@ -174,7 +175,7 @@ static_assert(offsetof(PJ_toolbox_host_vtable_t, read_series_arrow) == 64, "tool
174175
static_assert(sizeof(PJ_toolbox_host_vtable_t) == 72, "Toolbox host size");
175176

176177
// --- ABI version symbol ------------------------------------------------------
177-
static_assert(PJ_ABI_VERSION == 4, "v4 ABI version");
178+
static_assert(PJ_ABI_VERSION == 5, "v5 ABI version");
178179

179180
// This translation unit has no runtime behavior; the above are all
180181
// compile-time assertions. Linking only confirms the TU compiled.

pj_plugins/docs/ARCHITECTURE.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Plugin System Architecture
22

3-
## 0a. ABI stability and evolution rules (v4)
3+
## 0a. ABI stability and evolution rules (v5)
44

55
Seven rules the loader and every plugin author rely on. Breaking any of
6-
these is an ABI break and requires a v5 bump.
6+
these is an ABI break and requires a future `PJ_ABI_VERSION` bump.
77

88
1. **Boot-level ABI symbol.** Every plugin .so exports
99
`pj_plugin_abi_version` as a `uint32_t` symbol independent of any
@@ -18,13 +18,13 @@ these is an ABI break and requires a v5 bump.
1818
duplicate definitions across translation units in one DSO, so a single
1919
.so can host multiple plugin families (e.g. DataSource + Dialog) with
2020
one `PJ_*_PLUGIN(...)` macro per family — no duplicate-symbol error.
21-
Current value is `PJ_ABI_VERSION == 4`.
21+
Current value is `PJ_ABI_VERSION == 5`.
2222

2323
2. **Min-vtable-size floor, pinned at v4.0.** Each family header defines
2424
`PJ_<FAMILY>_MIN_VTABLE_SIZE` — the byte count of the vtable as
2525
shipped in v4.0. The loader accepts
2626
`struct_size >= MIN_VTABLE_SIZE`. This constant MUST NEVER GROW
27-
within the v4 series. Growing it would reject plugins compiled
27+
within a major series. Growing it would reject plugins compiled
2828
against older v4 headers (which correctly report a smaller size),
2929
silently breaking the forward-compatibility promise.
3030

@@ -40,7 +40,7 @@ these is an ABI break and requires a v5 bump.
4040
- **ABI-FROZEN**: `PJ_error_t`, `PJ_string_view_t`, `PJ_bytes_view_t`,
4141
`PJ_borrowed_dialog_t`, `PJ_service_t`, `PJ_service_registry_t`,
4242
handle types, primitive-value unions. Layout permanent; any change
43-
is a v4 break. `PJ_error_t` has `extended` + `extended_kind` slots
43+
is an ABI break. `PJ_error_t` has `extended` + `extended_kind` slots
4444
reserved as its one growth path — do not add further top-level
4545
fields.
4646
- **ABI-APPENDABLE**: all `*_vtable_t` types, service-host vtables,
@@ -101,10 +101,11 @@ for known ids or `nullptr`. Hosts call via `handle.getPluginExtension(id)`
101101
(tail-slot-gated). Use the experimental namespace for work-in-progress
102102
extensions; graduate to stable (`pj.<name>.v1`) once locked in.
103103

104-
## 0. Protocol v4 (current)
104+
## 0. C protocol v4 (current under ABI v5)
105105

106-
All four plugin families (DataSource, MessageParser, Toolbox, Dialog) track
107-
protocol v4. Key v4 distinguishing features (a superset of everything the
106+
All four plugin families (DataSource, MessageParser, Toolbox, Dialog) keep
107+
the v4 C protocol layouts under the v5 boot ABI. Key v4 distinguishing
108+
features (a superset of everything the
108109
previously-circulated pre-v4 design included):
109110

110111
- **Arrow C Data Interface at the data boundary.** The write-host

pj_plugins/docs/data-source-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Writing a DataSource Plugin
22

3-
> **Tracks the v4 plugin ABI** (`PJ_ABI_VERSION == 4`). For the full
3+
> **Tracks the v5 plugin ABI** (`PJ_ABI_VERSION == 5`). For the full
44
> evolution rules (tail-slot gating, MIN_VTABLE_SIZE, ABI-FROZEN vs
55
> ABI-APPENDABLE structs, Arrow C Data Interface at the write boundary,
66
> PJ_NOEXCEPT discipline) see `ARCHITECTURE.md`. This guide walks

0 commit comments

Comments
 (0)