Skip to content

Realign the binding with zenoh-flat HEAD - #16

Merged
milyin merged 6 commits into
mainfrom
zenoh-flat-api-realign
Jul 28, 2026
Merged

Realign the binding with zenoh-flat HEAD#16
milyin merged 6 commits into
mainfrom
zenoh-flat-api-realign

Conversation

@milyin

@milyin milyin commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

zenoh-flat renamed part of its surface and moved several handle types to value
forms, so this binding's declarations named items that no longer exist and
Registry::resolve refused the whole generation (12 missing declarations, 14
missing ignores). Nothing new is bound here — the same surface, remapped.

Renames

keyexpr_get_strkeyexpr_as_str, zbytes_as_byteszbytes_to_bytes,
*_get_keyexpr*_get_key_expr, and every *_get_zid / *_get_eid pair →
a single *_get_id returning EntityGlobalId. config_new_from_json is gone
(json5 remains).

Kotlin method names follow their source idents, as they always have, so getStr
becomes asStr and asBytes becomes toBytes. Nothing hand-written referenced
either.

Handles that became values

Each is now declared as what it is, rather than as an opaque handle plus
accessors:

  • Timestampptr_class! + timestamp_get_ntp64 / _get_id becomes
    data_class!, so it crosses as leaves reassembled in Kotlin bytecode.
  • SourceInfo replaces sample_get_source_{zid,eid,sn}; EntityGlobalId
    replaces reply_get_replier_{zid,eid}. Optionality now lives on the whole
    value, which is what the source crate models.
  • ZenohIdzenoh_id_to_bytes is redundant: the blob is the value's
    bytes property.
  • Selectorsession_get takes the whole selector, so its
    .split_on_param("key_expr") no longer has a param to split.
  • RecoveryMode became a data-carrying enum → sealed_class!.

Test fallout

Both from source-crate signature changes rather than from this binding:

  • encoding_get_schema yields raw bytes now, so EncodingCorrespondenceTest
    transcodes UTF-8 at the boundary — its whole corpus is text.
  • zenoh_id_to_string is fallible, so ZenohIdCorrespondenceTest takes the
    typed onError and drops the all-zero id: those bytes are not an identifier,
    so there is no native rendering to correspond to.

What is deliberately NOT here

The ~70 zenoh-flat items added alongside these changes (links, transports,
timestamp stacks, the *_to_struct value forms, publisher matching listeners)
stay unbound, and therefore visible as skipping undeclared warnings. They
are pending work, not acknowledged exclusions, so they do not belong in the
ignore list — happy to move them if you'd rather have a quiet build.

Generator dependencies (all landed)

This branch tracks generator changes in milyin/prebindgen, and was regenerated
three times as they merged:

  • #205 — a converter's full stage chain at every leaf site. RecoveryMode's
    Duration payload needs it at sum-payload leaves; without it the generated
    Rust does not compile.
  • #207 — byte-backed values compare by content. Timestamp, ZenohId,
    EntityGlobalId and SourceInfo compared by array identity, so two
    identically built values were unequal. Regenerated, plus a regression test.
  • #209 — the value blob is gone; fixed-size arrays cross as Kotlin primitive
    arrays. ZenohId is consequently declared data_class! rather than as a raw
    memory image of the Rust struct. The Kotlin API is unchanged —
    data class ZenohId(val bytes: ByteArray) either way.

All three are in prebindgen main, and CI (which resolves prebindgen from that
branch) is green.

Verification

cargo build clean, gradle test 18/18, fmt + clippy clean. The zenoh-flat CI
pin moves to 3f431b6b; the generated artifacts here are byte-identical whether
generated against that commit or against the local checkout.

🤖 Generated with Claude Code

milyin and others added 2 commits July 27, 2026 09:11
zenoh-flat renamed part of its surface and moved several handle types to
value forms, so this binding's declarations named items that no longer
exist and `Registry::resolve` refused the whole generation. Nothing new is
bound here: this is the same surface, remapped.

