Skip to content

Encoding as a pure JVM value + native publisher default + pinned per-put override#484

Merged
milyin merged 3 commits into
zenoh-flat-transitionfrom
encoding-pure-value
Jul 16, 2026
Merged

Encoding as a pure JVM value + native publisher default + pinned per-put override#484
milyin merged 3 commits into
zenoh-flat-transitionfrom
encoding-pure-value

Conversation

@milyin

@milyin milyin commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Constituent of the zenoh-flat transition (#482). Reworks the Encoding story on the shared-tier principle: all logic lives in zenoh-flat-jni (reused by zenoh-java and zenoh-kotlin); this repo keeps only the established API facade.

1. Encoding is a pure JVM value, logic in the shared tier

An encoding is its (id, schema) pair — Zenoh's own representation. The conversion logic (id↔name table + Zenoh's exact parse/render/with_schema rules) lives in zenoh-flat-jni's shared EncodingCodec (zenoh-flat-jni#4); io.zenoh.bytes.Encoding here is a thin facade — the established constants and methods, pure delegation, zero logic. Same for ZenohId.toString() (shared zidString()).

  • Fixes a native memory leak: received Sample/Query/ReplyError previously retained a cloned native Encoding handle with no close path.
  • No native handle, no native calls, no lazy caches, no locking in the SDK (was 353 lines of lifecycle machinery).
  • Correspondence tests (EncodingCorrespondenceTest, ZenohIdCorrespondenceTest) verify the shared codecs against the native oracle across the whole predefined id range, parse/render edges, and random ids — one caught a real divergence (custom-encoding withSchema preserves the name inside the schema slot).

2. Publisher default encoding — set natively at declare, zero crossing per put

PublisherOptions.encoding was emulated JVM-side by re-sending (id, schema) on every put. Now it's passed once to the generated declarePublisher (zenoh-flat#3, merged — exposes zenoh's PublisherBuilder::encoding), and plain put(payload) passes absent — the native default applies, no encoding data crosses at all. Per-call PutOptions.encoding overrides still work (crossing the cheap (id, schema) pair).

Design note: PinnedEncoding was implemented, then deliberately dropped

The branch history contains a PinnedEncoding (a preallocated native encoding handle crossing per call via a dual-arm expansion), later removed by design decision. Rationale:

  • A per-call (id, schema) crossing costs only a short schema-string decode (~tens of ns for schema-bearing encodings, zero for predefined ones) against a put path of ≈1µs — noise in practice.
  • The publisher's declare-time native default (section 2) already removes even that cost for the hot publish loop.
  • Amortizing the string inside the plain Encoding value would hide a native handle in a value class — reintroducing exactly the lifecycle machinery (close/finalize, locking, leak hazard) this PR deletes; a separate resource class earned its ~30 lines only for a narrow niche (schema-bearing per-call overrides in non-publisher hot loops).

The enabling generator feature (prebindgen #80: Optional selector-dispatched expansion, selector -1 = absent) is merged and covertest-covered, so the pinned-handle arm can be reintroduced wire-compatibly if a real workload ever demands it. Encoding params meanwhile keep the simple (present, id, schema) crossing.

Verification

Merge order: zenoh-flat-jni#4 → this.

milyin added 2 commits July 16, 2026 14:33
An encoding IS its (id, schema) pair — Zenoh's own representation; the
textual form is derived from a fixed table. With the id<->name table already
JVM-side (the generated ENCODING_* constants) and the companion
zenoh-flat-jni change crossing Encoding by value in both directions, the SDK
Encoding becomes a plain immutable value: no native handle, no native calls,
no lazy caches, no locking, nothing to close. This also removes a native
memory leak: received Sample/Query/ReplyError previously retained a cloned
Encoding handle with no close path.

- Encoding.kt: immutable (id, schema); from()/toString()/withSchema()
  implement Zenoh's exact conversion rules (incl. the custom-encoding
  name-preserving withSchema and the render-only CUSTOM<->"" table row);
  equality on (id, schema), matching Zenoh core.
- ZenohId.toString: the little-endian lowercase-hex rule in pure Kotlin.
- Callback/fromParts leaves follow the new (encId, encSchema) decomposition;
  outbound sites pass .id/.schema directly.
- Correspondence tests (EncodingCorrespondenceTest, ZenohIdCorrespondenceTest)
  verify the pure implementations against the native ones across the whole
  predefined id range, parse/render edge shapes, and random ids — the
  contract for any JVM-side reimplementation of zenoh-flat API. One test
  caught and fixed a real divergence (custom-encoding withSchema).

Pairs with ZettaScaleLabs/zenoh-flat-jni#4 (CI pin bumped). jvmTest: 105
tests, 0 failures.
Two encoding fast paths over the dual-arm expansion (prebindgen#80,
zenoh-flat#3, zenoh-flat-jni#4):

- The publisher's default encoding is set NATIVELY once at declare time
  (PublisherOptions.encoding -> declarePublisher's encoding arm); plain
  put(payload) passes absent (sel -1), so no encoding data crosses per call.
  Previously the JVM tier re-sent (id, schema) on every put.
- PinnedEncoding (Encoding.pinned()) preallocates the native form; passing it
  in PutOptions/ReplyOptions/GetOptions crosses only a borrowed handle (native
  clone = Arc bump) — no schema-string traffic in hot publish loops. close()
  releases the handle (falls back to the plain (id, schema) arm); a finalizer
  backstops leaks. A PinnedEncoding equals its plain counterpart.
- All outbound sites route through one Encoding?.forWire() helper emitting the
  selector tuple (-1 absent / 0 value / 1 pinned handle).

Tests: PinnedEncodingTest (publisher-default applies natively end-to-end;
pinned override reused across puts; post-close fallback), correspondence
tests updated to the dual-arm newWithSchema. jvmTest: 107 tests, 0 failures.
CI pins: zenoh-flat-jni cbbca40, zenoh-flat d792547.
zenoh-flat-jni is the shared tier reused by both zenoh-java and zenoh-kotlin,
so the pure-JVM conversion logic moves there (EncodingCodec, ZenohId.zidString
— next to the generated constants they are built from). io.zenoh.bytes.Encoding
shrinks to a thin facade: the established constants and API delegating to the
shared codec, zero logic.

Drop PinnedEncoding and the selector-tuple wire helper: with the publisher's
default encoding set natively at declare time, the hot publish loop crosses no
encoding data at all, and per-call overrides crossing (id, schema) cost only a
short string decode — not worth a resource class in the public API. Encoding
params revert to the plain (present, id, schema) crossing.

jvmTest: 106 tests, 0 failures (correspondence suites now exercise the shared
codec; PublisherEncodingTest verifies the native declare-time default +
per-put override end-to-end). CI pins: zenoh-flat-jni 9bc155b, zenoh-flat
main (6d22091, #3 merged).
@milyin milyin merged commit aa2a8bd into zenoh-flat-transition Jul 16, 2026
13 checks passed
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.

1 participant