diff --git a/CHANGELOG.md b/CHANGELOG.md index dc18db92..52a00154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,19 @@ Fixes: - The Conan package now propagates `PJ_ASSERT_THROWS` to consumers when built with `assert_throws=True`; consumers compiled `PJ_ASSERT` as `assert` against archives built to throw. +The direct C++ parser bridge (docs only, no code change): + +- The docs said SDK 1.0 removes the bridge through which a host casts a parser's context to + `MessageParserPluginBase*`. It does not: PlotJuggler 4 reads objects that way for every parser. + The bridge is now documented as deprecated but supported through 1.x, with the rule that follows + from it (`pj_plugins/docs/ARCHITECTURE.md`, section 0a): the `MessageParserPluginBase` prefix, + `sdk::ObjectRecord`, `sdk::BuiltinObject`, `PJ::Expected` and every builtin object struct are + frozen layouts, and a member change in any of them is a MAJOR. A 1.x release lifts the freeze once + the host reads objects through `pj.parser_functional.v2` only. +- 1.0.0 itself changes two of those layouts: `sdk::Log` (`category`) and `sdk::DepthImage` + (`frame_id`). A parser built against 0.x that returns either type must be rebuilt for a 1.0 host. + No parser in pj-official-plugins returns them. + ## [0.40.0] Host contract: extended: pj.mcp_endpoint.v1, PJ_mcp_endpoint_host_vtable_t::acquire_endpoint, diff --git a/CLAUDE.md b/CLAUDE.md index 4e67b0f1..6cd3a9fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,10 @@ Propose a release only according to plugin impact: Tail-append optional slots and gate them by `struct_size`; keep minimum vtable sizes frozen. `abidiff` must show additions only for a MINOR. +A member change in a builtin object struct (`pj_base/builtin/`) is a MAJOR while +PlotJuggler reads parser objects through the direct C++ bridge; `abidiff` does +not see it. See [ABI evolution rules](pj_plugins/docs/ARCHITECTURE.md#0a-abi-stability-and-evolution-rules-v5). + The SDK version is **build identity only** — it names what a plugin compiled against, never what host it runs on. A release that only adds client-side helpers (static-library code compiled into the plugin) does NOT change runtime diff --git a/docs/builtin_type.md b/docs/builtin_type.md index d8d95a6c..b6216de9 100644 --- a/docs/builtin_type.md +++ b/docs/builtin_type.md @@ -98,6 +98,14 @@ frozen eligible bulk field as a splice into the input payload; the host still reconstructs a canonical SDK object. The schema and wire-format details stay private; public SDK headers expose only SDK structs. +**A struct member is binary interface for now.** PlotJuggler 4 does not use that +boundary yet: it reads the C++ struct a parser returns directly. Until it moves +to functional v2, adding, removing or reordering a member of a builtin struct +breaks a parser and a host built from different SDK releases, and is released +as a MAJOR. A new builtin type, or a proto field the struct does not expose, is +unaffected. See +[the direct C++ parser bridge](../pj_plugins/docs/ARCHITECTURE.md#0a-abi-stability-and-evolution-rules-v5). + ## Serialization Families Builtin objects fall into two serialization families: diff --git a/pj_base/include/pj_base/message_parser_protocol.h b/pj_base/include/pj_base/message_parser_protocol.h index 3ff1b091..d03753a4 100644 --- a/pj_base/include/pj_base/message_parser_protocol.h +++ b/pj_base/include/pj_base/message_parser_protocol.h @@ -13,10 +13,11 @@ * extension declared in parser_functional_protocol.h. Scalar fields cross * as synchronous borrowed C values; canonical objects cross as a stable type * tag plus canonical wire bytes or a v2 splice. No C++ parser class or STL - * value crosses the new path. Parsers built before SDK 0.22 do not expose the - * extension and may use a deprecated host compatibility bridge until the next - * SDK major version. Pure-C plugins may continue using parse() to write - * scalars to writeHost. + * value crosses the new path. A host that has not adopted the extension reads + * objects through the direct C++ bridge instead, which freezes the C++ layouts + * it shares with the plugin: see "The direct C++ parser bridge" in + * pj_plugins/docs/ARCHITECTURE.md, section 0a. Pure-C plugins may continue + * using parse() to write scalars to writeHost. * * The host obtains the plugin's vtable via `PJ_get_message_parser_vtable()` * and drives the plugin through: create -> bind(registry) -> diff --git a/pj_base/include/pj_base/plugin_data_api.h b/pj_base/include/pj_base/plugin_data_api.h index 4c1caf7c..a8f4c5e0 100644 --- a/pj_base/include/pj_base/plugin_data_api.h +++ b/pj_base/include/pj_base/plugin_data_api.h @@ -49,8 +49,9 @@ extern "C" { * pre-v5 parser DSOs because the C++ MessageParserPluginBase * pure-functional contract changed parseScalars()/parseObject() return * types to ScalarRecord/ObjectRecord. SDK 0.21 subsequently placed those - * results behind an additive C extension, now `pj.parser_functional.v2`; direct - * host calls on the C++ base remain only as a temporary old-plugin bridge. + * results behind an additive C extension, now `pj.parser_functional.v2`. Direct + * host calls on the C++ base remain supported through 1.x as a deprecated + * bridge (pj_plugins/docs/ARCHITECTURE.md, section 0a). * * The C data-plane layout remains the v4 layout: * - Arrow C Data Interface replaces Arrow IPC bytes at the boundary diff --git a/pj_plugins/docs/ARCHITECTURE.md b/pj_plugins/docs/ARCHITECTURE.md index 8fc1aece..251e3bc1 100644 --- a/pj_plugins/docs/ARCHITECTURE.md +++ b/pj_plugins/docs/ARCHITECTURE.md @@ -13,6 +13,31 @@ diffs the extracted host-surface inventory against the checked-in snapshot), and the installed copy of the table is what downstream plugin repositories validate `min_sdk_required` against. +**The direct C++ parser bridge is a second, temporary ABI.** PlotJuggler 4 +reads objects from a MessageParser by casting the plugin's opaque context to +`MessageParserPluginBase*` and calling `parseObject()`. It does this for every +parser, not only for the pre-0.22 ones that lack `pj.parser_functional.v2`. +Until the host reads objects through v2 only, these C++ layouts cross the +plugin boundary and are frozen like a C struct: + +- the member prefix of `MessageParserPluginBase` and the order of its virtual + functions (members do not move, additions are tail-only); +- `sdk::ObjectRecord`, `sdk::BuiltinObject` and `PJ::Expected`; +- every builtin object struct under `pj_base/builtin/` (`sdk::Image`, + `sdk::PointCloud`, ...), including the structs nested in them. + +Adding, removing, reordering or retyping a member of any of them breaks a +parser and a host built from different SDK releases. Nothing detects it: +`PJ_ABI_VERSION` covers the C structs, and the `abidiff` gate checks a +DataSource canary. Such a change is therefore released as a MAJOR. A new +builtin type is not affected (it adds a struct; it changes none), and neither +is a new proto field that the C++ struct does not expose. + +The freeze ends in a 1.x release, once the host no longer casts. That release +states it in its changelog. Lifting it changes nothing for a plugin's sources +or binary, except that a parser built before SDK 0.22, which has no v2 +extension, can no longer emit objects. + 1. **Boot-level ABI symbol.** Every plugin .so exports `pj_plugin_abi_version` as a `uint32_t` symbol independent of any vtable. The host `dlsym`s it BEFORE fetching the family vtable; @@ -837,10 +862,10 @@ when the separate type tag is known. Concrete builtins and codecs live under `pj_base/builtin/`; see `docs/builtin_type.md` for the catalog. Parsers built before SDK 0.22 expose no usable functional extension: pre-0.21 -builds expose none, and 0.21 builds expose only the retired v1. The host keeps a -clearly isolated, deprecated direct-C++ bridge for those binaries. New plugins -and new host code use v2; SDK 1.0 can remove the bridge and the frozen -`MessageParserPluginBase` layout constraint. +builds expose none, and 0.21 builds expose only the retired v1. New plugins and +new host code use v2. PlotJuggler 4 still reads objects through the direct C++ +bridge for every parser; section 0a states what that freezes and when the +freeze ends. ## Per-topic pause (demand-driven subscription) diff --git a/pj_plugins/docs/REQUIREMENTS.md b/pj_plugins/docs/REQUIREMENTS.md index 56530e4b..855f73cb 100644 --- a/pj_plugins/docs/REQUIREMENTS.md +++ b/pj_plugins/docs/REQUIREMENTS.md @@ -137,9 +137,13 @@ Independent decoder, typically driven by a DataSource via the host. parser that still overrides only legacy `parse()` must not advertise the functional extension. Hosts query after `bind_schema` and do not cache an earlier absence. -- Until SDK 1.0, a host may use an isolated deprecated direct-C++ bridge for a - pre-0.22 parser, which exposes no v2 extension. The bridge and its - class-layout constraint are removed together at 1.0. +- A host may read objects through the deprecated direct-C++ bridge (a cast of + the parser context to `MessageParserPluginBase*`) instead of v2. The bridge + is supported through 1.x. While any supported host uses it, the layouts it + shares with the plugin are frozen: the `MessageParserPluginBase` member + prefix and virtual order, `sdk::ObjectRecord`, `sdk::BuiltinObject`, + `PJ::Expected` and every builtin object struct. A 1.x release lifts the + freeze once the host reads objects through v2 only. ### Dialog diff --git a/pj_plugins/docs/message-parser-guide.md b/pj_plugins/docs/message-parser-guide.md index cd86e220..babf3fdb 100644 --- a/pj_plugins/docs/message-parser-guide.md +++ b/pj_plugins/docs/message-parser-guide.md @@ -683,9 +683,18 @@ Do not bypass it with direct C++ calls. Parsers built before SDK 0.22 expose no usable functional extension: pre-0.21 builds expose none, and 0.21 builds expose only the retired v1. New host code -negotiates v2 and may use a deprecated, isolated direct-C++ bridge only when v2 -is absent. It must not otherwise cast an opaque parser context; SDK 1.0 removes -the bridge. +negotiates v2. + +PlotJuggler 4 has not made that move: it reads objects from every parser by +casting the opaque context to `MessageParserPluginBase*` and calling +`parseObject()`. That direct C++ bridge is deprecated but supported through +1.x. For a parser author it has one consequence: a parser and the host must +agree on the layout of the builtin struct the parser returns, so a parser that +emits a type whose struct changed in an SDK release has to be rebuilt against +that release. The SDK treats such a change as a MAJOR for exactly that reason +(see [ABI rules](ARCHITECTURE.md#0a-abi-stability-and-evolution-rules-v5)). +Nothing in a parser's source depends on the bridge, and nothing changes for +it when the host moves to v2. Builtin types and their canonical codecs live under `pj_base/builtin/`, one header per type. `serializeBuiltinObject()` / `deserializeBuiltinObject()` diff --git a/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp b/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp index bf9851ca..a9068080 100644 --- a/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp +++ b/pj_plugins/include/pj_plugins/host/message_parser_handle.hpp @@ -135,8 +135,9 @@ class MessageParserHandle { } /// True when this parser exposes the pure-functional C extension (v2, SDK - /// 0.22 and later). Older parsers return false and can be handled through the deprecated - /// in-process C++ compatibility bridge until the next SDK major version. + /// 0.22 and later). Older parsers return false; a host can only read their + /// objects through the deprecated in-process C++ bridge + /// (pj_plugins/docs/ARCHITECTURE.md, section 0a). [[nodiscard]] bool supportsFunctionalParsing() const { return functionalExtension() != 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 62fc43f8..1b0d946c 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 @@ -86,13 +86,16 @@ struct MessageParserPluginBaseLayoutSentinel; /** * Base class for MessageParser plugins (protocol v4). * - * TRANSITIONAL ABI LAYOUT CONTRACT: hosts use the - * `pj.parser_functional.v2` C extension and never cast a newly-built plugin's - * opaque context to this class. Hosts may retain a deprecated direct-C++ - * bridge only for pre-0.22 plugins that do not expose the extension. Therefore - * the existing member prefix remains frozen within PJ_ABI_VERSION 5: members - * must not move, and additions are tail-only. SDK 1.0 may remove that bridge - * and this layout constraint together. + * ABI LAYOUT CONTRACT (the direct C++ parser bridge): the intended host path + * is the `pj.parser_functional.v2` C extension, which shares no C++ type with + * the plugin. PlotJuggler 4 does not use it yet: it casts every parser's + * opaque context to this class and calls parseObject(). While a host does + * that, this class is binary interface: the existing member prefix and the + * order of its virtual functions are frozen (members must not move, additions + * are tail-only), and so are the layouts of sdk::ObjectRecord, + * sdk::BuiltinObject, PJ::Expected and every builtin object struct. A 1.x + * release lifts the constraint once the host has moved to v2. Full rule: + * pj_plugins/docs/ARCHITECTURE.md, section 0a. */ class MessageParserPluginBase { public: