Skip to content

fix(message-parser): gate the functional route on the bound schema - #174

Merged
facontidavide merged 1 commit into
mainfrom
fix/parser-functional-bound-schema
Aug 9, 2026
Merged

fix(message-parser): gate the functional route on the bound schema#174
facontidavide merged 1 commit into
mainfrom
fix/parser-functional-bound-schema

Conversation

@facontidavide

Copy link
Copy Markdown
Contributor

Closes #171.

Problem

trampoline_get_plugin_extension advertised pj.parser_functional.v1 whenever the instance had any registered SchemaHandler:

if (sv == PJ_PARSER_FUNCTIONAL_EXTENSION_V1 && !self->handlers_.empty()) {

But parseScalars/parseObject are final and dispatch on the bound schema, erroring with "parser does not register schema: <bound>" when that specific schema has no handler. So a mixed-model plugin — handlers for some schemas, legacy parse() for the rest, which is exactly the shape parse()'s own doc-comment sanctions ("a fallback for type names not in the registered table (e.g. a ROS-style generic flattener)") — claimed the functional route on every schema. An SDK 0.21 host that prefers that route then fails every message on the unhandled topics.

This isn't hypothetical: PJ4's streaming_caching_parser_plugin test fixture had this shape and went zero-rows the moment the host started preferring the functional route; it had to have its handler removed to keep working.

Fix

Once a schema is bound, advertisement requires a handler for that schema. Before binding, any registered handler still advertises the capability — no schema-specific answer exists yet, and hosts are already required to re-query after binding rather than cache an earlier absence (the contract HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching pins).

Header-inline behavior change only: no ABI, vtable, protocol, struct_size, or member-layout change; abi/baseline.abi untouched.

Tests

Two new cases, both observed failing before the fix with Actual: true (i.e. they reproduce the bug), passing after:

  • MixedModelParserAdvertisesOnlyForHandledSchemas — a plugin with a handler for example/Image plus legacy parse(): bound to the handled schema it advertises; bound to example/Unhandled it does not, and parseScalarsFunctional reports the extension as unavailable rather than the host discovering it via a per-message parse error.
  • RebindingToAnUnhandledSchemaWithdrawsTheFunctionalRoute — advertisement follows a re-bind in both directions.

Pre-existing contracts verified unchanged: NewlyBuiltParserExposesStableExtensionAutomatically (pre-bind capability advertisement) and HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching (handler registered during bindSchema) both still pass. Full suite green (64/64).

Release

Per the versioning policy this is a PATCH-level fix to installed-header behavior; I propose 0.21.1 when a release is next cut. Following the precedent of #169 (same class of fix) I have not bumped VERSION in this PR, and have not tagged anything. Note that open PRs #172/#173 currently claim 0.22.0/0.23.0.

Downstream

No PJ4 change is required — its parser host already re-queries supportsFunctionalParsing() per call on both routes, so it simply stops taking the functional route for unhandled schemas. This should land before the official plugin fleet rebuilds on 0.21.

🤖 Generated with Claude Code

trampoline_get_plugin_extension advertised pj.parser_functional.v1
whenever the instance had ANY registered SchemaHandler. A mixed-model
plugin — handlers for some schemas, legacy parse() for the rest, the
shape parse()'s own doc-comment sanctions for a generic flattener —
therefore claimed the functional route on every schema, and the final
parseScalars/parseObject dispatchers then rejected each message for the
unhandled ones ("parser does not register schema"). A 0.21 host that
prefers the functional route fails every message on those topics.

Once a schema is bound, advertisement now requires a handler for THAT
schema. Before binding, any registered handler still advertises the
capability: no schema-specific answer exists yet, and hosts are already
required to re-query after binding rather than cache an earlier absence.
Header-inline behavior fix; no ABI, vtable, protocol, or member-layout
change.

Closes #171

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit 8dd36c5 into main Aug 9, 2026
4 checks passed
@facontidavide
facontidavide deleted the fix/parser-functional-bound-schema branch August 9, 2026 13:51
facontidavide added a commit that referenced this pull request Aug 9, 2026
…0.22.0)

SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272) — the complete wasmer-free surface, with the dual-target module ABI
frozen here:

- pj.parser_route_claims.v1 extension: exact handler-table route claims
  (scalar/object, exact-only match), auto-implemented by
  MessageParserPluginBase from its handler table; delivery via
  get_plugin_extension, zero layout changes
- pj.parser_functional.v2: object sink gains accept_object_spliced (one
  splice per object, input-space offsets, frozen per-type eligibility table
  in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical
- parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space
  tokens, token-0 creation-error channel, 512-byte error buffer) +
  bounds-checked little-endian codecs for BindingInfo / parse-input /
  output-descriptor blocks
- host claim catalog + route resolver: §4 admission matrix (priority bounds,
  wildcard/object rules, encoding registry, duplicate identity, provenance
  never from manifests), module-manifest ingestion, synthesized plugin claim
  ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection
  (pin fail-closed → exact > wildcard → provenance tier → priority →
  identity tie-break) with split per-route probe caches and selection traces
- native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export
  resolution, session never-unload) + module runtime over the codecs with
  splice eligibility/bounds validation and the fault-vs-data-error strike
  tracker (3 strikes → quarantine → recreate; repeat → session disable)
- authoring kit pj_base/include/pj_base/parser_module/: header-only,
  C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable);
  CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds),
  ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire
  ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser +
  PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target
- wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps
  it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same
  toy module, binary-format audit of export names/signatures, reactor model,
  and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired
  into linux CI

Docs and the in-repo authoring skill are synchronized to this surface: a new
parser-module authoring reference, the parser-module choice rule and route-claim
semantics in the MessageParser guidance, and corrections to stale claims (wrong
plugin-base include path, two-builder ObjectWriter, builtin-type inventory).
Editing VERSION now re-runs configure, so a stale build tree can no longer stamp
a previous version into the generated version header.

Rebased onto main: the schema-aware functional-route gate from #174 now governs
the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it
only implements through legacy parse() withdraws both revisions (a host prefers
v2, so leaving v2 advertised would route every message on those topics into a
parser that can only reject them).

Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout
sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a
kit-authored module E2E (load → admit → bind → full + spliced PointCloud).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
facontidavide added a commit that referenced this pull request Aug 9, 2026
…0.22.0) (#172)

SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272) — the complete wasmer-free surface, with the dual-target module ABI
frozen here:

- pj.parser_route_claims.v1 extension: exact handler-table route claims
  (scalar/object, exact-only match), auto-implemented by
  MessageParserPluginBase from its handler table; delivery via
  get_plugin_extension, zero layout changes
- pj.parser_functional.v2: object sink gains accept_object_spliced (one
  splice per object, input-space offsets, frozen per-type eligibility table
  in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical
- parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space
  tokens, token-0 creation-error channel, 512-byte error buffer) +
  bounds-checked little-endian codecs for BindingInfo / parse-input /
  output-descriptor blocks
- host claim catalog + route resolver: §4 admission matrix (priority bounds,
  wildcard/object rules, encoding registry, duplicate identity, provenance
  never from manifests), module-manifest ingestion, synthesized plugin claim
  ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection
  (pin fail-closed → exact > wildcard → provenance tier → priority →
  identity tie-break) with split per-route probe caches and selection traces
- native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export
  resolution, session never-unload) + module runtime over the codecs with
  splice eligibility/bounds validation and the fault-vs-data-error strike
  tracker (3 strikes → quarantine → recreate; repeat → session disable)
- authoring kit pj_base/include/pj_base/parser_module/: header-only,
  C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable);
  CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds),
  ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire
  ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser +
  PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target
- wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps
  it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same
  toy module, binary-format audit of export names/signatures, reactor model,
  and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired
  into linux CI

Docs and the in-repo authoring skill are synchronized to this surface: a new
parser-module authoring reference, the parser-module choice rule and route-claim
semantics in the MessageParser guidance, and corrections to stale claims (wrong
plugin-base include path, two-builder ObjectWriter, builtin-type inventory).
Editing VERSION now re-runs configure, so a stale build tree can no longer stamp
a previous version into the generated version header.

Rebased onto main: the schema-aware functional-route gate from #174 now governs
the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it
only implements through legacy parse() withdraws both revisions (a host prefers
v2, so leaving v2 advertised would route every message on those topics into a
parser that can only reject them).

Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout
sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a
kit-authored module E2E (load → admit → bind → full + spliced PointCloud).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parser_functional: gate extension advertisement on the bound schema, not on any registered handler

1 participant