Renames: `keyexpr_get_str` -> `keyexpr_as_str`, `zbytes_as_bytes` ->
`zbytes_to_bytes`, `*_get_keyexpr` -> `*_get_key_expr`, and the
`*_get_zid` / `*_get_eid` pairs -> a single `*_get_id` returning
`EntityGlobalId`. `config_new_from_json` is gone (json5 remains). The
Kotlin method names follow their source idents, as they always have, so
`getStr` becomes `asStr` and `asBytes` becomes `toBytes`; nothing
hand-written referenced either.

Handles that became values, each now declared as what it is rather than as
an opaque handle plus accessors:

* `Timestamp` — `ptr_class!` + `timestamp_get_ntp64` / `_get_id` becomes
  `data_class!`, so it crosses as leaves reassembled in Kotlin bytecode.
* `SourceInfo` replaces `sample_get_source_{zid,eid,sn}`; `EntityGlobalId`
  replaces `reply_get_replier_{zid,eid}`. Optionality now lives on the
  whole value, which is what the source crate models.
* `ZenohId`'s `zenoh_id_to_bytes` accessor is redundant — the blob IS the
  value class's `bytes` property.
* `Selector` — `session_get` takes the whole selector, so its
  `.split_on_param("key_expr")` no longer has a param to split.
* `RecoveryMode` became a data-carrying enum, so it is `sealed_class!`.

Test fallout, both from source-crate signature changes rather than from
this binding: `encoding_get_schema` yields raw bytes now, so the encoding
correspondence test transcodes UTF-8 at the boundary (its whole corpus is
text); `zenoh_id_to_string` is fallible, so the zid test takes the typed
`onError` and drops the all-zero id — those bytes are not an identifier,
so there is no native rendering to correspond to.

The zenoh-flat CI pin moves to 3f431b6b, whose surface this generates
against byte-for-byte.

The ~70 zenoh-flat items added alongside these changes (links, transports,
timestamp stacks, the `*_to_struct` value forms, publisher matching
listeners) stay UNBOUND and therefore visible as `skipping undeclared`
warnings. They are pending work, not acknowledged exclusions, so they do
not belong in the `ignore` list.

Requires milyin/prebindgen#205: `RecoveryMode`'s `Duration` payload needs
the converter stage chain that PR restores at sum-payload leaves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The committed artifacts were generated before prebindgen picked up the
boundary aliasing guards (#200) and the converter stage-chain fix (#205),
so a plain `cargo build` no longer reproduced them.

The one behavioral change is in `session.get`: `Selector` carries its key
expression as a nested handle, so the new guards now reject a call that
passes the same native resource twice — as `this` and the selector's key
expression, or as the selector's key expression and the encoding. Those
are consumed-handle aliases, which the previous generation let through.

The Rust side is cosmetic only: a chain-less converter call is now
wrapped in a block, which is how the composed form degenerates when a
type has no conversion stages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@milyin

milyin commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

The newly generated byte-backed value types do not have value equality. For example, against this PR's generated JAR:

val a = Timestamp(1uL, byteArrayOf(1, 2, 3))
val b = Timestamp(1uL, byteArrayOf(1, 2, 3))
check(a == b)

fails. I also reproduced the same behavior for EntityGlobalId(ZenohId(byteArrayOf(1)), 7). The output from a small executable check was:

timestampEqual=false hashEqual=true
entityEqual=false hashEqual=true

Kotlin arrays compare by identity, so the generated Timestamp data class and the ZenohId inline value class make logically identical Rust values unequal; SourceInfo inherits the problem through EntityGlobalId. These Rust types derive PartialEq/Eq, and downstream code already treats ZenohId as a value using contentEquals.

Could we add content-based equals/hashCode generation (or use a wrapper that provides it) for byte-backed values, plus a Kotlin regression test for these nested cases?

@milyin

milyin commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

This API realignment currently breaks the existing zenoh-java transition integration branch. I checked out eclipse-zenoh/zenoh-java PR #482 at 3548122, pointed its composite build at this PR, and ran:

./gradlew :zenoh-java:compileKotlinJvm --console=plain

The compile fails across the changed surface, including Config.newFromJson, ZBytes.asBytes, KeyExpr.getStr, the new Selector argument to Session.get, callback shapes for Timestamp/SourceInfo/EntityGlobalId, and the encoding schema change from String? to ByteArray?.

The repository-local checks are green, but they do not exercise the documented Maven/composite consumer, so merging this independently leaves the current integration branch uncompilable. Could PR #482 be updated/linked with the matching consumer adaptation and this downstream compile added to the coordinated merge verification?

`Timestamp`, `ZenohId`, `EntityGlobalId` and `SourceInfo` compared by array
IDENTITY, so two identically built values were unequal — the shape a
consumer hits keying a map on a peer id or comparing a sample's timestamp.
Fixed in the generator (milyin/prebindgen#207); this is the regeneration
plus the regression test.

`ZenohId` is no longer a `@JvmInline value class`. Kotlin 1.9 reserves
`equals`/`hashCode` members on a value class, so an inline one cannot carry
the value equality its Rust counterpart has; it is a plain `data class`
now. The JNI ABI is unchanged — the externs already took `ByteArray` and
the wrapper already passed `.bytes`.

`ValueEqualityTest` covers the reported cases and their nesting, asserting
`HashSet` de-duplication rather than only `==`: Kotlin's `data class`
codegen special-cases arrays in `hashCode` but not in `equals`, so equal
hashes prove nothing on their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@milyin

milyin commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed and fixed. Generator fix in milyin/prebindgen#207; this branch carries the regeneration plus ValueEqualityTest (116480c).

Measured first

Before writing any code, I probed Kotlin 1.9.0 with the exact shapes:

== hashCode equal
@JvmInline value class V(val b: ByteArray) ❌ false ✅ true
data class D(val n: ULong, val id: ByteArray) ❌ false ✅ true
data class holding the value class ❌ false ✅ true
plain class + explicit content ops ✅ true ✅ true
GEN Timestamp / ZenohId / EntityGlobalId / SourceInfo ❌ false ✅ true
hashSetOf(t1, t2).size 2 (want 1)

Your hashEqual=true was right and my initial scepticism about it was wrong — the cause is that Kotlin's data class codegen is inconsistent: hashCode/toString do special-case arrays (contentHashCode/contentToString), equals does not. Timestamp.hashCode() really is 30848 for both instances, while the raw arrays hash 888287133 vs 1025001676. So a broken value finds the right HashMap bucket and is rejected there — which is why the tests assert HashSet de-duplication, not just ==.

ZenohId is no longer @JvmInline

Kotlin 1.9 refuses the members outright:

e: Member with the name 'equals' is reserved for future releases
e: The feature "custom equals in value classes" is experimental and should be enabled explicitly

An inline value class therefore cannot be given value equality at this language level, and the experimental typed-equals alternative would force an opt-in flag on every consumer of a shared tier. It is a plain data class now.

The JNI ABI is unchanged — externs already declared ByteArray (external fun zenohIdToString(z: ByteArray, …)) and the wrapper already passed .bytes, so the erasure was never load-bearing. The cost is one small allocation per crossing at the wrapper tier, on a cold path.

Coverage

The root gap: covertest had the Stamp value blob but no data class with a ByteArray field, so the Timestamp shape was never generated anywhere under test. #207 adds BlobValue (a Vec<u8> field beside a scalar, plus a nested value blob) and asserts each component participates. I verified the new section fails without the fix rather than assuming it would.

Here: ValueEqualityTest covers your exact cases and their nesting — 18/18 JVM tests green, fmt/clippy clean.

On your second comment (zenoh-java #482)

Confirmed as a real blocker and not addressed here — scoped out deliberately, since the surface adaptation (newFromJson5, toBytes, asStr, the Selector argument, String?ByteArray?, the Timestamp/SourceInfo callback shapes) belongs in that repo. Note this equality change adds one more item to that list: ZenohId ceasing to be a value class means zenoh-java's own ZenohId wrapper can drop its hand-written contentEquals/contentHashCode, since the shared tier now provides them. Happy to take that on as a follow-up if you want it coordinated before merge.

This PR stays blocked on #207 landing on prebindgen main (CI resolves prebindgen from there).

The value blob is gone from the generator (milyin/prebindgen), so `ZenohId`
is declared `data_class!`: a plain value with one fixed-width byte field,
which now crosses as a Kotlin `ByteArray` through the generic fixed-size
array support rather than as the raw memory image of the Rust struct.

The Kotlin API is unchanged. `data class ZenohId(val bytes: ByteArray)`
either way, with the same content `equals`/`hashCode`/`toString`; the ctor
property loses an explicit `public` that a data-class property has by
default anyway. `zidString()` and the SDKs' `inner.bytes` keep working.

What this buys: the JVM no longer holds a `repr(Rust)` struct's memory
image (padding included, layout unguaranteed), and the decode no longer
`read_unaligned`s caller-supplied bytes after only a length check — `Copy`
never implied every bit pattern was a valid value.

One cold-path cost: `session.zid()` and the other bare `ZenohId` returns now
go through the `__ZenohIdBuilder` upcall, where the blob returned its wire
directly. Nested uses (`EntityGlobalId.zid` inside `SourceInfo`, the sample
callback's hot path) ride their parent's single `fromParts` and are
unaffected. Tracked as milyin/prebindgen#208 together with packing a small
array into scalar slots.

18/18 JVM tests, fmt + clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two jobs in the same run could see different generators, and a run could
see a generator older than its own cache.

`branch = "main"` was resolved independently by each of the four jobs, so a
prebindgen merge landing mid-run split the run across two generators. Resolve
main to a commit once, in a `resolve-prebindgen` job, and pin every job to
that rev. Still always the latest main — just one of them per run.

The `target/` cache was worse. zenoh-flat's build-script OUT_DIR lives at
`target/<profile>/build/zenoh-flat-<hash>/out`; the proc-macro only ever
*adds* uniquely-named `.jsonl` files there (`create_new`, never truncating),
and `Source::read_group` reads every matching file in the directory and dedups
by record name in `read_dir` order. A `target/` restored from a run with
different sources therefore hands the generator the union of old and new
records: items deleted upstream survive, and collisions resolve arbitrarily.

The prefix `restore-keys` made that the normal case, since the key hashed
`Cargo.lock` files that are gitignored and never exist. In run 30292836956,
Lint and Build both pinned zenoh-flat at 3f431b6b yet reported different line
numbers for the same file (`advanced_subscriber/mod.rs:164` vs `:184`,
`session/mod.rs:262` vs `:327`) — two different stale unions.

So key the target caches on both source revisions and drop the prefix
restore-keys: a hit is coherent, a miss is a clean build. Hoist the zenoh-flat
pin to `env.ZENOH_FLAT_REF` so the four copies cannot drift, and echo the
rewritten dependency lines to make the resolved generator visible in the log.

This is a workaround for the generator reading stale sibling files; the
directory-hygiene fix belongs in prebindgen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
milyin referenced this pull request in eclipse-zenoh/zenoh-java Jul 28, 2026
zenoh-flat renamed part of its surface and moved several handle types to
value forms (ZettaScaleLabs/zenoh-flat-jni#16). Mostly mechanical, with one
change that reaches the public API.

## A reply timestamp needs a real originating-node id

`ReplyOptions.timeStamp` was an Apache commons-net `TimeStamp`: an NTP64
instant and nothing else. Zenoh's timestamp is the pair `(ntp64, id)`, and it
orders and de-duplicates on the pair — so the missing half was supplied by the
JNI layer, which built one with a **random** id:

    Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand())   // zenoh-jni/src/query.rs:82

The time looked right while the id was untethered from any node. zenoh-flat
removed that (ZettaScaleLabs/zenoh-flat#47) and `query_reply_success` now takes
a whole `Timestamp`, so there is no longer anywhere for a fabricated id to
come from — which is the point.

`io.zenoh.time.Timestamp` is that pair, mirroring `zenoh::time::Timestamp`.
`ReplyOptions.timeStamp` and `Sample.timestamp` carry it instead of the
commons-net type. **This is source-breaking**: a caller now writes

    options.setTimeStamp(Timestamp.ofNtp64(ntp64, session.info().zid()));

`ofNtp64` exists because the primary constructor takes `ULong`, which Java
cannot express; it takes the same 64 bits `TimeStamp(long)` does, so
`TimeStamp.getCurrentTime().ntpValue()` still feeds it. The id is the
replying session's — the reply does originate here — and `Sample.timestamp`
now surfaces the sender's id, which was previously discarded on receive.

The library no longer depends on commons-net; the tests and the ZQueryable
example still use it, as an NTP64 clock.

## Mechanical

* `keyexpr_get_str` -> `asStr`, `zbytes_as_bytes` -> `toBytes`.
* `session_get` takes a whole `Selector`, so the folded (key_expr, parameters)
  pair is gone. A Selector holds an owned key-expr handle with no string arm,
  so `KeyExpr.intoJniHandle()` materializes one for a string-backed key
  expression — the slot trio cannot express this case.
* An encoding schema is raw bytes: zenoh transmits it verbatim and does not
  require UTF-8. This SDK's `Encoding` carries a String, so it is encoded at
  the boundary on send and decoded lossily on receive, rather than throwing on
  a received message.
* A sample's source and a reply's replier arrive as whole `SourceInfo` /
  `EntityGlobalId` values, so optionality lives on the value rather than on a
  leading zid leaf.
* `config_new_from_json` is gone as an invented constructor (base zenoh has no
  `from_json`). `Config.fromJson` parses via JSON5, of which JSON is a subset,
  so every input accepted before still parses to the same config.

CI pins move to zenoh-flat 3f431b6b and zenoh-flat-jni 5e0ae509.

112 JVM tests pass; examples compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit dropped them on a mistaken diagnosis of mine, which this
corrects.

I read run 30292836956's Lint and Build jobs reporting different source
locations for the same pinned zenoh-flat (`advanced_subscriber/mod.rs:164` vs
`:184`) as a stale `target/` feeding the generator a union of old and new
JSONL records. It is not. Both jobs report the *same* 18 unresolved types;
only the call site each is attributed to differs, because `Source::read_group`
dedups records into a `HashMap` and returns `into_values()`, so when several
functions share an unresolved type — several take an `impl Fn(Miss)`, several
reference `ZenohId` — which one is blamed is unordered. And the hygiene works:
`init_prebindgen_out_dir()` wipes the directory on every build-script run, and
a source change does re-run it, verified by renaming a `#[prebindgen]` item and
watching the old record disappear.

So there was no corruption to protect against, and keying the cache to an exact
prebindgen commit with no fallback bought nothing while making every prebindgen
merge — several a day right now — a cold rebuild of zenoh and its dependency
tree. Cargo's own fingerprinting is what keeps an incremental build correct; a
cache key only has to avoid an absurd mismatch.

The exact key stays keyed on both source revisions, which is still an
improvement on the old one: that hashed `Cargo.lock` files that are gitignored
and never exist, so it was the constant `${{ runner.os }}-cargo-build-target-`
and never varied at all. Now a run prefers its own revisions, falls back to the
same zenoh-flat, then to any.

The `resolve-prebindgen` job is unaffected and stands on its own: pinning every
job in a run to one resolved `main` removes a real race where a merge landing
mid-run splits the run across two generators.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
milyin referenced this pull request in eclipse-zenoh/zenoh-kotlin Jul 28, 2026
zenoh-flat renamed part of its surface and moved several handle types to
value forms (ZettaScaleLabs/zenoh-flat-jni#16). Mostly mechanical, with one
change that reaches the public API. Mirrors the same realignment in
zenoh-java.

## A reply timestamp needs a real originating-node id

`Query.reply(timestamp = ...)` took an Apache commons-net `TimeStamp`: an
NTP64 instant and nothing else. Zenoh's timestamp is the pair `(ntp64, id)`,
and it orders and de-duplicates on the pair — so the missing half was supplied
by the JNI layer, which built one with a **random** id:

    Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand())   // zenoh-jni/src/query.rs:82

The time looked right while the id was untethered from any node. zenoh-flat
removed that (ZettaScaleLabs/zenoh-flat#47) and `query_reply_success` now takes
a whole `Timestamp`, so there is no longer anywhere for a fabricated id to
come from — which is the point.

`io.zenoh.time.Timestamp` is that pair, mirroring `zenoh::time::Timestamp`.
The reply/replyDel `timestamp` parameters and `Sample.timestamp` carry it
instead of the commons-net type. **This is source-breaking**: a caller now
writes

    query.reply(keyExpr, payload, timestamp = Timestamp.ofNtp64(ntp64, zid))

`ofNtp64` exists because the primary constructor takes `ULong`, which Java
cannot express; it takes the same 64 bits `TimeStamp(long)` does, so
`TimeStamp.getCurrentTime().ntpValue()` still feeds it. The id is the
replying session's — the reply does originate here — and `Sample.timestamp`
now surfaces the sender's id, which was previously discarded on receive.

The library no longer depends on commons-net; the tests and the ZQueryable
example still use it, as an NTP64 clock.

## Mechanical

* `keyexpr_get_str` -> `asStr`, `zbytes_as_bytes` -> `toBytes`.
* `session_get` takes a whole `Selector`, so the folded (key_expr, parameters)
  pair is gone. A Selector holds an owned key-expr handle with no string arm,
  so `KeyExpr.intoJniHandle()` materializes one for a string-backed key
  expression — the slot trio cannot express this case.
* An encoding schema is raw bytes: zenoh transmits it verbatim and does not
  require UTF-8. This SDK's `Encoding` carries a String, so it is encoded at
  the boundary on send and decoded lossily on receive, rather than throwing on
  a received message.
* A sample's source and a reply's replier arrive as whole `SourceInfo` /
  `EntityGlobalId` values, so optionality lives on the value rather than on a
  leading zid leaf.
* `RecoveryMode` is a data-carrying enum, so the choice and its payload are one
  value rather than a `(period, flag)` pair in which only one of the two was
  ever meaningful. `RecoveryConfig.retentionPeriod` has no counterpart on the
  SDK config and stays absent, as it effectively was before.
* `config_new_from_json` is gone as an invented constructor (base zenoh has no
  `from_json`). `Config.fromJson` parses via JSON5, of which JSON is a subset,
  so every input accepted before still parses to the same config.

CI pins move to zenoh-flat 3f431b6b and zenoh-flat-jni 5e0ae509.

121 JVM tests pass; examples compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@milyin

milyin commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Re-reviewed current head 7f964313; I found no additional blocking issues.

Verification:

  • Ran the Gradle 8.5 test suite locally against zenoh-flat 3f431b6b and current prebindgen main: 18 tests passed, including the new nested byte-value equality cases.
  • The build regenerated the committed Rust and Kotlin outputs and left the worktree clean, so the checked-in artifacts are reproducible with the current generator.
  • Checked the new value paths specifically: Selector transfers and marks its nested KeyExpr consistently, SourceInfo / EntityGlobalId preserve whole-value optionality, and the payload-bearing RecoveryMode is decoded through the sealed-class arm.
  • The earlier downstream coordination blocker is resolved by zenoh-flat transition (integration branch) zenoh-java#482 at 93c6f057, which carries the matching API adaptation and has green CI.
  • All checks on this PR head are green; resolving prebindgen once per run also removes the cross-job moving-branch race.

The two earlier findings are addressed. No further findings from this pass.

@milyin
milyin merged commit 498ba26 into main Jul 28, 2026
11 checks passed
milyin referenced this pull request in eclipse-zenoh/zenoh-java Jul 28, 2026
ZettaScaleLabs/zenoh-flat-jni#16 merged as 498ba26, so the pin moves off the
PR branch commit it was tracking.

The squash merge's tree is byte-identical to the branch tip this was verified
against, and 498ba26 pins zenoh-flat at the same 3f431b6b already pinned here,
so nothing but the SHA changes. 112 JVM tests pass against it; examples
compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
milyin referenced this pull request in eclipse-zenoh/zenoh-kotlin Jul 28, 2026
ZettaScaleLabs/zenoh-flat-jni#16 merged as 498ba26, so the pin moves off the
PR branch commit it was tracking.

The squash merge's tree is byte-identical to the branch tip this was verified
against, and 498ba26 pins zenoh-flat at the same 3f431b6b already pinned here,
so nothing but the SHA changes. 121 JVM tests pass against it; examples
compile.

Co-Authored-By: Claude Opus 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.

1 participant