diff --git a/prebindgen-ext/Cargo.lock b/prebindgen-ext/Cargo.lock new file mode 100644 index 00000000..0c1b7121 --- /dev/null +++ b/prebindgen-ext/Cargo.lock @@ -0,0 +1,560 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.0.0" +dependencies = [ + "itertools 0.12.1", + "jni", + "prebindgen", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/prebindgen-ext/Cargo.toml b/prebindgen-ext/Cargo.toml new file mode 100644 index 00000000..bb678a35 --- /dev/null +++ b/prebindgen-ext/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "prebindgen-ext" +version = "1.0.0" +edition = "2021" +license = "EPL-2.0 OR Apache-2.0" +description = "Prebindgen JNI extensions for Zenoh." +repository = "https://github.com/eclipse-zenoh/zenoh" + +[dependencies] +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" +prebindgen = "0.4.1" +itertools = "0.12" +jni = "0.21.1" diff --git a/prebindgen-ext/src/core/mod.rs b/prebindgen-ext/src/core/mod.rs new file mode 100644 index 00000000..fa59eb08 --- /dev/null +++ b/prebindgen-ext/src/core/mod.rs @@ -0,0 +1,17 @@ +//! Core: language-agnostic primitives for the Registry-based pipeline. +//! +//! Public entry point: [`registry::Registry::from_items`] scans a stream +//! of `(syn::Item, SourceLocation)` into a flat type table; +//! [`registry::Registry::write_rust`] +//! resolves every required type using the configured +//! [`prebindgen_ext::PrebindgenExt`] and emits the bindings file. Kotlin +//! emission for the JNI back-end lives in `crate::jni::JniExt::write_kotlin`. + +pub mod niches; +pub mod prebindgen_ext; +pub mod registry; +pub(crate) mod resolve; +pub(crate) mod write; + +pub use niches::{NicheSlot, Niches}; +pub use registry::Registry; diff --git a/prebindgen-ext/src/core/niches.rs b/prebindgen-ext/src/core/niches.rs new file mode 100644 index 00000000..63a1a7fc --- /dev/null +++ b/prebindgen-ext/src/core/niches.rs @@ -0,0 +1,212 @@ +//! Niche optimisation for FFI-wire encodings. +//! +//! A *niche* is a bit-pattern that the wire type *can* represent but that +//! a particular converter is guaranteed to never produce on output and +//! always reject on input. Wrappers like `Option<_>` and sum-typed enums +//! carve niches one at a time for their own discriminants and re-export +//! the remainder so further wrappers stack. +//! +//! Direct analogy with Rust's niche optimisation: +//! +//! | Rust | This crate | +//! | ---------------------------- | --------------------------------------- | +//! | `NonZeroU32` declares `{0}` | converter sets `niches = Niches::one(…)`| +//! | `Option` is u32 | `Option` reuses inner's wire | +//! | `Option>` | falls back unless inner exposes ≥2 | +//! +//! In the FFI setting the canonical example is a Rust value encoded as a +//! raw `Box::into_raw` pointer carried over the wire as `jlong`: real +//! `Box::into_raw` results are never `0`, so the converter declares the +//! single niche `{0}`. `Option` then automatically reuses the same +//! `jlong` wire with `0` meaning `None`, matching the C-pointer-with-null +//! ABI most JNI bindings already use. +//! +//! ## Cascading +//! +//! [`Niches::carve`] returns the next slot together with the remainder. +//! The wrapper places the carved value into its own emitted code (output: +//! `None` is encoded as `slot.value`; input: `slot.matches` is the +//! discriminator predicate) and stores `rest` on its own +//! [`crate::core::prebindgen_ext::ConverterImpl::niches`] so any +//! enclosing wrapper can keep carving. Once `rest` is empty further +//! wrappers must fall back to a tag/box scheme. +//! +//! ## Soundness +//! +//! For the carve to be sound, the inner converter's outputs must +//! genuinely avoid the carved bit pattern, and its input must reject it +//! (typically by erroring). The plugin author guarantees this — `Niches` +//! is a *declaration* that the resolver and wrappers trust. +//! +//! ## Calling convention for `matches` +//! +//! The `matches` predicate is spliced into the input wrapper's body where +//! the wire-typed parameter `v` is in scope. The exact shape of `v` +//! depends on the wire kind: +//! +//! * Standard wires (e.g. `JObject`, `jlong`): `v: &` — write +//! `*v == 0` for `jlong`, `v.is_null()` for `JObject` (autoderef). +//! * Raw-pointer wires (`*const T`): `v: ` — write `v.is_null()` +//! directly, no `*` deref. +//! +//! The plugin producing the niche knows which wire kind it is using and +//! must write `matches` accordingly. +//! +//! `value` is a wire-typed *constant* expression with no `v`, no `env` — +//! just the bit pattern (e.g. `0i64`, `jni::objects::JObject::null()`, +//! `std::ptr::null()`). + +/// One free bit-pattern slot in the wire encoding. +/// +/// See the module-level docs for the calling convention of `matches` and +/// `value`. +#[derive(Clone)] +pub struct NicheSlot { + /// Wire-typed constant expression evaluating to this niche's bit + /// pattern. Used by output wrappers to emit the discriminant. + pub value: syn::Expr, + /// Predicate testing whether the wire value `v` (in the local + /// wrapper convention — see module docs) is *this* slot. Used by + /// input wrappers to detect the discriminant. + pub matches: syn::Expr, +} + +/// An ordered set of [`NicheSlot`]s that a converter's wire type can +/// represent but that this converter never produces (output) and always +/// rejects (input). +/// +/// Ordering: the *first* slot is the next one taken by [`Self::carve`]. +/// Wrappers carve from the front; the remaining slots are passed up so +/// that further wrappers can stack their own discriminants. +#[derive(Clone, Default)] +pub struct Niches { + pub slots: Vec, +} + +impl Niches { + /// No free bit-patterns. The default for converters whose wire + /// encoding uses every bit-pattern as a valid value (e.g. `i64` + /// over `jlong`). + pub fn empty() -> Self { + Self::default() + } + + /// Convenience for the common single-niche case. + pub fn one(value: syn::Expr, matches: syn::Expr) -> Self { + Self { slots: vec![NicheSlot { value, matches }] } + } + + /// Build from any iterable of slots; ordering is preserved. + pub fn from_slots>(slots: I) -> Self { + Self { slots: slots.into_iter().collect() } + } + + /// Take the first slot for use as a wrapper's discriminant. Returns + /// the carved slot and the remaining niches (which the wrapper + /// should re-export on its own [`ConverterImpl`]). `None` if the + /// set is empty — the caller must fall back to a tag/box scheme. + pub fn carve(mut self) -> Option<(NicheSlot, Niches)> { + if self.slots.is_empty() { + None + } else { + let head = self.slots.remove(0); + Some((head, self)) + } + } + + pub fn len(&self) -> usize { + self.slots.len() + } + + pub fn is_empty(&self) -> bool { + self.slots.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::ToTokens; + + fn slot_strs(s: &NicheSlot) -> (String, String) { + ( + s.value.to_token_stream().to_string(), + s.matches.to_token_stream().to_string(), + ) + } + + #[test] + fn empty_is_empty() { + let n = Niches::empty(); + assert!(n.is_empty()); + assert_eq!(n.len(), 0); + assert!(n.carve().is_none()); + } + + #[test] + fn one_constructs_single_slot() { + let n = Niches::one(syn::parse_quote!(0i64), syn::parse_quote!(*v == 0)); + assert_eq!(n.len(), 1); + let (slot, rest) = n.carve().unwrap(); + let (val, pred) = slot_strs(&slot); + assert_eq!(val, "0i64"); + assert_eq!(pred, "* v == 0"); + assert!(rest.is_empty()); + } + + #[test] + fn from_slots_preserves_order() { + let n = Niches::from_slots([ + NicheSlot { value: syn::parse_quote!(0i32), matches: syn::parse_quote!(*v == 0) }, + NicheSlot { value: syn::parse_quote!(-1i32), matches: syn::parse_quote!(*v == -1) }, + NicheSlot { value: syn::parse_quote!(99i32), matches: syn::parse_quote!(*v == 99) }, + ]); + assert_eq!(n.len(), 3); + let (s0, n) = n.carve().unwrap(); + assert_eq!(slot_strs(&s0).0, "0i32"); + let (s1, n) = n.carve().unwrap(); + assert_eq!(slot_strs(&s1).0, "- 1i32"); + let (s2, n) = n.carve().unwrap(); + assert_eq!(slot_strs(&s2).0, "99i32"); + assert!(n.is_empty()); + } + + /// Carving propagates the remainder, allowing wrappers to stack. + /// This mirrors `Option>` collapsing to + /// the same wire as the inner type. + #[test] + fn cascading_carve() { + let n = Niches::from_slots([ + NicheSlot { value: syn::parse_quote!(jni::sys::jint::MIN), matches: syn::parse_quote!(*v == jni::sys::jint::MIN) }, + NicheSlot { value: syn::parse_quote!(jni::sys::jint::MAX), matches: syn::parse_quote!(*v == jni::sys::jint::MAX) }, + ]); + + // Outer wrapper takes the first niche. + let (outer, rest1) = n.carve().unwrap(); + assert_eq!(slot_strs(&outer).0, "jni :: sys :: jint :: MIN"); + assert_eq!(rest1.len(), 1); + + // Inner wrapper (carving from `rest1`) takes the second. + let (inner, rest2) = rest1.carve().unwrap(); + assert_eq!(slot_strs(&inner).0, "jni :: sys :: jint :: MAX"); + assert!(rest2.is_empty()); + } + + /// `Niches::default()` equivalence to `empty()`. + #[test] + fn default_is_empty() { + let n = Niches::default(); + assert!(n.is_empty()); + } + + /// Cloning produces independent ownership; carving the clone + /// doesn't disturb the original (each carve consumes by value). + #[test] + fn clone_independence() { + let original = Niches::one(syn::parse_quote!(0i32), syn::parse_quote!(*v == 0)); + let cloned = original.clone(); + let (_slot, rest) = cloned.carve().unwrap(); + assert!(rest.is_empty()); + assert_eq!(original.len(), 1, "original unaffected by clone's carve"); + } +} diff --git a/prebindgen-ext/src/core/prebindgen_ext.rs b/prebindgen-ext/src/core/prebindgen_ext.rs new file mode 100644 index 00000000..307a1b3e --- /dev/null +++ b/prebindgen-ext/src/core/prebindgen_ext.rs @@ -0,0 +1,386 @@ +//! `PrebindgenExt` — the single extension point for the new pipeline. +//! +//! One method per `#[prebindgen]` item kind (`on_function`, `on_struct`, +//! `on_enum`, `on_const`) returning the wrapper Rust tokens to emit, plus a +//! family of converter methods split by direction and rank: +//! +//! * Input (wire → rust): `on_input_type_rank_0..3` +//! * Output (rust → wire): `on_output_type_rank_0..3` +//! +//! Each converter method returns `Some(ConverterImpl)` if the ext handles +//! the type, or `None` to fall through to higher-rank wildcard attempts (and +//! ultimately to an "unresolved required type" error if the resolver can't +//! fill the cell). +//! +//! `ConverterImpl::function` is the **complete** Rust function for the +//! converter — signature, body, attributes, lifetimes. The plugin owns +//! 100% of the shape. Other code that wants to call this converter reads +//! the name from `function.sig.ident`; the wire form from `destination`. + +use std::collections::HashSet; + +use proc_macro2::TokenStream; + +use crate::core::niches::Niches; +use crate::core::registry::{Registry, TypeKey}; + +/// One link in a converter's [stage chain](`ConverterImpl::pre_stages`) — +/// a value-inspecting throw stage that sits between the rust value the +/// `#[prebindgen]` fn yields/receives and the wire-facing +/// [`ConverterImpl::function`]. +/// +/// Each stage emits an `In → Result` function plus a JVM +/// exception class to raise when its `Err` arm fires. The function +/// wrapper drives them in chain order, emitting one match-throw per +/// stage (see `emit_jni_function_wrapper` in the JNI back-end). +#[derive(Clone)] +pub struct Stage { + /// Complete function definition for this stage. Same shape as + /// [`ConverterImpl::function`] but typed for this stage's own `In → + /// Out` and own error type. + pub function: syn::ItemFn, + /// JVM exception class FQN this stage raises on `Err` (e.g. + /// `"io.zenoh.jni.ZError"`). Contributes to the Kotlin emitter's + /// `@Throws(...)` union; the framework treats the chain's @Throws as + /// the set of every stage's `throws_fqn` plus the wire-facing + /// function's. + pub throws_fqn: String, + /// Bare-ident path to the generated `throw_` free fn the + /// function wrapper calls on this stage's `Err` (e.g. + /// `throw_ZError`). Resolved through the same mechanism as + /// [`crate::core::registry::TypeEntry::metadata`]'s + /// `throws_action`. + pub throws_action: syn::Path, +} + +/// Result of resolving one converter — the wire (destination) type the rest +/// of the registry sees, plus the complete generated function. +/// +/// Invariant: `function.sig.ident` MUST be a deterministic function of the +/// `(rust_type, destination)` pair so that callers of this converter — both +/// other generated converters in the same plugin and any hand-written code +/// that knows the convention — can compute or look up the name. +#[derive(Clone)] +pub struct ConverterImpl { + /// Wire/destination type. Other converters that ask "what's the wire + /// form of this rust type?" read this. The actual function may return + /// a wrapped form (e.g. `ZResult`) — that is the plugin's + /// internal calling convention; `destination` is the value the wire + /// carries on success. + pub destination: syn::Type, + /// Complete function definition for the **wire-facing** stage. The + /// plugin owns the parameter list, return type, `unsafe`/`pub` + /// modifiers, lifetime parameters, and any attribute annotations. + /// For input direction this is the FIRST stage in execution order + /// (it takes the wire); for output direction this is the LAST stage + /// (it produces the wire). + pub function: syn::ItemFn, + /// **Rust-side** stages that compose with [`Self::function`] to form + /// the full conversion chain. Default empty — a 1-stage converter + /// is just `function`. + /// + /// Order is rust-side-first → function-side-last. Concretely: + /// * **Input** (wire → rust): chain runs `wire → function → + /// pre_stages[0] → pre_stages[1] → … → pre_stages[N-1] → rust`. + /// * **Output** (rust → wire): chain runs `rust → pre_stages[N-1] → + /// … → pre_stages[1] → pre_stages[0] → function → wire`. + /// + /// Each stage raises its own configured exception (see + /// [`Stage::throws_fqn`]); the function wrapper emits one match-throw + /// per stage. + pub pre_stages: Vec, + /// Bit-patterns the wire type can represent but this converter never + /// produces (output) and rejects (input). Wrapper handlers like + /// `Option<_>` consume one slot for their own discriminant and + /// re-export the rest — see [`Niches`] for the cascade model. + /// Default is empty (no niche optimisation). + pub niches: Niches, + /// Language-specific extras carried alongside the converter. Filled by + /// the same handler that produces `destination` / `function` / + /// `niches`, copied through into `TypeEntry::metadata` by the resolver, + /// and read by language-side emitters. Set this where you build the + /// converter, not in a side channel. + pub metadata: M, +} + +/// How a single `impl Into` source arm consumes the Java-side +/// value when the source maps to an opaque-handle Rust type (i.e. the +/// source's registered input decoder returns `OwnedObject`). The +/// mode is a no-op for non-opaque sources (they have no `Box` slot to +/// manage). +/// +/// Used by [`IntoSource`] to drive +/// [`PrebindgenExt::dispatch_into_input`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum IntoSourceMode { + /// Borrow: opaque sources decode via + /// `OwnedObject::from_raw(...).clone()` (Java's `Box` slot stays + /// live across the call; requires `T: Clone`). + Borrow, + /// Consume: opaque sources decode via + /// `*Box::from_raw(ptr as *mut T)` (Java's `Box` slot is taken; + /// caller's typed handle is invalidated by the call). No + /// `T: Clone` bound. + Consume, +} + +/// One source arm in the dispatcher for an +/// `impl Into + Send + 'static` parameter — the Rust source +/// type plus the borrow/consume mode that determines how the opaque +/// `Box` slot is treated when the source is an opaque-handle type. +/// +/// Order in [`PrebindgenExt::into_sources`]'s returned vector +/// determines the runtime dispatch order in the emitted converter. +#[derive(Clone)] +pub struct IntoSource { + /// Rust source type the arm decodes from before + /// `TryInto::::try_into` runs. + pub source_type: syn::Type, + /// Borrow vs. Consume — relevant only when `source_type` is an + /// opaque-handle type (input decoder returns `OwnedObject`). + pub mode: IntoSourceMode, +} + +impl IntoSource { + /// Borrow-mode arm — opaque sources keep the Java handle live + /// (`OwnedObject::from_raw(...).clone()`); non-opaque sources are + /// unaffected. Equivalent to today's universal behavior. + pub fn borrow(ty: syn::Type) -> Self { + Self { + source_type: ty, + mode: IntoSourceMode::Borrow, + } + } + + /// Consume-mode arm — opaque sources take ownership of the Java + /// slot (`*Box::from_raw(ptr as *mut T)`), invalidating the + /// caller's typed handle. Non-opaque sources are unaffected. + pub fn consume(ty: syn::Type) -> Self { + Self { + source_type: ty, + mode: IntoSourceMode::Consume, + } + } +} + +/// Implemented by destination-language back-ends (e.g. JNI). The resolver +/// drives this trait to fill `Registry::input_types` / `output_types` +/// entries; the file emitter calls `on_function` / `on_struct` / `on_enum` / +/// `on_const` to produce per-item wrapper code. +/// +/// Back-ends pick a [`Self::Metadata`] type to carry language-specific +/// extras (Kotlin names, C header names, …) end-to-end through the +/// pipeline — set in each `ConverterImpl::metadata`, propagated by the +/// resolver into `TypeEntry::metadata`, and read directly by emitter +/// code. Back-ends that don't need any extras leave it at the default +/// `()`. +pub trait PrebindgenExt { + /// Language-specific extras every resolved converter carries. The + /// resolver copies this from each `ConverterImpl` it accepts into + /// the matching `TypeEntry`, so emitter code reads metadata off + /// the registry rather than through a parallel side channel. + type Metadata: Clone + Default; + + /// Rust items the plugin's emitted converters depend on (helper + /// structs, type aliases, runtime-support code). Emitted at the top + /// of the destination file, before all auto-generated converters. + /// + /// Default: none. Wrapper exts that compose a base ext should + /// forward to / extend the base's `prerequisites()`. The resolved + /// `registry` is supplied so prerequisites can be gated on what the + /// (feature-aware) scan actually contains — e.g. emitting a + /// per-opaque-handle item only for handles a scanned `#[prebindgen]` + /// fn references. + fn prerequisites(&self, _registry: &Registry) -> Vec { + Vec::new() + } + + // ── Declaration queries ──────────────────────────────────────── + + /// Idents of `#[prebindgen]` functions the ext claims for emission. + /// Anything not in this set is left in the registry's `functions` + /// map but never scanned for type requirements and never emitted — + /// the build prints a `cargo:warning=` line per skip. + /// + /// Default: empty (strict allowlist; an ext with no declarations + /// emits nothing for functions). + fn declared_functions(&self) -> HashSet { + HashSet::new() + } + + /// Canonical keys of types (structs / enums) the ext claims for + /// emission. Matched against `Registry::structs` and `Registry::enums` + /// by bare-ident lookup. Anything not in this set is left in the + /// registry but never scanned for body type requirements and never + /// emitted — the build prints a `cargo:warning=` line per skip. + /// + /// Default: empty (strict allowlist). + fn declared_types(&self) -> HashSet { + HashSet::new() + } + + /// Final post-processing pass applied to every emitted item right + /// before write. Default: no-op. + /// + /// Use this for cross-cutting transforms that would otherwise have + /// to be remembered at every individual emit site — e.g. qualifying + /// bare type references against a source module so the emitted + /// converter bodies compile in the binding crate's scope. Walks the + /// entire AST, not just signatures, so type ascriptions and casts + /// inside function bodies are covered. + fn post_process_item(&self, _item: &mut syn::Item) {} + + // ── Item methods ─────────────────────────────────────────────── + + /// Wrap a `#[prebindgen]` fn into the destination-language wrapper + /// (e.g. JNI `extern "C"` fn). + fn on_function(&self, f: &syn::ItemFn, registry: &Registry) -> TokenStream; + + /// Per-struct emission. Typically empty for languages that get + /// everything they need from auto-generated converters. + fn on_struct(&self, s: &syn::ItemStruct, registry: &Registry) -> TokenStream; + + /// Per-enum emission. + fn on_enum(&self, e: &syn::ItemEnum, registry: &Registry) -> TokenStream; + + /// Per-const emission. Default: pass-through. + fn on_const(&self, c: &syn::ItemConst, _registry: &Registry) -> TokenStream { + use quote::ToTokens; + c.to_token_stream() + } + + // ── Input direction (wire → rust) ────────────────────────────── + + /// Whole-type input converter. Returns `Some(ConverterImpl)` if the + /// ext handles `ty`. + fn on_input_type_rank_0( + &self, + ty: &syn::Type, + registry: &Registry, + ) -> Option>; + + /// Single-wildcard input pattern. `pat` contains one `_`; `t1` is the + /// type the wildcard slot held in the original. + fn on_input_type_rank_1( + &self, + pat: &syn::Type, + t1: &syn::Type, + registry: &Registry, + ) -> Option>; + + fn on_input_type_rank_2( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + registry: &Registry, + ) -> Option>; + + fn on_input_type_rank_3( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + t3: &syn::Type, + registry: &Registry, + ) -> Option>; + + /// Source types accepted at `impl Into + Send + 'static` + /// parameters. The caller is fully responsible for the list — if + /// the identity arm `target → target` is wanted, spell it out + /// with [`IntoSource::borrow`] / [`IntoSource::consume`]. The + /// resolver does **not** auto-prepend an identity arm. + /// + /// Default: no sources. Wrappers override (match on `target`) to + /// declare project-specific source types, e.g. `String → KeyExpr` + /// via `TryFrom`. The returned vector's order determines + /// the runtime dispatch order in the emitted converter. + fn into_sources(&self, target: &syn::Type) -> Vec { + let _ = target; + Vec::new() + } + + /// Build the dispatcher converter for an + /// `impl Into + Send + 'static` parameter, given the + /// source list returned by [`Self::into_sources`]. The resolver + /// calls this only after [`Self::on_input_type_rank_1`] has + /// returned `None` for the Into pattern, so wrappers that need + /// full custom dispatch can intercept earlier and skip this path. + /// + /// Default: `None`. Backends that support Into-source dispatch + /// (e.g. [`crate::jni::JniExt`]) override this to delegate to + /// their own emitter such as + /// [`crate::jni::JniExt::emit_into_dispatcher`]. + fn dispatch_into_input( + &self, + target: &syn::Type, + sources: &[IntoSource], + registry: &Registry, + ) -> Option> { + let _ = (target, sources, registry); + None + } + + /// Build the wrapper converter for an + /// `impl Fn(args...) + Send + Sync + 'static` parameter, given the + /// already-extracted arg types in declaration order. The resolver + /// calls this only after [`Self::on_input_type_rank_0`] / + /// [`Self::on_input_type_rank_1`] / [`Self::on_input_type_rank_2`] / + /// [`Self::on_input_type_rank_3`] (for the appropriate arity) has + /// returned `None`, so wrappers that need custom callback dispatch + /// can intercept earlier and skip this path. + /// + /// `args` are the rust-side argument types as they appear in the + /// source signature. Note that callback args flow inverse to the + /// callback parameter itself: the callback parameter is *input*, + /// but its args are produced by the rust side and consumed by the + /// foreign side, so they are *output* direction for converter + /// resolution. The framework handles this direction-flip at + /// registration time (`register_type_inner` in `core::registry`), + /// so implementations of this method should look up + /// already-registered *output* converters for each arg type. + /// + /// Default: `None`. Backends that support `impl Fn` callbacks + /// (e.g. [`crate::jni::JniExt`]) override this. + fn dispatch_fn_input( + &self, + args: &[syn::Type], + registry: &Registry, + ) -> Option> { + let _ = (args, registry); + None + } + + // ── Output direction (rust → wire) ───────────────────────────── + + /// Whole-type output converter. + fn on_output_type_rank_0( + &self, + ty: &syn::Type, + registry: &Registry, + ) -> Option>; + + fn on_output_type_rank_1( + &self, + pat: &syn::Type, + t1: &syn::Type, + registry: &Registry, + ) -> Option>; + + fn on_output_type_rank_2( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + registry: &Registry, + ) -> Option>; + + fn on_output_type_rank_3( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + t3: &syn::Type, + registry: &Registry, + ) -> Option>; +} diff --git a/prebindgen-ext/src/core/registry.rs b/prebindgen-ext/src/core/registry.rs new file mode 100644 index 00000000..8cbcaa33 --- /dev/null +++ b/prebindgen-ext/src/core/registry.rs @@ -0,0 +1,988 @@ +//! Single owner of everything parsed from the prebindgen source stream. +//! +//! [`Registry`] holds: +//! * Item maps (`functions`, `structs`, `enums`, `consts`) indexed by ident. +//! Duplicate names across kinds OR within a kind are an error — prebindgen +//! items live in one flat namespace. +//! * `passthrough` — items that aren't function/struct/enum/const (use, mod, +//! type alias, macro_rules) emitted verbatim. +//! * `input_types` / `output_types` — type tables split by rank +//! (`[HashMap>; 4]`). Each type encountered in +//! a `#[prebindgen]` fn signature or struct/enum body lands here. +//! +//! See the plan at `~/.claude/plans/are-there-any-reasons-hazy-brook.md` for +//! the full rationale. + +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use prebindgen::SourceLocation; +use quote::ToTokens; + +use crate::core::niches::Niches; +use crate::core::prebindgen_ext::{IntoSource, Stage}; + +/// Canonical type-shape key — the `to_token_stream().to_string()` form of a +/// `syn::Type`. Whitespace-normalised (`"Vec"` and `"Vec < u8 >"` produce +/// the same key after parse-and-restringify). +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct TypeKey(String); + +impl TypeKey { + /// Build a key by parsing the input as a type and re-serialising. Panics + /// if the input does not parse as a `syn::Type`. + pub fn parse(s: &str) -> Self { + let ty: syn::Type = syn::parse_str(s) + .unwrap_or_else(|e| panic!("TypeKey::parse: invalid type `{}`: {}", s, e)); + Self::from_type(&ty) + } + + /// Build a key directly from a `syn::Type`. + pub fn from_type(ty: &syn::Type) -> Self { + Self(ty.to_token_stream().to_string()) + } + + /// The canonical string form. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Parse the key back into a `syn::Type`. Always succeeds because the + /// key was originally constructed from a parseable type. + pub fn to_type(&self) -> syn::Type { + syn::parse_str(&self.0).unwrap_or_else(|e| { + panic!("TypeKey::to_type: stored key `{}` no longer parses: {}", self.0, e) + }) + } +} + +impl fmt::Display for TypeKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +/// Per-cell registry entry. +#[derive(Clone)] +pub struct TypeEntry { + /// Wire/destination type (e.g. `jni::sys::jlong`). Other converters + /// that ask "what's the wire form of this rust type?" read this. + pub destination: syn::Type, + /// Complete generated function for the **wire-facing** stage of the + /// converter (signature, body, attributes, lifetimes). Plugin owns + /// the shape. Callers compute this stage's name via + /// `function.sig.ident`. + pub function: syn::ItemFn, + /// **Rust-side** stages that compose with [`Self::function`] to form + /// the full chain — copied verbatim from the resolving + /// [`crate::core::prebindgen_ext::ConverterImpl::pre_stages`]. See + /// that field's docs for the chain-order semantics. + pub pre_stages: Vec, + /// Inner types whose function delegates to their converters. Empty for + /// rank-0 resolutions; equal to the rank-N `subs` array for rank-N≥1 + /// resolutions. Used by the post-resolution propagation pass. + pub subs: Vec, + /// Initially true for types that appear directly in a `#[prebindgen]` fn + /// signature; false for sub-positions. Promoted true by the propagation + /// pass for any type reachable via `subs` from another required type. + pub required: bool, + /// Wire bit-patterns this converter never produces / always rejects. + /// Wrappers (`Option<_>`, sum-typed enums) carve from this set for + /// their own discriminants. See [`Niches`] for the cascade model. + pub niches: Niches, + /// Source-arm metadata for `impl Into` dispatcher entries — + /// `Some(...)` only when this entry was produced by + /// [`crate::core::prebindgen_ext::PrebindgenExt::dispatch_into_input`]. + /// Language-side wrapper emitters (e.g. the Kotlin emitter in + /// `jni_kotlin_ext.rs`) read this to drive per-source `is JNI` + /// fan-out — one Java-side arm per declared + /// [`crate::core::prebindgen_ext::IntoSource`] with the source's + /// borrow/consume mode. `None` for every non-dispatcher entry. + pub into_sources: Option>, + /// Language-specific extras carried in by the + /// [`crate::core::prebindgen_ext::ConverterImpl`] that filled this + /// slot. Emitter code reads this directly — the registry is the + /// single source of truth for cross-language facts (Kotlin names, + /// C header names, etc.). Defaults to `()` for back-ends that don't + /// need any. + pub metadata: M, +} + +/// Direction of a converter pair. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum Direction { + /// Wire → Rust. + Input, + /// Rust → Wire. + Output, +} + +impl Direction { + pub fn flip(self) -> Self { + match self { + Direction::Input => Direction::Output, + Direction::Output => Direction::Input, + } + } +} + +/// Maximum rank the resolver supports (rank 0..=3). +pub const MAX_RANK: usize = 3; + +/// Single owner of everything parsed from the prebindgen source stream. +/// +/// The metadata parameter `M` is the language back-end's per-converter +/// extra type, supplied via +/// [`crate::core::prebindgen_ext::PrebindgenExt::Metadata`]. Each +/// [`TypeEntry`] carries one `M` copied in by the resolver from the +/// [`crate::core::prebindgen_ext::ConverterImpl`] that produced it. +/// Back-ends that don't carry extras leave `M = ()`. +pub struct Registry { + pub functions: HashMap, + pub structs: HashMap, + pub enums: HashMap, + pub consts: HashMap, + /// Anything else (use, mod, type alias, macro_rules) — passed through. + pub passthrough: Vec<(syn::Item, SourceLocation)>, + + /// Type tables. `input_types[N]` holds types whose rank is exactly `N`. + /// A given key appears in exactly one bucket. + pub input_types: [HashMap>>; 4], + pub output_types: [HashMap>>; 4], + + /// First-seen source location for each type key. Used in error messages + /// to point the user at where a required-but-unresolved type came from. + pub type_locations: HashMap, + + /// Sidecar tracking which keys were registered as top-level fn-signature + /// types, separate from per-entry `required` (which the resolver flips + /// into `TypeEntry::required` once an entry is filled). + pub required_inputs_scan: HashSet, + pub required_outputs_scan: HashSet, +} + +impl Default for Registry { + fn default() -> Self { + Self { + functions: HashMap::new(), + structs: HashMap::new(), + enums: HashMap::new(), + consts: HashMap::new(), + passthrough: Vec::new(), + input_types: Default::default(), + output_types: Default::default(), + type_locations: HashMap::new(), + required_inputs_scan: HashSet::new(), + required_outputs_scan: HashSet::new(), + } + } +} + + +/// Errors surfaced by the scan phase. +#[derive(Debug)] +pub enum ScanError { + DuplicateName { + name: syn::Ident, + first: SourceLocation, + second: SourceLocation, + }, + DisallowedImplTrait { + ty: String, + loc: SourceLocation, + }, + UnsupportedReceiver { + loc: SourceLocation, + }, + UnsupportedParamPattern { + loc: SourceLocation, + }, +} + +impl fmt::Display for ScanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScanError::DuplicateName { name, first, second } => write!( + f, + "duplicate prebindgen name `{}`: first at {}, second at {}", + name, first, second + ), + ScanError::DisallowedImplTrait { ty, loc } => write!( + f, + "`impl Trait` is not allowed at {}: `{}` (only `impl Fn(...) + Send + Sync + 'static` is supported)", + loc, ty + ), + ScanError::UnsupportedReceiver { loc } => { + write!(f, "method receiver (`self`) parameters are not supported at {}", loc) + } + ScanError::UnsupportedParamPattern { loc } => { + write!(f, "non-ident parameter pattern is not supported at {}", loc) + } + } + } +} + +impl std::error::Error for ScanError {} + +/// Combined error surfaced by [`Registry::write_rust`]. +#[derive(Debug)] +pub enum WriteRustError { + Scan(ScanError), + Resolve(crate::core::resolve::ResolveError), + Write(crate::core::write::WriteError), +} + +impl fmt::Display for WriteRustError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WriteRustError::Scan(e) => write!(f, "{}", e), + WriteRustError::Resolve(e) => write!(f, "{}", e), + WriteRustError::Write(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for WriteRustError {} + +impl From for WriteRustError { + fn from(e: ScanError) -> Self { + WriteRustError::Scan(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::core::resolve::ResolveError) -> Self { + WriteRustError::Resolve(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::core::write::WriteError) -> Self { + WriteRustError::Write(e) + } +} + +impl Registry { + /// Construct a `Registry` by indexing a stream of source items. + /// + /// Callers feed any `(syn::Item, SourceLocation)` iterator — typically + /// `source.items_all()`, `source.items_except_groups(...)`, or a + /// hand-rolled filter chain — so item-level selection happens upstream + /// of the registry rather than inside it. + /// + /// This step only populates the item maps (`functions`, `structs`, + /// `enums`, `consts`, `passthrough`). Signature/body scanning that + /// drives type-resolution requirements happens later, in + /// [`Self::scan_declared`], and is gated on what the language ext + /// has explicitly declared. Items that are never declared remain in + /// the registry but never drive type resolution and never emit. + pub fn from_items(items: I) -> Result + where + I: IntoIterator, + { + let mut registry = Registry::default(); + for (item, loc) in items { + registry.index_item(item, loc)?; + } + Ok(registry) + } + + /// Scan the signature/body of every item declared by `ext`. + /// + /// * For each ident in `ext.declared_functions()` ∩ indexed functions, + /// call [`Self::scan_fn_signature`] so parameter and return types + /// are registered as required. + /// * For each `TypeKey` in `ext.declared_types()`, mark the key as + /// required in both directions; if the key resolves to an indexed + /// struct/enum, also scan its body so field types are registered + /// (still `required: false` — propagation later promotes them + /// through `subs`). + /// + /// Declared items that don't match any indexed body get a build + /// warning (likely a typo in the build script). Indexed items that + /// were never declared also get a `cargo:warning=` skip line so the + /// user sees the full skip list per build. + pub fn scan_declared(&mut self, ext: &E) -> Result<(), ScanError> + where + E: crate::core::prebindgen_ext::PrebindgenExt, + { + let declared_fns = ext.declared_functions(); + let declared_types = ext.declared_types(); + + // Scan declared functions. + for ident in &declared_fns { + if let Some((item_fn, loc)) = self.functions.get(ident).cloned() { + self.scan_fn_signature(&item_fn, &loc)?; + } else { + println!( + "cargo:warning=prebindgen-ext: declared function `{}` not found among #[prebindgen] items", + ident + ); + } + } + + // Scan declared types. + for key in &declared_types { + let ty = key.to_type(); + let mut matched = false; + if let Some(ident) = type_path_tail_ident(&ty) { + if let Some((s, loc)) = self.structs.get(&ident).cloned() { + self.scan_struct(&s, &loc)?; + self.ensure_entry(Direction::Input, &ty, true, &loc); + self.ensure_entry(Direction::Output, &ty, true, &loc); + matched = true; + } else if let Some((e, loc)) = self.enums.get(&ident).cloned() { + self.scan_enum(&e, &loc)?; + self.ensure_entry(Direction::Input, &ty, true, &loc); + self.ensure_entry(Direction::Output, &ty, true, &loc); + matched = true; + } + } + if !matched { + // Declared type without an indexed body (e.g. + // `ptr_class(ZKeyExpr<'static>)` on a re-exported + // foreign type). Still mark required so the resolver + // tries to produce a converter for it. + let loc = self.type_locations.get(key).cloned().unwrap_or_default(); + self.ensure_entry(Direction::Input, &ty, true, &loc); + self.ensure_entry(Direction::Output, &ty, true, &loc); + } + } + + // Warn about indexed items that the ext never claimed. + let mut skipped_fns: Vec = self + .functions + .keys() + .filter(|k| !declared_fns.contains(*k)) + .map(|k| k.to_string()) + .collect(); + skipped_fns.sort(); + for name in &skipped_fns { + println!( + "cargo:warning=prebindgen-ext: skipping undeclared #[prebindgen] fn `{}`", + name + ); + } + + let mut skipped_types: Vec = Vec::new(); + for ident in self.structs.keys() { + let key = TypeKey::parse(&ident.to_string()); + if !declared_types.contains(&key) { + skipped_types.push(ident.to_string()); + } + } + for ident in self.enums.keys() { + let key = TypeKey::parse(&ident.to_string()); + if !declared_types.contains(&key) { + skipped_types.push(ident.to_string()); + } + } + skipped_types.sort(); + for name in &skipped_types { + println!( + "cargo:warning=prebindgen-ext: skipping undeclared #[prebindgen] struct/enum `{}`", + name + ); + } + + Ok(()) + } + + /// True iff the key was scanned as a top-level fn-signature input type. + pub fn is_required_input_at_scan(&self, key: &TypeKey) -> bool { + self.required_inputs_scan.contains(key) + } + pub fn is_required_output_at_scan(&self, key: &TypeKey) -> bool { + self.required_outputs_scan.contains(key) + } + + /// Look up the resolved input entry for `ty`, returning `None` if it + /// was never registered or is still unresolved. The returned entry's + /// `function.sig.ident` is the converter's call name; `destination` is + /// its wire form. + pub fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + let key = TypeKey::from_type(ty); + for bucket in &self.input_types { + if let Some(slot) = bucket.get(&key) { + return slot.as_ref(); + } + } + None + } + + /// Look up the resolved output entry for `ty`. See [`Self::input_entry`]. + pub fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + let key = TypeKey::from_type(ty); + for bucket in &self.output_types { + if let Some(slot) = bucket.get(&key) { + return slot.as_ref(); + } + } + None + } + + fn index_item(&mut self, item: syn::Item, loc: SourceLocation) -> Result<(), ScanError> { + match item { + syn::Item::Fn(f) => { + self.check_no_duplicate(&f.sig.ident, &loc)?; + self.functions.insert(f.sig.ident.clone(), (f, loc)); + Ok(()) + } + syn::Item::Struct(s) => { + self.check_no_duplicate(&s.ident, &loc)?; + self.structs.insert(s.ident.clone(), (s, loc)); + Ok(()) + } + syn::Item::Enum(e) => { + self.check_no_duplicate(&e.ident, &loc)?; + self.enums.insert(e.ident.clone(), (e, loc)); + Ok(()) + } + syn::Item::Const(c) => { + self.check_no_duplicate(&c.ident, &loc)?; + self.consts.insert(c.ident.clone(), (c, loc)); + Ok(()) + } + other => { + self.passthrough.push((other, loc)); + Ok(()) + } + } + } + + fn check_no_duplicate(&self, name: &syn::Ident, loc: &SourceLocation) -> Result<(), ScanError> { + if let Some(first) = self.first_seen_loc(name) { + return Err(ScanError::DuplicateName { + name: name.clone(), + first, + second: loc.clone(), + }); + } + Ok(()) + } + + fn first_seen_loc(&self, name: &syn::Ident) -> Option { + if let Some((_, loc)) = self.functions.get(name) { return Some(loc.clone()); } + if let Some((_, loc)) = self.structs.get(name) { return Some(loc.clone()); } + if let Some((_, loc)) = self.enums.get(name) { return Some(loc.clone()); } + if let Some((_, loc)) = self.consts.get(name) { return Some(loc.clone()); } + None + } + + fn scan_fn_signature(&mut self, f: &syn::ItemFn, loc: &SourceLocation) -> Result<(), ScanError> { + // Mechanical: register every fn-signature type as the user wrote it. + // No semantic transformations (no &T→T strip, no ZResult→T strip, + // no skip for () / ZResult<()>). The plugin handles those via rank + // handlers; propagation through `subs` then marks transitive deps + // (e.g. &Foo's `& _` rank-1 handler returns subs=[Foo], so Foo + // becomes required). + for input in &f.sig.inputs { + match input { + syn::FnArg::Receiver(_) => { + return Err(ScanError::UnsupportedReceiver { loc: loc.clone() }); + } + syn::FnArg::Typed(pt) => { + if !matches!(&*pt.pat, syn::Pat::Ident(_)) { + return Err(ScanError::UnsupportedParamPattern { loc: loc.clone() }); + } + self.register_type_recursive(Direction::Input, &*pt.ty, true, loc)?; + } + } + } + let ret_ty: syn::Type = match &f.sig.output { + syn::ReturnType::Default => syn::parse_quote!(()), + syn::ReturnType::Type(_, ty) => (**ty).clone(), + }; + self.register_type_recursive(Direction::Output, &ret_ty, true, loc)?; + Ok(()) + } + + fn scan_struct(&mut self, s: &syn::ItemStruct, loc: &SourceLocation) -> Result<(), ScanError> { + // The struct itself can appear in either direction. + let ty: syn::Type = syn::parse_str(&s.ident.to_string()).expect("ident is a valid type"); + self.ensure_entry(Direction::Input, &ty, false, loc); + self.ensure_entry(Direction::Output, &ty, false, loc); + + if let syn::Fields::Named(named) = &s.fields { + for field in &named.named { + self.register_type_recursive(Direction::Input, &field.ty, false, loc)?; + self.register_type_recursive(Direction::Output, &field.ty, false, loc)?; + } + } + Ok(()) + } + + fn scan_enum(&mut self, e: &syn::ItemEnum, loc: &SourceLocation) -> Result<(), ScanError> { + let ty: syn::Type = syn::parse_str(&e.ident.to_string()).expect("ident is a valid type"); + self.ensure_entry(Direction::Input, &ty, false, loc); + self.ensure_entry(Direction::Output, &ty, false, loc); + + for variant in &e.variants { + for field in &variant.fields { + self.register_type_recursive(Direction::Input, &field.ty, false, loc)?; + self.register_type_recursive(Direction::Output, &field.ty, false, loc)?; + } + } + Ok(()) + } + + /// Register `ty` as an entry in the given direction, then recurse into + /// every nested position. `top_required` applies only to `ty` itself; + /// nested positions are always recorded as not-required. + fn register_type_recursive( + &mut self, + dir: Direction, + ty: &syn::Type, + top_required: bool, + loc: &SourceLocation, + ) -> Result<(), ScanError> { + let mut visited: HashSet = HashSet::new(); + self.register_type_inner(dir, ty, top_required, loc, &mut visited) + } + + fn register_type_inner( + &mut self, + dir: Direction, + ty: &syn::Type, + is_top: bool, + loc: &SourceLocation, + visited: &mut HashSet, + ) -> Result<(), ScanError> { + // Reject `impl Trait` except `impl Fn(...) + Send + Sync + 'static` + // and `impl Into + Send + 'static`. + if let syn::Type::ImplTrait(it) = ty { + if extract_fn_trait_args(ty).is_none() && extract_into_trait_arg(ty).is_none() { + return Err(ScanError::DisallowedImplTrait { + ty: it.to_token_stream().to_string(), + loc: loc.clone(), + }); + } + } + + let key = TypeKey::from_type(ty); + if !visited.insert(key.clone()) { + return Ok(()); // cycle guard + } + + self.ensure_entry(dir, ty, is_top, loc); + + for (child_dir, sub) in self.immediate_edges(dir, ty) { + self.register_type_inner(child_dir, &sub, false, loc, visited)?; + } + Ok(()) + } + + fn ensure_entry( + &mut self, + dir: Direction, + ty: &syn::Type, + required: bool, + loc: &SourceLocation, + ) { + let key = TypeKey::from_type(ty); + let rank = compute_rank(ty).min(MAX_RANK); + let bucket = match dir { + Direction::Input => &mut self.input_types[rank], + Direction::Output => &mut self.output_types[rank], + }; + bucket.entry(key.clone()).or_insert(None); + if required { + match dir { + Direction::Input => self.required_inputs_scan.insert(key.clone()), + Direction::Output => self.required_outputs_scan.insert(key.clone()), + }; + } + self.type_locations.entry(key).or_insert_with(|| loc.clone()); + } + + /// Enumerate the immediate type-graph edges out of `(dir, ty)`: + /// generic args / Fn args / tuple elements / ref/array/slice/ptr targets, + /// plus — if `ty` is the bare ident of an indexed struct or enum — the + /// field types of that struct/enum. + /// + /// `impl Fn(args)` arg types flow with `dir.flip()`; everything else + /// inherits `dir`. Used by both `register_type_inner` (during scan) and + /// the unresolved-descendants BFS in `resolve` (for diagnostics). + pub(crate) fn immediate_edges( + &self, + dir: Direction, + ty: &syn::Type, + ) -> Vec<(Direction, syn::Type)> { + let mut out: Vec<(Direction, syn::Type)> = Vec::new(); + let (positions, child_dir) = if let Some(args) = extract_fn_trait_args(ty) { + (args, dir.flip()) + } else { + (immediate_subtype_positions(ty), dir) + }; + for sub in positions { + out.push((child_dir, sub)); + } + if let Some(name) = type_path_tail_ident(ty) { + if let Some((s, _)) = self.structs.get(&name) { + if let syn::Fields::Named(named) = &s.fields { + for field in &named.named { + out.push((dir, field.ty.clone())); + } + } + } + if let Some((e, _)) = self.enums.get(&name) { + for variant in &e.variants { + for field in &variant.fields { + out.push((dir, field.ty.clone())); + } + } + } + } + out + } + + /// One-shot: resolve every required type using `ext`, then write the + /// generated Rust bindings file. The single public entry point for + /// language-specific binding generation — language-agnostic because + /// `ext` is any [`crate::core::prebindgen_ext::PrebindgenExt`] impl + /// whose `Metadata` matches this registry's `M` parameter. + pub fn write_rust( + &mut self, + ext: &E, + out_path: impl AsRef, + ) -> Result + where + E: crate::core::prebindgen_ext::PrebindgenExt, + M: Clone + Default, + { + self.scan_declared(ext)?; + crate::core::resolve::resolve(self, ext)?; + Ok(crate::core::write::write_rust(self, ext, out_path)?) + } +} + +// ────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────── + +/// Number of leaves in a type's substitutable-position tree. +pub fn compute_rank(ty: &syn::Type) -> usize { + let positions = immediate_subtype_positions(ty); + if positions.is_empty() { + return 0; + } + positions.iter().map(|p| std::cmp::max(1, compute_rank(p))).sum() +} + +/// Immediate child type positions of `ty` (one level deep). +pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { + match ty { + syn::Type::Path(p) => { + if let Some(last) = p.path.segments.last() { + if let syn::PathArguments::AngleBracketed(ab) = &last.arguments { + return ab + .args + .iter() + .filter_map(|a| { + if let syn::GenericArgument::Type(t) = a { + Some(t.clone()) + } else { + None + } + }) + .collect(); + } + } + vec![] + } + syn::Type::Reference(r) => vec![(*r.elem).clone()], + syn::Type::Tuple(t) => t.elems.iter().cloned().collect(), + syn::Type::Array(a) => vec![(*a.elem).clone()], + syn::Type::Slice(s) => vec![(*s.elem).clone()], + syn::Type::Ptr(p) => vec![(*p.elem).clone()], + syn::Type::Group(g) => immediate_subtype_positions(&g.elem), + syn::Type::Paren(p) => immediate_subtype_positions(&p.elem), + syn::Type::ImplTrait(_) => extract_fn_trait_args(ty) + .or_else(|| extract_into_trait_arg(ty).map(|t| vec![t])) + .unwrap_or_default(), + _ => vec![], + } +} + + +/// If `ty` is exactly `impl Into + Send + 'static`, return `T`. Any +/// other bound combination (missing `Send`/`'static`, extra traits, no +/// `Into`) returns `None` — the framework rejects bare `impl Into` +/// at scan time. +/// +/// Mirrors [`extract_fn_trait_args`] in shape and intent: a single +/// well-formed exception to the otherwise-blanket "no `impl Trait`" +/// rule, picked up by every framework helper that handles parameter +/// type recognition (scan, rank, wildcard enumeration, rebuild). +pub fn extract_into_trait_arg(ty: &syn::Type) -> Option { + let syn::Type::ImplTrait(it) = ty else { + return None; + }; + let mut target: Option = None; + let mut has_send = false; + let mut has_static = false; + for bound in &it.bounds { + match bound { + syn::TypeParamBound::Trait(tb) => { + let last = tb.path.segments.last()?; + let name = last.ident.to_string(); + match name.as_str() { + "Into" => { + let syn::PathArguments::AngleBracketed(ab) = &last.arguments else { + return None; + }; + let mut tys = ab.args.iter().filter_map(|a| match a { + syn::GenericArgument::Type(t) => Some(t.clone()), + _ => None, + }); + let t = tys.next()?; + if tys.next().is_some() { + return None; + } + target = Some(t); + } + "Send" => has_send = true, + _ => return None, + } + } + syn::TypeParamBound::Lifetime(lt) if lt.ident == "static" => has_static = true, + _ => return None, + } + } + if has_send && has_static { + target + } else { + None + } +} + +/// If `ty` is `impl Fn(T1, T2, ...) + Send + Sync + 'static`, return the +/// `Fn` argument types in declaration order. Otherwise None. +pub fn extract_fn_trait_args(ty: &syn::Type) -> Option> { + let syn::Type::ImplTrait(it) = ty else { + return None; + }; + let mut args: Option> = None; + let mut has_send = false; + let mut has_sync = false; + let mut has_static = false; + for bound in &it.bounds { + match bound { + syn::TypeParamBound::Trait(tb) => { + let last = tb.path.segments.last()?; + let name = last.ident.to_string(); + match name.as_str() { + "Fn" => { + let syn::PathArguments::Parenthesized(p) = &last.arguments else { + return None; + }; + args = Some(p.inputs.iter().cloned().collect()); + } + "Send" => has_send = true, + "Sync" => has_sync = true, + _ => return None, + } + } + syn::TypeParamBound::Lifetime(lt) if lt.ident == "static" => has_static = true, + _ => return None, + } + } + if has_send && has_sync && has_static { + args + } else { + None + } +} + +/// Return the bare last-path-segment ident of `ty` if `ty` is a path type +/// like `Sample` (not generic). None for `Option`, `&T`, `(A, B)`. +pub(crate) fn type_path_tail_ident(ty: &syn::Type) -> Option { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + if matches!(last.arguments, syn::PathArguments::None) { + return Some(last.ident.clone()); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::niches::Niches; + use crate::core::prebindgen_ext::{ConverterImpl, PrebindgenExt}; + use proc_macro2::TokenStream; + use std::collections::HashSet; + + /// Minimal `PrebindgenExt` for scan-pipeline tests. Carries the + /// declared sets the test wants and stubs every emission/converter + /// hook into something inert. + #[derive(Default)] + struct StubExt { + functions: HashSet, + types: HashSet, + } + + impl PrebindgenExt for StubExt { + type Metadata = (); + + fn declared_functions(&self) -> HashSet { + self.functions.clone() + } + fn declared_types(&self) -> HashSet { + self.types.clone() + } + + fn on_function(&self, _f: &syn::ItemFn, _registry: &Registry<()>) -> TokenStream { + TokenStream::new() + } + fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry<()>) -> TokenStream { + TokenStream::new() + } + fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { + TokenStream::new() + } + fn on_input_type_rank_0( + &self, + _ty: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_input_type_rank_1( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_input_type_rank_2( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _t2: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_input_type_rank_3( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _t2: &syn::Type, + _t3: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_output_type_rank_0( + &self, + _ty: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_output_type_rank_1( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_output_type_rank_2( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _t2: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + fn on_output_type_rank_3( + &self, + _pat: &syn::Type, + _t1: &syn::Type, + _t2: &syn::Type, + _t3: &syn::Type, + _registry: &Registry<()>, + ) -> Option> { + None + } + } + + // suppress unused warning on Niches — kept available for richer tests + #[allow(dead_code)] + fn _force_niches_use() -> Niches { + Niches::empty() + } + + fn fn_item(src: &str) -> (syn::Item, SourceLocation) { + let item: syn::ItemFn = syn::parse_str(src).expect("test fn parse"); + (syn::Item::Fn(item), SourceLocation::default()) + } + + #[test] + fn from_items_does_not_scan_signatures() { + // A `#[prebindgen]`-marked fn whose return is a bare `impl Foo` + // would have failed `from_items` under the old code path + // (ScanError::DisallowedImplTrait). Now `from_items` is index- + // only and accepts it without complaint. + let items = vec![fn_item( + "fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }", + )]; + let reg: Registry<()> = Registry::from_items(items).expect("from_items must succeed"); + assert!(reg.required_inputs_scan.is_empty()); + assert!(reg.required_outputs_scan.is_empty()); + // The fn is indexed but no types are pre-required. + assert!(reg.functions.contains_key(&syn::parse_str("bogus").unwrap())); + } + + #[test] + fn scan_declared_empty_ext_marks_nothing_required() { + let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; + let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let ext = StubExt::default(); + reg.scan_declared(&ext).expect("empty ext = no scan"); + assert!(reg.required_inputs_scan.is_empty()); + assert!(reg.required_outputs_scan.is_empty()); + } + + #[test] + fn scan_declared_marks_types_required_only_for_declared_fns() { + let items = vec![ + fn_item("fn a(x: u64) -> u64 { x }"), + fn_item("fn b(x: u32) -> u32 { x }"), + ]; + let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("a").unwrap()); + reg.scan_declared(&ext).unwrap(); + assert!(reg.required_inputs_scan.contains(&TypeKey::parse("u64"))); + assert!(reg.required_outputs_scan.contains(&TypeKey::parse("u64"))); + assert!(!reg.required_inputs_scan.contains(&TypeKey::parse("u32"))); + assert!(!reg.required_outputs_scan.contains(&TypeKey::parse("u32"))); + } + + #[test] + fn scan_declared_fails_disallowed_impl_trait_only_when_fn_declared() { + let items = vec![fn_item( + "fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }", + )]; + let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + + // Empty ext: the bogus fn is not scanned, so no error. + let empty = StubExt::default(); + assert!(reg.scan_declared(&empty).is_ok()); + + // Declare the fn: scan now fires the disallowed-impl-Trait error. + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("bogus").unwrap()); + match reg.scan_declared(&ext) { + Err(ScanError::DisallowedImplTrait { .. }) => (), + other => panic!("expected DisallowedImplTrait, got {:?}", other), + } + } +} diff --git a/prebindgen-ext/src/core/resolve.rs b/prebindgen-ext/src/core/resolve.rs new file mode 100644 index 00000000..16760f02 --- /dev/null +++ b/prebindgen-ext/src/core/resolve.rs @@ -0,0 +1,999 @@ +//! Rank-based resolver and the post-resolution `required` propagation pass. +//! +//! The resolver fills `Registry::input_types` / `output_types` cells by +//! interrogating the language ext at successive rank phases: +//! * Phase 0: `on_*_type_rank_0(ty)` is asked about every unresolved +//! entry, regardless of the entry's own rank. +//! * Phases 1..3: for each still-unresolved entry of rank ≥ N, +//! [`enumerate_wildcard_subs`] yields all `(pattern, subs)` of size N +//! and asks the matching rank-N method. +//! +//! Within each phase, a fixed-point sub-loop runs PASS A (read-only, build +//! deltas) then PASS B (apply deltas) until no entry advances. This handles +//! same-rank dependencies (e.g. `Vec>` whose `Vec<_>` body +//! needs `Option`'s wire which is itself a rank-1 resolution). +//! +//! After all phases finish, [`propagate_required`] performs a BFS from the +//! scan-time required entries through `subs` edges; the final invariant is +//! that every `required: true && None` is reported as an error. +//! +//! Variant ordering within a single rank-N attempt is **deepest first**, +//! left-to-right; the first `Some` returned by the ext wins. + +use std::collections::VecDeque; + +use prebindgen::SourceLocation; + +use crate::core::prebindgen_ext::{ConverterImpl, PrebindgenExt}; +use crate::core::registry::{ + immediate_subtype_positions, Direction, Registry, TypeEntry, TypeKey, MAX_RANK, +}; + +/// Errors surfaced by the resolution phase. +#[derive(Debug)] +pub enum ResolveError { + /// A type that was scanned as required (or transitively reached from a + /// required type via `subs`) ended up with no converter. + Unresolved { entries: Vec }, +} + +#[derive(Debug)] +pub struct UnresolvedEntry { + pub key: TypeKey, + pub direction: Direction, + pub location: Option, +} + +impl std::fmt::Display for ResolveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResolveError::Unresolved { entries } => { + writeln!(f, "{} required type(s) could not be resolved:", entries.len())?; + for e in entries { + let dir = match e.direction { + Direction::Input => "input", + Direction::Output => "output", + }; + if let Some(loc) = e.location.as_ref() { + writeln!( + f, + "{}:{}:{}: error: unresolved prebindgen-ext {} type `{}`", + loc.file, + loc.line, + loc.column, + dir, + e.key + )?; + } else { + writeln!( + f, + "error: unresolved prebindgen-ext {} type `{}`", + dir, + e.key + )?; + } + } + Ok(()) + } + } + } +} + +impl std::error::Error for ResolveError {} + +/// Top-level resolution entry point. +/// +/// Runs ONE big fixed-point loop covering both directions and all ranks. +/// Each iteration sweeps every unresolved entry (both input and output) at +/// every rank; deltas are collected without mutating the registry, then +/// applied at the end of the iteration. Loops until a full sweep produces +/// zero deltas. +/// +/// The single-loop design lets cross-direction dependencies converge: e.g. +/// `impl Fn(Sample)` is an INPUT entry whose callback wrapper needs +/// `Sample`'s OUTPUT converter (callback args flow Rust→Kotlin). Sample's +/// output resolves in the same iteration as everything else, then +/// `impl Fn(Sample)`'s rank-1 attempt succeeds in the next. +pub fn resolve( + registry: &mut Registry, + ext: &E, +) -> Result<(), ResolveError> { + loop { + let mut deltas_in: Vec<(usize, TypeKey, TypeEntry)> = Vec::new(); + let mut deltas_out: Vec<(usize, TypeKey, TypeEntry)> = Vec::new(); + for n in 0..=MAX_RANK { + deltas_in.extend(collect_phase_deltas(registry, Direction::Input, n, ext)); + deltas_out.extend(collect_phase_deltas(registry, Direction::Output, n, ext)); + } + if deltas_in.is_empty() && deltas_out.is_empty() { + break; + } + apply_deltas(registry, Direction::Input, deltas_in); + apply_deltas(registry, Direction::Output, deltas_out); + } + propagate_required(registry); + final_invariant_check(registry) +} + +/// PASS A — walk every unresolved entry in buckets `n..=MAX_RANK`, ask the +/// ext, collect successful results without mutating the registry. +fn collect_phase_deltas( + registry: &Registry, + dir: Direction, + n: usize, + ext: &E, +) -> Vec<(usize, TypeKey, TypeEntry)> { + let mut deltas: Vec<(usize, TypeKey, TypeEntry)> = Vec::new(); + let buckets = match dir { + Direction::Input => ®istry.input_types, + Direction::Output => ®istry.output_types, + }; + + for bucket_idx in n..=MAX_RANK { + for (key, slot) in &buckets[bucket_idx] { + if slot.is_some() { + continue; + } + let key_ty = key.to_type(); + let scan_required = match dir { + Direction::Input => registry.is_required_input_at_scan(key), + Direction::Output => registry.is_required_output_at_scan(key), + }; + if let Some(entry) = try_resolve_entry(ext, &key_ty, n, dir, scan_required, registry) + { + deltas.push((bucket_idx, key.clone(), entry)); + } + } + } + deltas +} + +/// PASS B — apply collected deltas. Sole writer to the registry maps in +/// this iteration. +fn apply_deltas( + registry: &mut Registry, + dir: Direction, + deltas: Vec<(usize, TypeKey, TypeEntry)>, +) { + let buckets = match dir { + Direction::Input => &mut registry.input_types, + Direction::Output => &mut registry.output_types, + }; + for (bucket_idx, key, entry) in deltas { + if let Some(slot) = buckets[bucket_idx].get_mut(&key) { + if slot.is_none() { + *slot = Some(entry); + } + } + } +} + +/// Attempt to resolve one entry at exactly rank N (N=0 is whole-type; +/// N≥1 enumerates wildcard substitutions deepest-first). +fn try_resolve_entry( + ext: &E, + key_ty: &syn::Type, + n: usize, + dir: Direction, + scan_required: bool, + registry: &Registry, +) -> Option> { + if n == 0 { + let res: Option> = match dir { + Direction::Input => ext.on_input_type_rank_0(key_ty, registry), + Direction::Output => ext.on_output_type_rank_0(key_ty, registry), + }; + // Zero-arg `impl Fn() + Send + Sync + 'static` fallback: after + // the implementer's own rank-0 handler returns None, route the + // empty arg-list to `dispatch_fn_input`. Non-empty Fn arities + // are handled in the rank-N loop below. + let res = res.or_else(|| { + if dir != Direction::Input { + return None; + } + let args = crate::core::registry::extract_fn_trait_args(key_ty)?; + if args.is_empty() { + ext.dispatch_fn_input(&args, registry) + } else { + None + } + }); + return res.map(|c| TypeEntry { + destination: c.destination, + function: c.function, + pre_stages: c.pre_stages, + subs: vec![], + required: scan_required, + niches: c.niches, + into_sources: None, + metadata: c.metadata, + }); + } + + for (pattern, subs) in enumerate_wildcard_subs(key_ty, n) { + let result: Option> = match (dir, n) { + (Direction::Input, 1) => ext.on_input_type_rank_1(&pattern, &subs[0], registry), + (Direction::Input, 2) => { + ext.on_input_type_rank_2(&pattern, &subs[0], &subs[1], registry) + } + (Direction::Input, 3) => { + ext.on_input_type_rank_3(&pattern, &subs[0], &subs[1], &subs[2], registry) + } + (Direction::Output, 1) => ext.on_output_type_rank_1(&pattern, &subs[0], registry), + (Direction::Output, 2) => { + ext.on_output_type_rank_2(&pattern, &subs[0], &subs[1], registry) + } + (Direction::Output, 3) => { + ext.on_output_type_rank_3(&pattern, &subs[0], &subs[1], &subs[2], registry) + } + _ => unreachable!("rank N is bounded to 0..=3 by MAX_RANK"), + }; + // Fallback for `impl Fn(args...) + Send + Sync + 'static`: after + // the implementer's own rank-N handler returns None, route the + // canonical `impl Fn(_, _, …)` pattern (every arg slot is a + // wildcard) to `dispatch_fn_input`. Non-canonical patterns like + // `impl Fn(Option<_>, _)` are left for user rank handlers — only + // the all-wildcards shape gets the framework default. + let result = result.or_else(|| { + if dir != Direction::Input { + return None; + } + let pat_args = crate::core::registry::extract_fn_trait_args(&pattern)?; + if pat_args.len() != subs.len() { + return None; + } + if !pat_args.iter().all(|t| matches!(t, syn::Type::Infer(_))) { + return None; + } + ext.dispatch_fn_input(subs.as_slice(), registry) + }); + // Last-step fallback for `impl Into<_> + Send + 'static`: + // after the implementer's own rank-1 handler returns None, + // ask `ext.into_sources(target)` for the source list and + // route to the dedicated dispatcher. The caller spells out + // every arm (including the identity arm `target → target` + // with its own borrow/consume mode) — no implicit prepend + // here. Source metadata is captured alongside the converter + // so it propagates into `TypeEntry::into_sources` (read by + // language-side wrapper emitters for per-arm fan-out). + let mut captured_sources: Option> = None; + let result = result.or_else(|| { + if dir == Direction::Input + && n == 1 + && crate::core::registry::extract_into_trait_arg(&pattern).is_some() + { + let target = &subs[0]; + let sources = ext.into_sources(target); + if sources.is_empty() { + None + } else { + let dispatched = ext.dispatch_into_input(target, &sources, registry); + if dispatched.is_some() { + captured_sources = Some(sources); + } + dispatched + } + } else { + None + } + }); + if let Some(c) = result { + let sub_keys: Vec = subs.iter().map(TypeKey::from_type).collect(); + return Some(TypeEntry { + destination: c.destination, + function: c.function, + pre_stages: c.pre_stages, + subs: sub_keys, + required: scan_required, + niches: c.niches, + into_sources: captured_sources, + metadata: c.metadata, + }); + } + } + None +} + +// ────────────────────────────────────────────────────────────────────── +// Wildcard enumeration +// ────────────────────────────────────────────────────────────────────── + +/// Yield every `(pattern, subs)` where `subs` is a set of `n` pairwise +/// non-overlapping positions from `ty`'s tree, and `pattern` is `ty` with +/// each chosen position replaced by `_`. Returned in **deepest-first**, +/// left-to-right document order. +pub fn enumerate_wildcard_subs(ty: &syn::Type, n: usize) -> Vec<(syn::Type, Vec)> { + if n == 0 { + return vec![]; + } + // Collect all substitutable position paths in the type tree. + let mut paths: Vec = Vec::new(); + collect_positions(ty, &mut Vec::new(), &mut paths); + + // Enumerate every size-n subset of paths. + let mut variants: Vec<(usize, syn::Type, Vec)> = Vec::new(); + for choice in choose_indices(paths.len(), n) { + let chosen: Vec<&PositionPath> = choice.iter().map(|&i| &paths[i]).collect(); + if !pairwise_non_overlapping(&chosen) { + continue; + } + let max_depth = chosen.iter().map(|p| p.path.len()).max().unwrap_or(0); + let mut subs = Vec::with_capacity(n); + let pattern = substitute_wildcards(ty, &chosen, &mut subs); + // `subs` is filled by substitute_wildcards in document order of where + // the wildcards appear in the pattern. + variants.push((max_depth, pattern, subs)); + } + + // Sort by (max_depth desc) then by stable original order. + variants.sort_by(|a, b| b.0.cmp(&a.0)); + variants.into_iter().map(|(_, p, s)| (p, s)).collect() +} + +/// Path from the root of a `syn::Type` to one specific subtype position. +/// Represented as a sequence of child-indices into `immediate_subtype_positions`. +#[derive(Clone, Debug)] +struct PositionPath { + path: Vec, +} + +fn collect_positions(ty: &syn::Type, prefix: &mut Vec, out: &mut Vec) { + let positions = positions_for_traversal(ty); + for (i, sub) in positions.iter().enumerate() { + prefix.push(i); + out.push(PositionPath { path: prefix.clone() }); + collect_positions(sub, prefix, out); + prefix.pop(); + } +} + +/// Same as `immediate_subtype_positions` but for the impl-Trait +/// exceptions (`impl Fn(args)`, `impl Into + Send + 'static`) +/// returns the substitutable inner positions. +fn positions_for_traversal(ty: &syn::Type) -> Vec { + if let Some(args) = crate::core::registry::extract_fn_trait_args(ty) { + return args; + } + if let Some(t) = crate::core::registry::extract_into_trait_arg(ty) { + return vec![t]; + } + immediate_subtype_positions(ty) +} + +/// True iff none of `paths` is a strict prefix of another. Equal paths +/// trivially overlap (we don't generate equal paths anyway). +fn pairwise_non_overlapping(paths: &[&PositionPath]) -> bool { + for i in 0..paths.len() { + for j in (i + 1)..paths.len() { + if is_prefix(&paths[i].path, &paths[j].path) || is_prefix(&paths[j].path, &paths[i].path) + { + return false; + } + } + } + true +} + +fn is_prefix(short: &[usize], long: &[usize]) -> bool { + if short.len() > long.len() { + return false; + } + short.iter().zip(long.iter()).all(|(a, b)| a == b) +} + +/// Iterate every size-`k` subset of `0..n` as `Vec` in lex order. +fn choose_indices(n: usize, k: usize) -> Vec> { + if k == 0 || k > n { + return vec![]; + } + let mut out = Vec::new(); + let mut current: Vec = (0..k).collect(); + loop { + out.push(current.clone()); + // Find the rightmost element that can be incremented. + let mut i = k; + while i > 0 { + i -= 1; + if current[i] < n - (k - i) { + current[i] += 1; + for j in (i + 1)..k { + current[j] = current[j - 1] + 1; + } + break; + } + if i == 0 { + return out; + } + } + if current[0] > n - k { + break; + } + } + out +} + +/// Build the pattern by walking `ty` and replacing each chosen position +/// with `_`. Subtypes at the chosen positions are pushed into `subs` in +/// the document order of where the wildcards appear in the pattern. +fn substitute_wildcards( + ty: &syn::Type, + chosen: &[&PositionPath], + subs: &mut Vec, +) -> syn::Type { + let mut prefix = Vec::new(); + walk_substitute(ty, &mut prefix, chosen, subs) +} + +fn walk_substitute( + ty: &syn::Type, + prefix: &mut Vec, + chosen: &[&PositionPath], + subs: &mut Vec, +) -> syn::Type { + let positions = positions_for_traversal(ty); + if positions.is_empty() { + return ty.clone(); + } + let mut new_subs: Vec = Vec::with_capacity(positions.len()); + for (i, sub) in positions.iter().enumerate() { + prefix.push(i); + let is_chosen = chosen.iter().any(|p| p.path == *prefix); + if is_chosen { + subs.push(sub.clone()); + new_subs.push(syn::parse_quote!(_)); + } else { + new_subs.push(walk_substitute(sub, prefix, chosen, subs)); + } + prefix.pop(); + } + rebuild_type_with_positions(ty, &new_subs) +} + +/// Rebuild a type by replacing its immediate child positions with `new_subs`. +fn rebuild_type_with_positions(ty: &syn::Type, new_subs: &[syn::Type]) -> syn::Type { + if let Some(_args) = crate::core::registry::extract_fn_trait_args(ty) { + // Reconstruct `impl Fn(new_subs[0], new_subs[1], ...) + Send + Sync + 'static`. + let args = new_subs; + let tokens = quote::quote!(impl Fn(#(#args),*) + Send + Sync + 'static); + return syn::parse2(tokens).expect("rebuild impl Fn must parse"); + } + if crate::core::registry::extract_into_trait_arg(ty).is_some() { + // Reconstruct `impl Into + Send + 'static`. + let t = &new_subs[0]; + let tokens = quote::quote!(impl Into<#t> + Send + 'static); + return syn::parse2(tokens).expect("rebuild impl Into must parse"); + } + match ty { + syn::Type::Path(p) => { + let mut new = p.clone(); + if let Some(last) = new.path.segments.last_mut() { + if let syn::PathArguments::AngleBracketed(ab) = &mut last.arguments { + let mut idx = 0; + for arg in ab.args.iter_mut() { + if let syn::GenericArgument::Type(t) = arg { + *t = new_subs[idx].clone(); + idx += 1; + } + } + } + } + syn::Type::Path(new) + } + syn::Type::Reference(r) => { + let mut new = r.clone(); + *new.elem = new_subs[0].clone(); + syn::Type::Reference(new) + } + syn::Type::Tuple(t) => { + let mut new = t.clone(); + new.elems.clear(); + for s in new_subs { + new.elems.push(s.clone()); + } + syn::Type::Tuple(new) + } + syn::Type::Array(a) => { + let mut new = a.clone(); + *new.elem = new_subs[0].clone(); + syn::Type::Array(new) + } + syn::Type::Slice(s) => { + let mut new = s.clone(); + *new.elem = new_subs[0].clone(); + syn::Type::Slice(new) + } + syn::Type::Ptr(p) => { + let mut new = p.clone(); + *new.elem = new_subs[0].clone(); + syn::Type::Ptr(new) + } + syn::Type::Group(g) => { + let mut new = g.clone(); + *new.elem = rebuild_type_with_positions(&g.elem, new_subs); + syn::Type::Group(new) + } + syn::Type::Paren(p) => { + let mut new = p.clone(); + *new.elem = rebuild_type_with_positions(&p.elem, new_subs); + syn::Type::Paren(new) + } + other => other.clone(), + } +} + +// ────────────────────────────────────────────────────────────────────── +// Required-flag propagation (BFS from required entries through `subs`) +// ────────────────────────────────────────────────────────────────────── + +fn propagate_required(registry: &mut Registry) { + // Seed the queue from scan-time required keys plus any `required: true` + // already on resolved entries. + let mut queue: VecDeque<(Direction, TypeKey)> = VecDeque::new(); + for k in ®istry.required_inputs_scan { + queue.push_back((Direction::Input, k.clone())); + } + for k in ®istry.required_outputs_scan { + queue.push_back((Direction::Output, k.clone())); + } + + while let Some((dir, key)) = queue.pop_front() { + // Mark this entry's `required: true` if it's resolved. + let subs = mark_and_get_subs(registry, dir, &key); + // Subs travel in the same direction as the parent — they're the + // inner converters this body delegates to. + for sub_key in subs { + if !is_required_resolved(registry, dir, &sub_key) { + set_required(registry, dir, &sub_key); + queue.push_back((dir, sub_key)); + } + } + } +} + +fn mark_and_get_subs(registry: &mut Registry, dir: Direction, key: &TypeKey) -> Vec { + let buckets = match dir { + Direction::Input => &mut registry.input_types, + Direction::Output => &mut registry.output_types, + }; + for bucket in buckets.iter_mut() { + if let Some(slot) = bucket.get_mut(key) { + if let Some(entry) = slot { + entry.required = true; + return entry.subs.clone(); + } + return vec![]; + } + } + vec![] +} + +fn is_required_resolved(registry: &Registry, dir: Direction, key: &TypeKey) -> bool { + let buckets = match dir { + Direction::Input => ®istry.input_types, + Direction::Output => ®istry.output_types, + }; + for bucket in buckets { + if let Some(slot) = bucket.get(key) { + return slot.as_ref().is_some_and(|e| e.required); + } + } + false +} + +fn set_required(registry: &mut Registry, dir: Direction, key: &TypeKey) { + match dir { + Direction::Input => { + registry.required_inputs_scan.insert(key.clone()); + } + Direction::Output => { + registry.required_outputs_scan.insert(key.clone()); + } + } + let buckets = match dir { + Direction::Input => &mut registry.input_types, + Direction::Output => &mut registry.output_types, + }; + for bucket in buckets.iter_mut() { + if let Some(Some(entry)) = bucket.get_mut(key) { + entry.required = true; + return; + } + } +} + +fn lookup_slot<'a, M>( + registry: &'a Registry, + dir: Direction, + key: &TypeKey, +) -> Option<&'a Option>> { + let buckets = match dir { + Direction::Input => ®istry.input_types, + Direction::Output => ®istry.output_types, + }; + for bucket in buckets { + if let Some(slot) = bucket.get(key) { + return Some(slot); + } + } + None +} + +/// BFS from unresolved required-roots through the type graph, surfacing +/// further unresolved entries reachable through struct fields, enum variants, +/// generic args, and `impl Fn(...)` args. Stops at resolved nodes — their +/// `subs` were already walked by `propagate_required`, so traversing through +/// them risks reporting dependents the resolved converter doesn't actually +/// need. +fn collect_unresolved_descendants( + registry: &Registry, + seeds: &[(Direction, TypeKey)], + seen: &mut std::collections::HashSet<(Direction, TypeKey)>, + out: &mut Vec, +) { + let mut queue: VecDeque<(Direction, TypeKey)> = VecDeque::new(); + let enqueue_edges_from = |dir: Direction, + key: &TypeKey, + queue: &mut VecDeque<(Direction, TypeKey)>, + seen: &mut std::collections::HashSet<(Direction, TypeKey)>| { + let ty = key.to_type(); + for (child_dir, sub) in registry.immediate_edges(dir, &ty) { + let dep = (child_dir, TypeKey::from_type(&sub)); + if seen.insert(dep.clone()) { + queue.push_back(dep); + } + } + }; + + for (dir, key) in seeds { + enqueue_edges_from(*dir, key, &mut queue, seen); + } + + while let Some((dir, key)) = queue.pop_front() { + match lookup_slot(registry, dir, &key) { + Some(None) => { + // Registered but unresolved — report it and keep walking. + out.push(UnresolvedEntry { + key: key.clone(), + direction: dir, + location: registry.type_locations.get(&key).cloned(), + }); + enqueue_edges_from(dir, &key, &mut queue, seen); + } + None => { + // Not in the registry at all — can't report (no key/location + // worth surfacing), but its structural children may still + // include registered-but-unresolved types worth flagging. + enqueue_edges_from(dir, &key, &mut queue, seen); + } + Some(Some(_)) => { + // Resolved — `propagate_required` already walked its `subs`. + // Stop here to avoid spurious reports for descendants the + // resolved converter doesn't need. + } + } + } +} + +fn final_invariant_check(registry: &Registry) -> Result<(), ResolveError> { + let mut entries: Vec = Vec::new(); + let scan_required_input = ®istry.required_inputs_scan; + let scan_required_output = ®istry.required_outputs_scan; + let mut unresolved_required_roots: Vec<(Direction, TypeKey)> = Vec::new(); + let mut seen_unresolved: std::collections::HashSet<(Direction, TypeKey)> = + std::collections::HashSet::new(); + + for bucket in ®istry.input_types { + for (key, slot) in bucket { + let needs = match slot { + Some(e) => e.required, + None => scan_required_input.contains(key), + }; + if needs && slot.is_none() { + unresolved_required_roots.push((Direction::Input, key.clone())); + seen_unresolved.insert((Direction::Input, key.clone())); + entries.push(UnresolvedEntry { + key: key.clone(), + direction: Direction::Input, + location: registry.type_locations.get(key).cloned(), + }); + } + } + } + for bucket in ®istry.output_types { + for (key, slot) in bucket { + let needs = match slot { + Some(e) => e.required, + None => scan_required_output.contains(key), + }; + if needs && slot.is_none() { + unresolved_required_roots.push((Direction::Output, key.clone())); + seen_unresolved.insert((Direction::Output, key.clone())); + entries.push(UnresolvedEntry { + key: key.clone(), + direction: Direction::Output, + location: registry.type_locations.get(key).cloned(), + }); + } + } + } + + collect_unresolved_descendants( + registry, + &unresolved_required_roots, + &mut seen_unresolved, + &mut entries, + ); + + if entries.is_empty() { + Ok(()) + } else { + Err(ResolveError::Unresolved { entries }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::ToTokens; + + fn ty(s: &str) -> syn::Type { + syn::parse_str(s).unwrap() + } + + fn variant_strs(v: &[(syn::Type, Vec)]) -> Vec<(String, Vec)> { + v.iter() + .map(|(p, s)| { + ( + p.to_token_stream().to_string(), + s.iter().map(|t| t.to_token_stream().to_string()).collect(), + ) + }) + .collect() + } + + #[test] + fn rank_1_variants_for_result_option_string() { + let t = ty("Result, String>"); + let v = enumerate_wildcard_subs(&t, 1); + let s = variant_strs(&v); + // Three rank-1 variants. Deepest-first: u64-substitution comes + // before Option-substitution and String-substitution (both depth 1). + assert_eq!(s.len(), 3); + assert_eq!(s[0].0, "Result < Option < _ > , String >"); + assert_eq!(s[0].1, vec!["u64"]); + } + + #[test] + fn rank_2_variants_for_result_option_string() { + let t = ty("Result, String>"); + let v = enumerate_wildcard_subs(&t, 2); + let s = variant_strs(&v); + // Two rank-2 variants. Deepest-first: (u64, String) before (Option, String). + assert_eq!(s.len(), 2); + assert!( + s[0].0.contains("Option < _ >") && s[0].0.contains(", _"), + "expected Result, _>, got {}", + s[0].0 + ); + } + + #[test] + fn rank_3_zero_variants_for_rank_2_type() { + let t = ty("Result, String>"); + assert!(enumerate_wildcard_subs(&t, 3).is_empty()); + } + + #[test] + fn rank_1_for_vec_option_u64_deepest_first() { + let t = ty("Vec>"); + let v = enumerate_wildcard_subs(&t, 1); + let s = variant_strs(&v); + assert_eq!(s.len(), 2); + assert_eq!(s[0].0, "Vec < Option < _ > >"); + assert_eq!(s[0].1, vec!["u64"]); + assert_eq!(s[1].0, "Vec < _ >"); + assert_eq!(s[1].1, vec!["Option < u64 >"]); + } + + #[test] + fn impl_fn_decomposition() { + let t = ty("impl Fn(u64, String) + Send + Sync + 'static"); + let v = enumerate_wildcard_subs(&t, 2); + let s = variant_strs(&v); + assert_eq!(s.len(), 1); + assert!(s[0].0.contains("impl Fn (_ , _)"), "got {}", s[0].0); + } + + #[test] + fn rank_0_no_variants() { + let t = ty("u64"); + assert!(enumerate_wildcard_subs(&t, 1).is_empty()); + } + + #[test] + fn impl_into_decomposition_rebuilds_pattern() { + let t = ty("impl Into> + Send + 'static"); + let v = enumerate_wildcard_subs(&t, 1); + let s = variant_strs(&v); + assert_eq!(s.len(), 1); + // Pattern keeps the bound triple, with the wildcard at the + // Into target slot. tokens-to-string roundtrips with spaces. + assert!( + s[0].0.contains("impl Into < _ >") && s[0].0.contains("+ Send + 'static"), + "expected `impl Into<_> + Send + 'static`, got `{}`", + s[0].0, + ); + assert_eq!(s[0].1, vec!["KeyExpr < 'static >"]); + } + + /// Codifies the canonical-shape detection used by the rank-N + /// `dispatch_fn_input` fallback in [`try_resolve_entry`]. For + /// `impl Fn(Option)` at rank 1, `enumerate_wildcard_subs` + /// emits both `impl Fn(_)` (canonical — every Fn arg slot is a + /// wildcard, fallback should fire) and `impl Fn(Option<_>)` + /// (non-canonical — fallback must skip so a user rank-1 handler + /// can claim it). The fallback distinguishes them by checking that + /// every element of `extract_fn_trait_args(&pattern)` is a + /// `Type::Infer`; this test pins that invariant. + #[test] + fn impl_fn_canonical_pattern_is_distinguishable_from_nested() { + use crate::core::registry::extract_fn_trait_args; + let t = ty("impl Fn(Option) + Send + Sync + 'static"); + let v = enumerate_wildcard_subs(&t, 1); + // Two rank-1 variants: deepest-first (Option<_> over Sample), + // then the canonical Fn(_) over Option. + assert_eq!(v.len(), 2); + let mut canonical_count = 0; + let mut nested_count = 0; + for (pattern, _subs) in &v { + let args = extract_fn_trait_args(pattern).unwrap(); + let all_infer = args.iter().all(|a| matches!(a, syn::Type::Infer(_))); + if all_infer { + canonical_count += 1; + } else { + nested_count += 1; + } + } + assert_eq!( + canonical_count, 1, + "exactly one canonical `impl Fn(_)` pattern is expected" + ); + assert_eq!( + nested_count, 1, + "exactly one nested `impl Fn(Option<_>)` pattern is expected" + ); + } + + #[test] + fn impl_into_recognized_only_with_send_static() { + use crate::core::registry::extract_into_trait_arg; + // Accepted: bare `Into + Send + 'static`. + assert!(extract_into_trait_arg(&ty("impl Into> + Send + 'static")).is_some()); + // Order doesn't matter (parser preserves bound order, but extractor walks all). + assert!(extract_into_trait_arg(&ty("impl Send + Into> + 'static")).is_some()); + + // Rejected: missing Send. + assert!(extract_into_trait_arg(&ty("impl Into> + 'static")).is_none()); + // Rejected: missing 'static. + assert!(extract_into_trait_arg(&ty("impl Into> + Send")).is_none()); + // Rejected: missing Into entirely. + assert!(extract_into_trait_arg(&ty("impl Send + 'static")).is_none()); + // Rejected: extra unrelated trait. + assert!(extract_into_trait_arg(&ty("impl Into + Send + Sync + 'static")).is_none()); + // Rejected: not impl-Trait at all. + assert!(extract_into_trait_arg(&ty("KeyExpr<'static>")).is_none()); + } + + /// Regression: when a required type is itself unresolved AND has fields + /// that are also unresolved, the diagnostic must list both. Previously + /// `propagate_required` could not cross an unresolved parent (no `subs` + /// edges exist past it), so a missing build.rs declaration for `ZKeyExpr` + /// — only referenced as a field of an unresolved `Outer` — went silent. + #[test] + fn final_invariant_reports_unresolved_field_of_unresolved_struct() { + use crate::core::registry::{Registry, TypeKey}; + + let mut reg: Registry<()> = Registry::default(); + + // Index a struct `Outer { inner: ZKeyExpr }` so the BFS can walk + // into its field. `ZKeyExpr` itself stays *unindexed* (the user's + // build.rs forgot to declare it), but it does appear in the type + // tables because scan-recursion would have registered it as a field + // of `Outer`. Simulate the post-scan registry state directly. + let outer_struct: syn::ItemStruct = syn::parse_str( + "struct Outer { inner: ZKeyExpr }", + ) + .unwrap(); + reg.structs.insert( + outer_struct.ident.clone(), + (outer_struct, SourceLocation::default()), + ); + + // `Outer` is a required INPUT, unresolved (slot stays `None`). + let outer_key = TypeKey::parse("Outer"); + reg.input_types[0].insert(outer_key.clone(), None); + reg.required_inputs_scan.insert(outer_key.clone()); + + // `ZKeyExpr` is also in the type table (scan recursed into the + // field) but unresolved and NOT marked required at scan time — + // exactly the case the BFS is here to catch. + let zke_key = TypeKey::parse("ZKeyExpr"); + reg.input_types[0].insert(zke_key.clone(), None); + + let err = final_invariant_check(®).expect_err("must surface unresolved"); + let ResolveError::Unresolved { entries } = err; + let reported: std::collections::HashSet = + entries.iter().map(|e| e.key.to_string()).collect(); + assert!( + reported.contains("Outer"), + "expected `Outer` in report, got {:?}", + reported + ); + assert!( + reported.contains("ZKeyExpr"), + "expected `ZKeyExpr` (transitively unresolved via Outer.inner) in report, got {:?}", + reported + ); + } + + /// Counterpart to the regression above: the BFS must NOT walk through + /// resolved nodes. `propagate_required` already covers their `subs` + /// edges, so re-walking them risks reporting deeper unresolved entries + /// that the resolved converter doesn't actually depend on. + #[test] + fn final_invariant_stops_at_resolved_nodes() { + use crate::core::registry::{Direction, Registry, TypeEntry, TypeKey}; + use prebindgen::SourceLocation as Loc; + + let mut reg: Registry<()> = Registry::default(); + + let outer_struct: syn::ItemStruct = + syn::parse_str("struct Outer { inner: Inner }").unwrap(); + let inner_struct: syn::ItemStruct = + syn::parse_str("struct Inner { unused: Unrelated }").unwrap(); + reg.structs + .insert(outer_struct.ident.clone(), (outer_struct, Loc::default())); + reg.structs + .insert(inner_struct.ident.clone(), (inner_struct, Loc::default())); + + // `Outer` required & unresolved; `Inner` RESOLVED (with a dummy + // entry); `Unrelated` unresolved but only reachable through Inner. + let outer_key = TypeKey::parse("Outer"); + let inner_key = TypeKey::parse("Inner"); + let unrelated_key = TypeKey::parse("Unrelated"); + + reg.input_types[0].insert(outer_key.clone(), None); + reg.required_inputs_scan.insert(outer_key.clone()); + + reg.input_types[0].insert( + inner_key.clone(), + Some(TypeEntry { + destination: syn::parse_quote!(jni::sys::jlong), + function: syn::parse_quote!(fn __dummy() {}), + pre_stages: vec![], + subs: vec![], + required: false, + niches: crate::core::niches::Niches::empty(), + into_sources: None, + metadata: (), + }), + ); + + reg.input_types[0].insert(unrelated_key.clone(), None); + + let err = final_invariant_check(®).expect_err("must surface Outer"); + let ResolveError::Unresolved { entries } = err; + let reported: std::collections::HashSet = + entries.iter().map(|e| e.key.to_string()).collect(); + assert!(reported.contains("Outer")); + // Inner is resolved -> not reported. + assert!(!reported.contains("Inner")); + // Unrelated sits behind a resolved Inner -> must NOT be reported. + assert!( + !reported.contains("Unrelated"), + "BFS must stop at resolved nodes, got report: {:?}", + reported + ); + let _ = Direction::Input; // keep import used + } +} diff --git a/prebindgen-ext/src/core/write.rs b/prebindgen-ext/src/core/write.rs new file mode 100644 index 00000000..9acaa274 --- /dev/null +++ b/prebindgen-ext/src/core/write.rs @@ -0,0 +1,215 @@ +//! Rust file emission for the resolved `Registry`. +//! +//! `write_rust` collects every resolved input/output converter (each entry +//! already carries its full `ItemFn`), every per-item `on_` output, +//! and every passthrough item; concatenates them; and hands them to +//! `prebindgen::collect::Destination::write` (which does prettyplease +//! formatting and resolves the path against `OUT_DIR`). + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use prebindgen::collect::Destination; +use proc_macro2::TokenStream; + +use crate::core::prebindgen_ext::PrebindgenExt; +use crate::core::registry::{Registry, TypeEntry, TypeKey}; + +/// Errors surfaced by the file-emission phase. +#[derive(Debug)] +pub enum WriteError { + /// A `TokenStream` produced by an `on_*` trait method failed to parse + /// as `syn::Item`s. Indicates a codegen bug in the ext. + BadTokens(syn::Error), +} + +impl std::fmt::Display for WriteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WriteError::BadTokens(e) => write!(f, "generated tokens did not parse: {}", e), + } + } +} + +impl std::error::Error for WriteError {} + +/// Emit the resolved registry to a Rust file. +/// +/// `out_path` may be relative (resolved against `OUT_DIR` by prebindgen) or +/// absolute. Returns the path actually written. +pub fn write_rust, E: PrebindgenExt>( + registry: &Registry, + ext: &E, + out_path: P, +) -> Result { + let mut items: Vec = Vec::new(); + + // 0. Plugin prerequisites — runtime-support items (helper structs, + // type aliases) the converter bodies depend on. Emitted first so + // everything below can reference them. + items.extend(ext.prerequisites(registry)); + + // 1. Auto-generated converter wrappers (sorted by ident, deduped). + for (_, item_fn) in collect_converter_items(registry) { + items.push(syn::Item::Fn(item_fn)); + } + + // 2. Per-item Rust output from the ext — only for items the ext + // explicitly declared. Undeclared items were already announced + // via `cargo:warning=` in `Registry::scan_declared`. + let declared_fns = ext.declared_functions(); + let declared_types = ext.declared_types(); + items.extend(parse_items_from_tokens( + registry + .functions + .iter() + .filter(|(ident, _)| declared_fns.contains(*ident)) + .map(|(_, (item, _))| ext.on_function(item, registry)), + )?); + items.extend(parse_items_from_tokens( + registry + .structs + .iter() + .filter(|(ident, _)| { + declared_types.contains(&TypeKey::parse(&ident.to_string())) + }) + .map(|(_, (item, _))| ext.on_struct(item, registry)), + )?); + items.extend(parse_items_from_tokens( + registry + .enums + .iter() + .filter(|(ident, _)| { + declared_types.contains(&TypeKey::parse(&ident.to_string())) + }) + .map(|(_, (item, _))| ext.on_enum(item, registry)), + )?); + // Consts: always emit verbatim — declaration mechanism for consts + // is future work (see plan). + items.extend(parse_items_from_tokens( + registry + .consts + .values() + .map(|(item, _)| ext.on_const(item, registry)), + )?); + + // 3. Passthrough items verbatim. + for (item, _) in ®istry.passthrough { + items.push(item.clone()); + } + + // 4. Cross-cutting post-process pass. Plugins use this to qualify + // bare type references etc. — see PrebindgenExt::post_process_item. + for item in &mut items { + ext.post_process_item(item); + } + + let dest: Destination = items.into_iter().collect(); + Ok(dest.write(out_path)) +} + +/// Walk both type tables, dedupe each entry's stored `function` AND each +/// of its [`crate::core::prebindgen_ext::Stage`] functions by name, sort +/// for determinism. Names are read directly off `function.sig.ident` — +/// the plugin owns the naming. +pub fn collect_converter_items(registry: &Registry) -> Vec<(syn::Ident, syn::ItemFn)> { + let mut by_name: BTreeMap = BTreeMap::new(); + let mut collect = |entry: &TypeEntry| { + let name = entry.function.sig.ident.clone(); + by_name + .entry(name.to_string()) + .or_insert_with(|| (name, entry.function.clone())); + for stage in &entry.pre_stages { + let sname = stage.function.sig.ident.clone(); + by_name + .entry(sname.to_string()) + .or_insert_with(|| (sname, stage.function.clone())); + } + }; + walk_resolved(®istry.input_types, |_, entry| collect(entry)); + walk_resolved(®istry.output_types, |_, entry| collect(entry)); + by_name.into_values().collect() +} + +fn walk_resolved)>( + buckets: &[std::collections::HashMap>>; 4], + mut f: F, +) { + for bucket in buckets { + let mut keys: Vec<&TypeKey> = bucket.keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + for key in keys { + if let Some(Some(entry)) = bucket.get(key) { + f(key, entry); + } + } + } +} + +/// Parse a per-item `TokenStream` (which may be empty) as a sequence of +/// `syn::Item`s. Empty token streams yield zero items. +fn parse_items_from_tokens>( + iter: I, +) -> Result, WriteError> { + let mut out = Vec::new(); + for ts in iter { + if ts.is_empty() { + continue; + } + let file: syn::File = syn::parse2(ts.clone()).map_err(WriteError::BadTokens)?; + out.extend(file.items); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dedup_and_sort() { + let mut reg: Registry<()> = Registry::default(); + let key_a = TypeKey::parse("u64"); + let key_b = TypeKey::parse("Sample"); + let wire: syn::Type = syn::parse_quote!(jni::sys::jlong); + let wire2: syn::Type = syn::parse_quote!(jni::objects::JObject); + + reg.input_types[0].insert( + key_a.clone(), + Some(TypeEntry { + destination: wire.clone(), + function: syn::parse_quote!( + fn jlong_to_u64_aaaa(v: jni::sys::jlong) -> u64 { v as u64 } + ), + pre_stages: vec![], + subs: vec![], + required: true, + niches: crate::core::niches::Niches::empty(), + into_sources: None, + metadata: (), + }), + ); + reg.input_types[0].insert( + key_b.clone(), + Some(TypeEntry { + destination: wire2.clone(), + function: syn::parse_quote!( + fn JObject_to_Sample_bbbb(v: jni::objects::JObject) -> Sample { decode_sample(v) } + ), + pre_stages: vec![], + subs: vec![], + required: true, + niches: crate::core::niches::Niches::empty(), + into_sources: None, + metadata: (), + }), + ); + + let items = collect_converter_items(®); + assert_eq!(items.len(), 2); + // Sorted ASCII: "JObject_to_Sample_bbbb" < "jlong_to_u64_aaaa" + // (uppercase J < lowercase j). + assert_eq!(items[0].0.to_string(), "JObject_to_Sample_bbbb"); + assert_eq!(items[1].0.to_string(), "jlong_to_u64_aaaa"); + } +} diff --git a/prebindgen-ext/src/jni/byte_array_helpers.rs b/prebindgen-ext/src/jni/byte_array_helpers.rs new file mode 100644 index 00000000..df8803ec --- /dev/null +++ b/prebindgen-ext/src/jni/byte_array_helpers.rs @@ -0,0 +1,24 @@ +//! JNI byte-array conversion helpers for use in type converters and runtime code. + +use jni::objects::{JByteArray, JObject}; +use jni::JNIEnv; + +/// Converts a JNI `JByteArray` into a Rust `Vec`. +pub fn decode_byte_array(env: &mut JNIEnv, payload: &JByteArray) -> Result, String> { + env.convert_byte_array(payload) + .map_err(|err| format!("Error while decoding JByteArray: {}", err)) +} + +/// Converts a Rust byte slice into a JNI `JByteArray`. +pub fn encode_byte_array<'local>( + env: &mut JNIEnv<'local>, + bytes: &[u8], +) -> Result, String> { + env.byte_array_from_slice(bytes) + .map_err(|err| format!("Error while encoding JByteArray: {}", err)) +} + +/// Returns a null JNI byte-array handle. +pub fn null_byte_array() -> JByteArray<'static> { + JByteArray::from(JObject::null()) +} diff --git a/prebindgen-ext/src/jni/jni_binding_error.rs b/prebindgen-ext/src/jni/jni_binding_error.rs new file mode 100644 index 00000000..6d8d800d --- /dev/null +++ b/prebindgen-ext/src/jni/jni_binding_error.rs @@ -0,0 +1,41 @@ +//! Framework-owned exception type for JNI binding failures. +//! +//! Raised when a built-in converter can't honour the JNI shape it was +//! given: UTF-8 decode of a JString, `instanceof` check failure, null +//! JObject where a value was required, struct field read failure, etc. +//! Distinct from any application exception class declared via +//! [`crate::jni::JniExt::throwable`] — application errors +//! still flow through their own throw fns; binding errors land here. +//! +//! `JniExt::new()` pre-registers this type as `exceptions[0]` so it is +//! always available as `throw_JniBindingError(env, &err)` in the +//! generated bindings, and its Kotlin class is auto-emitted under the +//! app's configured package (e.g. `io.zenoh.jni.JniBindingError`). +//! +//! Implements `From` so framework-generated converter bodies +//! can keep their existing `<__JniErr as From>::from(...)` +//! shape — the `__JniErr` alias resolves to this type. + +/// Universal binding-failure error type raised by framework-built JNI +/// converters. Carries a single message describing the failure +/// context (e.g. `"Sample.encodingId: …"`, `"Option unbox: …"`). +#[derive(Clone)] +pub struct JniBindingError(pub String); + +impl From for JniBindingError { + fn from(s: String) -> Self { + Self(s) + } +} + +impl core::fmt::Display for JniBindingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.0) + } +} + +impl core::fmt::Debug for JniBindingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "JniBindingError({:?})", self.0) + } +} diff --git a/prebindgen-ext/src/jni/jni_ext.rs b/prebindgen-ext/src/jni/jni_ext.rs new file mode 100644 index 00000000..a509c6b5 --- /dev/null +++ b/prebindgen-ext/src/jni/jni_ext.rs @@ -0,0 +1,5269 @@ +//! JNI implementation of [`PrebindgenExt`]. +//! +//! Provides the universal JNI patterns: +//! * **Wrapper signatures**: input converter is +//! `fn(env: &mut JNIEnv, v: ) -> ZResult<>`; output converter +//! is `fn(env: &mut JNIEnv, v: &) -> ZResult<>`. +//! * **`on_function`**: emits a JNI `extern "C"` wrapper that delegates each +//! parameter conversion to the auto-generated `_to__` +//! converter, calls the original `#[prebindgen]` fn, and routes errors +//! through the generated `throw_` free function emitted +//! alongside the registered throwable-class entries (`.throwable()`). +//! * **Primitive types**: `bool`, `i64`, `f64`, `Duration`, `String`, +//! `Vec` rank-0 input/output bodies. +//! * **Wildcard wrappers**: `Option<_>` (input + output, including +//! primitive boxing), `ZResult<_>` (output only), `impl Fn(_..)` rank-1/2/3 +//! input (callback wrappers). +//! * **Structs/enums**: rank-0 input/output bodies are built from the +//! `Registry`'s `structs` / `enums` maps — fields and variants get +//! converted via the same auto-generated converter names. +//! +//! Crate-specific match arms (zenoh's `legacy_bindings` rows like +//! `CongestionControl`, manual callback overrides, opaque borrows, etc.) +//! belong in a thin wrapper trait impl in the consuming crate's `build.rs`, +//! NOT in this module — keeps `prebindgen-ext` reusable for any JNI/Kotlin +//! project. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; + +use crate::core::niches::Niches; +use crate::core::prebindgen_ext::{ConverterImpl, IntoSource, IntoSourceMode, PrebindgenExt, Stage}; +use crate::core::registry::{extract_fn_trait_args, Registry, TypeKey}; +use crate::jni::wire_access::jni_field_access; +use crate::util::snake_to_camel; + +// ────────────────────────────────────────────────────────────────────── +// Language metadata (PrebindgenExt::Metadata for JniExt) +// ────────────────────────────────────────────────────────────────────── + +/// Folded nullability / collection layers wrapping a closeable native +/// handle, outermost first. Mirrors how the type folds: the opaque-handle +/// leaf is [`CloseStrategy::Direct`]; an `Option<_>` wrapper adds +/// [`CloseStrategy::Nullable`]; a collection wrapper would add +/// [`CloseStrategy::Iterable`]. Drives both the typed Kotlin rendering of +/// a handle-bearing field/return and the generated `close()` expression, +/// uniformly across whatever wrappers compose. +#[derive(Clone, Debug)] +pub enum CloseStrategy { + /// The receiver *is* the handle. + Direct, + /// `T?` — receiver may be null. + Nullable(Box), + /// `List` — receiver is a collection. EXTENSION POINT: no + /// `Vec` shape exists today, so the emitters guard this arm + /// loudly rather than silently mis-generating. + Iterable(Box), +} + +/// Folded description of a closeable native handle reached through zero or +/// more wrapper layers. Set at the opaque-handle leaf, transformed by each +/// wrapper as the type folds (see [`CloseStrategy`]), and read by every +/// typed-surface emitter (data-class fields, struct encode/decode, +/// `classify_return`, param classification) so "is this an owned closeable +/// handle, what is its Kotlin class, how do I close it" has one source of +/// truth instead of a parallel ad-hoc decision tree. +#[derive(Clone, Debug)] +pub struct HandleInfo { + /// Canonical [`TypeKey`] string of the leaf opaque handle (e.g. + /// `"ZKeyExpr"`); look up [`JniExt::kotlin_type_fqns`] for the typed + /// Kotlin FQN. + pub leaf_key: String, + /// `false` for `&T` borrows — still an opaque handle (param + /// classification needs this), but not the holder's to close, so + /// `close()` emission skips it. + pub owned: bool, + /// Nullability / collection layers. + pub strategy: CloseStrategy, +} + +/// Per-converter language-specific extras carried by every +/// [`ConverterImpl`] this back-end produces. Filled by the same handler +/// that builds the wire/body, propagated by the resolver into +/// [`crate::core::registry::TypeEntry::metadata`], and read directly by +/// the Kotlin emitter — so cross-language facts flow through the +/// existing wrapper machinery rather than a parallel side channel. +#[derive(Clone, Debug, Default)] +pub struct KotlinMeta { + /// Value-context Kotlin type name. `Some("Long")` for opaque + /// handles (jlong wire mention), `Some("io.zenoh.jni.JNIEncoding")` + /// for user-declared decoder types whose wire isn't primitive, + /// `Some("List")` when a wrapper composes a primitive + /// inner. `None` only for entries that must not appear in any + /// Kotlin signature — the emitter treats that as a hard error. + pub kotlin_name: Option, + /// Kotlin fully-qualified exception class this converter can raise + /// when used as a function's return-type output converter. Populated + /// by [`JniExt::throws`]; the Kotlin emitter uses this for + /// `@Throws` annotations on the corresponding wrappers. `None` means + /// "non-throwing converter" (no `@Throws` emitted). + pub throws: Option, + /// Rust path of the generated `throw_` free function + /// the framework invokes as `(&mut env, &err)` for + /// wrapper-internal failures (e.g. input-decode `?` propagation) that + /// surface above this converter. Populated alongside + /// [`Self::throws`] by [`JniExt::throws`]; `None` when no + /// throwing behavior is configured for this converter. Replaces the + /// earlier `throw_exception!` macro path with a direct function call + /// emitted by [`JniExt::write_exceptions_rust`]. + pub throws_action: Option, + /// For wrapper converters whose Kotlin projection is the *inner* + /// type's projection (e.g. `ZResult` → `Publisher`), + /// this carries the inner Rust type's canonical key so downstream + /// emitters (typed-handle constructor lookup in + /// [`crate::jni::jni_kotlin_ext::classify_return`]) can find the + /// wrapped value's identity without baking in any specific shape + /// (no `peel_zresult` / `peel_result`-style framework hardcoding). + /// Populated by [`JniExt::throws`] with `args[0]`'s canonical + /// key for arity-1 wrappers, and inherited by the built-in + /// `Option<_>` / `Vec<_>` / `&_` rank-1 handlers from their inner + /// type's metadata. `None` for plain values and arity-0 converters. + pub value_rust_key: Option, + /// Present iff this (possibly wrapped) value is an opaque native + /// handle. Set at the opaque-handle leaf and folded outward by the + /// rank-1 `&_` / `Option<_>` handlers and the `lookup_*` composed + /// branches. The single source of truth for typed-handle rendering + /// and `close()` generation — see [`HandleInfo`]. + pub handle: Option, +} + +impl KotlinMeta { + pub fn from_name(name: impl Into) -> Self { + Self { + kotlin_name: Some(name.into()), + throws: None, + throws_action: None, + value_rust_key: None, + handle: None, + } + } +} + +// ────────────────────────────────────────────────────────────────────── +// Structured type-conversion configuration +// ────────────────────────────────────────────────────────────────────── + +/// Per-exception-class configuration (driven by +/// [`JniExt::throwable`]). +/// +/// One entry per Rust error type the binding surfaces to the JVM as a +/// Java exception. Declaration order matters: `exceptions[0]` is the +/// *primary* — its `From` impl is the universal converter-failure +/// path and its Kotlin FQN is used for `NativeHandle`'s closed-handle +/// exception. The throw function emitted into the generated file +/// (`throw_`) does the `find_class`/`throw_new` dance and +/// is referenced by the throws-marked wrapper code through +/// [`KotlinMeta::throws_action`]. +#[derive(Clone)] +pub(crate) struct ExceptionConfig { + /// Absolute Rust path of the error type, as a `syn::Type::Path` + /// (e.g. `zenoh_flat::errors::ZError`). Used both to splice the + /// `pub(crate) type __JniErr = ...` alias and as the function- + /// argument type of the generated `throw_`. Stored as a + /// `syn::Type` so it round-trips identically with the + /// closure-slot exception bindings in [`WrapperFn`] — both ends + /// spell the type the same way. + pub rust_type: syn::Type, + /// Last path segment of `rust_type` (e.g. `"ZError"`). Used to + /// derive the `throw_` function name and to provide the + /// Kotlin class name (relative; qualified against + /// [`JniExt::package`]). Exception class names are not currently + /// routed through any `kotlin_*_name_mangle` closure — the + /// short-name lands in the FQN verbatim. + pub rust_short: String, + /// Kotlin fully-qualified exception class name (e.g. + /// `"io.zenoh.jni.ZError"`) — `.`. Used for the + /// Kotlin class file path, `@Throws` annotations, and the JNI + /// `find_class("io/zenoh/jni/ZError")` literal inside the generated + /// `throw_` body. + pub kotlin_fqn: String, + /// Identifier of the generated `throw_` function. + pub throw_fn_name: syn::Ident, +} + +/// Per-opaque-handle configuration (driven by `JniExt::ptr_class`). +/// +/// The typed-handle Kotlin FQN (e.g. `"io.zenoh.jni.JNISession"`) lives +/// in the surrounding [`TypeConfig::kotlin_name`] slot — FQN-consumers +/// (typed-handle class emission, `instanceof` dispatch, +/// return-value constructor wrap) read it from there. The +/// value-context Kotlin name for the same type (`"Long"`) is produced +/// independently by the rank-0 opaque handler in [`KotlinMeta`], so +/// the two roles don't collide despite sharing the `TypeConfig`. +#[derive(Clone, Default)] +pub(crate) struct OpaqueConfig { + /// When `false` (default), the unified Kotlin emitter writes a + /// typed-handle `.kt` file for this opaque type. Set to `true` by + /// [`JniExt::suppress_kotlin_code`] to indicate the Kotlin file is + /// hand-maintained — only the Rust-side converter and `instanceof` + /// dispatch wire up. + pub suppress_kotlin_code: bool, +} + +/// Per-enum configuration (driven by `JniExt::enum_class`). +/// +/// Marks a `#[prebindgen]`-scanned `enum` as a Kotlin enum class — the +/// rank-0 converter arms emit `jint ↔ Rust enum` bodies (via `TryFrom` +/// for input and `as jni::sys::jint` for output), and the Kotlin emitter +/// writes an `enum class` file with SCREAMING_SNAKE_CASE variants and a +/// discriminant-keyed `fromInt(...)` companion. The Kotlin FQN lives in +/// the surrounding [`TypeConfig::kotlin_name`] slot, same as +/// [`OpaqueConfig`]. +#[derive(Clone, Default)] +pub(crate) struct EnumConfig { + /// When `false` (default), the unified Kotlin emitter writes an + /// `enum class` `.kt` file for this enum. Set to `true` by + /// [`JniExt::suppress_kotlin_code`] when the Kotlin source is + /// hand-maintained — only the Rust-side converter wires up. + pub suppress_kotlin_code: bool, +} + +/// One registered `.method(...)` / `.companion_method(...)` / +/// `.function(...)` entry. The Rust identifier is captured at build-script +/// time via `syn::parse_quote` (i.e. `pq!(rust_fn_name)`); the optional +/// override sets the Kotlin-side name when the default +/// `snake_to_camel(rust_ident)` derivation isn't what the user wants. +#[derive(Clone, Debug)] +pub struct MethodEntry { + /// Rust function ident — must match a `#[prebindgen]`-marked free + /// function in the registered source module. Looked up by + /// `registry.functions[ident]`. + pub rust_ident: syn::Ident, + /// Kotlin-side name override, set by chaining `.name("...")` after + /// the entry's registration. `None` = derive from `rust_ident` via + /// `snake_to_camel`. + pub kotlin_name_override: Option, +} + +impl MethodEntry { + pub fn new(rust_ident: syn::Ident) -> Self { + Self { rust_ident, kotlin_name_override: None } + } +} + +/// Back-pointer to the last entry added via `.method` / `.companion_method` +/// / `.function`, used by `.name(...)` to find what to mutate. Cleared by +/// every other builder call. +#[derive(Clone, Debug)] +pub(crate) enum NamedEntryRef { + /// Index into `types[key].instance_methods`. + Method(TypeKey, usize), + /// Index into `types[key].companion_methods`. + Companion(TypeKey, usize), + /// Index into `packages[subpackage].functions`. + Function(String, usize), +} + +/// All configuration the structured builder accumulates for one +/// canonical Rust type key. Every field is `None` by default; +/// builder methods populate the ones they care about. +#[derive(Clone, Default)] +pub(crate) struct TypeConfig { + /// Short Kotlin name or FQN. Required for any type emitted in + /// Kotlin (`Sample` → `"io.zenoh.jni.Sample"`, + /// `Vec` → `"ByteArray"`). + pub kotlin_name: Option, + /// If `Some`, this is an opaque-handle type — gets jlong wire, + /// `Box::into_raw`/`Box::from_raw` conventions, instanceof + /// dispatch, and Kotlin typed-handle class emission. + pub opaque: Option, + /// If `Some`, this is a `#[prebindgen]` enum mirrored as a Kotlin + /// `enum class` — gets jint wire (input + output via `TryFrom` + /// / `as jint`) and a generated `.kt` file. Mutually exclusive with + /// [`Self::opaque`]; builder enforces it. + pub enum_cfg: Option, + /// Kotlin FQN override for `impl Fn(...)` keys (the + /// closure-mangled callback name, e.g. zenoh-jni stamps `JNIOn...` + /// here via [`JniExt::auto_callback_fqn`]). + pub callback_kotlin_fqn: Option, + /// `#[prebindgen]` fns declared as **instance methods** on this type + /// via [`JniExt::method`]. The fn's first parameter must syntactically + /// match this type (modulo `&T` / `&mut T`) — validation happens at + /// render time in `render_wrapper_fn`. Param-promotion drops the + /// matched param from the Kotlin signature and substitutes inherited + /// `withPtr` / `consume` scope (opaque handles) or `this` (data / + /// value classes). + pub instance_methods: Vec, + /// `#[prebindgen]` fns declared as **companion-object methods** on + /// this type via [`JniExt::companion_method`]. No first-param + /// constraint; the wrapper is emitted inside `companion object { ... }` + /// using the same body shape as the package-level wrapper form (all + /// params present, no `this` substitution). + pub companion_methods: Vec, + /// Set by [`JniExt::throwable`]: the emitted Kotlin class extends + /// `Exception` and a structured `throw_` is generated that + /// constructs the JVM object via this type's own output converter. + /// `Result<_, ThisType>` Err arms route through that throw fn. + pub throwable: bool, + /// Set by [`JniExt::value_class`]: emit the Kotlin class as + /// `@JvmInline public value class` instead of `public data class`. + /// Requires exactly one field on the underlying struct (validated + /// at render time) and is mutually exclusive with + /// [`Self::throwable`] (value classes cannot extend `Exception`). + pub value_class: bool, +} + +/// Free-standing functions emitted into a synthetic package-level wrapper +/// object. One entry per `.package(subpackage)` context that +/// received `.function(...)` calls. +#[derive(Clone, Default)] +pub(crate) struct PackageConfig { + /// `#[prebindgen]` fns declared as free-standing wrappers under this + /// subpackage via [`JniExt::function`]. + pub functions: Vec, +} + +/// Boxed closure that builds a converter when applied to the wildcard +/// substitutions. Returns `None` to defer (an inner converter the +/// builder depends on isn't yet resolved; the resolver retries on the +/// next phase), or `Some((ty, exc, body))` where: +/// +/// * `ty` — the type the body produces. Auto-classified at lookup: +/// a wire shape (or the self-converter case) ⇒ terminal converter +/// with `destination = ty`; a rust type with its own converter ⇒ +/// composed as a value-inspecting stage onto that converter's chain. +/// * `exc` — the bound exception **as a Rust type**, matched by exact +/// canonical-form equality against a [`JniExt::throwable`] +/// registration's `rust_type` (use the same full path the +/// registration was declared with, e.g. +/// `parse_quote!(zenoh_flat::errors::ZError)` — no short-name +/// matching). `Some(...)` ⇒ throwing: the body evaluates to +/// `Result` and is emitted as-is. `None` ⇒ non-throwing: +/// the body evaluates to a bare `ty` and the framework wraps it +/// `Ok(body)` with `Result` (= `JniBindingError`). +/// * `body` — the closure body. The decision between Ok-wrap vs +/// verbatim is keyed on `exc` (see [`JniExt::build_input_fn`] / +/// [`JniExt::build_output_fn`]). +/// +/// Receives `&Registry` so the closure can look up +/// inner-type entries (`registry.output_entry(t)`). +pub(crate) type WrapperFn = Arc< + dyn Fn(&[syn::Type], &Registry) + -> Option<(syn::Type, Option, syn::Expr)> + + Send + + Sync, +>; + +/// Closure that transforms a Kotlin short name. Installed via the +/// per-kind setters ([`JniExt::kotlin_fun_name_mangle`], + /// [`JniExt::kotlin_data_class_name_mangle`], etc.); the framework calls +/// the matching closure wherever it needs to derive a Kotlin/JNI +/// short name for a generated element. Closure-unset = identity. +pub(crate) type NameMangle = Arc String + Send + Sync>; + +/// Trait selecting the arity-appropriate impl of +/// [`JniExt::input_wrapper`] / [`JniExt::output_wrapper`]. The phantom +/// type parameter discriminates closures of arity 0..3 so a single +/// public method name accepts any of them. Closures take the wildcard +/// substitutions plus the registry, and return `Some((ty, exc, body))` +/// or `None` (defer to a later resolver phase). See [`WrapperFn`] for +/// the triple's semantics. +pub trait WrapperBuilder: Send + Sync + 'static { + fn into_wrapper_fn(self) -> WrapperFn; + fn rank() -> usize; +} + +/// Arity-discriminating marker types. `Arity0` is for non-wildcard +/// patterns (e.g. `"i32"`); `Arity1`/`2`/`3` carry that many `_` slots. +pub struct Arity0; +pub struct Arity1; +pub struct Arity2; +pub struct Arity3; + +impl WrapperBuilder for F +where + F: Fn(&Registry) -> Option<(syn::Type, Option, syn::Expr)> + + Send + + Sync + + 'static, +{ + fn into_wrapper_fn(self) -> WrapperFn { + Arc::new(move |_args: &[syn::Type], reg: &Registry| self(reg)) + } + fn rank() -> usize { 0 } +} + +impl WrapperBuilder for F +where + F: Fn(&syn::Type, &Registry) -> Option<(syn::Type, Option, syn::Expr)> + + Send + + Sync + + 'static, +{ + fn into_wrapper_fn(self) -> WrapperFn { + Arc::new(move |args: &[syn::Type], reg: &Registry| { + self(&args[0], reg) + }) + } + fn rank() -> usize { 1 } +} + +impl WrapperBuilder for F +where + F: Fn(&syn::Type, &syn::Type, &Registry) -> Option<(syn::Type, Option, syn::Expr)> + + Send + + Sync + + 'static, +{ + fn into_wrapper_fn(self) -> WrapperFn { + Arc::new(move |args: &[syn::Type], reg: &Registry| { + self(&args[0], &args[1], reg) + }) + } + fn rank() -> usize { 2 } +} + +impl WrapperBuilder for F +where + F: Fn(&syn::Type, &syn::Type, &syn::Type, &Registry) + -> Option<(syn::Type, Option, syn::Expr)> + + Send + + Sync + + 'static, +{ + fn into_wrapper_fn(self) -> WrapperFn { + Arc::new(move |args: &[syn::Type], reg: &Registry| { + self(&args[0], &args[1], &args[2], reg) + }) + } + fn rank() -> usize { 3 } +} + +/// JNI back-end. Configure paths in the Rust crate (zresult, throw macro, +/// source module the original fns live in) and the JNI/Kotlin classpath +/// (java class prefix, callback Kotlin package + output dir). +#[derive(Clone)] +pub struct JniExt { + /// Module path the original `#[prebindgen]` fns live under (e.g. + /// the host crate of `#[prebindgen]` items). The wrapper body calls + /// `::(args)`. + pub source_module: syn::Path, + /// Registered exception classes in declaration order. The first entry + /// (`exceptions[0]`) is the framework `JniBindingError` — emitted as + /// the `__JniErr` alias by [`Self::prerequisites`] and used for + /// `NativeHandle`'s closed-handle exception. Populated by repeated + /// [`Self::throwable`] calls; consumed by: + /// [`Self::prerequisites`] (framework error type → `__JniErr`), + /// [`Self::write_exceptions_rust`] (one `throw_` per entry), + /// [`Self::write_native_handle`] (framework FQN), and + /// [`Self::lookup_input`] / [`Self::lookup_output`] (per-converter + /// bound-exception FQN + throw fn). + pub(crate) exceptions: Vec, + /// Single source of truth for the JVM/Kotlin namespace this binding + /// targets, dot-separated (e.g. `io.zenoh.jni`). Empty = no prefix. + /// Drives every derived form: slash-separated for `FindClass`, + /// `_`-mangled for JNI extern idents, and dot-separated for Kotlin + /// `package` declarations. + pub package: String, + /// Sub-package leaf appended to [`Self::package`] for the auto-emitted + /// callback fun-interface files. Combined as + /// `.`; empty = same package as + /// [`Self::package`]. + pub callback_subpackage: String, + /// Derived: `package.replace('.', '/')`. Read by + /// [`struct_output_body`] when building `FindClass` strings. + pub(crate) java_class_prefix: String, + /// Derived: `"Java_" + package.replace('.', '_') + "_" + + /// mangle_harness("Native")`. Read by [`mangle_jni_name`] when + /// building the JNI extern symbol path for every emitted wrapper. + pub(crate) jni_class_path: String, + /// Derived: `package + "." + callback_subpackage` (or just `package` + /// when the subpackage is empty). Also drives the on-disk subdirectory + /// under the `kotlin_root` passed to [`Self::write_kotlin`] + /// (`a.b.c` → `a/b/c/`). + pub(crate) kotlin_callback_package: String, + + /// Mangler for function names (scanned `#[prebindgen]` free fns and + /// the synthetic `freePtr` destructor). Default = identity; in + /// zenoh-jni the closure returns `format!("{name}ViaJNI")` so the + /// generated JNI extern symbols and matching Kotlin `external fun`s + /// both pick up the `ViaJNI` suffix. + pub(crate) kotlin_fun_name_mangle: Option, + /// Mangler for Kotlin ptr-class names declared via + /// [`Self::ptr_class`]. Default = identity. + pub(crate) kotlin_ptr_class_name_mangle: Option, + /// Mangler for Kotlin data-class names declared via + /// [`Self::data_class`]. Default = identity. + pub(crate) kotlin_data_class_name_mangle: Option, + /// Mangler for [`Self::enum_class`]-declared C-like enum class + /// names. Default = identity. + pub(crate) kotlin_enum_name_mangle: Option, + /// Mangler for the package-level wrapper object created by + /// [`Self::package`]. Default = identity. + pub(crate) kotlin_package_name_mangle: Option, + /// Mangler for `impl Fn(...)` callback Kotlin class names. The + /// closure receives the auto-derived callback name + /// ([`crate::jni::jni_kotlin_ext::derive_callback_name`], always + /// concatenated parameter type shorts + `"Callback"` suffix — e.g. + /// `"QueryCallback"`, `"ReplyCallback"`, `"Callback"` for `Fn()`); + /// the return value is qualified against + /// [`Self::kotlin_callback_package`]. Default = identity. + pub(crate) kotlin_callback_name_mangle: Option, + /// Mangler for rank-0 user-registered + /// [`Self::input_wrapper`] / [`Self::output_wrapper`] pattern names + /// — the Rust short name of the pattern (`Encoding`, + /// `SetIntersectionLevel`, …). Rank-N wrappers (`Option<_>`, + /// `ZResult<_>`, `Vec<_>`, `&_`) are NOT routed through any + /// mangler — they inherit from the inner type's metadata via the + /// existing rank-N handlers. Default = identity. + pub(crate) kotlin_wrapper_name_mangle: Option, + /// Mangler for the framework "harness" class name — + /// `"Native"` (the centralized JNI extern holder). Default when + /// unset = prepend `"JNI"`, so you get `JNINative`. Override to + /// plug in a different convention. + pub(crate) kotlin_harness_name_mangle: Option, + /// Derived `` view — + /// populated alongside [`Self::types`] by the structured builders + /// ([`Self::ptr_class`], [`Self::data_class`], + /// [`Self::callback_input`], [`Self::input_wrapper`] / + /// [`Self::output_wrapper`]). Internal readers + /// (`emit_into_dispatcher`, callback FQN merging) consume this flat + /// list directly; the structured `types` map is the source of + /// truth. + pub(crate) kotlin_type_fqns: Vec<(String, String)>, + + /// Structured per-type configuration keyed by canonical Rust type. + /// One entry per `Rust type ↔ JNI/Kotlin` rule; populated by the + /// structured builders (`ptr_class`, `enum_class`, + /// `data_class`, `input_wrapper`, `output_wrapper`, + /// `callback_input`). Holds opaque-handle config, enum config, + /// Kotlin names, and callback FQNs; the converter bodies themselves + /// live in [`Self::input_wrappers`] / [`Self::output_wrappers`]. + /// The rank-0 dispatch order is opaque → enum → wrapper-table → + /// primitive → struct. + pub(crate) types: HashMap, + + /// Free-standing package-level wrappers, keyed by subpackage path + /// (relative to [`Self::package`], dot-separated; never empty for an + /// entry to be emitted). Populated by [`Self::function`] under the + /// currently-active [`Self::active_subpackage`]. + pub(crate) packages: BTreeMap, + + /// `impl Into + Send + 'static` source arms per target type. + pub(crate) into_sources_map: HashMap>, + + /// Per-rank input converters — index `n` holds rank-`n` registrations + /// keyed by the pattern's `TypeKey`. Rank 0 is non-wildcard (e.g. + /// `"i32"`); ranks 1..3 carry that many `_` slots (e.g. `"Vec < _ >"`). + /// Each [`WrapperFn`] closure carries the builder body AND the bound + /// exception (the closure returns `(ty, exc, body)`); terminal vs + /// composed is derived at lookup time, throwing vs non-throwing + /// from the closure's `Option` middle slot. + pub(crate) input_wrappers: [HashMap; 4], + + /// Per-rank output converters. Same shape as [`Self::input_wrappers`]. + pub(crate) output_wrappers: [HashMap; 4], + + /// Tracks the last [`Self::ptr_class`] key registered so + /// [`Self::method`] / [`Self::suppress_kotlin_code`] know which + /// entry to extend. Cleared after each unrelated builder call. + last_opaque_key: Option, + + /// Tracks the last rank-0 wrapper registration so chained per-type + /// builders ([`Self::suppress_kotlin_code`], [`Self::with_kotlin_type`]) + /// know which entry to extend. Set by `input_wrapper` / + /// `output_wrapper` (rank 0 only), `enum_class`, `callback_input`, + /// and `data_class`; cleared by other unrelated builders. + last_meta_key: Option, + + /// The currently-active subpackage set by [`Self::package`]. + /// Drives where [`Self::function`] entries land and is folded into + /// the FQN of any class declared while it's `Some(_)`. Package + /// inheritance via chaining is **not** supported — each + /// `package` call overwrites the previous; nest via dotted + /// paths (`package("a.b")`) instead. + pub(crate) active_subpackage: Option, + + /// Back-pointer to the entry the next [`Self::name`] call should + /// mutate (the most recent `.method` / `.companion_method` / + /// `.function`). Cleared by every other builder call so `.name(...)` + /// only applies right after a fn-entry registration. + last_entry_ref: Option, +} + +impl JniExt { + /// Convenience constructor with sensible defaults; the paths still need + /// to be set explicitly via the field-mutation builder methods. + /// + /// Pre-registers the framework's [`crate::jni::JniBindingError`] as + /// `exceptions[0]` so it's always available as `throw_JniBindingError` + /// in the generated bindings and as the default `__JniErr` alias. + /// Its Kotlin FQN is `(empty).JniBindingError` until [`Self::package`] + /// is called, then auto-rebases via [`Self::recompute_derived`]. + pub fn new() -> Self { + let framework_exc = build_exception_config( + syn::parse_quote!(::prebindgen_ext::jni::JniBindingError), + "", + &[], + ); + let base = Self { + source_module: syn::parse_str("crate").unwrap(), + // exceptions[0] is the framework slot (JniBindingError); + // user `.throwable()` calls append at 1+. + exceptions: vec![framework_exc], + package: String::new(), + callback_subpackage: "callbacks".to_string(), + java_class_prefix: String::new(), + jni_class_path: "Java_JNINative".to_string(), + kotlin_callback_package: "callbacks".to_string(), + kotlin_fun_name_mangle: None, + kotlin_ptr_class_name_mangle: None, + kotlin_data_class_name_mangle: None, + kotlin_enum_name_mangle: None, + kotlin_package_name_mangle: None, + kotlin_callback_name_mangle: None, + kotlin_wrapper_name_mangle: None, + kotlin_harness_name_mangle: None, + kotlin_type_fqns: Vec::new(), + types: HashMap::new(), + packages: BTreeMap::new(), + into_sources_map: HashMap::new(), + input_wrappers: [ + HashMap::new(), + HashMap::new(), + HashMap::new(), + HashMap::new(), + ], + output_wrappers: [ + HashMap::new(), + HashMap::new(), + HashMap::new(), + HashMap::new(), + ], + last_opaque_key: None, + last_meta_key: None, + active_subpackage: None, + last_entry_ref: None, + }; + // Built-in rank-2 `Result<_, _>` peel: every Result succeeds + // as T and throws E on Err. E must be declared throwable via + // `.throwable()` (chained after a class declaration); the + // resulting peel stage is composed via `lookup_output`'s + // exact-canonical-form match in `find_exception`. Consumers may + // override per-binding by registering a more specific rank-1 + // `Result<_, ConcreteErr>` (rank-1 phase fires before rank-2). + base.output_wrapper( + syn::parse_quote!(Result<_, _>), + |ok: &syn::Type, err: &syn::Type, _: &Registry| { + Some((ok.clone(), Some(err.clone()), syn::parse_quote!(v))) + }, + ) + } + pub fn source_module(mut self, p: syn::Path) -> Self { + self.source_module = p; + self + } + + /// Mark the most recently declared class + /// ([`Self::data_class`], [`Self::ptr_class`], or + /// [`Self::enum_class`]) as throwable. Two effects: + /// + /// 1. The emitted Kotlin class extends `Exception` — see + /// [`crate::jni::jni_kotlin_ext`] for the renderer's branch. + /// 2. A `throw_` free function is generated that + /// constructs the JVM object via this type's own output converter + /// and throws it. The Result-peel stages built by the rank-2 + /// `Result<_, _>` wrapper (`JniExt::new`) call into this fn. + /// + /// Chains exactly like [`Self::method`] / [`Self::suppress_kotlin_code`]; + /// panics if no class was just declared. The framework's own + /// [`crate::jni::JniBindingError`] is pre-registered at + /// `exceptions[0]` directly inside [`Self::new`] and bypasses this + /// builder, so its stub-template Kotlin emission stays as-is. + pub fn throwable(mut self) -> Self { + let key = self.last_meta_key.clone().expect( + "JniExt::throwable must be chained immediately after a \ + `data_class`, `ptr_class`, or `enum_class` call", + ); + let rust_type = key.to_type(); + let cfg = build_exception_config(rust_type, &self.package, &self.exceptions); + let entry = self.types.get_mut(&key).expect("type entry vanished"); + assert!( + !entry.value_class, + "JniExt::throwable: `{}` was declared via `value_class` — \ + @JvmInline value classes cannot extend `Exception`. Use \ + `data_class` for throwable types.", + key.as_str() + ); + self.exceptions.push(cfg); + entry.throwable = true; + self + } + + /// Set the JVM/Kotlin base package (dot-separated, e.g. + /// `"io.zenoh.jni"`). All derived forms (`java_class_prefix`, + /// `kotlin_callback_package`) are recomputed. + pub fn package_prefix(mut self, p: impl Into) -> Self { + self.package = p.into().trim_matches('.').trim_matches('/').to_string(); + self.recompute_derived(); + self + } + /// Set the closure that mangles the framework "harness" class name + /// `"Native"` (the centralized extern holder). Default = prepend + /// `"JNI"` (yielding `JNINative`). Affects the generated Kotlin + /// class name and, via [`Self::jni_class_path`], the JNI extern + /// symbol path on the Rust side. + pub fn kotlin_harness_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_harness_name_mangle = Some(Arc::new(f)); + self.recompute_derived(); + self + } + /// Set the leaf appended to [`Self::package`] for the auto-emitted + /// callback fun-interface files (e.g. `"callbacks"`). Affects + /// `kotlin_callback_package`. + pub fn callback_subpackage(mut self, s: impl Into) -> Self { + self.callback_subpackage = s.into().trim_matches('.').to_string(); + self.recompute_derived(); + self + } + /// Set the closure that mangles function names. Called for every + /// scanned `#[prebindgen]` free function and the synthetic + /// `freePtr` destructor; receives the camelCased Kotlin-side name + /// and returns the final form (e.g. `"putPublisher"` → + /// `"putPublisherViaJNI"`). Default = identity. + pub fn kotlin_fun_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_fun_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles Kotlin ptr-class names declared + /// via [`Self::ptr_class`]. Receives the Rust short name. + /// Default = identity. + pub fn kotlin_ptr_class_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_ptr_class_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles Kotlin data-class names declared + /// via [`Self::data_class`]. Receives the Rust short name. + /// Default = identity. + pub fn kotlin_data_class_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_data_class_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles [`Self::enum_class`]-declared + /// enum class names. Receives the Rust short name. Default = + /// identity. + pub fn kotlin_enum_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_enum_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles the package-level wrapper object + /// name created by [`Self::package`]. Default = identity. + pub fn kotlin_package_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_package_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles `impl Fn(...)` callback class + /// names. Receives the auto-derived callback name + /// ([`crate::jni::jni_kotlin_ext::derive_callback_name`], always + /// concatenated parameter type shorts + `"Callback"` suffix — e.g. + /// `"QueryCallback"`, `"ReplyCallback"`, `"Callback"` for `Fn()`); + /// the returned relative name is qualified against + /// [`Self::kotlin_callback_package`]. Default = identity. + pub fn kotlin_callback_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_callback_name_mangle = Some(Arc::new(f)); + self + } + /// Set the closure that mangles rank-0 + /// [`Self::input_wrapper`] / [`Self::output_wrapper`] pattern + /// names (e.g. `"Encoding"`). Rank-N patterns are NOT routed + /// through this closure — they inherit from the inner type's + /// metadata via the existing rank-N handlers, preserving the + /// structural invariant `Option` ↔ `JNIEncoding?`. + /// Default = identity. + pub fn kotlin_wrapper_name_mangle(mut self, f: F) -> Self + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.kotlin_wrapper_name_mangle = Some(Arc::new(f)); + self + } + + /// Activate a subpackage context. Subsequent [`Self::function`] + /// calls land in this subpackage, and any class declared + /// ([`Self::ptr_class`] / [`Self::data_class`] / + /// [`Self::enum_class`] / [`Self::value_class`]) while the + /// subpackage is active gets an FQN of + /// `..`. + /// + /// Package inheritance is **not** supported — chaining + /// `.package("a").package("b")` does not produce + /// `"a.b"`; each call overwrites the previous active subpackage. + /// To nest, pass a dotted path: `.package("a.b")`. + /// + /// Passing an empty string clears the active subpackage (classes / + /// functions revert to the base ``). + pub fn package(mut self, subpackage: impl Into) -> Self { + self.last_opaque_key = None; + self.last_meta_key = None; + self.last_entry_ref = None; + let sub = subpackage.into().trim_matches('.').trim_matches('/').to_string(); + if sub.is_empty() { + self.active_subpackage = None; + } else { + self.packages.entry(sub.clone()).or_default(); + self.active_subpackage = Some(sub); + } + self + } + + /// Recompute the derived caches (`java_class_prefix`, + /// `jni_class_path`, `kotlin_callback_package`) from (`package`, + /// `kotlin_harness_name_mangle`, `callback_subpackage`). Called by + /// every setter that touches one of those source fields. The JNI + /// extern symbol path resolves to the centralized Native object, + /// whose mangled name comes from the harness mangle (default + /// `"JNI" + n` → `JNINative`). + fn recompute_derived(&mut self) { + self.java_class_prefix = self.package.replace(".", "/"); + let native_class = self.mangle_harness("Native"); + self.jni_class_path = if self.package.is_empty() { + format!("Java_{}", native_class) + } else { + format!("Java_{}_{}", self.package.replace(".", "_"), native_class) + }; + self.kotlin_callback_package = if self.package.is_empty() { + self.callback_subpackage.clone() + } else if self.callback_subpackage.is_empty() { + self.package.clone() + } else { + format!("{}.{}", self.package, self.callback_subpackage) + }; + // Re-anchor every exception's Kotlin FQN against the (possibly + // new) package. Each entry's `rust_short` is stable; the FQN is + // a derived view. In practice `package` is called first in + // every binding's build script, before any exception class is + // declared, so the framework slot at index 0 always re-derives + // cleanly. + for exc in &mut self.exceptions { + exc.kotlin_fqn = if self.package.is_empty() { + exc.rust_short.clone() + } else { + format!("{}.{}", self.package, exc.rust_short) + }; + } + } + + /// Apply [`Self::kotlin_fun_name_mangle`] to `name`, returning the + /// closure result or `name` verbatim when unset. Called everywhere + /// the framework derives a function-shaped Kotlin/JNI short name — + /// scanned `#[prebindgen]` extern symbols, the synthetic `freePtr` + /// destructor, and the Kotlin-side `external fun` that pairs with + /// each. + pub(crate) fn mangle_fun(&self, name: &str) -> String { + match &self.kotlin_fun_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_ptr_class_name_mangle`] to `name`, + /// returning the closure result or `name` verbatim when unset. + pub(crate) fn mangle_ptr_class(&self, name: &str) -> String { + match &self.kotlin_ptr_class_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_data_class_name_mangle`] to `name`, + /// returning the closure result or `name` verbatim when unset. + pub(crate) fn mangle_data_class(&self, name: &str) -> String { + match &self.kotlin_data_class_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_enum_name_mangle`] to `name`, returning the + /// closure result or `name` verbatim when unset. + pub(crate) fn mangle_enum(&self, name: &str) -> String { + match &self.kotlin_enum_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_callback_name_mangle`] to `name`, returning + /// the closure result or `name` verbatim when unset. + pub(crate) fn mangle_callback(&self, name: &str) -> String { + match &self.kotlin_callback_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_wrapper_name_mangle`] to `name`, returning + /// the closure result or `name` verbatim when unset. + pub(crate) fn mangle_wrapper(&self, name: &str) -> String { + match &self.kotlin_wrapper_name_mangle { + Some(f) => f(name), + None => name.to_string(), + } + } + /// Apply [`Self::kotlin_harness_name_mangle`] to `name`. The + /// closure defaults to `|n| format!("JNI{n}")` when unset, so calling + /// `mangle_harness("Native")` yields `"JNINative"`. + pub(crate) fn mangle_harness(&self, name: &str) -> String { + match &self.kotlin_harness_name_mangle { + Some(f) => f(name), + None => format!("JNI{name}"), + } + } + /// The mangled name of the centralized Native object that hosts + /// every JNI `external fun`. Drives both the Kotlin class emission + /// and the JNI extern symbol path on the Rust side. + pub(crate) fn jni_native_class_name(&self) -> String { + self.mangle_harness("Native") + } + /// The mangled wrapper-object class name for a given subpackage + /// (one wrapper object per [`Self::package`] context). + /// Derives from the subpackage's last dot-segment so + /// `package("a.b")` yields a class named after `b`. + pub(crate) fn jni_package_class_name(&self, subpackage: &str) -> String { + let leaf = subpackage.rsplit('.').next().filter(|s| !s.is_empty()).unwrap_or("Package"); + match &self.kotlin_package_name_mangle { + Some(f) => f(leaf), + None => self.mangle_harness(leaf), + } + } + + /// Resolve a relative class name against [`Self::package`]. Panics + /// if `name` contains a `.` (a check that catches accidental FQNs in + /// the relative-name builders). The framework refuses dotted names + /// on purpose: a binding crate owns one package and must not write + /// classes into anyone else's namespace. Higher layers wrap or + /// re-export — they don't get injected into. + pub(crate) fn resolve_class_fqn(&self, name: &str) -> String { + assert!( + !name.contains('.'), + "Kotlin class name `{}` must be relative (no dots) — FQNs are derived from JniExt::package", + name + ); + // If a `package(p)` context is active, classes declared + // while it's active land under `.

` instead of just + // ``. The user explicitly opts in by ordering the + // declaration after the `package` call. + let base: String = match (&self.package, &self.active_subpackage) { + (p, Some(sub)) if !p.is_empty() => format!("{}.{}", p, sub), + (p, Some(sub)) if p.is_empty() => sub.clone(), + (p, None) => p.clone(), + _ => String::new(), + }; + if base.is_empty() { + name.to_string() + } else { + format!("{}.{}", base, name) + } + } + + /// Resolve a relative callback class name against + /// `package + "." + callback_subpackage`. Panics if `name` contains a `.`. + pub(crate) fn resolve_callback_fqn(&self, name: &str) -> String { + assert!( + !name.contains('.'), + "Kotlin callback name `{}` must be relative (no dots) — FQNs are derived from JniExt::package + callback_subpackage", + name + ); + if self.kotlin_callback_package.is_empty() { + name.to_string() + } else { + format!("{}.{}", self.kotlin_callback_package, name) + } + } + // ── Structured type-conversion builders ────────────────────────── + + /// Declare a typed Kotlin handle class backed by an opaque Rust + /// type. Configures: jlong wire for both input and output, + /// `Box::into_raw`/`Box::from_raw` lifecycle, the `instanceof` + /// dispatch class, and the Kotlin typed-handle class FQN. By + /// default a `.kt` shell is auto-emitted — chain + /// [`Self::suppress_kotlin_code`] to keep the file hand-maintained, + /// or chain one or more [`Self::method`] calls to promote + /// `#[prebindgen]` functions onto the class as instance methods. + pub fn ptr_class(mut self, rust_type: syn::Type) -> Self { + let key = TypeKey::from_type(&rust_type); + let short = rust_short_name(&key); + let fqn = self.resolve_class_fqn(&self.mangle_ptr_class(&short)); + let entry = self.types.entry(key.clone()).or_default(); + entry.opaque = Some(OpaqueConfig::default()); + // `kotlin_name` holds the typed-handle FQN for FQN-consumers + // (typed-handle class emission, `instanceof` dispatch, return- + // value constructor wrap). The value-context Kotlin name for + // opaque types — `"Long"` — flows separately through + // [`KotlinMeta::kotlin_name`] produced by the rank-0 opaque + // handler, so wire-level mentions don't collide with the FQN. + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + self.last_opaque_key = Some(key.clone()); + self.last_meta_key = Some(key); + self.last_entry_ref = None; + self + } + + /// Declare a `#[prebindgen]` function as an **instance method** on + /// the class declared by the most recent + /// [`Self::ptr_class`] / [`Self::data_class`] / + /// [`Self::enum_class`] / [`Self::value_class`] call. The + /// function's first parameter must syntactically match the class's + /// Rust type (`&T` / `&mut T` / `T` for opaque handles; `&T` for + /// non-opaque data/value classes); the wrapper drops it from the + /// Kotlin signature and substitutes `this`/inherited scope at the + /// JNI call site. Mismatch is a build-time error (caught when the + /// wrapper is rendered). + /// + /// Panics if no class context is active. For free-standing functions + /// under [`Self::package`], use [`Self::function`]. + /// For companion-object (`static`-style) methods on a class, use + /// [`Self::companion_method`]. + pub fn class_fun(mut self, ident: syn::Ident) -> Self { + let key = self + .last_meta_key + .clone() + .or_else(|| self.last_opaque_key.clone()) + .expect( + "JniExt::method must be chained immediately after a `ptr_class`, \ + `data_class`, `enum_class`, or `value_class` call — \ + for free-standing fns inside `package`, use `.function(...)`; \ + for static-style class methods, use `.companion_method(...)`", + ); + let entry = self.types.get_mut(&key).expect("type entry vanished"); + let idx = entry.instance_methods.len(); + entry.instance_methods.push(MethodEntry::new(ident)); + self.last_entry_ref = Some(NamedEntryRef::Method(key, idx)); + self + } + + /// Declare a `#[prebindgen]` function as a **companion-object method** + /// on the class declared by the most recent class builder. No + /// first-param constraint; the wrapper is emitted in `companion + /// object { ... }` using the same form as a package-level wrapper + /// (all params present). Panics if no class context is active. + pub fn class_object_fun(mut self, ident: syn::Ident) -> Self { + let key = self + .last_meta_key + .clone() + .or_else(|| self.last_opaque_key.clone()) + .expect( + "JniExt::companion_method must be chained immediately after a \ + `ptr_class`, `data_class`, `enum_class`, or \ + `value_class` call", + ); + let entry = self.types.get_mut(&key).expect("type entry vanished"); + let idx = entry.companion_methods.len(); + entry.companion_methods.push(MethodEntry::new(ident)); + self.last_entry_ref = Some(NamedEntryRef::Companion(key, idx)); + self + } + + /// Declare a `#[prebindgen]` function as a free-standing wrapper + /// under the currently-active [`Self::package`] context. If a + /// class context is also live, calling `function` clears it — the + /// idea being that "leak class context to package level" makes the + /// chain unambiguous after one fn-level declaration. Panics if no + /// `package` is active. + pub fn package_fun(mut self, ident: syn::Ident) -> Self { + let sub = self.active_subpackage.clone().expect( + "JniExt::function must be chained inside a `package(...)` context", + ); + // Leak any class context back to package level. + self.last_meta_key = None; + self.last_opaque_key = None; + let pkg = self.packages.entry(sub.clone()).or_default(); + let idx = pkg.functions.len(); + pkg.functions.push(MethodEntry::new(ident)); + self.last_entry_ref = Some(NamedEntryRef::Function(sub, idx)); + self + } + + /// Override the Kotlin-side name for the most recent + /// [`Self::method`] / [`Self::companion_method`] / [`Self::function`] + /// entry. Default (without `.name(...)`) is + /// `snake_to_camel(rust_ident)` (e.g. `z_hello_whatami` → `zHelloWhatami`). + /// Panics if not chained immediately after a fn-level builder. + pub fn name(mut self, kotlin_name: impl Into) -> Self { + let r = self.last_entry_ref.clone().expect( + "JniExt::name must be chained immediately after `.method(...)`, \ + `.companion_method(...)`, or `.function(...)`", + ); + let name = kotlin_name.into(); + match r { + NamedEntryRef::Method(key, idx) => { + let entry = self.types.get_mut(&key).expect("type entry vanished"); + entry.instance_methods[idx].kotlin_name_override = Some(name); + } + NamedEntryRef::Companion(key, idx) => { + let entry = self.types.get_mut(&key).expect("type entry vanished"); + entry.companion_methods[idx].kotlin_name_override = Some(name); + } + NamedEntryRef::Function(sub, idx) => { + let pkg = self.packages.get_mut(&sub).expect("package entry vanished"); + pkg.functions[idx].kotlin_name_override = Some(name); + } + } + self + } + + /// Opt out of Kotlin class emission for the most recent + /// [`Self::ptr_class`] / [`Self::enum_class`] — the `.kt` file is + /// assumed to be hand-written. Without this, a typed-handle shell + /// class (or an `enum class`) is auto-emitted. Panics if no + /// `ptr_class` / `enum_class` is in scope. + pub fn suppress_kotlin_code(mut self) -> Self { + let key = self.last_meta_key.clone().expect( + "JniExt::suppress_kotlin_code must be chained immediately after a \ + `ptr_class` or `enum_class` call", + ); + let entry = self.types.get_mut(&key).expect("type entry vanished"); + if let Some(opaque) = entry.opaque.as_mut() { + opaque.suppress_kotlin_code = true; + } else if let Some(enum_cfg) = entry.enum_cfg.as_mut() { + enum_cfg.suppress_kotlin_code = true; + } else { + panic!( + "JniExt::suppress_kotlin_code: type entry for `{}` has neither \ + `opaque` nor `enum_cfg` set — chain after `ptr_class` or \ + `enum_class`", + key.as_str() + ); + } + self + } + + /// Whether `ty` was registered via [`Self::enum_class`] — used by + /// the Kotlin wrapper generator to decide if a parameter needs a + /// `.value` projection between the typed enum (Kotlin signature) and + /// the `Int` wire (JNI `external fun`). + pub(crate) fn is_kotlin_enum(&self, ty: &syn::Type) -> bool { + let key = TypeKey::from_type(ty); + self.types + .get(&key) + .and_then(|c| c.enum_cfg.as_ref()) + .is_some() + } + + /// Declare a `#[prebindgen]`-marked `enum` as a Kotlin `enum class`. + /// Configures: `jni::sys::jint` wire (input + output), `TryFrom` + /// decode on input, `as jint` encode on output, and Kotlin enum-file + /// emission. The enum must be C-like (unit variants only) and either + /// `#[repr(i32)]` / `#[repr(u8)]` (or similar) with explicit + /// discriminants — the Kotlin emitter and the generated + /// `TryFrom` decode rely on the discriminant values matching the + /// jint wire. + /// + /// By default a `.kt` file is auto-emitted under [`Self::package`]; chain + /// [`Self::suppress_kotlin_code`] to keep the file hand-maintained. + /// The class name passes through + /// [`Self::kotlin_enum_name_mangle`] (default = Rust short name). + pub fn enum_class(mut self, rust_type: syn::Type) -> Self { + let key = TypeKey::from_type(&rust_type); + let short = rust_short_name(&key); + let fqn = self.resolve_class_fqn(&self.mangle_enum(&short)); + let entry = self.types.entry(key.clone()).or_default(); + assert!( + entry.opaque.is_none(), + "JniExt::enum_class: `{}` is already registered as an opaque \ + handle via `ptr_class` — a type can be one or the other, \ + not both", + short + ); + entry.enum_cfg = Some(EnumConfig::default()); + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + // Clear opaque tracker so a stray `.method(...)` doesn't latch onto + // this entry; `last_meta_key` is what `.suppress_kotlin_code` reads + // for chained config. + self.last_opaque_key = None; + self.last_meta_key = Some(key); + self.last_entry_ref = None; + self + } + + /// Stamp a verbatim Kotlin type expression (e.g. `"List"`) + /// onto the entry registered by the most recent type-config builder. + /// Use this when the Kotlin type is not a class FQN (generics, + /// primitives, container types). For class names, the per-kind + /// `kotlin_*_name_mangle` closures (configured on [`JniExt`]) own + /// derivation — `with_kotlin_type` is the escape hatch for verbatim + /// expressions that don't map onto any one element kind. + pub fn with_kotlin_type(mut self, kotlin_expr: impl Into) -> Self { + let key = self + .last_meta_key + .clone() + .or_else(|| self.last_opaque_key.clone()) + .expect( + "JniExt::with_kotlin_type must be chained immediately after a \ + type-config builder", + ); + let expr = kotlin_expr.into(); + let entry = self.types.get_mut(&key).expect("meta entry vanished"); + entry.kotlin_name = Some(expr.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), expr)); + self + } + + /// Install a manual input converter for an `impl Fn(...)` callback + /// parameter (`JObject` wire). `exc` selects the body convention, + /// matching the unified [`Self::input_wrapper`] rule: + /// + /// * `exc = None` ⇒ non-throwing: emitted body is + /// `(env, &v)?` (framework `?`-propagation); only + /// valid if the dispatcher returns the framework error. + /// * `exc = Some()` ⇒ throwing: the dispatcher is + /// expected to return `Result>` (e.g. + /// `ZResult<_>`), and the emitted body is the dispatcher call + /// directly — no `?`/`Ok`, per the body↔exception coupling. The + /// type must match a [`Self::throwable`] declaration + /// by exact canonical-form equality (see [`Self::find_exception`]). + /// + /// The Kotlin FQN auto-derives via + /// [`Self::kotlin_callback_name_mangle`] applied to the per-callback + /// name ([`crate::jni::jni_kotlin_ext::derive_callback_name`]) and + /// then qualified against [`Self::kotlin_callback_package`]. Set + /// the mangler closure on [`JniExt`] to control naming (default = + /// identity). + pub fn callback_input( + mut self, + impl_fn_type: syn::Type, + exc: Option, + dispatcher_path: syn::Path, + ) -> Self { + let key = TypeKey::from_type(&impl_fn_type); + let dispatcher_path_str = dispatcher_path.to_token_stream().to_string(); + let body_path = dispatcher_path_str.clone(); + // `syn::Type` holds `Rc` internally and is neither + // `Send` nor `Sync`, so we can't capture it directly in a builder + // closure that satisfies `WrapperBuilder`'s `Send + Sync` + // bounds. Serialise to its canonical token form here and re-parse + // inside the closure — same dance the path captures use. + let exc_str = exc.as_ref().map(|t| t.to_token_stream().to_string()); + let builder = move |_reg: &Registry| { + let path: syn::Path = syn::parse_str(&body_path).ok()?; + // Throwing: dispatcher already returns `Result<_, exc>` — emit + // the call verbatim. Non-throwing: framework `?`-propagation + // unwraps, and the framework `Ok`-wraps later. + let body: syn::Expr = if exc_str.is_some() { + syn::parse_quote!(#path(env, &v)) + } else { + syn::parse_quote!(#path(env, &v)?) + }; + let exc_ty = exc_str.as_deref().and_then(|s| syn::parse_str::(s).ok()); + Some(( + syn::parse_quote!(jni::objects::JObject), + exc_ty, + body, + )) + }; + // Auto-derive the callback Kotlin FQN via + // `kotlin_callback_name_mangle` applied to the per-callback name. + // Stamped at registration time so downstream consumers + // (`dispatch_fn_input`, `collect_kotlin_callback_fqns`) read a + // resolved FQN rather than re-deriving it. The presence of + // `callback_kotlin_fqn` also flags this entry as a callback for + // emission paths that need to distinguish. + let args = crate::core::registry::extract_fn_trait_args(&impl_fn_type) + .unwrap_or_default(); + let name = crate::jni::jni_kotlin_ext::derive_callback_name(&args); + let fqn = self.resolve_callback_fqn(&self.mangle_callback(&name)); + let entry = self.types.entry(key.clone()).or_default(); + entry.callback_kotlin_fqn = Some(fqn.clone()); + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + self.input_wrappers[0].insert(key.clone(), builder.into_wrapper_fn()); + self.note_wrapper_registration(key, 0); + self + } + + /// Mark an `impl Fn(...)` callback type as having a hand-written + /// Kotlin fun-interface. The framework keeps its default Rust-side + /// auto-dispatcher (no [`Self::callback_input`] override here) but + /// skips emitting the Kotlin auto-stub — the binding crate provides + /// the `.kt` file itself. The Kotlin FQN is auto-derived via + /// [`Self::mangle_callback`] applied to the callback's name so the + /// hand-written file name and the JNI-side mention stay in sync. + /// Equivalent to chaining `.suppress_kotlin_code()` after a + /// [`Self::ptr_class`] / [`Self::enum_class`] declaration, but + /// inline because callbacks don't have a `kotlin_callback` builder + /// to chain off. + pub fn suppress_kotlin_callback_code(mut self, impl_fn_type: syn::Type) -> Self { + let key = TypeKey::from_type(&impl_fn_type); + let args = crate::core::registry::extract_fn_trait_args(&impl_fn_type) + .unwrap_or_default(); + let name = crate::jni::jni_kotlin_ext::derive_callback_name(&args); + let fqn = self.resolve_callback_fqn(&self.mangle_callback(&name)); + let entry = self.types.entry(key.clone()).or_default(); + entry.callback_kotlin_fqn = Some(fqn.clone()); + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + self.last_opaque_key = None; + self.last_meta_key = None; + self.last_entry_ref = None; + self + } + + /// Declare a Rust struct that should appear in Kotlin as a data + /// class under a derived name. The name passes through + /// [`Self::kotlin_data_class_name_mangle`] (default = Rust short + /// name, generics / lifetimes stripped). Only affects Kotlin + /// emission — no Rust-side converter override. + pub fn data_class(mut self, rust_type: syn::Type) -> Self { + let key = TypeKey::from_type(&rust_type); + let short = rust_short_name(&key); + let fqn = self.resolve_class_fqn(&self.mangle_data_class(&short)); + let entry = self.types.entry(key.clone()).or_default(); + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + self.last_opaque_key = None; + self.last_meta_key = Some(key); + self.last_entry_ref = None; + self + } + + /// Declare a Rust struct that should appear in Kotlin as an + /// `@JvmInline public value class` rather than a `public data class`. + /// The wrapped struct must have **exactly one named field** and + /// must not be marked [`Self::throwable`] — both constraints are + /// enforced at render time with a hard error. + /// + /// Why a dedicated builder rather than auto-promoting one-field + /// data classes: value-class semantics are observable (method-name + /// mangling, boxing on interface dispatch / generics / nullable + /// receivers), so the decision must be explicit per-type. Naming + /// passes through [`Self::kotlin_data_class_name_mangle`] — the + /// same Kotlin-side namespace as `data_class`. + /// + /// Note: `ptr_class` deliberately does **not** support + /// value-class emission. Typed-handle classes extend + /// `JNINativeHandle`, hold a `Cleaner.Cleanable` field, and + /// implement `AutoCloseable` — none of which a value class can + /// express without abandoning the Cleaner-based finalization + /// safety net. + pub fn value_class(mut self, rust_type: syn::Type) -> Self { + let key = TypeKey::from_type(&rust_type); + let short = rust_short_name(&key); + let fqn = self.resolve_class_fqn(&self.mangle_data_class(&short)); + let entry = self.types.entry(key.clone()).or_default(); + entry.kotlin_name = Some(fqn.clone()); + entry.value_class = true; + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + self.last_opaque_key = None; + self.last_meta_key = Some(key); + self.last_entry_ref = None; + self + } + + /// Register `impl Into` source arms. `target_key` is the + /// canonical Rust type (e.g. `"ZKeyExpr<'static>"`); `sources` is + /// an ordered list of [`IntoSource`] arms (dispatch order matches + /// iteration order). + pub fn into_sources(mut self, target_type: syn::Type, sources: I) -> Self + where + I: IntoIterator, + { + let key = TypeKey::from_type(&target_type); + self.into_sources_map + .insert(key, sources.into_iter().collect()); + self.last_opaque_key = None; + self.last_meta_key = None; + self.last_entry_ref = None; + self + } + + /// Register a rank-N **input converter**. `pattern` contains 0–3 + /// `_` placeholders; the closure's arity selects the rank table. + /// The closure returns `Some((ty, exc, body))` (see [`WrapperFn`] + /// for the triple's full semantics) or `None` (defer to a later + /// resolver phase). The body sees `env: &mut JNIEnv` and `v: &` + /// in scope. + /// + /// * `exc = None` ⇒ non-throwing: `body` evaluates to a bare `ty`; + /// framework emits `-> Result` with an `Ok(...)` + /// wrap, and `?` inside propagates the framework error. + /// * `exc = Some()` ⇒ throwing: `body` evaluates to + /// `Result>`; framework emits it verbatim. The + /// type must match a [`Self::throwable`] declaration + /// by **exact canonical-form equality** with its `rust_type` (see + /// [`Self::find_exception`] — no short-name fallback). The match + /// is validated at lookup time. + /// + /// `ty` is auto-classified at resolve: a wire shape ⇒ terminal + /// converter; a distinct rust type with its own converter ⇒ a + /// value-inspecting stage composed onto that converter's chain + /// (see [`Self::lookup_input`]). + pub fn input_wrapper(self, pattern: syn::Type, builder: B) -> Self + where + B: WrapperBuilder, + { + let key = TypeKey::from_type(&pattern); + let rank = B::rank(); + let mut s = self; + s.input_wrappers[rank].insert(key.clone(), builder.into_wrapper_fn()); + s.note_wrapper_registration(key, rank); + s + } + + /// Output-direction counterpart of [`Self::input_wrapper`]. Same + /// closure shape, same `exc = None` / `Some()` semantics, + /// same terminal-vs-composed classification — see that method's docs. + /// (`Some(parse_quote!())` with a rust-typed `ty`, e.g. + /// `(T, Some(parse_quote!(zenoh_flat::errors::ZError)), v)` for + /// `ZResult`, gives the auto-composed peel that the deleted + /// `output_throw_stage` used to register.) + pub fn output_wrapper(self, pattern: syn::Type, builder: B) -> Self + where + B: WrapperBuilder, + { + let key = TypeKey::from_type(&pattern); + let rank = B::rank(); + let mut s = self; + s.output_wrappers[rank].insert(key.clone(), builder.into_wrapper_fn()); + s.note_wrapper_registration(key, rank); + s + } + + /// Shared post-registration bookkeeping for wrapper inserts. Rank-0 + /// patterns identify a concrete type — auto-stamp `kotlin_name` via + /// [`Self::mangle_wrapper`] (skipping callback entries, whose + /// `kotlin_name` is already stamped via + /// [`Self::mangle_callback`] in [`Self::callback_input`], and + /// non-path patterns like `()` where there is no sensible short + /// name). Rank ≥1 patterns are wildcards — per-outer-type names + /// come from inner-metadata propagation via + /// [`Self::override_kotlin_name`]. + fn note_wrapper_registration(&mut self, key: TypeKey, rank: usize) { + self.last_opaque_key = None; + self.last_entry_ref = None; + if rank == 0 { + let entry = self.types.entry(key.clone()).or_default(); + // Skip callbacks (handled by callback_input) and any entry + // whose kotlin_name has already been stamped (e.g. by an + // earlier data_class / ptr_class call for the + // same type — a wrapper layered on top should not override + // it). Then derive the short name from the canonical + // TypeKey; non-path patterns ($()$, references, etc.) + // yield no Kotlin class name and are left as `None`. + if entry.kotlin_name.is_none() && entry.callback_kotlin_fqn.is_none() { + if let Some(short) = rust_short_name_opt(&key) { + let fqn = self.resolve_class_fqn(&self.mangle_wrapper(&short)); + let entry = self.types.get_mut(&key).expect("just-inserted entry"); + entry.kotlin_name = Some(fqn.clone()); + self.kotlin_type_fqns + .push((key.as_str().to_string(), fqn)); + } + } + self.last_meta_key = Some(key); + } else { + self.last_meta_key = None; + } + } + + /// [`Self::find_exception`] with a uniform fail-fast panic. `who` is + /// the caller name for the message. + fn find_exception_or_panic(&self, who: &str, ty: &syn::Type) -> usize { + self.find_exception(ty).unwrap_or_else(|| { + let needle = ty.to_token_stream().to_string(); + panic!( + "JniExt::{who}: no exception class registered matching `{needle}` — \ + declare it via `.data_class().throwable()` (or the \ + ptr/enum equivalent) first, and bind closures to it with \ + `Some(parse_quote!())`. The framework default is \ + `::prebindgen_ext::jni::JniBindingError` (or omit the closure's middle \ + slot — pass `None` — for non-throwing)." + ) + }) + } + + /// Resolve an exception type against the registered + /// [`Self::exceptions`] by **exact canonical-form equality** with the + /// declaration's `rust_type`. No short-name fallback — the closure / + /// caller must spell the same full path + /// `.throwable()` declared the type with. Returns the + /// index into the `exceptions` vec on match. + fn find_exception(&self, ty: &syn::Type) -> Option { + let needle = ty.to_token_stream().to_string(); + self.exceptions.iter().position(|e| { + e.rust_type.to_token_stream().to_string() == needle + }) + } + + /// The framework's pre-registered [`crate::jni::JniBindingError`] + /// exception. Always exists at `exceptions[0]` after [`Self::new`]. + pub(crate) fn framework_exception(&self) -> &ExceptionConfig { + &self.exceptions[0] + } + + /// Build a `KotlinMeta` stamped with the framework's + /// `JniBindingError` as the converter's *thrown JVM class*. Used by + /// every built-in fallible converter (primitives, structs, + /// `Option<_>`, `Vec<_>`, callbacks). Both fields point at the + /// framework exception: + /// * `throws` (FQN) → the Kotlin `@Throws(...)` aggregator; + /// * `throws_action` (`throw_JniBindingError`) → the throw fn the + /// function wrapper calls when this converter's `?` fails. + /// The Rust error value flowing in is the unified `__JniErr` + /// (domain error type), but `throw_JniBindingError` is generic over + /// `Display`, so it surfaces that value as `JniBindingError` on the + /// JVM regardless of the value's Rust type. (Bare-wire vs `Result` + /// output converters are discriminated by signature inspection + /// [`converter_returns_result`], not by `throws_action`.) + pub(crate) fn framework_meta(&self, kotlin_name: Option) -> KotlinMeta { + let exc = self.framework_exception(); + KotlinMeta { + kotlin_name, + throws: Some(exc.kotlin_fqn.clone()), + throws_action: Some(exception_throw_path(exc)), + value_rust_key: None, + handle: None, + } + } + + // ── Wrapper-table lookups (used by PrebindgenExt impl) ─────────── + + /// Look up a registered input converter for `pat` with `args` + /// substituted into its `_` slots. The closure's middle slot (see + /// [`WrapperFn`]) carries the bound exception — `None` ⇒ framework + /// `__JniErr` with an `Ok`-wrap, `Some()` ⇒ + /// `Result>` emitted verbatim, decided in + /// [`Self::build_input_fn`]. + /// + /// The closure's returned type is classified by [`is_wire_type`]: + /// * **wire** ⇒ terminal: a single converter `wire → outer`. + /// * **rust type** ⇒ composed: that type's input converter runs + /// first (`wire → ty`), then this registration's body is a + /// value-inspecting stage `ty → outer` (built by-value via + /// [`Self::build_output_fn`]) prepended to the inner chain. Defer + /// (`None`) if the inner converter isn't resolved yet. + pub(crate) fn lookup_input( + &self, + pat: &syn::Type, + args: &[syn::Type], + registry: &Registry, + ) -> Option> { + let rank = args.len(); + if rank > 3 { + return None; + } + let key = TypeKey::from_type(pat); + let f = self.input_wrappers[rank].get(&key)?; + let (ty, exc_ty, body) = f(args, registry)?; + // Resolve the exception type lazily: validated here, at lookup + // time, rather than at the `input_wrapper` call site — the + // closure is the single source of truth for both body shape and + // bound exception (see [`WrapperFn`]). + let exc = exc_ty + .as_ref() + .map(|t| &self.exceptions[self.find_exception_or_panic("input_wrapper", t)]); + let outer = substitute_wildcards(pat, args); + let throw_exc = exc.unwrap_or_else(|| self.framework_exception()); + // Terminal vs composed: `ty` is composed iff it's a *distinct* + // rust type with its own input converter. The self-check guards + // the void/identity case (`output_wrapper("()")` returns `ty == + // outer`), and the registered-converter probe distinguishes a + // rust continue-type (compose) from a wire (terminal) without + // forcing `()` either way. A non-wire `ty` that isn't yet + // resolved defers. + let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(&outer); + let inner = if is_self { None } else { registry.input_entry(&ty) }; + match inner { + None if is_self || is_wire_type(&ty) => { + // Terminal: `ty` is the wire; the body produces `outer`. + let (niches, kotlin_name) = if rank == 0 { + let kn = self + .types + .get(&key) + .and_then(|c| c.kotlin_name.clone()) + .or_else(|| crate::jni::jni_kotlin_ext::kotlin_for_wire(&ty)); + (Niches::empty(), kn) + } else { + (default_niches_for_wire(&ty), None) + }; + Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(&outer, &ty, &body, exc), + destination: ty, + niches, + metadata: KotlinMeta { + kotlin_name, + throws: Some(throw_exc.kotlin_fqn.clone()), + throws_action: Some(exception_throw_path(throw_exc)), + value_rust_key: None, + // Terminal: body produces the wire directly, no inner + // converter composed, so no handle to carry. + handle: None, + }, + }) + } + // Non-wire `ty` whose converter isn't resolved yet — defer. + None => None, + Some(inner) => { + // Composed: `ty` is the inner source rust type. Its input + // converter (`wire → ty`) is the wire-facing function; + // this body is a stage `ty → outer` that runs after it. + // The stage takes the inner-produced value BY VALUE and + // yields `outer`, i.e. the same shape an output converter + // has — so it's built with `build_output_fn`. + let stage = Stage { + function: self.build_output_fn(&ty, &outer, &body, exc), + throws_fqn: throw_exc.kotlin_fqn.clone(), + throws_action: exception_throw_path(throw_exc), + }; + let mut pre_stages = vec![stage]; + pre_stages.extend(inner.pre_stages.iter().cloned()); + let (kotlin_name, value_rust_key) = if rank >= 1 { + ( + inner.metadata.kotlin_name.clone(), + Some(TypeKey::from_type(&args[0]).as_str().to_string()), + ) + } else { + (inner.metadata.kotlin_name.clone(), None) + }; + let niches = if rank == 0 { + Niches::empty() + } else { + default_niches_for_wire(&inner.destination) + }; + Some(ConverterImpl { + function: inner.function.clone(), + destination: inner.destination.clone(), + pre_stages, + niches, + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key, + // Identity propagation: a composed wrapper (e.g. + // `Result`) projects to its inner value, + // so a handle inner stays a handle (same strategy). + handle: inner.metadata.handle.clone(), + }, + }) + } + } + } + + /// Look up a registered output converter for `pat` with `args` + /// substituted into its `_` slots. Mirror of [`Self::lookup_input`]. + /// + /// The closure's returned type is classified by [`is_wire_type`]: + /// * **wire** ⇒ terminal: a single converter `outer → wire`, + /// returning `Result` (throwing iff [`ConverterReg::exc`] + /// is set). + /// * **rust type** ⇒ composed: this body is a value-inspecting stage + /// `outer → ty` prepended to `ty`'s own output converter chain + /// (e.g. `ZResult` returns rust `T`, so the peel stage raises + /// its exception and `T`'s converter marshals the wire). Defer + /// (`None`) if `ty`'s converter isn't resolved yet. + pub(crate) fn lookup_output( + &self, + pat: &syn::Type, + args: &[syn::Type], + registry: &Registry, + ) -> Option> { + let rank = args.len(); + if rank > 3 { + return None; + } + let key = TypeKey::from_type(pat); + let f = self.output_wrappers[rank].get(&key)?; + let (ty, exc_ty, body) = f(args, registry)?; + // Resolve at lookup — see [`Self::lookup_input`] for the rationale. + let exc = exc_ty + .as_ref() + .map(|t| &self.exceptions[self.find_exception_or_panic("output_wrapper", t)]); + let outer = substitute_wildcards(pat, args); + let throw_exc = exc.unwrap_or_else(|| self.framework_exception()); + // Terminal vs composed — see [`Self::lookup_input`] for the rule. + let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(&outer); + let inner = if is_self { None } else { registry.output_entry(&ty) }; + match inner { + None if is_self || is_wire_type(&ty) => { + // Terminal: `ty` is the wire; the body produces it from `outer`. + let (kotlin_name, value_rust_key) = if rank >= 1 { + registry + .output_entry(&args[0]) + .map(|e| { + ( + e.metadata.kotlin_name.clone(), + Some(TypeKey::from_type(&args[0]).as_str().to_string()), + ) + }) + .unwrap_or((None, None)) + } else { + let kn = self + .types + .get(&key) + .and_then(|c| c.kotlin_name.clone()) + .or_else(|| crate::jni::jni_kotlin_ext::kotlin_for_wire(&ty)); + (kn, None) + }; + let niches = if rank == 0 { + Niches::empty() + } else { + default_niches_for_wire(&ty) + }; + Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(&outer, &ty, &body, exc), + destination: ty, + niches, + metadata: KotlinMeta { + kotlin_name, + throws: Some(throw_exc.kotlin_fqn.clone()), + throws_action: Some(exception_throw_path(throw_exc)), + value_rust_key, + // Terminal: body produces the wire directly, no inner + // converter composed, so no handle to carry. + handle: None, + }, + }) + } + // Non-wire `ty` whose converter isn't resolved yet — defer. + None => None, + Some(inner) => { + // Composed: `ty` is the continue rust type; chain its converter. + let stage = Stage { + function: self.build_output_fn(&outer, &ty, &body, exc), + throws_fqn: throw_exc.kotlin_fqn.clone(), + throws_action: exception_throw_path(throw_exc), + }; + let mut pre_stages = vec![stage]; + pre_stages.extend(inner.pre_stages.iter().cloned()); + let (kotlin_name, value_rust_key) = if rank >= 1 { + ( + inner.metadata.kotlin_name.clone(), + Some(TypeKey::from_type(&args[0]).as_str().to_string()), + ) + } else { + (inner.metadata.kotlin_name.clone(), None) + }; + let niches = if rank == 0 { + Niches::empty() + } else { + default_niches_for_wire(&inner.destination) + }; + Some(ConverterImpl { + function: inner.function.clone(), + destination: inner.destination.clone(), + pre_stages, + niches, + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key, + // Identity propagation: a composed wrapper (e.g. + // `Result`) projects to its inner value, + // so a handle inner stays a handle (same strategy). + handle: inner.metadata.handle.clone(), + }, + }) + } + } + } +} + +/// Recognise the JNI **wire** shapes a converter body may return as a +/// terminal destination. Reuses the back-end's existing wire knowledge: +/// every `jni::sys::*` / `jni::objects::*` wire is recognised by +/// [`crate::jni::jni_kotlin_ext::kotlin_for_wire`] (returns `Some`), plus +/// raw pointers structurally — so there is no separate wire-type +/// allowlist to keep in sync. +/// +/// `()` is deliberately **not** treated as a wire here: it is ambiguous +/// (the void wire of the `output_wrapper("()")` self-converter *and* the +/// unit continue-type of `ZResult<()>`). The terminal-vs-composed +/// decision in [`JniExt::lookup_input`] / [`JniExt::lookup_output`] +/// resolves that ambiguity via the self-check + registered-converter +/// probe, so `()` flows correctly without being force-classified here. +fn is_wire_type(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Ptr(_)) || crate::jni::jni_kotlin_ext::kotlin_for_wire(ty).is_some() +} + +/// Bare-ident path to the generated `throw_` free function for +/// `exc` (e.g. `throw_ZError`). Spliced into wrapper code as a direct +/// call — `(env, &err)` — so the trait/macro dance the legacy +/// `throw_exception!` indirection performed is replaced with a plain +/// function call. The path is unqualified because the throw fn lands +/// in the same generated file as every wrapper (emitted from +/// [`PrebindgenExt::prerequisites`]); same-module name resolution +/// finds it. +pub(crate) fn exception_throw_path(exc: &ExceptionConfig) -> syn::Path { + let ident = exc.throw_fn_name.clone(); + syn::Path::from(ident) +} + +/// Bare-ident type `__JniErr` — the generated file's alias for the +/// framework `JniBindingError`. Non-throwing converters use this as +/// their `Result<…, _>` error type so their bodies' `<__JniErr as +/// From>::from(...)` calls keep compiling, and so a +/// `?`-propagated framework failure surfaces as the framework +/// exception on the JVM. Throwing converters bypass this in favour of +/// their bound exception's own type (see [`JniExt::lookup_input`] / +/// [`JniExt::lookup_output`]). Returned as `syn::Type` so it shares the +/// `err_type` binding with [`ExceptionConfig::rust_type`]. +pub(crate) fn default_err_type() -> syn::Type { + syn::parse_quote!(__JniErr) +} + +/// The body expression to splice into a converter `fn` returning +/// `Result<_, E>`, per the body↔exception coupling: a non-throwing +/// converter's `body` is a bare value, so wrap it `Ok(body)`; a throwing +/// converter's `body` already evaluates to the `Result`, so emit it +/// verbatim. (Replaces the old "always-`Ok`-wrap then strip" dance.) +fn body_for_exc(body: &syn::Expr, exc: Option<&ExceptionConfig>) -> syn::Expr { + if exc.is_some() { + body.clone() + } else { + syn::parse_quote!(Ok(#body)) + } +} + +/// Construct an [`ExceptionConfig`] from a path-shaped `syn::Type` and +/// the current Kotlin package. Shared by [`JniExt::new`] (framework +/// `JniBindingError` slot) and [`JniExt::throwable`] (user-declared slots). +/// (user-declared slots). Panics if `rust_type` isn't a `Type::Path` +/// or if its short-name collides with an already-registered exception. +fn build_exception_config( + rust_type: syn::Type, + package: &str, + existing: &[ExceptionConfig], +) -> ExceptionConfig { + let segs = match &rust_type { + syn::Type::Path(tp) => &tp.path.segments, + _ => panic!( + "throwable: expected a path-shaped type, got `{}`", + rust_type.to_token_stream() + ), + }; + let short = segs + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_else(|| { + panic!( + "throwable: rust type `{}` has no path segments", + rust_type.to_token_stream() + ) + }); + let kotlin_fqn = if package.is_empty() { + short.clone() + } else { + format!("{}.{}", package, short) + }; + let throw_fn_name = format_ident!("throw_{}", short); + if existing.iter().any(|e| e.throw_fn_name == throw_fn_name) { + panic!( + "throwable: another exception is already \ + registered with Rust short name `{}` — rename the Rust \ + type to disambiguate", + short + ); + } + ExceptionConfig { + rust_type, + rust_short: short, + kotlin_fqn, + throw_fn_name, + } +} + +/// Substitute the wildcard `_` slots of `pat` with `args` (left-to-right +/// depth-first), returning the concrete outer `syn::Type`. Mirrors the +/// substitution the resolver performs to derive a wildcard pattern from +/// a concrete type. +fn substitute_wildcards(pat: &syn::Type, args: &[syn::Type]) -> syn::Type { + let mut idx = 0usize; + fn walk(ty: &mut syn::Type, args: &[syn::Type], idx: &mut usize) { + match ty { + syn::Type::Infer(_) => { + if let Some(replacement) = args.get(*idx) { + *ty = replacement.clone(); + } + *idx += 1; + } + syn::Type::Path(tp) => { + for seg in &mut tp.path.segments { + if let syn::PathArguments::AngleBracketed(ab) = &mut seg.arguments { + for arg in &mut ab.args { + if let syn::GenericArgument::Type(inner) = arg { + walk(inner, args, idx); + } + } + } + } + } + syn::Type::Reference(r) => walk(&mut r.elem, args, idx), + syn::Type::Tuple(t) => { + for e in &mut t.elems { + walk(e, args, idx); + } + } + syn::Type::Array(a) => walk(&mut a.elem, args, idx), + syn::Type::Slice(s) => walk(&mut s.elem, args, idx), + syn::Type::Ptr(p) => walk(&mut p.elem, args, idx), + syn::Type::Paren(p) => walk(&mut p.elem, args, idx), + syn::Type::Group(g) => walk(&mut g.elem, args, idx), + _ => {} + } + } + let mut out = pat.clone(); + walk(&mut out, args, &mut idx); + out +} + +impl Default for JniExt { + fn default() -> Self { + Self::new() + } +} + +// ────────────────────────────────────────────────────────────────────── +// Inherent helpers — wrapper builders (used by both PrebindgenExt impl +// and consuming-crate wrapper exts like ZenohJniExt). +// ────────────────────────────────────────────────────────────────────── + +impl JniExt { + /// Build the standard JNI input-converter `fn`. Body assumes in-scope + /// `env: &mut JNIEnv` and `v: &` (or `v: ` for raw-pointer + /// wires); produces a value of `rust`. Returned function has its name + /// already set per the JNI plugin's naming convention. + /// + /// `exc` ties the body convention to the bound exception: + /// * `None` (non-throwing) → signature `Result` and + /// the body is wrapped `Ok()`; `?` inside propagates the + /// framework error. + /// * `Some(X)` (throwing) → signature `Result` + /// and the body is emitted as-is — `` already evaluates to + /// that `Result`, so no `Ok` wrap (and no cross-type `From`). + pub(crate) fn build_input_fn( + &self, + rust: &syn::Type, + wire: &syn::Type, + body: &syn::Expr, + exc: Option<&ExceptionConfig>, + ) -> syn::ItemFn { + let name = input_name(rust, wire); + let rust_with_lifetime = annotate_borrow_with_lifetime(rust, "env"); + let wire_with_lifetime = annotate_jobject_with_lifetime(wire, "v"); + let err_type = exc.map(|e| e.rust_type.clone()).unwrap_or_else(default_err_type); + let ret_body = body_for_exc(body, exc); + if matches!(wire, syn::Type::Ptr(_)) { + syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'env>(env: &mut jni::JNIEnv<'env>, v: #wire) -> ::core::result::Result<#rust_with_lifetime, #err_type> { + #ret_body + } + ) + } else { + syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'env, 'v>(env: &mut jni::JNIEnv<'env>, v: &#wire_with_lifetime) -> ::core::result::Result<#rust_with_lifetime, #err_type> { + #ret_body + } + ) + } + } + + /// Build the standard JNI output-converter `fn`. Body assumes in-scope + /// `env: &mut JNIEnv` and `v: ` (by value — handles like + /// `Subscriber<()>` aren't `Clone`, so callers move into the converter). + /// + /// `exc` — see [`Self::build_input_fn`]; same body↔exception coupling, + /// output side. + pub(crate) fn build_output_fn( + &self, + rust: &syn::Type, + wire: &syn::Type, + body: &syn::Expr, + exc: Option<&ExceptionConfig>, + ) -> syn::ItemFn { + let name = output_name(rust, wire); + let wire_with_lifetime = annotate_jobject_with_lifetime(wire, "a"); + let err_type = exc.map(|e| e.rust_type.clone()).unwrap_or_else(default_err_type); + let ret_body = body_for_exc(body, exc); + syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'a>(env: &mut jni::JNIEnv<'a>, v: #rust) -> ::core::result::Result<#wire_with_lifetime, #err_type> { + #ret_body + } + ) + } + + + /// Universal "opaque Box-handle as `jlong`" pair — input side. + /// + /// Use for any Rust type whose lifecycle is owned by the Java side: + /// Java holds the raw `Box` pointer as a `Long` and calls Rust + /// passing the pointer. The converter handles both parameter + /// shapes, the decision is taken in `on_function` from the + /// parameter's syntax: + /// + /// **`&T` sites (borrow)**: `OwnedObject::from_raw` stores the + /// pointer without taking ownership of the `Box`; `Deref` exposes `&*ptr` so the generated call site can borrow it + /// as `&T`. The wrapper has no `Drop` — nothing is freed, the + /// heap allocation stays with Java. The Java side must take the + /// pointer out of its `NativeHandle.withPtr` (read lock) so the + /// borrow is sequenced against any concurrent consume / close. + /// + /// **`T` sites (consume, by-value)**: the call-site emitter + /// bypasses `OwnedObject` and inlines `*Box::from_raw(ptr)` — + /// infallible. The Java side must take the pointer out of its + /// `NativeHandle.consume` (write lock + atomic null) before + /// invoking this entry point; that write lock drains concurrent + /// borrows and the atomic-null ensures the same Long cannot be + /// passed twice. No `T: Clone` bound (Box requires nothing of T), + /// so non-Clone handles (`Publisher<'a>`, `Subscriber<()>`) can + /// consume. + /// + /// **Convention** (single rule for both input and output): + /// * Wire: `jni::sys::jlong` — the same width JNI hands across + /// the boundary on every platform (`*mut T` would mismatch + /// on 32-bit, where ptr size is 4 but jlong is 8). + /// * Output: `Box::into_raw(Box::new(v)) as i64` — leak the heap + /// allocation to Java; sole owner is whoever later calls + /// `Box::from_raw` on the same pointer. + /// * Input: `OwnedObject::from_raw(*v as *const T)` (borrow only). + /// * Niche: `0i64` / `*v == 0` — `Box::into_raw` never returns 0, + /// so `Option` automatically synthesises `0` = `None`, + /// matching the legacy "null pointer" ABI for nullable handles. + pub fn opaque_handle_input(&self, ty: &syn::Type) -> ConverterImpl { + let wire: syn::Type = syn::parse_quote!(jni::sys::jlong); + let name = input_name(ty, &wire); + let function: syn::ItemFn = syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::sys::jlong, + ) -> ::core::result::Result, __JniErr> { + Ok(unsafe { OwnedObject::from_raw(*v as *const #ty) }) + } + ); + ConverterImpl { + function, + destination: wire, + pre_stages: vec![], + niches: Niches::one( + syn::parse_quote!(0i64), + syn::parse_quote!(*v == 0), + ), + // Opaque handles' value-context Kotlin name stays `"Long"` + // (the jlong wire mention); the *typed* Kotlin rendering is + // derived from `handle` below. The wrapper's `?` path surfaces + // an `OwnedObject::from_raw` failure as the framework + // `JniBindingError`, so the throws fields point at the + // framework exception. + metadata: self.opaque_leaf_meta(ty), + } + } + + /// Leaf metadata for an opaque handle: value-context name `"Long"` + /// plus the [`HandleInfo`] that folds outward through wrappers (owned, + /// [`CloseStrategy::Direct`]). The single seam where a Rust type is + /// first marked a closeable native handle. + fn opaque_leaf_meta(&self, ty: &syn::Type) -> KotlinMeta { + KotlinMeta { + handle: Some(HandleInfo { + leaf_key: TypeKey::from_type(ty).as_str().to_string(), + owned: true, + strategy: CloseStrategy::Direct, + }), + ..self.framework_meta(Some("Long".to_string())) + } + } + + /// If the user pinned a Kotlin name for `outer_ty` via + /// [`Self::data_class`] (or it's an opaque-handle entry that + /// kept its FQN in `kotlin_name`), use that name; otherwise leave + /// the auto-derived `inherited` value untouched. Lets handler arms + /// inherit by default but yield to an explicit user pin when one + /// exists — same precedence the legacy `KotlinTypeMap.lookup` + /// fallback chain had. + pub(crate) fn override_kotlin_name( + &self, + outer_ty: &syn::Type, + inherited: Option, + ) -> Option { + let key = TypeKey::from_type(outer_ty); + if let Some(cfg) = self.types.get(&key) { + // Opaque-handle entries keep their typed FQN in + // `kotlin_name` for FQN-consumers, but the value-context + // name is `"Long"` (set on the rank-0 handler's metadata). + // Don't let that FQN leak into a wrapper's metadata. + if cfg.opaque.is_none() { + if let Some(name) = &cfg.kotlin_name { + return Some(name.clone()); + } + } + } + inherited + } + + /// Auto-derived Kotlin FQN for an `impl Fn(args)` callback. Same + /// convention `collect_kotlin_callback_fqns` uses, exposed here so + /// the rank-0/rank-1 callback dispatcher can stamp the FQN into + /// the converter's [`KotlinMeta`] at creation time. The relative + /// class name passes through [`Self::mangle_callback`] before + /// being qualified against + /// [`Self::kotlin_callback_package`]. + pub(crate) fn auto_callback_fqn(&self, args: &[syn::Type]) -> String { + let name = crate::jni::jni_kotlin_ext::derive_callback_name(args); + self.resolve_callback_fqn(&self.mangle_callback(&name)) + } + + /// Canonical input-converter name for `(rust, wire)` — exposed + /// for plugin wrapper exts that build `ConverterImpl::function` + /// manually with a non-standard return type (e.g. + /// `impl Into<…>` parameters that can't be expressed via + /// [`Self::input_wrapper`]'s fixed signature shape). + pub fn input_converter_name(&self, rust: &syn::Type, wire: &syn::Type) -> syn::Ident { + input_name(rust, wire) + } + + /// Symmetric to [`Self::input_converter_name`]. + pub fn output_converter_name(&self, rust: &syn::Type, wire: &syn::Type) -> syn::Ident { + output_name(rust, wire) + } + + fn emitted_source_type_names(&self) -> std::collections::HashSet { + let mut names = std::collections::HashSet::new(); + for key in self.types.keys() { + if let Some(short) = rust_short_name_opt(key) { + names.insert(short); + } + } + for exc in self.exceptions.iter().skip(1) { + if let Some(short) = type_last_ident(&exc.rust_type) { + names.insert(short.to_string()); + } + } + names + } + + /// Walk `item` and prefix every bare single-segment type reference + /// matching a [`Self::emitted_source_type_names`] name with + /// [`Self::source_module`]. Applied once per emitted item at write + /// time via [`PrebindgenExt::post_process_item`] so converter bodies, + /// type ascriptions, and casts all stay in sync without each emit + /// site having to remember to qualify. + fn qualify_item(&self, item: &mut syn::Item) { + let source_names = self.emitted_source_type_names(); + if source_names.is_empty() { + return; + } + let mut visitor = QualifyEmittedTypes { + source_module: &self.source_module, + source_names: &source_names, + }; + syn::visit_mut::VisitMut::visit_item_mut(&mut visitor, item); + } + + /// Output side of [`Self::opaque_handle_input`] — see that method's + /// docs for the full convention. + pub fn opaque_handle_output(&self, ty: &syn::Type) -> ConverterImpl { + let wire: syn::Type = syn::parse_quote!(jni::sys::jlong); + let body: syn::Expr = syn::parse_quote!( + std::boxed::Box::into_raw(std::boxed::Box::new(v)) as i64 + ); + ConverterImpl { + function: self.build_output_fn(ty, &wire, &body, None), + destination: wire, + pre_stages: vec![], + niches: Niches::one( + syn::parse_quote!(0i64), + syn::parse_quote!(*v == 0), + ), + // Opaque handles' value-context name `"Long"` + folded + // `HandleInfo` — see [`Self::opaque_handle_input`] / + // [`Self::opaque_leaf_meta`]. Framework throws because the + // wrapper's emitted match-arm still has a `JniBindingError` + // branch reachable via the chain. + metadata: self.opaque_leaf_meta(ty), + } + } + + /// Emit the JObject-typed dispatching input converter for + /// `impl Into + Send + 'static` given an already-assembled + /// source list. The caller — typically a + /// [`PrebindgenExt::dispatch_into_input`] implementation — + /// supplies every arm explicitly (including the identity arm + /// `target → target` if wanted) with each source's borrow/consume + /// mode. + /// + /// Emits an `instanceof` chain over each source `S`: every arm + /// calls `S`'s already-registered input decoder (wire-narrowed + /// from the parameter's `JObject`) and converts to `target` via + /// `TryInto`, so both `From for target` (zero-cost) and + /// `TryFrom for target` (fallible) work uniformly. + /// + /// Per-source mode handling (only relevant for opaque sources — + /// non-opaque sources have no `Box` slot, so mode is moot): + /// * [`IntoSourceMode::Borrow`] → decode via + /// `OwnedObject::from_raw(...).clone()`. Java's `Box` slot stays + /// live; requires `T: Clone`. + /// * [`IntoSourceMode::Consume`] → bypass `OwnedObject` and inline + /// `*Box::from_raw(ptr as *mut T)`. Java's `Box` slot is taken; + /// the caller's typed handle must be invalidated (the Kotlin + /// wrapper does this via `NativeHandle.consume`). No `T: Clone` + /// bound. + /// + /// Returns `None` when `sources` is empty or any source lacks a + /// registered input decoder; the resolver iterates to a fixed + /// point and will retry on a later round once all decoders exist. + pub fn emit_into_dispatcher( + &self, + target: &syn::Type, + sources: &[IntoSource], + registry: &Registry, + ) -> Option> { + if sources.is_empty() { + return None; + } + let target_key = TypeKey::from_type(target).as_str().to_string(); + + let mut arms: Vec = Vec::with_capacity(sources.len()); + for src in sources { + let src_ty = &src.source_type; + let src_key = TypeKey::from_type(src_ty).as_str().to_string(); + let src_entry = registry.input_entry(src_ty)?; + let decoder = src_entry.function.sig.ident.clone(); + let wire = src_entry.destination.clone(); + let (java_class, prelude, decoded_ref) = + jobject_to_wire_adapter(&wire, src_ty, &self.kotlin_type_fqns).unwrap_or_else( + || { + panic!( + "emit_into_dispatcher: source `{}` has wire `{}` which is not a \ + supported Into-source wire shape (target = `{}`)", + src_key, + wire.to_token_stream(), + target_key + ) + }, + ); + // Opaque sources branch on the declared mode. Non-opaque + // sources don't own a `Box` slot, so they just decode + // normally and `mode` has no effect on the emitted code. + let is_opaque = converter_returns_owned_object(&src_entry.function.sig.output); + let decode_expr: syn::Expr = if is_opaque { + match src.mode { + // Method-call `.clone()` triggers method auto-deref: + // OwnedObject has no Clone impl, so dispatch + // derefs to `&T` and calls `T::clone`. Requires + // `T: Clone`. Java's `Box` slot stays live. + IntoSourceMode::Borrow => syn::parse_quote!( + unsafe { #decoder(env, #decoded_ref)? }.clone() + ), + // Bypass the decoder entirely: reconstruct the + // unique `Box` from Java's pointer and move `T` + // out, freeing the heap allocation. Mirrors the + // direct-by-value consume codegen at + // `emit_jni_function_wrapper`. Unique-ownership + // invariant is upheld by `NativeHandle.consume` + // (write lock + atomic null) on the Kotlin side. + // `#decoded_ref` is `&__narrowed` for jlong wires; + // dereference to recover the `jlong` value. + IntoSourceMode::Consume => syn::parse_quote!( + unsafe { *std::boxed::Box::from_raw(*#decoded_ref as *mut #src_ty) } + ), + } + } else { + syn::parse_quote!(unsafe { #decoder(env, #decoded_ref)? }) + }; + arms.push(quote! { + { + let __class = env + .find_class(#java_class) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("find {}: {}", #java_class, e)))?; + let __is = env + .is_instance_of(v, &__class) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("instanceof {}: {}", #java_class, e)))?; + if __is { + #prelude + let __decoded: #src_ty = #decode_expr; + let __converted: #target = ::core::convert::TryInto::try_into(__decoded) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!( + "convert {} -> {}: {}", #src_key, #target_key, e)))?; + return Ok(__converted); + } + } + }); + } + + let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); + let pat: syn::Type = syn::parse_quote!(impl Into<#target> + Send + 'static); + let name = input_name(&pat, &wire); + let target_label = target_key.clone(); + let function: syn::ItemFn = syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, + ) -> ::core::result::Result<#target, __JniErr> { + #(#arms)* + Err(<__JniErr as ::core::convert::From>::from(format!( + "impl Into<{}>: no matching source arm for runtime class", #target_label))) + } + ); + + Some(ConverterImpl { + function, + destination: wire, + pre_stages: vec![], + niches: Niches::empty(), + // `impl Into` parameters surface as Kotlin `Any` — the + // safe wrapper does an `is JNI` chain on the value, and + // the JNI dispatcher's matching arm uses each source's + // typed FQN under the hood. The dispatcher's per-arm `?` + // decode + no-match `Err` fallthrough can fail, so it + // carries the framework throws. + metadata: self.framework_meta(Some("Any".to_string())), + }) + } + +} + +/// One `pub(crate) fn throw_(...)` item for an exception. +/// Emitted from [`PrebindgenExt::prerequisites`] so it lands at the +/// top of the same generated file as every other converter — wrapper +/// code below can call it by bare name (`throw_(env, &err)`); +/// hand-written modules in the binding crate reach it via the include +/// module path (e.g. `crate::generated::throw_`). The body +/// finds the JVM class by slash-form FQN and `throw_new`s with +/// `err.to_string()`, logging on either failure. +/// +/// The error parameter is generic over `Display` rather than the +/// exception's own Rust type. This decouples the *thrown JVM class* +/// from the *Rust error value*: the unified converter error type +/// (`__JniErr`, the binding's primary domain error) flows through +/// every converter, but each converter chooses which `throw_` +/// to call — so a built-in decode failure carries the domain error +/// value yet surfaces on the JVM as `JniBindingError`. (It also avoids +/// any cross-crate `From` bridge between the framework error type and +/// the domain error type, which the crate layering forbids.) +fn build_throw_fn_item( + ext: &JniExt, + registry: &Registry, + exc: &ExceptionConfig, +) -> syn::Item { + let throw_fn = &exc.throw_fn_name; + let class_path_slashes = exc.kotlin_fqn.replace('.', "/"); + // Structured path: when the exception's Rust type has its own + // registered output converter (i.e. `.data_class(...).throwable()`), + // construct the JVM object via that converter and throw it as the + // type's own JVM class — so a structured error carries its fields + // across the boundary, not just `Display::to_string`. Requires the + // type to be `Clone` (the converter consumes `v` by value). + let key = TypeKey::from_type(&exc.rust_type); + let is_data_class = ext + .types + .get(&key) + .map(|cfg| { + cfg.kotlin_name.is_some() + && cfg.opaque.is_none() + && cfg.enum_cfg.is_none() + && cfg.callback_kotlin_fqn.is_none() + && cfg.throwable + }) + .unwrap_or(false); + let output_conv = if is_data_class { + registry + .output_entry(&exc.rust_type) + .map(|e| e.function.sig.ident.clone()) + } else { + None + }; + if let Some(conv) = output_conv { + let rust_ty = &exc.rust_type; + let class_short = &exc.rust_short; + return syn::parse_quote!( + #[allow(non_snake_case)] + pub(crate) fn #throw_fn(env: &mut jni::JNIEnv, err: &#rust_ty) { + let jobj = match unsafe { #conv(env, err.clone()) } { + Ok(o) => o, + Err(e) => { + tracing::error!( + "Failed to encode {} for throw: {}", + #class_short, + e + ); + return; + } + }; + let throwable = jni::objects::JThrowable::from(jobj); + if let Err(e) = env.throw(throwable) { + tracing::error!("Failed to throw exception: {}", e); + } + } + ); + } + // Display path: framework `JniBindingError` (no `#[prebindgen]`, + // no data class — just a class name + a Display message). + syn::parse_quote!( + #[allow(non_snake_case)] + pub(crate) fn #throw_fn( + env: &mut jni::JNIEnv, + err: &(impl ::core::fmt::Display + ?Sized), + ) { + let exception_class = match env.find_class(#class_path_slashes) { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to retrieve exception class: {}", e); + return; + } + }; + if let Err(e) = env.throw_new(exception_class, err.to_string()) { + tracing::error!("Failed to throw exception: {}", e); + } + } + ) +} + +/// One `#[no_mangle] extern "C"` destructor per non-suppressed opaque +/// handle — the Rust counterpart to the `public fun free() = free { +/// freePtr(it) }` / `private external fun freePtr` pair +/// emitted by [`render_typed_handle_source`]. Each body is the uniform +/// `drop(Box::from_raw(ptr as *mut T))`; the inner `T`'s own `Drop` runs +/// (e.g. `Publisher` network-undeclare) with no special casing. +/// +/// Emitted under the same `opaque && !suppress_kotlin_code` condition as +/// the Kotlin shell, so the framework owns *both* halves of the +/// destructor exactly when it owns the typed-handle class. Suppressed +/// handles (hand-written Kotlin) keep their hand-written Rust destructor. +/// +/// The symbol follows the documented scheme +/// `Java___`, +/// where `class_short` is the last segment of the typed-handle FQN +/// (`TypeConfig::kotlin_name`) and the `freePtr` name passes through +/// [`JniExt::mangle_fun`] — exact symmetry with the Kotlin +/// `external fun ` declaration in +/// [`render_typed_handle_source`]. `ext.types` is a `HashMap`, so the +/// items are sorted by symbol to keep generated output deterministic. +/// +/// Emission is gated on the resolved `registry`: a destructor is only +/// emitted for an opaque handle whose type a scanned `#[prebindgen]` fn +/// actually references (as input or output). This mirrors converter +/// emission and keeps feature-gated handles (e.g. `zenoh-ext`-only types +/// whose declare/undeclare fns are `#[cfg]`'d out of the scan) from +/// producing destructors that reference types not in scope. +fn build_handle_destructor_items( + ext: &JniExt, + registry: &Registry, +) -> Vec { + let free_ptr = ext.mangle_fun("freePtr"); + let mut named: Vec<(String, syn::Item)> = Vec::new(); + for (key, cfg) in &ext.types { + let Some(opaque) = &cfg.opaque else { continue }; + if opaque.suppress_kotlin_code { + continue; + } + // Skip handles the (feature-aware) scan never references — their + // type may not be in scope in the generated module. + let ty = key.to_type(); + if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + continue; + } + let class_short = cfg + .kotlin_name + .as_deref() + .and_then(|fqn| fqn.rsplit('.').next()) + .unwrap_or_else(|| { + panic!( + "build_handle_destructor_items: opaque handle `{}` has no \ + kotlin_name to derive a destructor symbol from", + key.as_str() + ) + }); + let class_pkg = cfg + .kotlin_name + .as_deref() + .and_then(|fqn| fqn.rsplit_once('.').map(|(pkg, _)| pkg)) + .unwrap_or("") + .replace('.', "_"); + let symbol = if class_pkg.is_empty() { + format!("Java_{class_short}_{free_ptr}") + } else { + format!("Java_{class_pkg}_{class_short}_{free_ptr}") + }; + let ident = syn::Ident::new(&symbol, Span::call_site()); + let item: syn::Item = syn::parse_quote!( + #[no_mangle] + #[allow(non_snake_case, unused_variables)] + pub(crate) unsafe extern "C" fn #ident( + _env: jni::JNIEnv, + _class: jni::objects::JClass, + ptr: jni::sys::jlong, + ) { + if ptr != 0 { + drop(Box::from_raw(ptr as *mut #ty)); + } + } + ); + named.push((symbol, item)); + } + named.sort_by(|a, b| a.0.cmp(&b.0)); + named.into_iter().map(|(_, item)| item).collect() +} + +// ────────────────────────────────────────────────────────────────────── +// PrebindgenExt impl +// ────────────────────────────────────────────────────────────────────── + +impl PrebindgenExt for JniExt { + /// Cross-language extras every JNI converter carries — currently + /// the Kotlin value-context type name. Filled by the rank-N + /// handlers at the same point they build the wire/body; the + /// resolver propagates it into [`crate::core::registry::TypeEntry::metadata`]; + /// the Kotlin emitter reads it back to drive every wrapper / + /// typed-handle / `JNIWrappers` signature. + type Metadata = KotlinMeta; + + /// Union of every per-class `.method(...)` / `.companion_method(...)` + /// list and every `.function(...)` list across all + /// [`Self::package`] contexts. Each entry is a + /// `#[prebindgen]` fn ident the user explicitly hooked into the + /// binding; functions not in this set are skipped by the registry's + /// signature scan and by the per-item emitter. + fn declared_functions(&self) -> std::collections::HashSet { + let mut out = std::collections::HashSet::new(); + for cfg in self.types.values() { + for m in &cfg.instance_methods { + out.insert(m.rust_ident.clone()); + } + for m in &cfg.companion_methods { + out.insert(m.rust_ident.clone()); + } + } + for pkg in self.packages.values() { + for m in &pkg.functions { + out.insert(m.rust_ident.clone()); + } + } + out + } + + /// Every type registered via `.ptr_class`, + /// `.data_class`, or `.enum_class` — anything in + /// [`Self::types`]. These are the only structs/enums the + /// per-item emitter walks; bodies of undeclared types are + /// skipped. + fn declared_types(&self) -> std::collections::HashSet { + self.types.keys().cloned().collect() + } + + /// Emit the `OwnedObject` borrow wrapper used by + /// [`Self::opaque_handle_input`] into the destination file. + /// The struct is referenced by an unqualified `OwnedObject` from + /// the same generated file, so no `use` paths leak into the host + /// crate's source tree. + fn prerequisites(&self, registry: &Registry) -> Vec { + // `__JniErr` is the **framework** error type alias — always the + // pre-registered `JniBindingError`, never a user-declared + // application exception. Built-in converter bodies compose + // their `?` failures into this type via its `From` + // impl, so a built-in decode failure surfaces as + // `JniBindingError` on the JVM. Throwing converters + // (closures returning `Some(parse_quote!())` in the middle slot of + // `input_wrapper` / `output_wrapper`) instead emit functions + // typed `Result<…, X>` — they bypass `__JniErr` entirely so no + // cross-type bridge between the framework error and a domain + // error is needed (the orphan rule forbids one). + let error_type = &self.framework_exception().rust_type; + let alias: syn::Item = syn::parse_quote!( + #[allow(dead_code)] + pub(crate) type __JniErr = #error_type; + ); + let mut items = vec![alias]; + items.extend(owned_object_prerequisite_items()); + // Throw fns — one `pub(crate) fn throw_(env, &err)` per + // registered throwable class (via `.throwable()`). Emitted as prerequisites + // (above the converters) so the wrappers below can reference + // them by bare name; the binding crate references them as + // `::throw_` from outside the file. + items.extend( + self.exceptions + .iter() + .map(|exc| build_throw_fn_item(self, registry, exc)), + ); + // Handle destructors — one `extern "C" freePtr` per + // non-suppressed opaque handle (the Rust half of the typed-handle + // `free()` pair the Kotlin emitter generates). + items.extend(build_handle_destructor_items(self, registry)); + items + } + + fn post_process_item(&self, item: &mut syn::Item) { + self.qualify_item(item); + } + + // ── Item methods ───────────────────────────────────────────────── + + fn on_function(&self, f: &syn::ItemFn, registry: &Registry) -> TokenStream { + emit_jni_function_wrapper(self, f, registry) + } + + fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry) -> TokenStream { + // Struct converter bodies are emitted by the resolver via + // on_input_type_rank_0 / on_output_type_rank_0 below; no separate + // per-struct item is needed. + TokenStream::new() + } + + fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry) -> TokenStream { + TokenStream::new() + } + + fn on_const(&self, c: &syn::ItemConst, _registry: &Registry) -> TokenStream { + c.to_token_stream() + } + + // ── Input converters ───────────────────────────────────────────── + + fn on_input_type_rank_0( + &self, + ty: &syn::Type, + registry: &Registry, + ) -> Option> { + // Structured-config overrides first (opaque handles, then user- + // registered rank-0 wrappers, then built-ins). + let key = TypeKey::from_type(ty); + if let Some(cfg) = self.types.get(&key) { + if cfg.opaque.is_some() { + return Some(self.opaque_handle_input(ty)); + } + } + // `enum_class`-declared enums: jint wire, `TryFrom` decode. + // Registered before the user-wrapper lookup so a stray + // `input_wrapper` registration on the same key would have to be + // intentional. The rank-0 enum arm produces a terminal converter + // (jint → Rust enum) with the configured Kotlin FQN in metadata. + if let Some(cfg) = self.types.get(&key) { + if cfg.enum_cfg.is_some() { + if let Some(name) = bare_path_ident(ty) { + if let Some((e, _)) = registry.enums.get(&name) { + let (wire, body) = enum_input_body(self, e); + let niches = default_niches_for_wire(&wire); + let kotlin_name = cfg.kotlin_name.clone(); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + } + } + } + if let Some(conv) = self.lookup_input(ty, &[], registry) { + return Some(conv); + } + // `str` is unsized, so converters can't return it directly. + // Still register a rank-0 entry to satisfy resolution for + // borrowed `&str` parameters: decode `JString` to owned `String` + // and let call sites borrow as needed. + if TypeKey::from_type(ty).as_str() == "str" { + let wire: syn::Type = syn::parse_quote!(jni::objects::JString); + let body: syn::Expr = syn::parse_quote!({ + let s = env + .get_string(v) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("decode_string: {}", e)))?; + s.into() + }); + let rust_ty: syn::Type = syn::parse_quote!(String); + let kotlin_name = self.override_kotlin_name(ty, Some("String".to_string())); + let niches = default_niches_for_wire(&wire); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(&rust_ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + if let Some((wire, body)) = primitive_input(ty) { + let niches = default_niches_for_wire(&wire); + let kotlin_name = crate::jni::jni_kotlin_ext::kotlin_for_wire(&wire); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + if let Some(name) = bare_path_ident(ty) { + if let Some((s, _)) = registry.structs.get(&name) { + let (wire, body) = struct_input_body(self, s, registry)?; + let niches = default_niches_for_wire(&wire); + // Auto-generated struct: the value-context Kotlin name is + // whatever the user pinned via `data_class`. If + // they didn't, leave `kotlin_name = None` — emitter + // surfaces this as a build-time hard error. + let kotlin_name = self.types.get(&key).and_then(|c| c.kotlin_name.clone()); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + // Bare-ident enum: leave to the consuming crate to override + // (today's CongestionControl etc. fall here — caller's wrapper + // ext returns Some in its own on_input_type_rank_0). + } + None + } + + fn on_input_type_rank_1( + &self, + pat: &syn::Type, + t1: &syn::Type, + registry: &Registry, + ) -> Option> { + if let Some(conv) = self.lookup_input(pat, &[t1.clone()], registry) { + return Some(conv); + } + // `& _` borrow: a free-fn converter can't return `&T` (no borrow + // source), so we *share* T's resolved converter — `&T`'s entry + // points at the same `ItemFn`. The fn returns owned `T`; the + // call site in `emit_jni_function_wrapper` adds `&decoded` when + // the original param was `&T`. write.rs's dedup-by-name keeps + // the function emitted exactly once. + // + // This handler exists to make the wildcard-substitution machinery + // fire: it returns subs=[t1] (via the resolver), so propagation + // marks T as required transitively from `&T`. + if pat_match(pat, "& _") || pat_match(pat, "& mut _") { + let inner = registry.input_entry(t1)?; + let outer_ty: syn::Type = if pat_match(pat, "& mut _") { + syn::parse_quote!(&mut #t1) + } else { + syn::parse_quote!(&#t1) + }; + // `&T` / `&mut T` are Kotlin-side no-ops — inherit the inner + // type's name, unless the user pinned an explicit override + // on the outer form itself (rare but legal). + let kotlin_name = self.override_kotlin_name( + &outer_ty, + inner.metadata.kotlin_name.clone(), + ); + // The outer form shares T's converter function verbatim, so it + // inherits T's throws behaviour (whatever exception T's + // converter is bound to). Copy the inner's throws metadata. + // A borrowed handle (mut or not) is still opaque (param + // classification needs to see it), but the holder doesn't own + // it — mark `owned: false` so `close()` emission skips it. + let handle = inner.metadata.handle.clone().map(|h| HandleInfo { + owned: false, + ..h + }); + return Some(ConverterImpl { + destination: inner.destination.clone(), + function: inner.function.clone(), + pre_stages: vec![], + niches: inner.niches.clone(), + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key: None, + handle, + }, + }); + } + // `Option<&T>` / `Option<&mut T>` for opaque T: the general + // `Option<_>` handler below treats the inner type opaquely and + // would generate `Option<&T>` with no lifetime + a buggy + // `*const &T` cast. Route opaque borrows through their own path + // that returns `Option>`; the call site + // `.as_deref()` / `.as_deref_mut()` coerces back to `Option<&T>` + // / `Option<&mut T>` per OwnedObject's Deref / DerefMut impls. + // + // Falls through for non-opaque inners — the general handler + // produces sensible code (returns `Option` and the call site + // adds `.as_ref()` if needed; out of scope here). + if pat_match(pat, "Option < & _ >") || pat_match(pat, "Option < & mut _ >") { + let inner = registry.input_entry(t1)?; + if converter_returns_owned_object(&inner.function.sig.output) { + let is_mut = pat_match(pat, "Option < & mut _ >"); + let inner_wire = inner.destination.clone(); + let inner_conv = inner.function.sig.ident.clone(); + let outer_ty: syn::Type = if is_mut { + syn::parse_quote!(Option<&mut #t1>) + } else { + syn::parse_quote!(Option<&#t1>) + }; + let name = input_name(&outer_ty, &inner_wire); + let function: syn::ItemFn = syn::parse_quote!( + #[allow(non_snake_case, unused_mut, unused_variables, unused_braces, dead_code)] + pub(crate) unsafe fn #name<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &#inner_wire, + ) -> ::core::result::Result>, __JniErr> { + Ok({ + if *v == 0 { None } else { Some(#inner_conv(env, v)?) } + }) + } + ); + let kotlin_name = self.override_kotlin_name( + &outer_ty, + inner.metadata.kotlin_name.clone(), + ); + let handle = inner.metadata.handle.clone().map(|h| HandleInfo { + owned: false, + strategy: CloseStrategy::Nullable(Box::new(h.strategy)), + ..h + }); + return Some(ConverterImpl { + pre_stages: vec![], + function, + destination: inner_wire, + niches: Niches::empty(), + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key: None, + handle, + }, + }); + } + // Non-opaque: let the general `Option<_>` handler below take it. + } + // `Vec` (input side): wire is `JObject` carrying a Java + // `List`; we iterate, decode each element via the + // inner converter, collect into a `Vec`. `Vec` is already + // handled at rank-0 (special-cased in `primitive_input` to a + // `JByteArray` wire) so rank-1 never gets it. Non-opaque inners + // whose wire is a non-jobject primitive (e.g. `Vec`) aren't + // covered by this handler — extend if needed. + if pat_match(pat, "Vec < _ >") { + let inner = registry.input_entry(t1)?; + let inner_wire = inner.destination.clone(); + if !is_jobject_shaped_wire(&inner_wire) { + return None; + } + let inner_conv = inner.function.sig.ident.clone(); + let outer_ty: syn::Type = syn::parse_quote!(Vec<#t1>); + let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); + let body: syn::Expr = syn::parse_quote!({ + let __list = jni::objects::JList::from_env(env, v) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-from-env: {}", e)))?; + let mut __it = __list.iter(env) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-iter: {}", e)))?; + let mut __out: Vec<#t1> = Vec::new(); + while let Some(__obj) = __it.next(env) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-next: {}", e)))? + { + let __elem_wire: #inner_wire = __obj.into(); + let __elem: #t1 = #inner_conv(env, &__elem_wire)?; + __out.push(__elem); + } + __out + }); + let inner_kotlin = inner.metadata.kotlin_name.clone()?; + let kotlin_name = self.override_kotlin_name( + &outer_ty, + // `List` is auto-imported in Kotlin (default imports), so we + // skip the FQN to avoid `register_fqn` treating the generic + // as part of the import path. + Some(format!("List<{}>", inner_kotlin)), + ); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches: Niches::empty(), + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key: None, + handle: None, + }, + }); + } + if pat_match(pat, "Option < _ >") { + let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); + let (wire, body, niches) = option_input(t1, registry)?; + // Inherit the inner's name; user pins on `Option` win. + // The nullability marker (`?`) is added by the use site. + let inherited = registry + .input_entry(t1) + .and_then(|e| e.metadata.kotlin_name.clone()); + let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); + // Fold a Nullable layer over the inner handle (if any), so an + // `Option` field/param carries the full close strategy. + let handle = registry + .input_entry(t1) + .and_then(|e| e.metadata.handle.clone()) + .map(|h| HandleInfo { + strategy: CloseStrategy::Nullable(Box::new(h.strategy)), + ..h + }); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches, + metadata: KotlinMeta { + handle, + ..self.framework_meta(kotlin_name) + }, + }); + } + None + } + + fn dispatch_into_input( + &self, + target: &syn::Type, + sources: &[IntoSource], + registry: &Registry, + ) -> Option> { + self.emit_into_dispatcher(target, sources, registry) + } + + fn dispatch_fn_input( + &self, + args: &[syn::Type], + registry: &Registry, + ) -> Option> { + let outer_ty = build_fn_type(args); + let (wire, body) = callback_input(self, args, registry)?; + let niches = default_niches_for_wire(&wire); + // Kotlin sees `impl Fn(...)` as the matching mangled + // fun-interface (zenoh-jni: `JNIOn`). Use the + // registration-stamped FQN when set; fall back to the + // auto-derived name. + let outer_key = TypeKey::from_type(&outer_ty); + let kotlin_name = self + .types + .get(&outer_key) + .and_then(|c| c.callback_kotlin_fqn.clone()) + .or_else(|| Some(self.auto_callback_fqn(args))); + Some(ConverterImpl { + pre_stages: vec![], + function: self.build_input_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }) + } + + fn on_input_type_rank_2( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + registry: &Registry, + ) -> Option> { + let _ = registry; + self.lookup_input(pat, &[t1.clone(), t2.clone()], registry) + } + + fn on_input_type_rank_3( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + t3: &syn::Type, + registry: &Registry, + ) -> Option> { + let _ = registry; + self.lookup_input(pat, &[t1.clone(), t2.clone(), t3.clone()], registry) + } + + // ── Output converters ──────────────────────────────────────────── + + fn on_output_type_rank_0( + &self, + ty: &syn::Type, + registry: &Registry, + ) -> Option> { + // Structured-config overrides first (opaque handles, then the + // unified user-registered wrapper table, then built-ins). + let key = TypeKey::from_type(ty); + if let Some(cfg) = self.types.get(&key) { + if cfg.opaque.is_some() { + return Some(self.opaque_handle_output(ty)); + } + } + // `enum_class`-declared enums: jint wire, `as jni::sys::jint` + // encode. Symmetric to the input arm above; relies on + // `#[repr(i32)]` (or any repr that supports the cast) on the + // declared enum so the discriminant value round-trips identically. + if let Some(cfg) = self.types.get(&key) { + if cfg.enum_cfg.is_some() { + if let Some(name) = bare_path_ident(ty) { + if let Some((e, _)) = registry.enums.get(&name) { + let (wire, body) = enum_output_body(self, e); + let niches = default_niches_for_wire(&wire); + let kotlin_name = cfg.kotlin_name.clone(); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + } + } + } + if let Some(conv) = self.lookup_output(ty, &[], registry) { + return Some(conv); + } + // `()` — identity converter so `fn foo()` and `fn foo() -> ()` + // funnel through the same uniform output path as everything else. + // Wire is `()`. Body just returns `v`. No Kotlin name — Unit + // returns are dropped from emitted signatures, so metadata stays + // empty. + if pat_match(ty, "()") { + let wire: syn::Type = syn::parse_quote!(()); + let body: syn::Expr = syn::parse_quote!(v); + return Some(ConverterImpl { + function: self.build_output_fn(ty, &wire, &body, None), + destination: wire, + pre_stages: vec![], + niches: Niches::empty(), + metadata: KotlinMeta::default(), + }); + } + if let Some((wire, body)) = primitive_output(ty) { + let niches = default_niches_for_wire(&wire); + let kotlin_name = crate::jni::jni_kotlin_ext::kotlin_for_wire(&wire); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + if let Some(name) = bare_path_ident(ty) { + if let Some((s, _)) = registry.structs.get(&name) { + let (wire, body) = struct_output_body(self, s, registry)?; + let niches = default_niches_for_wire(&wire); + let kotlin_name = self.types.get(&key).and_then(|c| c.kotlin_name.clone()); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(kotlin_name), + }); + } + } + None + } + + fn on_output_type_rank_1( + &self, + pat: &syn::Type, + t1: &syn::Type, + registry: &Registry, + ) -> Option> { + if let Some(conv) = self.lookup_output(pat, &[t1.clone()], registry) { + return Some(conv); + } + // `Result<_, _>` is handled as a built-in rank-2 wrapper registered + // in `JniExt::new`. Bindings just declare the Err type via + // `.throwable()`. Per-error overrides are possible by registering a + // more specific rank-1 `output_wrapper(Result<_, ConcreteErr>, …)` + // — rank-1 fires before rank-2 in resolve and short-circuits here. + if pat_match(pat, "Option < _ >") { + let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); + let (wire, body, niches) = option_output(t1, registry)?; + let inherited = registry + .output_entry(t1) + .and_then(|e| e.metadata.kotlin_name.clone()); + let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); + // Fold a Nullable layer over the inner handle (if any), so an + // `Option` output carries the full close strategy. + let handle = registry + .output_entry(t1) + .and_then(|e| e.metadata.handle.clone()) + .map(|h| HandleInfo { + strategy: CloseStrategy::Nullable(Box::new(h.strategy)), + ..h + }); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches, + metadata: KotlinMeta { + handle, + ..self.framework_meta(kotlin_name) + }, + }); + } + // `Vec` (output side): encode as a `java.util.ArrayList`. + // Symmetric to the input handler. `Vec` is special-cased at + // rank-0 (primitive_output → JByteArray) so rank-1 never sees it. + if pat_match(pat, "Vec < _ >") { + let inner = registry.output_entry(t1)?; + let inner_wire = inner.destination.clone(); + if !is_jobject_shaped_wire(&inner_wire) { + return None; + } + let inner_conv = inner.function.sig.ident.clone(); + let outer_ty: syn::Type = syn::parse_quote!(Vec<#t1>); + let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); + let body: syn::Expr = syn::parse_quote!({ + let __list_obj = env + .new_object("java/util/ArrayList", "()V", &[]) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: new ArrayList: {}", e)))?; + let __list = jni::objects::JList::from_env(env, &__list_obj) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-from-env: {}", e)))?; + for __elem in v.into_iter() { + let __elem_wire = #inner_conv(env, __elem)?; + let __elem_obj: jni::objects::JObject = __elem_wire.into(); + __list.add(env, &__elem_obj) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-add: {}", e)))?; + } + __list_obj + }); + let inner_kotlin = inner.metadata.kotlin_name.clone()?; + let kotlin_name = self.override_kotlin_name( + &outer_ty, + // `List` is auto-imported in Kotlin (default imports), so we + // skip the FQN to avoid `register_fqn` treating the generic + // as part of the import path. + Some(format!("List<{}>", inner_kotlin)), + ); + return Some(ConverterImpl { + pre_stages: vec![], + function: self.build_output_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches: Niches::empty(), + metadata: KotlinMeta { + kotlin_name, + throws: inner.metadata.throws.clone(), + throws_action: inner.metadata.throws_action.clone(), + value_rust_key: None, + handle: None, + }, + }); + } + None + } + + fn on_output_type_rank_2( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + registry: &Registry, + ) -> Option> { + self.lookup_output(pat, &[t1.clone(), t2.clone()], registry) + } + + fn on_output_type_rank_3( + &self, + pat: &syn::Type, + t1: &syn::Type, + t2: &syn::Type, + t3: &syn::Type, + registry: &Registry, + ) -> Option> { + self.lookup_output(pat, &[t1.clone(), t2.clone(), t3.clone()], registry) + } + + fn into_sources(&self, target: &syn::Type) -> Vec { + let key = TypeKey::from_type(target); + self.into_sources_map + .get(&key) + .cloned() + .unwrap_or_default() + } +} + +// ────────────────────────────────────────────────────────────────────── +// Function-wrapper emission (JNI extern "C") +// ────────────────────────────────────────────────────────────────────── + +fn emit_jni_function_wrapper(ext: &JniExt, f: &syn::ItemFn, registry: &Registry) -> TokenStream { + let original_ident = &f.sig.ident; + let wrapper_ident = mangle_jni_name(ext, original_ident); + let source_module = &ext.source_module; + + // Throw fn for framework binding failures — the fallback when a + // converter carries no explicit bound exception (non-throwing). + let framework_throw = exception_throw_path(ext.framework_exception()); + + let mut wire_params: Vec = Vec::new(); + // Each entry is a per-input decode statement. Fallible decodes are + // `match`-arms that dispatch to the input converter's own throw fn + // on `Err` and `return ;` — so a malformed `Encoding` + // JObject raises `JniBindingError`, while a throwing input wrapper + // raises whatever exception it bound via `Some(parse_quote!(...))` in the closure. + let mut prelude: Vec = Vec::new(); + let mut call_args: Vec = Vec::new(); + + // Output is resolved first so the per-input `match`-arms can splice + // the function's sentinel into their early-`return` path. + let return_ty: syn::Type = match &f.sig.output { + syn::ReturnType::Default => syn::parse_quote!(()), + syn::ReturnType::Type(_, ty) => (**ty).clone(), + }; + let output_entry = registry.output_entry(&return_ty).unwrap_or_else(|| { + panic!( + "JniExt::on_function: return type `{}` of `{}` has no registered output \ + converter — register one via `JniExt::output_wrapper(pat, |…| Some((ty, exc, body)))` \ + (exc = `None` for non-throwing, `Some(parse_quote!())` \ + to bind a domain exception)", + TypeKey::from_type(&return_ty), + original_ident, + ) + }); + let wire_return_ty = output_entry.destination.clone(); + let conv_out = output_entry.function.sig.ident.clone(); + let wire_return_lt = annotate_jobject_with_lifetime(&wire_return_ty, "a"); + let wire_return = wire_return_lt.to_token_stream(); + let on_err: TokenStream = sentinel_for_wire(&wire_return_ty); + + // Input parameters: look up converter for the param type AS WRITTEN. + // No strip — a `&T` param looks up `&T`'s entry (which the `& _` + // rank-1 handler resolved by sharing `T`'s function). Call site adds + // `&decoded` only for `&T`-shaped originals; that's a Rust call- + // convention concern, not a converter concern. + for input in &f.sig.inputs { + let syn::FnArg::Typed(pt) = input else { continue }; + let syn::Pat::Ident(pat_id) = &*pt.pat else { continue }; + let arg_ident = &pat_id.ident; + let arg_ty = &*pt.ty; + + let entry = registry.input_entry(arg_ty).unwrap_or_else(|| { + panic!( + "JniExt::on_function: input type `{}` for `{}` is unresolved", + TypeKey::from_type(arg_ty), + original_ident, + ) + }); + let wire = &entry.destination; + let conv = entry.function.sig.ident.clone(); + // Each input converter carries the throw fn for its failures — + // framework `throw_JniBindingError` by default, or a custom one + // bound via `Some(parse_quote!())` in the input + // wrapper's closure. + let input_throw = entry + .metadata + .throws_action + .clone() + .unwrap_or_else(|| framework_throw.clone()); + let wire_ident = if matches!(wire, syn::Type::Ptr(_)) { + format_ident!("{}_ptr", arg_ident) + } else { + arg_ident.clone() + }; + + // By-value `T` opaque-handle parameter: emit the consume + // converter inline, bypassing `OwnedObject`. The Java side + // takes the pointer out of its `NativeHandle.consume` under + // the write lock and passes it here; `Box::from_raw` + // reconstructs the unique owner and `*box` moves `T` out, + // dropping the heap allocation. The unique-ownership + // invariant is upheld by `NativeHandle.consume` (write-lock + // + atomic pointer take), which drains all in-flight borrows + // and ensures no live borrow can outlive this point. No + // `T: Clone` bound, so non-Clone handles (e.g. `Publisher<'a>`) + // work too. This decode is infallible — no `match` needed. + let is_consume = !matches!(arg_ty, syn::Type::Reference(_)) + && converter_returns_owned_object(&entry.function.sig.output); + if is_consume { + wire_params.push(quote!(#wire_ident: jni::sys::jlong)); + prelude.push(quote!( + let #arg_ident: #arg_ty = unsafe { + *std::boxed::Box::from_raw(#wire_ident as *mut #arg_ty) + }; + )); + call_args.push(quote!(#arg_ident)); + continue; + } + + let wire_with_lifetime = annotate_jobject_with_lifetime(wire, "a"); + wire_params.push(quote!(#wire_ident: #wire_with_lifetime)); + // Input wrapper takes wires by ref except for raw pointers. The + // converter returns `Result`; on `Err` we throw via + // this input's own throw fn and bail with the function sentinel. + let decode_call = if matches!(wire, syn::Type::Ptr(_)) { + quote!(#conv(&mut env, #wire_ident)) + } else { + quote!(#conv(&mut env, &#wire_ident)) + }; + // Binding for the final `arg_ident` needs `mut` when the source + // fn takes `&mut T` — the call site below emits `&mut arg_ident`, + // which requires a mutable binding. Also for `Option<&mut T>` + // where the call site needs `.as_deref_mut()`. Intermediate stage + // bindings (`__{ident}_sN`) don't need it. + let arg_mut: TokenStream = if matches!(arg_ty, syn::Type::Reference(r) if r.mutability.is_some()) + || matches!(option_inner_ref_mutability(arg_ty), Some(true)) + { + quote!(mut) + } else { + quote!() + }; + // Stage 0: wire-facing function. Pre_stages then run in REVERSE + // (rust-side last). Even with no pre_stages this collapses to a + // single `let #arg_ident = match decode_call { ... }`, byte- + // identical to the pre-chain emission. + if entry.pre_stages.is_empty() { + prelude.push(quote!( + let #arg_mut #arg_ident = match #decode_call { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + #input_throw(&mut env, &__e); + return #on_err; + } + }; + )); + } else { + // Multi-stage: introduce a temporary for the function's + // result, then thread each pre_stage in reverse onto it. + let stage0_ident = format_ident!("__{}_s0", arg_ident); + prelude.push(quote!( + let #stage0_ident = match #decode_call { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + #input_throw(&mut env, &__e); + return #on_err; + } + }; + )); + let mut prev = stage0_ident; + // pre_stages[0] is closest to rust → iterated last; walk + // back from the function-adjacent end. + let n = entry.pre_stages.len(); + for (idx, stage) in entry.pre_stages.iter().enumerate().rev() { + let stage_fn = &stage.function.sig.ident; + let stage_throw = &stage.throws_action; + let is_last = idx == 0; + let out_ident = if is_last { + arg_ident.clone() + } else { + format_ident!("__{}_s{}", arg_ident, n - idx) + }; + // Final binding gets `mut` if the source fn takes `&mut`. + let bind_mut: TokenStream = if is_last { arg_mut.clone() } else { quote!() }; + prelude.push(quote!( + let #bind_mut #out_ident = match #stage_fn(&mut env, #prev) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + #stage_throw(&mut env, &__e); + return #on_err; + } + }; + )); + prev = out_ident; + } + } + match arg_ty { + syn::Type::Reference(r) if r.mutability.is_some() => { + call_args.push(quote!(&mut #arg_ident)); + } + syn::Type::Reference(_) => { + call_args.push(quote!(&#arg_ident)); + } + // `Option<&T>` / `Option<&mut T>` for opaque inner: the input + // converter produced `Option>` (see rank-1 + // handler above). `.as_deref()` / `.as_deref_mut()` coerces + // back to `Option<&T>` / `Option<&mut T>` via OwnedObject's + // Deref / DerefMut impls. + _ if matches!(option_inner_ref_mutability(arg_ty), Some(false)) => { + call_args.push(quote!(#arg_ident.as_deref())); + } + _ if matches!(option_inner_ref_mutability(arg_ty), Some(true)) => { + call_args.push(quote!(#arg_ident.as_deref_mut())); + } + _ => { + call_args.push(quote!(#arg_ident)); + } + } + } + + let call_expr = quote!(#source_module::#original_ident(#(#call_args),*)); + + // Output phase. Every output converter now returns + // `Result>` — the bare-wire shape is gone. + // Unwrap and dispatch to the converter's `throws_action` + // (framework `throw_JniBindingError` for plain wrappers, a domain + // throw fn for throws-marked wrappers). + // + // Pre_stages (rust-side throw stages) run in forward order BEFORE + // the wire-facing function: rust → pre_stages[0] → … → + // pre_stages[N-1] → function → wire. Each stage's match-throw + // dispatches to its own configured exception class. + let out_throw = output_entry + .metadata + .throws_action + .clone() + .unwrap_or_else(|| framework_throw.clone()); + let mut output_phase: TokenStream = quote! { let __out = #call_expr; }; + let mut prev_out: TokenStream = quote!(__out); + for (i, stage) in output_entry.pre_stages.iter().enumerate() { + let stage_fn = &stage.function.sig.ident; + let stage_throw = &stage.throws_action; + let next_ident = format_ident!("__out_s{}", i); + output_phase.extend(quote! { + let #next_ident = match #stage_fn(&mut env, #prev_out) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + #stage_throw(&mut env, &__e); + return #on_err; + } + }; + }); + prev_out = quote!(#next_ident); + } + output_phase.extend(quote! { + match #conv_out(&mut env, #prev_out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + #out_throw(&mut env, &__e); + #on_err + } + } + }); + + quote! { + #[no_mangle] + #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] + pub unsafe extern "C" fn #wrapper_ident<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + #(#wire_params),* + ) -> #wire_return { + #(#prelude)* + #output_phase + } + } +} + +/// Last-segment ident of a `TypeKey` — e.g. `"Publisher<'static>"` → +/// `"Publisher"`, `"AdvancedSubscriber<()>"` → `"AdvancedSubscriber"`. Used by +/// the structured builders ([`JniExt::ptr_class`], +/// [`JniExt::data_class`]) to derive a default Kotlin class name from +/// the Rust type-key. Panics for non-path types (e.g. closures, references) — +/// the per-kind `kotlin_*_name_mangle` closures see only path-shaped +/// shorts. For verbatim Kotlin expressions on non-path types, chain +/// [`JniExt::with_kotlin_type`] after the structured builder. +fn rust_short_name(key: &TypeKey) -> String { + rust_short_name_opt(key).unwrap_or_else(|| { + panic!( + "rust_short_name: cannot derive Kotlin name from type-key `{}` — \ + only path-shaped types are supported here; use \ + `with_kotlin_type(\"\")` to set the name explicitly", + key.as_str() + ) + }) +} + +/// Fallible variant of [`rust_short_name`] — returns `None` for +/// non-path types instead of panicking. Used by +/// [`JniExt::note_wrapper_registration`] which is called for rank-0 +/// wrapper patterns including non-path shapes like `()` where there +/// is no Kotlin short name to derive. +fn rust_short_name_opt(key: &TypeKey) -> Option { + let ty = key.to_type(); + if let syn::Type::Path(tp) = &ty { + if let Some(last) = tp.path.segments.last() { + return Some(last.ident.to_string()); + } + } + None +} + +fn type_last_ident(ty: &syn::Type) -> Option { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + return Some(last.ident.clone()); + } + } + None +} + +/// `VisitMut` that prefixes every bare single-segment `Type::Path` whose +/// ident lives in `source_names` with `source_module`. Walks the full +/// AST — function signatures, generic args, type ascriptions, casts, +/// turbofish — so any emitted item passes through one universal pass +/// instead of each emit site having to remember to qualify. +struct QualifyEmittedTypes<'a> { + source_module: &'a syn::Path, + source_names: &'a std::collections::HashSet, +} + +impl<'a> syn::visit_mut::VisitMut for QualifyEmittedTypes<'a> { + fn visit_type_path_mut(&mut self, tp: &mut syn::TypePath) { + if tp.qself.is_none() + && tp.path.leading_colon.is_none() + && tp.path.segments.len() == 1 + { + let ident = tp.path.segments[0].ident.to_string(); + if self.source_names.contains(&ident) { + let mut qualified = self.source_module.clone(); + qualified.segments.push(tp.path.segments[0].clone()); + tp.path = qualified; + } + } + syn::visit_mut::visit_type_path_mut(self, tp); + } +} + +fn mangle_jni_name(ext: &JniExt, ident: &syn::Ident) -> syn::Ident { + let camel = snake_to_camel(&ident.to_string()); + let mangled = ext.mangle_fun(&camel); + let mut name = ext.jni_class_path.clone(); + name.push('_'); + name.push_str(&mangled); + syn::Ident::new(&name, Span::call_site()) +} + +/// Sentinel value to return through the wrapper signature when the inner +/// closure errors. Must compile against any wire type we emit. +fn sentinel_for_wire(wire: &syn::Type) -> TokenStream { + // Unit wire (void-returning wrappers): the value *is* the sentinel. + if let syn::Type::Tuple(t) = wire { + if t.elems.is_empty() { + return quote!(()); + } + } + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + let name = last.ident.to_string(); + return match name.as_str() { + "jboolean" | "jbyte" | "jchar" | "jshort" | "jint" | "jlong" => quote!(0 as #wire), + "jfloat" | "jdouble" => quote!(0.0 as #wire), + "JObject" | "JString" | "JByteArray" | "JClass" => { + quote!(jni::objects::JObject::null().into()) + } + _ => quote!(unsafe { std::mem::zeroed::<#wire>() }), + }; + } + } + if matches!(wire, syn::Type::Ptr(_)) { + return quote!(std::ptr::null()); + } + quote!(unsafe { std::mem::zeroed::<#wire>() }) +} + +/// Detect whether an input converter's return type is `_::ZResult>` +/// (or whatever the `zresult` path happens to be — we only inspect the last +/// segment). Drives the borrow/consume codegen at the call site and the +/// `NativeHandle`-typed parameter detection in the Kotlin wrapper emitter. +pub(crate) fn converter_returns_owned_object(output: &syn::ReturnType) -> bool { + let syn::ReturnType::Type(_, ty) = output else { return false; }; + let syn::Type::Path(tp) = &**ty else { return false; }; + let Some(last) = tp.path.segments.last() else { return false; }; + let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return false; }; + let Some(syn::GenericArgument::Type(inner)) = args.args.first() else { return false; }; + let syn::Type::Path(itp) = inner else { return false; }; + let Some(last_inner) = itp.path.segments.last() else { return false; }; + last_inner.ident == "OwnedObject" +} + +// ────────────────────────────────────────────────────────────────────── +// Primitive bodies +// ────────────────────────────────────────────────────────────────────── + +fn primitive_input(ty: &syn::Type) -> Option<(syn::Type, syn::Expr)> { + let key = TypeKey::from_type(ty).as_str().to_string(); + // Bodies receive `v: &`; primitives are Copy so `*v` works. + Some(match key.as_str() { + "bool" => ( + syn::parse_quote!(jni::sys::jboolean), + syn::parse_quote!(*v != 0), + ), + "i32" => ( + syn::parse_quote!(jni::sys::jint), + syn::parse_quote!(*v), + ), + "i64" => ( + syn::parse_quote!(jni::sys::jlong), + syn::parse_quote!(*v), + ), + "f64" => ( + syn::parse_quote!(jni::sys::jdouble), + syn::parse_quote!(*v), + ), + "Duration" | "std :: time :: Duration" => ( + syn::parse_quote!(jni::sys::jlong), + syn::parse_quote!(std::time::Duration::from_millis(*v as u64)), + ), + "String" => ( + syn::parse_quote!(jni::objects::JString), + syn::parse_quote!({ + let s = env + .get_string(v) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("decode_string: {}", e)))?; + s.into() + }), + ), + "Vec < u8 >" => ( + syn::parse_quote!(jni::objects::JByteArray), + syn::parse_quote!({ + env.convert_byte_array(v) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("decode_byte_array: {}", e)))? + }), + ), + _ => return None, + }) +} + +fn primitive_output(ty: &syn::Type) -> Option<(syn::Type, syn::Expr)> { + let key = TypeKey::from_type(ty).as_str().to_string(); + // Output wrappers take v by value (move). Primitives are Copy, so + // `v as wire` works. String/Vec consume v. + Some(match key.as_str() { + "bool" => ( + syn::parse_quote!(jni::sys::jboolean), + syn::parse_quote!(v as jni::sys::jboolean), + ), + "i32" => ( + syn::parse_quote!(jni::sys::jint), + syn::parse_quote!(v as jni::sys::jint), + ), + "i64" => ( + syn::parse_quote!(jni::sys::jlong), + syn::parse_quote!(v as jni::sys::jlong), + ), + "f64" => ( + syn::parse_quote!(jni::sys::jdouble), + syn::parse_quote!(v as jni::sys::jdouble), + ), + "String" => ( + syn::parse_quote!(jni::objects::JString), + syn::parse_quote!({ + env.new_string(v.as_str()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("encode_string: {}", e)))? + }), + ), + "Vec < u8 >" => ( + syn::parse_quote!(jni::objects::JByteArray), + syn::parse_quote!({ + env.byte_array_from_slice(v.as_slice()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("encode_byte_array: {}", e)))? + }), + ), + _ => return None, + }) +} + +// ────────────────────────────────────────────────────────────────────── +// Option<_> wrappers +// ────────────────────────────────────────────────────────────────────── + +/// Build `Option`'s input converter. +/// +/// Two paths, picked in this order: +/// +/// 1. **Niche path** (preferred). If `T`'s converter exposes any niche +/// slots, carve the first one and use it as the `None` discriminator. +/// The wrapper keeps `T`'s wire unchanged — no boxing, no extra +/// allocation, ABI-identical to a hand-written `if v == sentinel`. +/// The `rest` of the niche set is re-exported on the wrapper so an +/// enclosing wrapper (e.g. `Option>`) can keep carving. +/// +/// 2. **Boxed-primitive fallback**. If `T`'s wire is a JNI primitive +/// (`jlong`, `jint`, …) and there is no niche, the wrapper widens +/// the wire to `JObject` carrying a Java boxed type (`java.lang.Long`, +/// `java.lang.Integer`, …). `null` denotes `None`. The wrapper +/// exposes no further niches — every `JObject` value already carries +/// meaning (null = None, non-null = Some). +/// +/// If neither path applies (non-primitive wire, no niche), the wrap +/// fails and the resolver falls through to other rank-1 attempts. +fn option_input( + t1: &syn::Type, + registry: &Registry, +) -> Option<(syn::Type, syn::Expr, Niches)> { + let inner_entry = registry.input_entry(t1)?; + let inner_wire = inner_entry.destination.clone(); + let inner_conv = inner_entry.function.sig.ident.clone(); + + // 1. Niche path. + if let Some((slot, rest)) = inner_entry.niches.clone().carve() { + let pred = &slot.matches; + let returns_owned_object = converter_returns_owned_object(&inner_entry.function.sig.output); + let body: syn::Expr = if returns_owned_object { + // Borrow semantics: the Java side still owns the boxed value + // (its `close()` will free the original Box later via the typed + // handle's `freePtr`). Cloning the inner T keeps the pointer + // live across this call — using `Box::from_raw` here would + // consume the box, leaving the Java slot dangling and causing + // a double-free the next time the same data-class instance is + // decoded. Requires `T: Clone`. + syn::parse_quote!({ + if #pred { + None + } else { + Some(unsafe { OwnedObject::from_raw(*v as *const #t1).clone() }) + } + }) + } else { + syn::parse_quote!({ + if #pred { None } else { Some(#inner_conv(env, v)?) } + }) + }; + return Some((inner_wire, body, rest)); + } + + // 2. Boxed-primitive fallback. + if is_jni_primitive(&inner_wire) { + let unbox_method = jni_unbox_method(&inner_wire); + let unbox_sig = jni_unbox_sig(&inner_wire); + let getter = jni_unbox_getter(&inner_wire); + let getter_id = format_ident!("{}", getter); + let body: syn::Expr = syn::parse_quote!({ + if !v.is_null() { + let __unboxed: #inner_wire = env + .call_method(&v, #unbox_method, #unbox_sig, &[]) + .and_then(|val| val.#getter_id()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Option unbox: {}", e)))?; + Some(#inner_conv(env, &__unboxed)?) + } else { + None + } + }); + let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); + return Some((wire, body, Niches::empty())); + } + + None +} + +/// Build `Option`'s output converter — symmetric to [`option_input`]. +fn option_output( + t1: &syn::Type, + registry: &Registry, +) -> Option<(syn::Type, syn::Expr, Niches)> { + let inner_entry = registry.output_entry(t1)?; + let inner_wire = inner_entry.destination.clone(); + let inner_conv = inner_entry.function.sig.ident.clone(); + + // 1. Niche path. + if let Some((slot, rest)) = inner_entry.niches.clone().carve() { + let none_value = &slot.value; + let body: syn::Expr = syn::parse_quote!({ + match v { + Some(value) => #inner_conv(env, value)?, + None => #none_value, + } + }); + return Some((inner_wire, body, rest)); + } + + // 2. Boxed-primitive fallback. + if is_jni_primitive(&inner_wire) { + let java_class = jni_box_class(&inner_wire); + let box_sig = jni_box_sig(&inner_wire); + let variant = jni_box_variant(&inner_wire); + let variant_id = format_ident!("{}", variant); + let body: syn::Expr = syn::parse_quote!({ + match v { + Some(value) => { + let __raw: #inner_wire = #inner_conv(env, value)?; + env.call_static_method( + #java_class, + "valueOf", + #box_sig, + &[jni::objects::JValue::#variant_id(__raw)], + ) + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), + } + }); + let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); + return Some((wire, body, Niches::empty())); + } + + None +} + +// ────────────────────────────────────────────────────────────────────── +// Callback wrappers — impl Fn(args) -> JObject (Kotlin fun-interface) +// ────────────────────────────────────────────────────────────────────── + +fn callback_input( + ext: &JniExt, + args: &[syn::Type], + registry: &Registry, +) -> Option<(syn::Type, syn::Expr)> { + let name = derive_callback_name(args); + + // Per-arg: encode call + JNI signature chunk. + let mut arg_idents: Vec = Vec::new(); + let mut arg_preludes: Vec = Vec::new(); + let mut jvalue_exprs: Vec = Vec::new(); + let mut sig = String::from("("); + + for (i, arg_ty) in args.iter().enumerate() { + let raw_ident = format_ident!("__arg{}", i); + let enc_ident = format_ident!("__arg{}_encoded", i); + let obj_ident = format_ident!("__arg{}_obj", i); + + // Args are output-direction (encoded outbound). Look up output entry. + let arg_entry = registry.output_entry(arg_ty)?; + let arg_wire = arg_entry.destination.clone(); + let conv = arg_entry.function.sig.ident.clone(); + + match jni_field_access(&arg_wire) { + Some((s, _, false)) => { + sig.push_str(s); + arg_preludes.push(quote! { + let #raw_ident = &__cb_args.#i; + let #enc_ident = #conv(&mut env, #raw_ident)?; + }); + jvalue_exprs.push(quote!(jni::objects::JValue::from(#enc_ident))); + } + Some((s, _, true)) => { + sig.push_str(s); + arg_preludes.push(quote! { + let #raw_ident = &__cb_args.#i; + let #enc_ident = #conv(&mut env, #raw_ident)?; + let #obj_ident: jni::objects::JObject = #enc_ident.into(); + }); + jvalue_exprs.push(quote!(jni::objects::JValue::Object(&#obj_ident))); + } + None if is_jobject_wire(&arg_wire) => { + // The callback's `run` method takes the Kotlin equivalent + // of this Rust arg type, not the callback interface itself. + // Look up the registered FQN and slash-encode it for the + // JVM method descriptor. + let arg_key = TypeKey::from_type(arg_ty).as_str().to_string(); + let arg_fqn = ext.kotlin_type_fqns + .iter() + .find(|(k, _)| k == &arg_key) + .map(|(_, v)| v.replace('.', "/")) + .unwrap_or_else(|| "java/lang/Object".to_string()); + sig.push_str(&format!("L{};", arg_fqn)); + arg_preludes.push(quote! { + let #enc_ident = #conv(&mut env, &__cb_args.#i)?; + let #obj_ident: jni::objects::JObject = #enc_ident; + }); + jvalue_exprs.push(quote!(jni::objects::JValue::Object(&#obj_ident))); + } + None => return None, // unsupported wire form + } + arg_idents.push(raw_ident); + } + sig.push_str(")V"); + + // Tuple destructure for closure args. + let arg_pat_ty: Vec = args.iter().map(|t| quote!(#t)).collect(); + let arg_pat_ident: Vec = (0..args.len()) + .map(|i| { + let ident = format_ident!("__cb_arg{}", i); + quote!(#ident) + }) + .collect(); + let _ = arg_pat_ident; + + let name_lit = syn::LitStr::new(&name, Span::call_site()); + let sig_lit = syn::LitStr::new(&sig, Span::call_site()); + + // Body: capture global ref, return a Box. + // The wrapper takes the raw JObject `v` (the Kotlin callback ref). + let arg_indices: Vec = (0..args.len()).map(syn::Index::from).collect(); + let _ = arg_indices; + + // Build the Fn closure body. + let arg_names: Vec = (0..args.len()) + .map(|i| format_ident!("__cb_arg{}", i)) + .collect(); + + // Convert (self.0, .1, ...) tuple field accesses into __cb_arg0, _arg1. + // Replace `__cb_args.0` with `__cb_arg0` etc. in arg_preludes by + // re-rendering: easier to just rebuild here. + let mut fixed_preludes: Vec = Vec::new(); + for (i, arg_ty) in args.iter().enumerate() { + let raw_ident = format_ident!("__arg{}", i); + let enc_ident = format_ident!("__arg{}_encoded", i); + let obj_ident = format_ident!("__arg{}_obj", i); + let cb_arg = &arg_names[i]; + let arg_entry = registry.output_entry(arg_ty)?; + let arg_wire = arg_entry.destination.clone(); + let conv = arg_entry.function.sig.ident.clone(); + // Output wrappers take rust by value (move). cb_arg is the + // closure parameter (by value), so pass it directly. + match jni_field_access(&arg_wire) { + Some((_, _, false)) => fixed_preludes.push(quote! { + let #enc_ident = #conv(&mut env, #cb_arg)?; + }), + Some((_, _, true)) => fixed_preludes.push(quote! { + let #enc_ident = #conv(&mut env, #cb_arg)?; + let #obj_ident: jni::objects::JObject = #enc_ident.into(); + }), + None if is_jobject_wire(&arg_wire) => fixed_preludes.push(quote! { + let #enc_ident = #conv(&mut env, #cb_arg)?; + let #obj_ident: jni::objects::JObject = #enc_ident; + }), + None => return None, + } + let _ = raw_ident; // unused with by-value flow + } + + let body: syn::Expr = syn::parse_quote!({ + use std::sync::Arc; + let java_vm = Arc::new(env.get_java_vm() + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Unable to retrieve JVM: {}", e)))?); + let callback_global_ref = env.new_global_ref(&v) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Unable to global-ref callback: {}", e)))?; + Box::new(move |#(#arg_names: #arg_pat_ty),*| { + let _ = (|| -> ::core::result::Result<(), __JniErr> { + let mut env = java_vm + .attach_current_thread_as_daemon() + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Attach thread for {}: {}", #name_lit, e)))?; + #(#fixed_preludes)* + env.call_method( + &callback_global_ref, + "run", + #sig_lit, + &[#(#jvalue_exprs),*], + ) + .map_err(|e| { + let _ = env.exception_describe(); + <__JniErr as ::core::convert::From>::from(e.to_string()) + })?; + Ok(()) + })() + .map_err(|e| tracing::error!("{} callback error: {e}", #name_lit)); + }) + }); + + // The destination type for an `impl Fn(args)` parameter is JObject (the + // Kotlin callback object). We return Box + // wrapped in a generic so it satisfies the impl-trait param type. + // Actually the SOURCE (rust) type IS `impl Fn(args) + Send + Sync + 'static`, + // so the wrapper's return type is that. Box coerces. + Some((syn::parse_quote!(jni::objects::JObject), body)) +} + +fn derive_callback_name(args: &[syn::Type]) -> String { + let mut s = String::new(); + for a in args { + s.push_str(&type_short_ident(a)); + } + s.push_str("Callback"); + s +} + +fn type_short_ident(ty: &syn::Type) -> String { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + return last.ident.to_string(); + } + } + "Unknown".into() +} + +fn is_jobject_wire(wire: &syn::Type) -> bool { + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + return last.ident == "JObject"; + } + } + false +} + +/// True if `wire` is a JNI handle (`JObject`, `JString`, `JByteArray`, +/// `JClass`) that natively supports a `null` discriminator. These types +/// all impl `is_null()` and accept `JObject::null().into()` for +/// construction. +fn is_jobject_shaped_wire(wire: &syn::Type) -> bool { + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + return matches!( + last.ident.to_string().as_str(), + "JObject" | "JString" | "JByteArray" | "JClass" + ); + } + } + false +} + +/// Default niche set for a JNI wrapper wire: every `J*` handle has a +/// genuine `null` value that no live conversion ever produces, so wrap +/// it as a single niche; everything else (`jlong`, `jint`, `()`, …) has +/// no implicit niche. +/// +/// Plugins are free to declare *additional* niches on top of this for +/// pointer-shape primitives like `Box::into_raw`-as-`jlong`. +fn default_niches_for_wire(wire: &syn::Type) -> Niches { + if is_jobject_shaped_wire(wire) { + Niches::one( + syn::parse_quote!(jni::objects::JObject::null().into()), + syn::parse_quote!(v.is_null()), + ) + } else { + Niches::empty() + } +} + +// ────────────────────────────────────────────────────────────────────── +// Struct rank-0 bodies +// ────────────────────────────────────────────────────────────────────── + +/// Resolve the typed-handle Kotlin FQN for a handle-bearing struct field +/// and assert its folded strategy is one the struct encode/decode bridge +/// supports. Today only scalar handle slots (`Direct`, optionally wrapped +/// in `Nullable`) are encodable as a single `L;` ctor arg; a +/// collection layer (`Iterable`, i.e. `Vec`) would need array +/// codegen and is a loud build-time error until implemented. +fn handle_field_fqn(ext: &JniExt, h: &HandleInfo) -> String { + fn assert_scalar(s: &CloseStrategy) { + match s { + CloseStrategy::Direct => {} + CloseStrategy::Nullable(inner) => assert_scalar(inner), + CloseStrategy::Iterable(_) => panic!( + "struct handle field: collection (Vec) layers are not yet \ + supported by the struct encode/decode bridge — add array codegen \ + to struct_output_body/struct_input_body to lift this guard" + ), + } + } + assert_scalar(&h.strategy); + ext.kotlin_type_fqns + .iter() + .find(|(k, _)| k == &h.leaf_key) + .map(|(_, v)| v.clone()) + .unwrap_or_else(|| { + panic!( + "struct handle field: leaf `{}` has no Kotlin FQN registered \ + (ptr_class)", + h.leaf_key + ) + }) +} + +fn struct_input_body( + ext: &JniExt, + s: &syn::ItemStruct, + registry: &Registry, +) -> Option<(syn::Type, syn::Expr)> { + let struct_name = s.ident.to_string(); + let struct_module = struct_module_path(ext, s); + let struct_ident = &s.ident; + + let syn::Fields::Named(named) = &s.fields else { + return None; + }; + + let mut field_preludes: Vec = Vec::new(); + let mut field_init: Vec = Vec::new(); + + for field in &named.named { + let fname_ident = field.ident.as_ref().unwrap().clone(); + let fname = fname_ident.to_string(); + let camel = snake_to_camel(&fname); + let err_prefix = format!("{struct_name}.{camel}: {{}}"); + let raw_ident = format_ident!("__{}_raw", fname_ident); + + // Defer if any field's input converter isn't resolved yet — the + // fixed-point loop will retry on the next iteration. + let field_entry = registry.input_entry(&field.ty)?; + let field_wire = field_entry.destination.clone(); + let field_conv = field_entry.function.sig.ident.clone(); + + // Mirror of the struct_output_body bridge: when the field is a + // closeable native handle (folded `HandleInfo` on its converter + // metadata), read the JNINativeHandle object from the JVM slot, + // `peek()` the raw jlong, then run the per-field input converter + // (still jlong-keyed). Null handle ⇒ jlong = 0 ⇒ `None` via the + // niche-path. Detection now flows from the type-unfolding metadata, + // not a syntactic `Option` peel. + if let Some(h) = &field_entry.metadata.handle { + let java_path = handle_field_fqn(ext, h).replace('.', "/"); + let sig = format!("L{};", java_path); + let tmp_ident = format_ident!("__{}_jobj", fname_ident); + field_preludes.push(quote! { + let #tmp_ident: jni::objects::JObject = env.get_field(v, #camel, #sig) + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!(#err_prefix, e)))?; + let #raw_ident: jni::sys::jlong = if #tmp_ident.is_null() { + 0 + } else { + env.call_method(&#tmp_ident, "peek", "()J", &[]) + .and_then(|val| val.j()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!(#err_prefix, e)))? + }; + let #fname_ident = #field_conv(env, &#raw_ident)?; + }); + field_init.push(quote!(#fname_ident)); + continue; + } + + match jni_field_access(&field_wire) { + Some((sig, accessor, false)) => { + field_preludes.push(quote! { + let #raw_ident: #field_wire = env.get_field(v, #camel, #sig) + .and_then(|val| val.#accessor()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!(#err_prefix, e)))? as _; + let #fname_ident = #field_conv(env, &#raw_ident)?; + }); + } + Some((sig, _, true)) => { + let tmp_ident = format_ident!("__{}_jobj", fname_ident); + field_preludes.push(quote! { + let #tmp_ident: jni::objects::JObject = env.get_field(v, #camel, #sig) + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!(#err_prefix, e)))?; + let #raw_ident: #field_wire = #tmp_ident.into(); + let #fname_ident = #field_conv(env, &#raw_ident)?; + }); + } + None => { + // Wire is JObject — fetch via .l() and pass by reference. + field_preludes.push(quote! { + let #raw_ident: jni::objects::JObject = env.get_field(v, #camel, "Ljava/lang/Object;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!(#err_prefix, e)))?; + let #fname_ident = #field_conv(env, &#raw_ident)?; + }); + } + } + field_init.push(quote!(#fname_ident)); + } + + let body: syn::Expr = syn::parse_quote!({ + #(#field_preludes)* + #struct_module::#struct_ident { #(#field_init),* } + }); + Some((syn::parse_quote!(jni::objects::JObject), body)) +} + +fn struct_output_body( + ext: &JniExt, + s: &syn::ItemStruct, + registry: &Registry, +) -> Option<(syn::Type, syn::Expr)> { + let struct_name = s.ident.to_string(); + // Prefer the registered Kotlin FQN (`io.zenoh.jni.JniSample`) so the + // mangle closure flows through; fall back to the bare struct ident + // qualified with the package when no `data_class` / + // `ptr_class` declaration exists for this Rust type. + let struct_ident = &s.ident; + let struct_ty: syn::Type = syn::parse_quote!(#struct_ident); + let registered_fqn = ext + .types + .get(&TypeKey::from_type(&struct_ty)) + .and_then(|cfg| cfg.kotlin_name.clone()); + let java_class_name = if let Some(fqn) = registered_fqn { + fqn.replace('.', "/") + } else if ext.java_class_prefix.is_empty() { + struct_name.clone() + } else { + format!("{}/{}", ext.java_class_prefix, struct_name) + }; + + let syn::Fields::Named(named) = &s.fields else { + return None; + }; + + let mut field_preludes: Vec = Vec::new(); + let mut ctor_args: Vec = Vec::new(); + let mut ctor_sig = String::from("("); + + for field in &named.named { + let fname_ident = field.ident.as_ref().unwrap().clone(); + let field_value_ident = format_ident!("__{}_value", fname_ident); + let encoded_ident = format_ident!("__{}_encoded", fname_ident); + let encoded_obj_ident = format_ident!("__{}_encoded_obj", fname_ident); + + // Value-class fields are JVM-erased to their wrapped inner + // type: `val zid: ZenohId` where ZenohId is `@JvmInline value + // class ZenohId(val bytes: ByteArray)` compiles to a `byte[] + // zid` slot in the enclosing class — both the JVM ctor + // signature and the runtime argument must use the inner type, + // not a boxed ZenohId. Detect it up front and route through + // the inner field's converter; the rest of the per-field + // dispatch is type-driven and naturally falls into the + // inner-type branch. + let (effective_ty, field_access): (syn::Type, TokenStream) = { + let key = TypeKey::from_type(&field.ty); + let is_value_class = ext + .types + .get(&key) + .map(|c| c.value_class) + .unwrap_or(false); + if is_value_class { + let ident = bare_path_ident(&field.ty).unwrap_or_else(|| { + panic!( + "struct_output_body: value-class field `{}.{}` type \ + is not a bare path — value classes must be \ + registered against a concrete struct type", + s.ident, fname_ident + ) + }); + let (item_struct, _) = registry.structs.get(&ident).unwrap_or_else(|| { + panic!( + "struct_output_body: value-class struct `{}` is not in \ + the registry — make sure it was scanned by \ + `#[prebindgen]`", + ident + ) + }); + let syn::Fields::Named(n) = &item_struct.fields else { + panic!( + "struct_output_body: value-class struct `{}` must use \ + named fields", + ident + ) + }; + assert_eq!( + n.named.len(), + 1, + "struct_output_body: value-class struct `{}` must have \ + exactly one field; found {}", + ident, + n.named.len() + ); + let inner = n.named.first().unwrap(); + let inner_ident = inner.ident.as_ref().unwrap().clone(); + ( + inner.ty.clone(), + quote! { v.#fname_ident.#inner_ident.clone() }, + ) + } else { + (field.ty.clone(), quote! { v.#fname_ident.clone() }) + } + }; + + // Defer if any field's output converter isn't resolved yet. + let field_entry = registry.output_entry(&effective_ty)?; + let field_wire = field_entry.destination.clone(); + let field_conv = field_entry.function.sig.ident.clone(); + + field_preludes.push(quote! { + let #field_value_ident = #field_access; + let #encoded_ident = #field_conv(env, #field_value_ident)?; + }); + + // Closeable native-handle fields are bridged: the per-field output + // converter still produces `jlong` (sentinel 0L for None); we wrap + // that into the typed-handle Kotlin class so the data-class field + // type matches. Detection flows from the folded `HandleInfo` the + // type-unfolding mechanism propagated onto this field's metadata. + // JVM ctor slot is `L;`, not `J`. + if let Some(h) = &field_entry.metadata.handle { + let java_path = handle_field_fqn(ext, h).replace('.', "/"); + let ctor_slot = format!("L{};", java_path); + ctor_sig.push_str(&ctor_slot); + let java_path_lit = syn::LitStr::new(&java_path, Span::call_site()); + field_preludes.push(quote! { + let #encoded_obj_ident: jni::objects::JObject<'a> = if #encoded_ident == 0 { + jni::objects::JObject::null() + } else { + env + .new_object(#java_path_lit, "(J)V", &[jni::objects::JValue::from(#encoded_ident)]) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("wrap typed handle {}: {}", #java_path_lit, e)))? + }; + }); + ctor_args.push(quote!(jni::objects::JValue::Object(&#encoded_obj_ident))); + continue; + } + // `enum_class`-declared enum fields: the per-field output + // converter produced a `jint` discriminant, but the Kotlin + // data class declares this field as the typed enum class. Wrap + // the int via the enum's `fromInt(Int)` companion method so the + // ctor slot matches the data-class field type. Uses + // `effective_ty` so a hypothetical value class wrapping an enum + // still routes through here against its inner type. + if ext.is_kotlin_enum(&effective_ty) { + if let Some(name) = bare_path_ident(&effective_ty) { + let enum_fqn = ext + .kotlin_type_fqns + .iter() + .find(|(k, _)| k == &name.to_string()) + .map(|(_, v)| v.clone()); + if let Some(fqn) = enum_fqn { + let java_path = fqn.replace('.', "/"); + let ctor_slot = format!("L{};", java_path); + ctor_sig.push_str(&ctor_slot); + let java_path_lit = syn::LitStr::new(&java_path, Span::call_site()); + let from_int_sig = format!("(I)L{};", java_path); + let from_int_sig_lit = syn::LitStr::new(&from_int_sig, Span::call_site()); + field_preludes.push(quote! { + let #encoded_obj_ident: jni::objects::JObject<'a> = { + let __cls = env + .find_class(#java_path_lit) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("find enum class {}: {}", #java_path_lit, e)))?; + let __result = env + .call_static_method(&__cls, "fromInt", #from_int_sig_lit, &[jni::objects::JValue::from(#encoded_ident)]) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("call {}.fromInt: {}", #java_path_lit, e)))?; + __result.l() + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("{}.fromInt result not an object: {}", #java_path_lit, e)))? + }; + }); + ctor_args.push(quote!(jni::objects::JValue::Object(&#encoded_obj_ident))); + continue; + } + } + } + + match jni_field_access(&field_wire) { + Some((sig, _, false)) => { + ctor_sig.push_str(sig); + ctor_args.push(quote!(jni::objects::JValue::from(#encoded_ident))); + } + Some((sig, _, true)) => { + ctor_sig.push_str(sig); + field_preludes.push(quote! { + let #encoded_obj_ident: jni::objects::JObject = #encoded_ident.into(); + }); + ctor_args.push(quote!(jni::objects::JValue::Object(&#encoded_obj_ident))); + } + None => { + // Object-shaped wire (jobject/jstring/jbyteArray/etc.) with no + // primitive descriptor. The JVM ctor slot must be the field's + // ACTUAL declared type — `Ljava/lang/Object;` won't match + // because Kotlin emits the typed class in the constructor + // signature. Detection order: + // 1. Bare data-class (registered via `data_class`): + // look up the typed FQN from `kotlin_type_fqns`. + // 2. `Vec` → `java/util/List` (the Vec rank-1 handler + // encodes to ArrayList, which implements List). + // 3. Anything else (e.g. `String` → `java/lang/String`): + // derive from the inner wire's J* type name. + // 4. Fallback → `Ljava/lang/Object;`. + let typed_slot = bare_path_ident(&effective_ty) + .and_then(|name| { + ext.kotlin_type_fqns + .iter() + .find(|(k, _)| k == &name.to_string()) + .map(|(_, v)| format!("L{};", v.replace('.', "/"))) + }) + .or_else(|| { + if pat_match_top(&effective_ty, "Vec") { + Some("Ljava/util/List;".to_string()) + } else if let syn::Type::Path(tp) = &field_wire { + tp.path.segments.last().and_then(|seg| { + match seg.ident.to_string().as_str() { + "JString" => Some("Ljava/lang/String;".to_string()), + "JByteArray" => Some("[B".to_string()), + _ => None, + } + }) + } else { + None + } + }) + .unwrap_or_else(|| "Ljava/lang/Object;".to_string()); + ctor_sig.push_str(&typed_slot); + field_preludes.push(quote! { + let #encoded_obj_ident: jni::objects::JObject = #encoded_ident; + }); + ctor_args.push(quote!(jni::objects::JValue::Object(&#encoded_obj_ident))); + } + } + } + ctor_sig.push_str(")V"); + let ctor_sig_lit = syn::LitStr::new(&ctor_sig, Span::call_site()); + + let body: syn::Expr = syn::parse_quote!({ + #(#field_preludes)* + let __obj = env.new_object( + #java_class_name, + #ctor_sig_lit, + &[#(#ctor_args),*], + ) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("encode struct: {}", e)))?; + __obj + }); + Some((syn::parse_quote!(jni::objects::JObject), body)) +} + +fn struct_module_path(ext: &JniExt, s: &syn::ItemStruct) -> syn::Path { + // Place the struct under ::::. Today's + // pipeline derives the module from the source file stem; here we ride + // on the same convention by inspecting the SourceLocation. Without a + // location handy at this stage we fall back to ::. + // In practice the actual file stem is added in the compose step at the + // call site by the consuming crate when needed. + let _ = s; + ext.source_module.clone() +} + +// ────────────────────────────────────────────────────────────────────── +// Enum rank-0 bodies +// ────────────────────────────────────────────────────────────────────── + +/// `jint → Rust enum` decoder body for a `enum_class`-declared enum. +/// Wire is `jni::sys::jint`. The framework builds the decode `match` +/// directly from the enum's own discriminants — no `TryFrom` impl +/// is required on the flat enum (the enum declaration is the single +/// source of truth for the int↔variant mapping, shared with the Kotlin +/// `value(N)` constants via [`enum_discriminant_values`]). An unknown +/// discriminant surfaces as the framework `__JniErr`. +/// +/// The arms use the bare ident — same shape as the wrapper function's +/// `v: ` signature — so binding crates can pick whichever +/// upstream type a bare `` resolves to in their include-site +/// `use` statements. Pairs with output body below. +fn enum_input_body(_ext: &JniExt, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { + assert_only_unit_variants(e); + let ident = &e.ident; + let ident_name = ident.to_string(); + let arms = crate::util::enum_discriminant_values(e).into_iter().map(|(variant, value)| { + let lit = proc_macro2::Literal::i64_unsuffixed(value); + quote! { #lit => #ident::#variant, } + }); + let body: syn::Expr = syn::parse_quote!({ + match *v as i64 { + #(#arms)* + other => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From>::from( + format!("invalid {} discriminant: {}", #ident_name, other) + ) + ); + } + } + }); + (syn::parse_quote!(jni::sys::jint), body) +} + +/// `Rust enum → jint` encoder body for a `enum_class`-declared enum. +/// Wire is `jni::sys::jint`. Relies on the declared enum's repr +/// supporting an `as` cast (i.e. C-like enum, no fields); the +/// [`assert_only_unit_variants`] check below catches violations +/// upstream of the cast. The body works without naming the enum type +/// at all — `v` is already typed via the wrapper signature, so the +/// `as` cast picks up the right type by inference. +fn enum_output_body(_ext: &JniExt, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { + assert_only_unit_variants(e); + let body: syn::Expr = syn::parse_quote!({ v as jni::sys::jint }); + (syn::parse_quote!(jni::sys::jint), body) +} + +/// Hard error on any enum that's not C-like (unit variants only). +/// `enum_class`'s discriminant-keyed Kotlin emission and `as jint` +/// encode both depend on unit variants — bail loudly at build time +/// rather than emitting wrong code. +fn assert_only_unit_variants(e: &syn::ItemEnum) { + for variant in &e.variants { + if !matches!(variant.fields, syn::Fields::Unit) { + panic!( + "enum_class only supports C-like enums (unit variants), \ + but `{}::{}` has fields", + e.ident, variant.ident + ); + } + } +} + +// ────────────────────────────────────────────────────────────────────── +// JNI primitive (un)boxing helpers +// ────────────────────────────────────────────────────────────────────── + +pub(crate) fn is_jni_primitive(ty: &syn::Type) -> bool { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + let name = last.ident.to_string(); + return matches!( + name.as_str(), + "jboolean" | "jbyte" | "jchar" | "jshort" | "jint" | "jlong" | "jfloat" | "jdouble" + ); + } + } + false +} + +fn jni_box_class(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "java/lang/Boolean", + "jbyte" => "java/lang/Byte", + "jchar" => "java/lang/Character", + "jshort" => "java/lang/Short", + "jint" => "java/lang/Integer", + "jlong" => "java/lang/Long", + "jfloat" => "java/lang/Float", + "jdouble" => "java/lang/Double", + _ => panic!("not a JNI primitive: {}", wire.to_token_stream()), + } +} + +fn jni_box_sig(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "(Z)Ljava/lang/Boolean;", + "jbyte" => "(B)Ljava/lang/Byte;", + "jchar" => "(C)Ljava/lang/Character;", + "jshort" => "(S)Ljava/lang/Short;", + "jint" => "(I)Ljava/lang/Integer;", + "jlong" => "(J)Ljava/lang/Long;", + "jfloat" => "(F)Ljava/lang/Float;", + "jdouble" => "(D)Ljava/lang/Double;", + _ => unreachable!(), + } +} + +fn jni_box_variant(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "Bool", + "jbyte" => "Byte", + "jchar" => "Char", + "jshort" => "Short", + "jint" => "Int", + "jlong" => "Long", + "jfloat" => "Float", + "jdouble" => "Double", + _ => unreachable!(), + } +} + +fn jni_unbox_method(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "booleanValue", + "jbyte" => "byteValue", + "jchar" => "charValue", + "jshort" => "shortValue", + "jint" => "intValue", + "jlong" => "longValue", + "jfloat" => "floatValue", + "jdouble" => "doubleValue", + _ => unreachable!(), + } +} + +fn jni_unbox_sig(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "()Z", + "jbyte" => "()B", + "jchar" => "()C", + "jshort" => "()S", + "jint" => "()I", + "jlong" => "()J", + "jfloat" => "()F", + "jdouble" => "()D", + _ => unreachable!(), + } +} + +fn jni_unbox_getter(wire: &syn::Type) -> &'static str { + match jni_prim_name(wire) { + "jboolean" => "z", + "jbyte" => "b", + "jchar" => "c", + "jshort" => "s", + "jint" => "i", + "jlong" => "j", + "jfloat" => "f", + "jdouble" => "d", + _ => unreachable!(), + } +} + +fn jni_prim_name(wire: &syn::Type) -> &str { + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + return Box::leak(last.ident.to_string().into_boxed_str()); + } + } + "" +} + +/// If `ty` is a `&T` borrow with no explicit lifetime, splice in `'`. +/// Otherwise return `ty` unchanged. +fn annotate_borrow_with_lifetime(ty: &syn::Type, life: &str) -> syn::Type { + if let syn::Type::Reference(r) = ty { + if r.lifetime.is_none() { + let mut new = r.clone(); + new.lifetime = Some(syn::Lifetime::new(&format!("'{}", life), proc_macro2::Span::call_site())); + return syn::Type::Reference(new); + } + } + ty.clone() +} + +/// If `ty` is `JObject` / `JString` / `JByteArray` (no explicit angle args), +/// splice in `<'>`. Otherwise return `ty` unchanged. +fn annotate_jobject_with_lifetime(ty: &syn::Type, life: &str) -> syn::Type { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + let name = last.ident.to_string(); + if matches!(name.as_str(), "JObject" | "JString" | "JByteArray" | "JClass") { + if matches!(last.arguments, syn::PathArguments::None) { + let mut new = tp.clone(); + if let Some(last) = new.path.segments.last_mut() { + let lt = syn::Lifetime::new(&format!("'{}", life), proc_macro2::Span::call_site()); + last.arguments = syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: syn::token::Lt::default(), + args: syn::punctuated::Punctuated::from_iter(std::iter::once(syn::GenericArgument::Lifetime(lt))), + gt_token: syn::token::Gt::default(), + }); + } + return syn::Type::Path(new); + } + } + } + } + ty.clone() +} + +// ────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────── + +/// Given a source type's wire shape, return the Java class to test via +/// `instanceof` and a prelude that narrows the dispatcher's +/// `v: &jni::objects::JObject` into something the source's existing +/// decoder accepts. The third element is the `decoded_ref` expression +/// passed as the decoder's `v` argument — typically `&__narrowed`, +/// except `JObject` is identity (`v` directly). +/// +/// `jlong`-wired sources (opaque handles) **require** a typed FQN in +/// `kotlin_type_fqns`. The generated arm does `instanceof ` + +/// `peek()` — each opaque source has its own Java class, so multiple +/// opaque sources in one `impl Into` dispatcher are distinguishable. +/// Works for both Borrow (read lock keeps `ptr` valid) and Consume +/// (write lock + null-after-action keeps `ptr` valid during the JNI +/// call). Missing-FQN panics at build time — register a typed FQN +/// (see `JniExt::kotlin_type_fqn`) and ensure the corresponding +/// Kotlin class exists. +/// +/// Returns `None` for wires not covered by the table — caller treats it +/// as a hard error (the source type can't participate in +/// `impl Into` dispatch via this generic builder). +fn jobject_to_wire_adapter( + wire: &syn::Type, + src_ty: &syn::Type, + kotlin_type_fqns: &[(String, String)], +) -> Option<(String, TokenStream, TokenStream)> { + let key = TypeKey::from_type(wire).as_str().to_string(); + match key.as_str() { + // ── Boxed primitives: unbox via the standard Java accessor ──── + "jni :: sys :: jlong" => { + let src_key = TypeKey::from_type(src_ty).as_str().to_string(); + let fqn = kotlin_type_fqns + .iter() + .find(|(k, _)| k == &src_key) + .map(|(_, v)| v.replace('.', "/")) + .unwrap_or_else(|| { + panic!( + "jobject_to_wire_adapter: opaque source `{}` (jlong wire) has no \ + typed Kotlin FQN registered. Register one via \ + `JniExt::kotlin_type_fqn(\"{}\", \".JNI\")` and \ + ensure the corresponding Kotlin class exists.", + src_key, src_key + ) + }); + Some(( + fqn, + quote!( + let __narrowed: jni::sys::jlong = env + .call_method(v, "peek", "()J", &[]) + .and_then(|val| val.j()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("NativeHandle.peek: {}", e)))?; + ), + quote!(&__narrowed), + )) + } + "jni :: sys :: jint" => Some(( + "java/lang/Integer".to_string(), + quote!( + let __narrowed: jni::sys::jint = env + .call_method(v, "intValue", "()I", &[]) + .and_then(|val| val.i()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Integer.intValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + "jni :: sys :: jshort" => Some(( + "java/lang/Short".to_string(), + quote!( + let __narrowed: jni::sys::jshort = env + .call_method(v, "shortValue", "()S", &[]) + .and_then(|val| val.s()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Short.shortValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + "jni :: sys :: jbyte" => Some(( + "java/lang/Byte".to_string(), + quote!( + let __narrowed: jni::sys::jbyte = env + .call_method(v, "byteValue", "()B", &[]) + .and_then(|val| val.b()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Byte.byteValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + "jni :: sys :: jboolean" => Some(( + "java/lang/Boolean".to_string(), + quote!( + let __narrowed: jni::sys::jboolean = env + .call_method(v, "booleanValue", "()Z", &[]) + .and_then(|val| val.z()) + .map(|b| if b { 1u8 } else { 0u8 }) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Boolean.booleanValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + "jni :: sys :: jfloat" => Some(( + "java/lang/Float".to_string(), + quote!( + let __narrowed: jni::sys::jfloat = env + .call_method(v, "floatValue", "()F", &[]) + .and_then(|val| val.f()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Float.floatValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + "jni :: sys :: jdouble" => Some(( + "java/lang/Double".to_string(), + quote!( + let __narrowed: jni::sys::jdouble = env + .call_method(v, "doubleValue", "()D", &[]) + .and_then(|val| val.d()) + .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Double.doubleValue: {}", e)))?; + ), + quote!(&__narrowed), + )), + // ── Reference wrappers — wrap `v.as_raw()`, release after use ─ + "jni :: objects :: JString" => Some(( + "java/lang/String".to_string(), + quote!( + let __narrowed: jni::objects::JString = + unsafe { jni::objects::JString::from_raw(v.as_raw()) }; + ), + quote!(&__narrowed), + )), + "jni :: objects :: JByteArray" => Some(( + "[B".to_string(), + quote!( + let __narrowed: jni::objects::JByteArray = + unsafe { jni::objects::JByteArray::from_raw(v.as_raw()) }; + ), + quote!(&__narrowed), + )), + // ── JObject ─────────────────────────────────────────────────── + "jni :: objects :: JObject" | "jni :: sys :: jobject" => { + // Need an explicit Java class — pull from kotlin_type_fqns. + let src_key = TypeKey::from_type(src_ty).as_str().to_string(); + let fqn = kotlin_type_fqns + .iter() + .find(|(k, _)| k == &src_key) + .map(|(_, v)| v.replace('.', "/"))?; + Some((fqn, quote!(), quote!(v))) + } + _ => None, + } +} + +fn pat_match(ty: &syn::Type, pat: &str) -> bool { + ty.to_token_stream().to_string() == pat +} + +/// `true` if `ty` is a path whose final segment is `name` (e.g. `Vec<_>` for +/// `name = "Vec"`, `Option<&T>` for `name = "Option"`). Ignores generic args. +fn pat_match_top(ty: &syn::Type, name: &str) -> bool { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + return last.ident == name; + } + } + false +} + +/// If `ty` is `Option<&T>` or `Option<&mut T>`, return `Some(is_mut)`. +/// Returns `None` for any other shape. Used by `emit_jni_function_wrapper` +/// to decide whether the call site needs `.as_deref()` / `.as_deref_mut()` +/// when the input converter produced `Option>`. +fn option_inner_ref_mutability(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + if seg.ident != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(ab) = &seg.arguments else { return None }; + let syn::GenericArgument::Type(inner) = ab.args.first()? else { return None }; + let syn::Type::Reference(r) = inner else { return None }; + Some(r.mutability.is_some()) +} + +fn bare_path_ident(ty: &syn::Type) -> Option { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + if matches!(last.arguments, syn::PathArguments::None) { + return Some(last.ident.clone()); + } + } + } + None +} + + +// ────────────────────────────────────────────────────────────────────── +// JNI-internal naming convention. Hand-written code in zenoh-jni +// (e.g. liveliness.rs, advanced_subscriber.rs) calls auto-generated +// converters by these computed names — so the convention is part of the +// JNI plugin's public contract, not a private implementation detail. +// ────────────────────────────────────────────────────────────────────── + +/// INPUT: wire → rust. Format `_to__`. Special +/// case: `impl Fn(...)` keeps the legacy `process_kotlin__callback` +/// name so existing hand-written call sites continue to resolve. With +/// the current [`derive_callback_name`] algorithm `` is +/// concatenated arg shorts + `"Callback"` (e.g. `process_kotlin_SampleCallback_callback`). +fn input_name(rust: &syn::Type, wire: &syn::Type) -> syn::Ident { + if let Some(args) = extract_fn_trait_args(rust) { + let name = derive_callback_name(&args); + let s = format!("process_kotlin_{}_callback", name); + return syn::Ident::new(&s, Span::call_site()); + } + let rust_id = sanitize_for_ident(&rust.to_token_stream().to_string()); + let wire_id = wire_short(wire); + let h = hash_pair(rust, wire); + let s = format!("{}_to_{}_{:08x}", wire_id, rust_id, h & 0xffff_ffff); + syn::Ident::new(&s, Span::call_site()) +} + +/// OUTPUT: rust → wire. Format `_to__`. +fn output_name(rust: &syn::Type, wire: &syn::Type) -> syn::Ident { + let rust_id = sanitize_for_ident(&rust.to_token_stream().to_string()); + let wire_id = wire_short(wire); + let h = hash_pair(rust, wire); + let s = format!("{}_to_{}_{:08x}", rust_id, wire_id, h & 0xffff_ffff); + syn::Ident::new(&s, Span::call_site()) +} + +fn sanitize_for_ident(s: &str) -> String { + // Special-case the empty tuple — the all-punctuation token stream + // would sanitize to a meaningless fallback. `unit` is recognisable. + if s.trim() == "()" { + return "unit".to_string(); + } + let mut out = String::with_capacity(s.len()); + let mut prev_underscore = false; + for c in s.chars() { + if c.is_ascii_alphanumeric() { + out.push(c); + prev_underscore = false; + } else if !prev_underscore { + out.push('_'); + prev_underscore = true; + } + } + while out.starts_with('_') { + out.remove(0); + } + while out.ends_with('_') { + out.pop(); + } + if out.is_empty() { + out.push_str("ty"); + } + if out.chars().next().is_some_and(|c| c.is_ascii_digit()) { + out.insert(0, '_'); + } + out +} + +fn wire_short(wire: &syn::Type) -> String { + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + return sanitize_for_ident(&last.ident.to_string()); + } + } + sanitize_for_ident(&wire.to_token_stream().to_string()) +} + +fn hash_pair(rust: &syn::Type, wire: &syn::Type) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut h = DefaultHasher::new(); + rust.to_token_stream().to_string().hash(&mut h); + "::".hash(&mut h); + wire.to_token_stream().to_string().hash(&mut h); + h.finish() +} + +/// Reconstruct the `impl Fn(args...) + Send + Sync + 'static` syn::Type +/// from a flat slice of arg types. Used by the rank-1/2/3 callback impls +/// to feed `input_wrapper` the original outer type. +fn build_fn_type(args: &[syn::Type]) -> syn::Type { + let arg_iter = args.iter(); + syn::parse_quote!(impl Fn( #(#arg_iter),* ) + Send + Sync + 'static) +} + +/// `OwnedObject` definition emitted into the destination Rust file. +/// +/// A non-owning borrow wrapper around a `*const T` whose backing +/// `Box` lives on the Java side. The Java side hands Rust the +/// pointer under its `NativeHandle.withPtr` read lock; for the +/// duration of the JNI call the heap allocation is guaranteed live, +/// so `Deref` exposing `&*ptr` is sound. The wrapper has +/// no `Drop`: nothing is freed here, the Box stays with Java. +/// +/// By-value `T` extraction is intentionally NOT through this wrapper. +/// Consume call sites use `*Box::from_raw(ptr)` inline, taking +/// ownership of Java's slot; `NativeHandle.consume` (write-lock + +/// atomic null) sequences that against any concurrent borrow. +/// +/// Co-locating the definition with the converters keeps the generated +/// file self-contained — no `use` statement or runtime-support module +/// is required from the host crate. +pub(crate) fn owned_object_prerequisite_items() -> Vec { + vec![ + syn::parse_quote!( + /// See module-level docs at [`owned_object_prerequisite_items`]. + #[allow(dead_code)] + pub(crate) struct OwnedObject { + ptr: *const T, + } + ), + syn::parse_quote!( + impl std::ops::Deref for OwnedObject { + type Target = T; + fn deref(&self) -> &Self::Target { + unsafe { &*self.ptr } + } + } + ), + syn::parse_quote!( + // `&mut OwnedObject` coerces to `&mut T` via this impl, + // letting source fns that take `&mut T` opaque-handle params + // be called from generated wrappers. The pointer originated + // from `Box::into_raw` (which produces `*mut T`); the + // `*const T → *mut T` cast just restores the original + // mutability. Sequencing against concurrent borrow / consume + // is upheld by `NativeHandle.withPtr` on the JVM side, same + // as `Deref`. + impl std::ops::DerefMut for OwnedObject { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *(self.ptr as *mut T) } + } + } + ), + syn::parse_quote!( + impl OwnedObject { + /// Borrow a `T` whose backing `Box` lives on the + /// Java side. Stores only the pointer; the wrapper + /// does not own the heap allocation and never frees + /// it on drop. + /// + /// # Safety + /// + /// `ptr` must be the result of an earlier + /// `Box::into_raw(Box::new(v))` and the allocation + /// must still be live (Java still owns it). The Java + /// side is responsible for sequencing this call + /// against any concurrent free or consume (via + /// `NativeHandle.withPtr` read-lock vs `consume` / + /// `close` write-lock) so the borrow cannot race a + /// deallocation on the same pointer. + #[allow(dead_code)] + pub(crate) unsafe fn from_raw(ptr: *const T) -> Self { + Self { ptr } + } + } + ), + ] +} + +// ────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────── +// +// These tests exercise the niche cascade by hand-building registry +// entries with deliberate niche shapes, then driving `option_input` / +// `option_output` directly. They mirror the documented `Niches` +// semantics: each `Option<_>` layer carves one slot and re-exports the +// rest; once the rest is exhausted, the next layer falls back to the +// boxed-Java-primitive scheme. + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::niches::{NicheSlot, Niches}; + use crate::core::registry::{Registry, TypeEntry, TypeKey}; + use quote::ToTokens; + + /// Build a `TypeEntry` for use in tests. The function body is not + /// inspected by `option_input` / `option_output`; only the ident, + /// destination, and niches matter, so we use a stub `ItemFn`. + fn entry(wire: syn::Type, conv_name: &str, niches: Niches) -> TypeEntry { + let ident = syn::Ident::new(conv_name, proc_macro2::Span::call_site()); + let func: syn::ItemFn = syn::parse_quote!( + unsafe fn #ident<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &#wire, + ) -> ::core::result::Result<(), __JniErr> { + Ok(()) + } + ); + TypeEntry { + destination: wire, + function: func, + pre_stages: vec![], + subs: vec![], + required: false, + niches, + into_sources: None, + metadata: KotlinMeta::default(), + } + } + + fn install_input(reg: &mut Registry, ty_str: &str, rank: usize, e: TypeEntry) { + reg.input_types[rank].insert(TypeKey::parse(ty_str), Some(e)); + } + fn install_output(reg: &mut Registry, ty_str: &str, rank: usize, e: TypeEntry) { + reg.output_types[rank].insert(TypeKey::parse(ty_str), Some(e)); + } + + /// Single niche, single Option layer — wire stays the inner wire, + /// remainder is empty. No widening to JObject. + #[test] + fn option_carves_single_niche() { + let mut reg = Registry::default(); + install_input( + &mut reg, + "TestType", + 0, + entry( + syn::parse_quote!(jni::sys::jlong), + "jlong_to_TestType_aaaa", + Niches::one(syn::parse_quote!(0i64), syn::parse_quote!(*v == 0)), + ), + ); + + let inner_ty: syn::Type = syn::parse_quote!(TestType); + let (wire, _body, niches) = option_input(&inner_ty, ®).expect("Option resolves"); + + assert_eq!( + wire.to_token_stream().to_string(), + "jni :: sys :: jlong", + "wire stays jlong (no JObject widening)" + ); + assert!(niches.is_empty(), "single niche fully consumed"); + } + + /// Two niches, two cascading Option layers, both stay on the same + /// wire. The third layer hits empty niches and falls back to box. + #[test] + fn option_cascades_through_multi_niche() { + let mut reg = Registry::default(); + + // TestType: jint with two niches (MIN, MAX). + install_input( + &mut reg, + "TestType", + 0, + entry( + syn::parse_quote!(jni::sys::jint), + "jint_to_TestType_aaaa", + Niches::from_slots([ + NicheSlot { + value: syn::parse_quote!(jni::sys::jint::MIN), + matches: syn::parse_quote!(*v == jni::sys::jint::MIN), + }, + NicheSlot { + value: syn::parse_quote!(jni::sys::jint::MAX), + matches: syn::parse_quote!(*v == jni::sys::jint::MAX), + }, + ]), + ), + ); + + // Layer 1: Option. + let layer1_ty: syn::Type = syn::parse_quote!(TestType); + let (w1, _, n1) = option_input(&layer1_ty, ®).expect("layer 1 resolves"); + assert_eq!(w1.to_token_stream().to_string(), "jni :: sys :: jint"); + assert_eq!(n1.len(), 1, "first carve leaves one niche"); + + // Install the layer-1 wrapper as a rank-1 entry so layer-2 can + // look it up. (In the real resolver this happens automatically; + // here we mimic it by installing the produced ConverterImpl.) + install_input( + &mut reg, + "Option < TestType >", + 1, + entry(w1.clone(), "jint_to_OptionTestType_bbbb", n1), + ); + + // Layer 2: Option>. + let layer2_ty: syn::Type = syn::parse_quote!(Option); + let (w2, _, n2) = option_input(&layer2_ty, ®).expect("layer 2 resolves"); + assert_eq!( + w2.to_token_stream().to_string(), + "jni :: sys :: jint", + "wire still jint at layer 2 — no widening" + ); + assert!(n2.is_empty(), "second carve consumes the last niche"); + + // Install layer-2 wrapper for the layer-3 lookup. + install_input( + &mut reg, + "Option < Option < TestType > >", + 1, + entry(w2.clone(), "jint_to_OptionOptionTestType_cccc", n2), + ); + + // Layer 3: Option>>. No niches left, + // inner wire is jint (a JNI primitive) → boxed-Long fallback. + let layer3_ty: syn::Type = syn::parse_quote!(Option>); + let (w3, _, n3) = option_input(&layer3_ty, ®).expect("layer 3 resolves via box fallback"); + assert_eq!( + w3.to_token_stream().to_string(), + "jni :: objects :: JObject", + "layer 3 widens to JObject (box fallback)" + ); + assert!( + n3.is_empty(), + "boxed wrapper exposes no further niches — every JObject carries meaning" + ); + } + + /// Output side mirrors input: niche values are emitted in the + /// `None` arm of the match, and the remainder is re-exported. + #[test] + fn option_output_cascades_through_multi_niche() { + let mut reg = Registry::default(); + install_output( + &mut reg, + "TestType", + 0, + entry( + syn::parse_quote!(jni::sys::jint), + "TestType_to_jint_aaaa", + Niches::from_slots([ + NicheSlot { + value: syn::parse_quote!(-1i32), + matches: syn::parse_quote!(*v == -1), + }, + NicheSlot { + value: syn::parse_quote!(-2i32), + matches: syn::parse_quote!(*v == -2), + }, + ]), + ), + ); + + let inner_ty: syn::Type = syn::parse_quote!(TestType); + let (w1, body1, n1) = + option_output(&inner_ty, ®).expect("Option output resolves"); + assert_eq!(w1.to_token_stream().to_string(), "jni :: sys :: jint"); + assert_eq!(n1.len(), 1, "one slot left after carving the first"); + // The body must reference the carved value (-1) in the None arm. + let body_str = body1.to_token_stream().to_string(); + assert!( + body_str.contains("None => - 1i32") || body_str.contains("None => -1i32"), + "expected `None => -1i32` in body; got:\n{}", + body_str, + ); + + install_output( + &mut reg, + "Option < TestType >", + 1, + entry(w1.clone(), "OptionTestType_to_jint_bbbb", n1), + ); + + let layer2_ty: syn::Type = syn::parse_quote!(Option); + let (w2, body2, n2) = + option_output(&layer2_ty, ®).expect("Option> output resolves"); + assert_eq!(w2.to_token_stream().to_string(), "jni :: sys :: jint"); + assert!(n2.is_empty()); + let body2_str = body2.to_token_stream().to_string(); + assert!( + body2_str.contains("None => - 2i32") || body2_str.contains("None => -2i32"), + "second layer must use the second niche (-2); got:\n{}", + body2_str, + ); + } + + /// JObject-shaped wires get the implicit `null` niche via + /// [`default_niches_for_wire`], so `Option` over a struct + /// decoder stays on `JObject` (no boxing). + #[test] + fn option_over_jobject_uses_default_null_niche() { + let mut reg = Registry::default(); + install_input( + &mut reg, + "MyStruct", + 0, + entry( + syn::parse_quote!(jni::objects::JObject), + "JObject_to_MyStruct_aaaa", + default_niches_for_wire(&syn::parse_quote!(jni::objects::JObject)), + ), + ); + + let ty: syn::Type = syn::parse_quote!(MyStruct); + let (wire, _, rest) = option_input(&ty, ®).expect("Option resolves"); + assert_eq!(wire.to_token_stream().to_string(), "jni :: objects :: JObject"); + assert!(rest.is_empty(), "JObject's single null niche is consumed"); + } + + /// No niche AND non-primitive wire → wrap fails (resolver falls + /// through). Demonstrates that the boxed fallback only kicks in for + /// JNI primitives. + #[test] + fn option_fails_when_no_niche_and_non_primitive_wire() { + let mut reg = Registry::default(); + install_input( + &mut reg, + "MyStruct", + 0, + entry( + syn::parse_quote!(jni::objects::JObject), + "JObject_to_MyStruct_aaaa", + Niches::empty(), // explicit empty — author opted out + ), + ); + let ty: syn::Type = syn::parse_quote!(MyStruct); + assert!(option_input(&ty, ®).is_none()); + } + + /// Boxed fallback widens to `JObject` and exposes no further + /// niches — protects callers from cascading when a layer has had + /// to widen. + #[test] + fn option_box_fallback_exposes_no_niches() { + let mut reg = Registry::default(); + install_input( + &mut reg, + "i64", + 0, + entry( + syn::parse_quote!(jni::sys::jlong), + "jlong_to_i64_aaaa", + Niches::empty(), // primitive `i64` — no niche + ), + ); + let ty: syn::Type = syn::parse_quote!(i64); + let (wire, _, rest) = option_input(&ty, ®).expect("Option via box fallback"); + assert_eq!(wire.to_token_stream().to_string(), "jni :: objects :: JObject"); + assert!(rest.is_empty()); + } +} diff --git a/prebindgen-ext/src/jni/jni_kotlin_ext.rs b/prebindgen-ext/src/jni/jni_kotlin_ext.rs new file mode 100644 index 00000000..fa2685d8 --- /dev/null +++ b/prebindgen-ext/src/jni/jni_kotlin_ext.rs @@ -0,0 +1,2500 @@ +//! `KotlinExt` impl for [`JniExt`]. +//! +//! [`JniExt::write_kotlin`] is the single entry point for every Kotlin +//! file the JNI back-end emits. Given one `kotlin_root` it writes: +//! * `NativeHandle.kt` (package `io.zenoh.jni`). +//! * One typed-handle class per `ptr_class` entry without +//! `.suppress_kotlin_code()`. +//! * One package-level wrapper file for `package()` (top-level +//! safe wrappers for `package_methods` fns). +//! * `JNINative.kt` — centralized `external fun` holder. +//! * One Kotlin fun-interface file per `impl Fn(args) + Send + Sync +//! + 'static` type, named via [`JniExt::kotlin_callback_name_mangle`] +//! (default = identity over the `"On"`-prefixed auto-derived name; +//! in zenoh-jni: `JNIOn`). Callback types overridden via +//! [`JniExt::callback_input`] are skipped — the override points at +//! a hand-written interface. +//! +//! Every `#[prebindgen]` function must be assigned a Kotlin home via +//! `.method(...)` on either a typed-handle / data-class / enum config +//! or on `package(...)`. Undeclared functions are skipped (see +//! `Registry::scan_declared` warnings). There is no "orphan" bucket. +//! +//! All emitters route through [`KotlinFile::write`], which translates +//! `package` into a sub-path under `kotlin_root`. + +use std::collections::{BTreeSet, HashSet}; +use std::path::{Path, PathBuf}; + +use quote::ToTokens; + +use crate::core::prebindgen_ext::{IntoSource, IntoSourceMode, PrebindgenExt}; +use crate::core::registry::{extract_fn_trait_args, Registry, TypeKey}; +use crate::jni::jni_ext::{converter_returns_owned_object, JniExt, KotlinMeta, MethodEntry}; +use crate::jni::templates; +use crate::kotlin::kotlin_ext::{KotlinFile, WriteKotlinError}; +use crate::kotlin::type_map::KotlinTypeMap; + +/// Declaration of one auto-generated typed `NativeHandle` subclass. +/// +/// Consumed by [`JniExt::write_typed_handles`] (and forwarded to +/// [`JniExt::write_jni_wrappers`] so the same promotion list can carve +/// the matching skip-list). Each entry says "this Kotlin class is the +/// home for the named `#[prebindgen]` functions"; everything else stays +/// in the catch-all `JNIWrappers` object. +#[derive(Clone, Copy)] +pub(crate) struct TypedHandle<'a> { + /// Short Rust name shown in the class doc comment (e.g. `"Publisher"`). + /// Pure documentation, doesn't have to match anything in the Registry. + pub rust_doc: &'a str, + /// Package-qualified Kotlin class name (e.g. + /// `"io.zenoh.jni.JNIPublisher"`). The Rust type-key registered for + /// this FQN via [`JniExt::kotlin_type_fqn`] identifies which + /// parameter of each promoted function becomes `this`. + pub kotlin_fqn: &'a str, + /// `#[prebindgen]` fns declared as **instance methods** via + /// [`JniExt::method`]. The matched first parameter is dropped from + /// the Kotlin signature and substituted by inherited `withPtr` / + /// `consume` scope. Mismatch (no param matches the class type) is a + /// build-time error. + pub instance_methods: &'a [MethodEntry], + /// `#[prebindgen]` fns declared as **companion-object methods** via + /// [`JniExt::companion_method`]. Rendered inside `companion object` + /// using the same shape as a package-level wrapper. + pub companion_methods: &'a [MethodEntry], +} + +/// Reverse-lookup the Rust type-key registered for a given Kotlin FQN +/// in [`JniExt::kotlin_type_fqns`]. Used by [`JniExt::write_typed_handles`] +/// to determine which parameter of each promoted function should be +/// dropped (becomes `this`). +fn rust_key_for_fqn<'a>(ext: &'a JniExt, fqn: &str) -> Option<&'a str> { + ext.kotlin_type_fqns + .iter() + .find_map(|(rust, k)| (k == fqn).then_some(rust.as_str())) +} + +impl JniExt { + /// Unified Kotlin emission — single public entry point that fans out + /// to per-callback fun-interface files, `NativeHandle.kt`, typed-handle + /// classes (one per `ptr_class` registration), and + /// `JNIWrappers.kt`. Reads all configuration (typed-handle methods, + /// callback FQN overrides, Kotlin type names) from internal state set + /// during the builder phase. Returns every path written. + pub fn write_kotlin( + &self, + registry: &Registry, + kotlin_root: &Path, + ) -> Result, WriteKotlinError> { + let mut written = Vec::new(); + written.extend(self.emit_callback_files(registry, kotlin_root)?); + written.extend(self.write_exception_classes(kotlin_root)?); + written.extend(self.write_enum_classes(registry, kotlin_root)?); + written.extend(self.write_data_classes(registry, kotlin_root)?); + written.push(self.write_native_handle(kotlin_root)?); + + // Build the borrowed `TypedHandle<'_>` view from internal config. + let owned = self.collect_typed_handles(); + let typed_handles: Vec> = owned + .iter() + .map(|h| TypedHandle { + rust_doc: &h.rust_doc, + kotlin_fqn: &h.kotlin_fqn, + instance_methods: h.instance_methods.as_slice(), + companion_methods: h.companion_methods.as_slice(), + }) + .collect(); + let kotlin_types = self.build_kotlin_type_map(); + written.extend(self.write_typed_handles( + &typed_handles, + registry, + &kotlin_types, + kotlin_root, + )?); + for (subpackage, pkg_cfg) in &self.packages { + if pkg_cfg.functions.is_empty() { + continue; + } + written.push(self.write_jni_package( + registry, + &kotlin_types, + kotlin_root, + subpackage, + pkg_cfg, + )?); + } + written.push(self.write_jni_native( + registry, + &kotlin_types, + kotlin_root, + )?); + Ok(written) + } + + /// Per-callback fun-interface emission (one `.kt` + /// file per `impl Fn(...)` type encountered in the resolved + /// registry). Skips writes for `impl Fn(...)` keys whose Kotlin + /// FQN was overridden via [`Self::callback_input`] — the override + /// already points at a hand-maintained callback interface, so the + /// auto-stub would be dead code. Each emitted file is placed + /// under `kotlin_root//`. + pub(crate) fn emit_callback_files( + &self, + registry: &Registry, + kotlin_root: &Path, + ) -> Result, WriteKotlinError> { + let mut seen: HashSet = HashSet::new(); + let mut written = Vec::new(); + for buckets in [®istry.input_types, ®istry.output_types] { + for bucket in buckets.iter() { + for (key, slot) in bucket { + if slot.is_none() { + continue; + } + if !seen.insert(key.clone()) { + continue; + } + let ty = key.to_type(); + if let Some(args) = extract_fn_trait_args(&ty) { + // A `callback_input` registration points the + // Kotlin signature at a hand-written interface + // — skip the auto-stub. + if self + .types + .get(key) + .and_then(|c| c.callback_kotlin_fqn.as_ref()) + .is_some() + { + continue; + } + let file = build_callback_kotlin_file(self, &args, registry); + written.push(file.write(kotlin_root)?); + } + } + } + } + Ok(written) + } + + /// Build the `TypedHandle` slice from internal `types` config. + /// Iterates entries where `opaque.is_some()` and emits one + /// `TypedHandle` per opaque-handle registration. Stable order by + /// canonical Rust type-key — keeps generated output deterministic. + fn collect_typed_handles(&self) -> Vec { + let mut handles: Vec = Vec::new(); + let mut keys: Vec<&TypeKey> = self.types.keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + for key in keys { + let cfg = &self.types[key]; + let Some(opaque) = &cfg.opaque else { continue }; + if opaque.suppress_kotlin_code { + continue; + } + let Some(kotlin_fqn) = &cfg.kotlin_name else { continue }; + // rust_doc — short last-segment of the Rust type key (best + // effort; only used in the generated doc comment). + let rust_doc = key + .as_str() + .split(|c: char| !c.is_ascii_alphanumeric() && c != '_') + .find(|s| !s.is_empty()) + .unwrap_or(key.as_str()) + .to_string(); + handles.push(OwnedTypedHandle { + rust_doc, + kotlin_fqn: kotlin_fqn.clone(), + instance_methods: cfg.instance_methods.clone(), + companion_methods: cfg.companion_methods.clone(), + }); + } + handles + } + + /// Build the `KotlinTypeMap` view consumed by the typed-handle and + /// JNIWrappers emitters. Combines callback FQNs from + /// [`Self::collect_kotlin_callback_fqns`] (auto-derived or + /// override) with `kotlin_name` entries from the structured config. + /// Structured-config entries win on conflict. + fn build_kotlin_type_map(&self) -> KotlinTypeMap { + let mut map = KotlinTypeMap::new().with_primitive_builtins(); + for (key, cfg) in &self.types { + if let Some(name) = &cfg.kotlin_name { + map = map.add(key.as_str(), name.clone()); + } + } + map + } +} + +/// Owned counterpart of [`TypedHandle`] — used internally so the +/// `collect_typed_handles` helper doesn't have to hand out borrows of +/// `self.types`. +pub(crate) struct OwnedTypedHandle { + pub rust_doc: String, + pub kotlin_fqn: String, + pub instance_methods: Vec, + pub companion_methods: Vec, +} + +impl JniExt { + /// Emit `NativeHandle.kt` under `output_dir` (package + /// `io.zenoh.jni`). The class is the Java-side half of the + /// borrow/consume contract — `withPtr` for `&T` opaque-handle + /// borrows, `consume` for by-value `T` opaque-handle drops. By + /// generating it here, the prebindgen-ext pipeline owns the lock + /// primitive the rest of the auto-generated wrappers depend on. + /// The Kotlin exception thrown on closed-handle access is the + /// framework `JniBindingError` — `NativeHandle` is itself a + /// framework artefact (the JNI ABI between the generated Rust + /// converters and the Kotlin handle), and closed-handle access is + /// a misuse of that infrastructure rather than a domain failure. + /// Keeping it on the framework exception matches the contract + /// drawn in [`feedback_internal_contracts`]: everything below the + /// public zenoh-java API surface is framework-internal. + pub(crate) fn write_native_handle(&self, output_dir: &Path) -> Result { + let exc = self.framework_exception(); + let class_name = self.mangle_harness("NativeHandle"); + let file = templates::native_handle::emit_native_handle( + &self.package, + &class_name, + &exc.kotlin_fqn, + ); + Ok(file.write(output_dir)?) + } + + /// Emit one Kotlin file per registered + /// throwable class (via [`crate::jni::JniExt::throwable`]) — each becomes a + /// `public class (message: String? = null) : Exception()` + /// landing under `/.kt`. Iterates `self.exceptions` + /// in declaration order; returns every path written. + pub(crate) fn write_exception_classes( + &self, + output_dir: &Path, + ) -> Result, WriteKotlinError> { + let mut written = Vec::new(); + for exc in &self.exceptions { + // Skip exceptions whose Rust type already has a data-class (or + // ptr/enum) Kotlin emission — those classes carry the `: Exception` + // extension themselves (via `cfg.throwable` in + // `render_data_class_source`). The stub-template path only runs + // for un-registered exception types — in practice that's the + // framework's `JniBindingError`, declared inside `JniExt::new` + // without going through `.throwable()`. + let key = TypeKey::from_type(&exc.rust_type); + if self + .types + .get(&key) + .map(|cfg| cfg.kotlin_name.is_some()) + .unwrap_or(false) + { + continue; + } + let (package, class_name) = match exc.kotlin_fqn.rsplit_once('.') { + Some((p, c)) => (p.to_string(), c.to_string()), + None => (String::new(), exc.kotlin_fqn.clone()), + }; + let file = templates::exception::emit_exception(&package, &class_name, &exc.rust_short); + written.push(file.write(output_dir)?); + } + Ok(written) + } + + /// Emit one Kotlin `enum class` file per `enum_class`-declared type + /// (skipping any flagged with `.suppress_kotlin_code()`). Variants + /// render in declaration order using SCREAMING_SNAKE_CASE names; the + /// constructor stores the Rust discriminant value (or the ordinal as + /// a fallback when the discriminant isn't a bare integer literal). + /// A `fromInt(value: Int)` companion mirrors the `Priority.fromInt` + /// shape that hand-written enums use today, so adapter code stays + /// uniform. + pub(crate) fn write_enum_classes( + &self, + registry: &Registry, + output_dir: &Path, + ) -> Result, WriteKotlinError> { + let mut written = Vec::new(); + let callback_fqns = self.collect_kotlin_callback_fqns(registry); + let mut kotlin_types = KotlinTypeMap::new(); + for (k, v) in callback_fqns.iter() { + kotlin_types = kotlin_types.add(k, v.clone()); + } + let configured_types = self.build_kotlin_type_map(); + for (k, v) in configured_types.iter() { + kotlin_types = kotlin_types.add(k, v.clone()); + } + // Deterministic order by canonical Rust type-key. + let mut keys: Vec<&TypeKey> = self.types.keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + for key in keys { + let cfg = &self.types[key]; + let Some(enum_cfg) = &cfg.enum_cfg else { + continue; + }; + if enum_cfg.suppress_kotlin_code { + continue; + } + let Some(kotlin_fqn) = &cfg.kotlin_name else { + continue; + }; + // Look up the syn::ItemEnum by the type-key's bare ident. + let ty = key.to_type(); + let Some(ident) = (if let syn::Type::Path(tp) = &ty { + tp.path.segments.last().map(|s| s.ident.clone()) + } else { + None + }) else { + continue; + }; + let Some((item_enum, _)) = registry.enums.get(&ident) else { + continue; + }; + let (package, class_name) = match kotlin_fqn.rsplit_once('.') { + Some((p, c)) => (p.to_string(), c.to_string()), + None => (String::new(), kotlin_fqn.clone()), + }; + let file = KotlinFile { + contents: render_enum_source( + self, + &package, + &class_name, + item_enum, + &cfg.instance_methods, + &cfg.companion_methods, + registry, + &kotlin_types, + ), + package, + class_name, + }; + written.push(file.write(output_dir)?); + } + Ok(written) + } + + /// Emit one Kotlin `data class` file per `data_class`-declared + /// struct. Uses resolved converter metadata to derive Kotlin field + /// types, so wrappers and data-class declarations stay in sync. + pub(crate) fn write_data_classes( + &self, + registry: &Registry, + output_dir: &Path, + ) -> Result, WriteKotlinError> { + let mut written = Vec::new(); + let callback_fqns = self.collect_kotlin_callback_fqns(registry); + let mut kotlin_types = KotlinTypeMap::new(); + for (k, v) in callback_fqns.iter() { + kotlin_types = kotlin_types.add(k, v.clone()); + } + let configured_types = self.build_kotlin_type_map(); + for (k, v) in configured_types.iter() { + kotlin_types = kotlin_types.add(k, v.clone()); + } + let mut rust_names: Vec = Vec::new(); + let mut aliases: Vec<(String, String)> = Vec::new(); + let mut keys: Vec<&TypeKey> = self.types.keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + + for key in keys { + let cfg = &self.types[key]; + if cfg.opaque.is_some() || cfg.enum_cfg.is_some() || cfg.callback_kotlin_fqn.is_some() { + continue; + } + let Some(kotlin_fqn) = &cfg.kotlin_name else { + continue; + }; + + let ty = key.to_type(); + let Some(ident) = (if let syn::Type::Path(tp) = &ty { + tp.path.segments.last().map(|s| s.ident.clone()) + } else { + None + }) else { + continue; + }; + let Some((item_struct, _)) = registry.structs.get(&ident) else { + continue; + }; + rust_names.push(item_struct.ident.to_string()); + + let (package, class_name) = match kotlin_fqn.rsplit_once('.') { + Some((p, c)) => (p.to_string(), c.to_string()), + None => (String::new(), kotlin_fqn.clone()), + }; + if item_struct.ident.to_string() != class_name { + aliases.push((item_struct.ident.to_string(), class_name.clone())); + } + let file = KotlinFile { + contents: render_data_class_source( + self, + &package, + &class_name, + item_struct, + registry, + &kotlin_types, + &cfg.instance_methods, + &cfg.companion_methods, + cfg.throwable, + cfg.value_class, + key.as_str(), + ), + package: package.clone(), + class_name, + }; + written.push(file.write(output_dir)?); + + // If data-class naming changed, remove stale legacy file that + // may have been generated under the old class name. + let legacy_path = output_dir + .join(package.replace('.', "/")) + .join(format!("{}.kt", item_struct.ident)); + if item_struct.ident.to_string() != file.class_name && legacy_path.exists() { + let _ = std::fs::remove_file(&legacy_path); + } + } + + if !rust_names.is_empty() { + strip_legacy_jni_native_data_classes(output_dir, &self.package, &rust_names)?; + } + + if !aliases.is_empty() { + let alias_file = KotlinFile { + contents: render_data_class_aliases_source(&self.package, &aliases), + package: self.package.clone(), + class_name: "JNIDataClassAliases".to_string(), + }; + written.push(alias_file.write(output_dir)?); + } + + Ok(written) + } + + /// Emit the package-level wrapper file under `output_dir`. One + /// Emit one package-level wrapper file for the given subpackage. + /// One top-level safe wrapper per `MethodEntry` in `pkg_cfg.functions`. + /// Wrappers delegate to the centralized Native object (see + /// [`Self::write_jni_native`]). Opaque-handle parameters become + /// `NativeHandle`; the wrapper body nests `withPtr` / `consume` per + /// the type-conversion rule. Non-opaque parameters pass through with + /// the Kotlin type from `kotlin_types`. Opaque-handle return values + /// are wrapped in `NativeHandle(...)` before return. + pub(crate) fn write_jni_package( + &self, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + output_dir: &Path, + subpackage: &str, + pkg_cfg: &crate::jni::jni_ext::PackageConfig, + ) -> Result { + let class_name = self.jni_package_class_name(subpackage); + let package = if self.package.is_empty() { + subpackage.to_string() + } else if subpackage.is_empty() { + self.package.clone() + } else { + format!("{}.{}", self.package, subpackage) + }; + let contents = render_jni_package_source( + self, + registry, + kotlin_types, + &pkg_cfg.functions, + &package, + ); + let file = KotlinFile { + package, + class_name, + contents, + }; + Ok(file.write(output_dir)?) + } + + /// Emit the centralized Native-object Kotlin file under `output_dir` + /// (class name from [`JniExt::jni_native_class_name`]). Holds one + /// `external fun` per `#[prebindgen]` function — names mangled via + /// `kotlin_fun_name_mangle`, parameter and return types rendered at + /// the JNI **wire** level so the declarations match the Rust extern + /// symbols generated under + /// `Java___`. Loading the native + /// library is the wrapper layer's responsibility — the auto-generated + /// holder stays free of any reference to higher-layer types so that + /// `io.zenoh.jni.*` doesn't depend on `io.zenoh.*`. Trigger + /// `System.load` / `System.loadLibrary` from wrapper entry points + /// (e.g. via a `companion object { init { ZenohLoad } }` block) so + /// the lib is in place before any extern call. + pub(crate) fn write_jni_native( + &self, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + output_dir: &Path, + ) -> Result { + let class_name = self.jni_native_class_name(); + let declared = self.declared_functions(); + let contents = render_jni_native_source(self, registry, kotlin_types, &declared, &class_name); + let file = KotlinFile { + package: self.package.clone(), + class_name, + contents, + }; + Ok(file.write(output_dir)?) + } + + /// Emit one Kotlin file per entry in `handles` — each becomes a + /// `public class (initialPtr: Long) : NativeHandle(initialPtr)` + /// with the standard `free()` + `private external fun (ptr: Long)` + /// destructor pair, plus one instance method per `#[prebindgen]` fn + /// listed in [`TypedHandle::functions`]. The promoted method's first + /// opaque parameter matching the handle's Rust type is dropped — the + /// method uses inherited `withPtr` / `consume` from [`NativeHandle`] + /// (i.e. `this` scope) for that param, while every remaining + /// parameter is emitted exactly as it would appear in the + /// `JNIWrappers` top-level wrapper (including `impl Into` + /// dispatch arms and opaque-return wrapping). + /// + /// Functions listed under any [`TypedHandle::functions`] are skipped + /// in [`Self::write_jni_wrappers`] — "Not mentioned functions remain + /// in `JNIWrapper`" is the assignment rule, exposed by passing the + /// same `handles` slice to both methods. + /// + /// Each handle's `kotlin_fqn` must be registered via + /// [`Self::kotlin_type_fqn`] so the generator can map it back to its + /// Rust type-key (which identifies the first param to drop in each + /// promoted method's signature). + pub(crate) fn write_typed_handles( + &self, + handles: &[TypedHandle<'_>], + registry: &Registry, + kotlin_types: &KotlinTypeMap, + output_dir: &Path, + ) -> Result, WriteKotlinError> { + // Merged Kotlin type map (callback FQNs + caller-supplied). + // Same merge order as `render_jni_wrappers_source` — kotlin_types + // entries WIN over the auto-derived callback FQNs. + let callback_fqns = self.collect_kotlin_callback_fqns(registry); + let mut merged_types = KotlinTypeMap::new(); + for (k, v) in callback_fqns.iter() { + merged_types = merged_types.add(k, v.clone()); + } + for (k, v) in kotlin_types.iter() { + merged_types = merged_types.add(k, v.clone()); + } + + let mut written = Vec::new(); + for handle in handles { + let (package, class_name) = match handle.kotlin_fqn.rsplit_once('.') { + Some((p, c)) => (p.to_string(), c.to_string()), + None => (String::new(), handle.kotlin_fqn.to_string()), + }; + // The typed-handle's Rust type-key is always required — it + // identifies which param of each `.method(...)` entry becomes + // `this`. Even with no methods declared we resolve it (cheap) + // so the wrapper API stays uniform. + let rust_key = rust_key_for_fqn(self, handle.kotlin_fqn) + .unwrap_or_else(|| { + panic!( + "write_typed_handles: kotlin_fqn `{}` is not registered via \ + JniExt::kotlin_type_fqn — required to identify the typed \ + handle's Rust type-key for promoted-method param matching.", + handle.kotlin_fqn + ) + }) + .to_string(); + let file = KotlinFile { + contents: render_typed_handle_source( + self, + &package, + &class_name, + handle.rust_doc, + handle.instance_methods, + handle.companion_methods, + &rust_key, + registry, + &merged_types, + ), + package, + class_name, + }; + written.push(file.write(output_dir)?); + } + Ok(written) + } + + /// Return the `` map for every + /// `impl Fn(args)` type the Registry has resolved. Use this to merge + /// into a `KotlinTypeMap` consumed by the aggregated-interface + /// generator (so it can refer to callbacks by their Kotlin FQN). + pub(crate) fn collect_kotlin_callback_fqns(&self, registry: &Registry) -> KotlinTypeMap { + let mut map = KotlinTypeMap::new(); + let mut seen: HashSet = HashSet::new(); + for buckets in [®istry.input_types, ®istry.output_types] { + for bucket in buckets.iter() { + for (key, slot) in bucket { + if slot.is_none() { + continue; + } + if !seen.insert(key.clone()) { + continue; + } + let ty = key.to_type(); + if let Some(args) = extract_fn_trait_args(&ty) { + // Re-use the single source of truth for callback + // FQN derivation — same closure-mangled name the + // converter dispatcher stamps into metadata. + let fqn = self.auto_callback_fqn(&args); + map = map.add(key.as_str(), fqn); + } + } + } + } + // Merge in plugin-supplied extra mappings (e.g. data-class FQNs + // that aren't reachable from impl-Fn types). + for (rust_canon, fqn) in &self.kotlin_type_fqns { + map = map.add(rust_canon.as_str(), fqn.clone()); + } + map + } +} + +fn build_callback_kotlin_file( + ext: &JniExt, + args: &[syn::Type], + registry: &Registry, +) -> KotlinFile { + let name = derive_callback_name(args); + let class_name = ext.mangle_callback(&name); + let package = ext.kotlin_callback_package.clone(); + + // Resolve each arg's Kotlin type by reading the output-direction + // entry's metadata — callback args flow inverse to the callback + // (Rust produces them, Java consumes them). Fall back to the bare + // last-segment ident when the metadata is missing (matches today's + // behavior; preserves the dead-stub compile path). + let mut params: Vec = Vec::new(); + let mut used_fqns: BTreeSet = BTreeSet::new(); + for (i, arg) in args.iter().enumerate() { + let kotlin_ty = registry + .output_entry(arg) + .and_then(|e| e.metadata.kotlin_name.clone()) + .or_else(|| { + if let syn::Type::Path(tp) = arg { + if let Some(last) = tp.path.segments.last() { + return Some(last.ident.to_string()); + } + } + None + }) + .unwrap_or_else(|| "Any".to_string()); + let short = register_fqn(&kotlin_ty, &mut used_fqns); + let optional_suffix = if is_option_type(arg) { "?" } else { "" }; + params.push(format!(" p{i}: {short}{optional_suffix},")); + } + + let contents = templates::callback::render_kotlin_interface( + &package, + &class_name, + ¶ms, + &used_fqns, + ); + KotlinFile { + package, + class_name, + contents, + } +} + +/// Derive the auto-callback short Kotlin name for an `impl Fn(args)` +/// signature. Always starts with the hardcoded `"On"` and appends each +/// concatenated parameter type Rust short idents + `"Callback"` suffix +/// (`Fn(Query)` → `"QueryCallback"`, `Fn(Reply)` → `"ReplyCallback"`, +/// `Fn(K, V)` → `"KVCallback"`, `Fn()` → `"Callback"`). The result +/// feeds [`JniExt::mangle_callback`] before the FQN is qualified +/// against [`JniExt::kotlin_callback_package`]. +pub(crate) fn derive_callback_name(args: &[syn::Type]) -> String { + let mut s = String::new(); + for a in args { + s.push_str(&type_short_ident(a)); + } + s.push_str("Callback"); + s +} + +fn type_short_ident(ty: &syn::Type) -> String { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + return last.ident.to_string(); + } + } + "Unknown".into() +} + +fn is_option_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(tp) = ty { + if let Some(last) = tp.path.segments.last() { + return last.ident == "Option"; + } + } + false +} + +/// `true` if `ty` is `Option<&T>` or `Option<&mut T>` (any inner T). +/// Mirrors `option_inner_ref_mutability` in `jni_ext.rs` — kept here too +/// to avoid a cross-module helper just for one call site. +fn is_option_ref(ty: &syn::Type) -> bool { + let syn::Type::Path(tp) = ty else { return false }; + let Some(seg) = tp.path.segments.last() else { return false }; + if seg.ident != "Option" { + return false; + } + let syn::PathArguments::AngleBracketed(ab) = &seg.arguments else { return false }; + let Some(syn::GenericArgument::Type(inner)) = ab.args.first() else { return false }; + matches!(inner, syn::Type::Reference(_)) +} + +/// Render the Kotlin type for a closeable handle reached through the +/// folded [`CloseStrategy`] layers, given the leaf typed-handle short +/// name (e.g. `"ZKeyExpr"`): `Direct → "ZKeyExpr"`, +/// `Nullable(inner) → "?"`, `Iterable(inner) → "List<>"`. +fn render_handle_type(strategy: &crate::jni::jni_ext::CloseStrategy, leaf: &str) -> String { + use crate::jni::jni_ext::CloseStrategy::*; + match strategy { + Direct => leaf.to_string(), + Nullable(inner) => format!("{}?", render_handle_type(inner, leaf)), + Iterable(inner) => format!("List<{}>", render_handle_type(inner, leaf)), + } +} + +/// Render the Kotlin `close()` expression for a handle `receiver` through +/// the folded [`CloseStrategy`] layers. Fresh lambda variable per nesting +/// level avoids `it` shadowing; the common single-layer cases are +/// special-cased for readable output (`x?.close()`, `x.forEach { it.close() }`). +fn render_handle_close(strategy: &crate::jni::jni_ext::CloseStrategy, receiver: &str) -> String { + use crate::jni::jni_ext::CloseStrategy::*; + fn go(strategy: &crate::jni::jni_ext::CloseStrategy, receiver: &str, depth: usize) -> String { + match strategy { + Direct => format!("{receiver}.close()"), + Nullable(inner) => match &**inner { + Direct => format!("{receiver}?.close()"), + _ => { + let v = format!("e{depth}"); + format!("{receiver}?.let {{ {v} -> {} }}", go(inner, &v, depth + 1)) + } + }, + Iterable(inner) => { + let v = format!("e{depth}"); + format!("{receiver}.forEach {{ {v} -> {} }}", go(inner, &v, depth + 1)) + } + } + } + go(strategy, receiver, 0) +} + +fn register_fqn(fqn: &str, used: &mut BTreeSet) -> String { + if fqn.contains('.') { + used.insert(fqn.to_string()); + fqn.rsplit('.').next().unwrap_or(fqn).to_string() + } else { + fqn.to_string() + } +} + +// ── Safe-wrapper emitters ────────────────────────────────────────────── + +/// One generated Kotlin `enum class` source — variants in +/// SCREAMING_SNAKE_CASE, each carrying the Rust discriminant as a +/// `val value: Int`, plus a `fromInt(value: Int)` companion. Mirrors +/// the hand-written `io.zenoh.qos.Priority` shape so adapter code that +/// already speaks the `.value` / `.fromInt(...)` idiom keeps working. +fn render_enum_source( + ext: &JniExt, + package: &str, + class_name: &str, + item_enum: &syn::ItemEnum, + instance_methods: &[MethodEntry], + companion_methods_in: &[MethodEntry], + registry: &Registry, + kotlin_types: &KotlinTypeMap, +) -> String { + assert!( + instance_methods.is_empty(), + "render_enum_source: `{class_name}` has `.method(...)` entries but instance \ + methods on `enum_class`-declared types are not supported yet — declare them \ + as `.companion_method(...)` for now", + ); + // Same discriminant source of truth the Rust `jint → variant` decode + // uses, so Kotlin `value(N)` and the generated decode agree. + let variants: Vec<(String, i64)> = crate::util::enum_discriminant_values(item_enum) + .into_iter() + .map(|(ident, value)| { + (crate::util::camel_to_screaming_snake(&ident.to_string()), value) + }) + .collect(); + + let mut imports: BTreeSet = BTreeSet::new(); + let mut companion_methods = String::new(); + for entry in companion_methods_in { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_enum_source: `{class_name}` promotes function `{}` \ + which is not present in `registry.functions` — check the spelling against \ + the matching `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + let (block, _kind) = render_wrapper_fn( + ext, + item_fn, + registry, + kotlin_types, + &mut imports, + None, + entry.kotlin_name_override.as_deref(), + ) + .unwrap_or_else(|| { + panic!( + "render_enum_source: `{class_name}` promotes function `{}` \ + but its parameter types couldn't be Kotlin-resolved — verify that all \ + non-opaque parameter types are registered in `kotlin_types`.", + entry.rust_ident, + ) + }); + if !companion_methods.is_empty() { + companion_methods.push('\n'); + } + companion_methods.push_str(&block); + companion_methods.push('\n'); + } + + let mut import_list: Vec = imports + .iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != package + }) + .cloned() + .collect(); + import_list.sort(); + import_list.dedup(); + + let mut s = String::new(); + s.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !package.is_empty() { + s.push_str(&format!("package {}\n\n", package)); + } + for imp in &import_list { + s.push_str(&format!("import {}\n", imp)); + } + if !import_list.is_empty() { + s.push('\n'); + } + s.push_str(&format!( + "/** JVM-side surface for the native Rust `{}` enum. */\n", + item_enum.ident + )); + s.push_str(&format!( + "public enum class {}(public val value: Int) {{\n", + class_name + )); + for (i, (name, value)) in variants.iter().enumerate() { + let sep = if i + 1 == variants.len() { ";" } else { "," }; + s.push_str(&format!(" {}({}){}\n", name, value, sep)); + } + s.push('\n'); + s.push_str(" public companion object {\n"); + // `@JvmStatic` exposes `fromInt` as a real static method on the enum + // class itself (rather than only on the `Companion` nested class). The + // generated struct-encoder calls it via `env.call_static_method`, which + // wouldn't find a companion-only method. + s.push_str(&format!( + " @JvmStatic\n public fun fromInt(value: Int): {} = entries.first {{ it.value == value }}\n", + class_name + )); + if !companion_methods.is_empty() { + s.push('\n'); + for line in companion_methods.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + } + s.push_str(" }\n"); + s.push_str("}\n"); + s +} + +/// One generated Kotlin `data class` (or `@JvmInline value class` when +/// `value_class` is set) source for a `data_class` / +/// `value_class`-declared Rust struct. +fn render_data_class_source( + ext: &JniExt, + package: &str, + class_name: &str, + item_struct: &syn::ItemStruct, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + instance_methods: &[MethodEntry], + companion_methods_in: &[MethodEntry], + throwable: bool, + value_class: bool, + rust_key: &str, +) -> String { + assert!( + !(value_class && !instance_methods.is_empty()), + "render_data_class_source: `{class_name}` is a `value_class` and has \ + `.method(...)` entries; instance methods on value classes aren't supported yet \ + — declare them as `.companion_method(...)` for now", + ); + let fields_named = match &item_struct.fields { + syn::Fields::Named(n) => &n.named, + _ => { + panic!( + "render_data_class_source: struct `{}` must use named fields to map onto Kotlin data class properties", + item_struct.ident + ) + } + }; + if value_class { + assert!( + !throwable, + "render_data_class_source: `{}` is registered as both \ + `value_class` and `throwable` — @JvmInline value \ + classes cannot extend `Exception`. Drop `.throwable()` or \ + switch to `data_class`.", + item_struct.ident + ); + assert!( + fields_named.len() == 1, + "render_data_class_source: `value_class` requires \ + struct `{}` to have exactly one field; found {}. Use \ + `data_class` for multi-field structs.", + item_struct.ident, + fields_named.len() + ); + } + + let mut imports: BTreeSet = BTreeSet::new(); + let mut field_lines: Vec = Vec::new(); + // Track per-field destructible (name, folded close strategy) so the + // bottom emitter can produce a matching `close()` body for each. + let mut destructible_fields: Vec<(String, crate::jni::jni_ext::CloseStrategy)> = Vec::new(); + for field in fields_named { + let field_ident = field.ident.as_ref().unwrap_or_else(|| { + panic!( + "render_data_class_source: struct `{}` has an unnamed field in named-fields context", + item_struct.ident + ) + }); + let kotlin_field_name = snake_to_camel(&field_ident.to_string()); + // When the class extends Exception (throwable), the `message` + // field shadows `Exception.message` — Kotlin requires `override`. + let override_prefix = if throwable && kotlin_field_name == "message" { + "override " + } else { + "" + }; + + // Closeable native-handle field: both the typed Kotlin type + // (`ZKeyExpr?`, `List`, …) and the `close()` expression + // are derived from the folded `HandleInfo` the type-unfolding + // mechanism propagated onto this field's converter metadata — + // instead of a syntactic `Option` peel. The struct + // encoder/decoder in jni_ext.rs bridges the JVM handle object to + // the per-field jlong-wired converter. + let field_handle = registry + .output_entry(&field.ty) + .and_then(|e| e.metadata.handle.clone()) + .or_else(|| { + registry + .input_entry(&field.ty) + .and_then(|e| e.metadata.handle.clone()) + }); + if let Some(h) = field_handle.filter(|h| h.owned) { + let fqn = ext + .kotlin_type_fqns + .iter() + .find(|(k, _)| k == &h.leaf_key) + .map(|(_, v)| v.clone()) + .unwrap_or_else(|| { + panic!( + "render_data_class_source: handle field `{}.{}` leaf `{}` has no \ + Kotlin FQN registered (ptr_class)", + item_struct.ident, field_ident, h.leaf_key + ) + }); + let short = register_fqn(&fqn, &mut imports); + field_lines.push(format!( + " {override_prefix}val {kotlin_field_name}: {},", + render_handle_type(&h.strategy, &short) + )); + destructible_fields.push((kotlin_field_name, h.strategy)); + continue; + } + + let kotlin_ty = registry + .output_entry(&field.ty) + .and_then(|e| e.metadata.kotlin_name.clone()) + .or_else(|| registry.input_entry(&field.ty).and_then(|e| e.metadata.kotlin_name.clone())) + .unwrap_or_else(|| { + panic!( + "render_data_class_source: field `{}.{}` has no Kotlin type mapping; register converters before declaring data_class", + item_struct.ident, + field_ident + ) + }); + let short = register_fqn(&kotlin_ty, &mut imports); + // `Option` whose wire is a JNI primitive (jlong/jint/jboolean/…) + // and that *isn't* an opaque handle (handled above) is encoded by + // the struct emitter as the bare primitive with a sentinel for + // `None` (0 / 0.0 / false). The Kotlin field must match that JVM + // slot: declare it non-nullable so the constructor signature + // stays primitive (`J` vs `Ljava/lang/Long;`). Nullable boxing + // for non-handle primitives would require generator-side changes + // in `struct_output_body` to `Long.valueOf(...)`. + let wire = registry + .output_entry(&field.ty) + .map(|e| e.destination.clone()); + let primitive_wire = wire + .as_ref() + .map(|w| crate::jni::jni_ext::is_jni_primitive(w)) + .unwrap_or(false); + let optional_suffix = if is_option_type(&field.ty) && !primitive_wire { "?" } else { "" }; + field_lines.push(format!(" {override_prefix}val {kotlin_field_name}: {short}{optional_suffix},")); + } + + let mut instance_body = String::new(); + for entry in instance_methods { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_data_class_source: `{class_name}` promotes function `{}` \ + which is not present in `registry.functions` — check the spelling against \ + the matching `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + let (block, kind) = render_wrapper_fn( + ext, + item_fn, + registry, + kotlin_types, + &mut imports, + Some(rust_key), + entry.kotlin_name_override.as_deref(), + ) + .unwrap_or_else(|| { + panic!( + "render_data_class_source: `{class_name}` promotes function `{}` \ + but its parameter types couldn't be Kotlin-resolved — verify that all \ + non-opaque parameter types are registered in `kotlin_types`.", + entry.rust_ident, + ) + }); + if kind != MethodKind::Instance { + panic!( + ".method({}) on `{class_name}`: the function's first parameter doesn't match \ + the class's Rust type ({rust_key}) — declare it as `.companion_method(...)` \ + if it isn't an instance method.", + entry.rust_ident, + ); + } + if !instance_body.is_empty() { + instance_body.push('\n'); + } + instance_body.push_str(&block); + instance_body.push('\n'); + } + + let mut companion_methods = String::new(); + for entry in companion_methods_in { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_data_class_source: `{class_name}` promotes function `{}` \ + which is not present in `registry.functions` — check the spelling against \ + the matching `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + let (block, _kind) = render_wrapper_fn( + ext, + item_fn, + registry, + kotlin_types, + &mut imports, + None, + entry.kotlin_name_override.as_deref(), + ) + .unwrap_or_else(|| { + panic!( + "render_data_class_source: `{class_name}` promotes function `{}` \ + but its parameter types couldn't be Kotlin-resolved — verify that all \ + non-opaque parameter types are registered in `kotlin_types`.", + entry.rust_ident, + ) + }); + if !companion_methods.is_empty() { + companion_methods.push('\n'); + } + companion_methods.push_str(&block); + companion_methods.push('\n'); + } + + // Wrapper methods emitted into subpackages still call the centralized + // Native object anchored at the base package. + if package != ext.package && (!instance_body.is_empty() || !companion_methods.is_empty()) { + imports.insert(format!("{}.{}", ext.package, ext.jni_native_class_name())); + } + + let mut import_list: Vec = imports + .iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != package + }) + .cloned() + .collect(); + import_list.sort(); + import_list.dedup(); + + let mut s = String::new(); + s.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !package.is_empty() { + s.push_str(&format!("package {}\n\n", package)); + } + for imp in &import_list { + s.push_str(&format!("import {}\n", imp)); + } + if !import_list.is_empty() { + s.push('\n'); + } + if value_class { + assert!( + destructible_fields.is_empty(), + "render_data_class_source: `value_class` struct `{}` \ + has a destructible native-handle field — value classes can \ + only express one inline-erased payload, not the \ + `AutoCloseable` + `close()` contract a handle field needs. \ + Use `data_class` for handle-bearing wrappers.", + item_struct.ident + ); + // Single line is enforced by the upstream `fields_named.len() == 1` + // assertion; strip the data-class formatting (leading indent and + // trailing comma) so the primary constructor reads cleanly. + let only = field_lines[0] + .trim_start() + .trim_end_matches(',') + .to_string(); + s.push_str("@JvmInline\n"); + s.push_str(&format!("public value class {}({})", class_name, only)); + if companion_methods.is_empty() { + s.push('\n'); + } else { + s.push_str(" {\n"); + s.push_str(" public companion object {\n"); + for line in companion_methods.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + s.push_str(" }\n"); + s.push_str("}\n"); + } + } else { + s.push_str(&format!("public data class {}(\n", class_name)); + for line in &field_lines { + s.push_str(line); + s.push('\n'); + } + // Supertype clause. `Exception(...)` (a class) and `AutoCloseable` + // (an interface) stack — Kotlin allows at most one class super + any + // interfaces. `: Exception(message)` picks the field literally named + // `message` to forward to Exception's message slot; falls back to + // `: Exception()` when no such field exists (data-class auto-toString + // still surfaces the structured fields). + let exception_clause: Option = if throwable { + let has_message = fields_named.iter().any(|f| { + f.ident + .as_ref() + .map(|i| i.to_string() == "message") + .unwrap_or(false) + }); + Some(if has_message { + "Exception(message)".to_string() + } else { + "Exception()".to_string() + }) + } else { + None + }; + let supertypes: Vec = match (&exception_clause, !destructible_fields.is_empty()) { + (Some(e), true) => vec![e.clone(), "AutoCloseable".to_string()], + (Some(e), false) => vec![e.clone()], + (None, true) => vec!["AutoCloseable".to_string()], + (None, false) => vec![], + }; + if supertypes.is_empty() { + s.push_str(") {\n"); + } else { + s.push_str(&format!(") : {} {{\n", supertypes.join(", "))); + } + if !destructible_fields.is_empty() { + // `close()` walks every destructible field via its folded close + // strategy. `JNINativeHandle.close()` is idempotent + // (Cleaner.Cleanable.clean() invokes exactly once), so calling + // this multiple times — or alongside the cleaner's own firing on + // GC — is safe. NOTE: `data class` copy() shares the handle + // reference between copies; if you intend to close independently, + // don't copy this class. + s.push_str(" override fun close() {\n"); + for (fname, strategy) in &destructible_fields { + s.push_str(&format!(" {}\n", render_handle_close(strategy, fname))); + } + s.push_str(" }\n\n"); + } + if !instance_body.is_empty() { + for line in instance_body.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + s.push('\n'); + } + s.push_str(" public companion object {\n"); + if !companion_methods.is_empty() { + for line in companion_methods.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + } + s.push_str(" }\n"); + s.push_str("}\n"); + } + s +} + +fn render_data_class_aliases_source(package: &str, aliases: &[(String, String)]) -> String { + let mut pairs = aliases.to_vec(); + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + pairs.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); + + let mut s = String::new(); + s.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !package.is_empty() { + s.push_str(&format!("package {}\n\n", package)); + } + s.push_str("// Compatibility aliases for legacy un-mangled data-class references.\n"); + for (legacy, current) in pairs { + s.push_str(&format!("public typealias {} = {}\n", legacy, current)); + } + s +} + +fn strip_legacy_jni_native_data_classes( + output_dir: &Path, + package: &str, + _rust_names: &[String], +) -> Result<(), WriteKotlinError> { + let jni_native_path = output_dir + .join(package.replace('.', "/")) + .join("JNINative.kt"); + if !jni_native_path.exists() { + return Ok(()); + } + + let source = std::fs::read_to_string(&jni_native_path)?; + let lines: Vec<&str> = source.lines().collect(); + let Some(object_start) = lines + .iter() + .position(|line| line.trim_start().starts_with("internal object JNINative {")) + else { + return Ok(()); + }; + + let mut filtered: Vec = Vec::new(); + for line in &lines[..object_start] { + let trimmed = line.trim_start(); + if trimmed.starts_with("package ") + || trimmed.starts_with("import ") + || trimmed.starts_with("//") + || trimmed.is_empty() + { + filtered.push((*line).to_string()); + } + } + for line in &lines[object_start..] { + filtered.push((*line).to_string()); + } + + let mut out = filtered.join("\n"); + out.push('\n'); + if out != source { + std::fs::write(jni_native_path, out)?; + } + Ok(()) +} + +/// Render one typed-handle Kotlin source file. Pure-shell form (with +/// the closure `|n| format!("{n}ViaJNI")` installed via +/// [`JniExt::kotlin_fun_name_mangle`]): +/// +/// ```kotlin +/// public class JNIFoo(initialPtr: Long) : NativeHandle(initialPtr) { +/// public fun free() = free { freePtrViaJNI(it) } +/// private external fun freePtrViaJNI(ptr: Long) +/// } +/// ``` +/// +/// When `promoted_functions` is non-empty, one extra instance method is +/// appended per `#[prebindgen]` fn — the matching opaque first param +/// (Rust type-key = `promoted_rust_key`) is dropped from the Kotlin +/// signature, and its `withPtr` / `consume` wrapper uses the +/// inherited [`NativeHandle`] scope. +/// +/// The free-pointer extern name is built as +/// ``. Kotlin/JVM's JNI name mangler binds it +/// to the matching `Java___` +/// extern on the Rust side (the auto-generated destructor). +fn render_typed_handle_source( + ext: &JniExt, + package: &str, + class_name: &str, + rust_doc_name: &str, + instance_methods: &[MethodEntry], + companion_methods: &[MethodEntry], + promoted_rust_key: &str, + registry: &Registry, + kotlin_types: &KotlinTypeMap, +) -> String { + // Build method bodies first so we can collect imports up front. + // Two buckets — instance methods land in the class body; companion + // methods are wrapped in a `companion object { ... }` block. All + // promoted wrappers dispatch into the centralized Native object; + // no per-class `external fun` declarations are emitted here. + let mut imports: BTreeSet = BTreeSet::new(); + let mut instance_body = String::new(); + let mut companion_body = String::new(); + for entry in instance_methods { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_typed_handle_source: `{class_name}` promotes function `{}` \ + which is not present in `registry.functions` — check the spelling against \ + the matching `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + let (block, kind) = render_wrapper_fn( + ext, + item_fn, + registry, + kotlin_types, + &mut imports, + Some(promoted_rust_key), + entry.kotlin_name_override.as_deref(), + ) + .unwrap_or_else(|| { + panic!( + "render_typed_handle_source: `{class_name}` promotes function `{}` \ + but its parameter types couldn't be Kotlin-resolved — verify that all \ + non-opaque parameter types are registered in `kotlin_types`.", + entry.rust_ident, + ) + }); + if kind != MethodKind::Instance { + panic!( + ".method({}) on `{class_name}`: the function's first parameter doesn't match \ + the class's Rust type ({promoted_rust_key}) — declare it as `.companion_method(...)` \ + if it isn't an instance method.", + entry.rust_ident, + ); + } + if !instance_body.is_empty() { + instance_body.push('\n'); + } + for line in block.lines() { + if line.is_empty() { + instance_body.push('\n'); + } else { + instance_body.push_str(line); + instance_body.push('\n'); + } + } + } + for entry in companion_methods { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_typed_handle_source: `{class_name}` promotes function `{}` \ + which is not present in `registry.functions` — check the spelling against \ + the matching `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + let (block, _kind) = render_wrapper_fn( + ext, + item_fn, + registry, + kotlin_types, + &mut imports, + None, + entry.kotlin_name_override.as_deref(), + ) + .unwrap_or_else(|| { + panic!( + "render_typed_handle_source: `{class_name}` promotes function `{}` \ + but its parameter types couldn't be Kotlin-resolved — verify that all \ + non-opaque parameter types are registered in `kotlin_types`.", + entry.rust_ident, + ) + }); + if !companion_body.is_empty() { + companion_body.push('\n'); + } + for line in block.lines() { + if line.is_empty() { + companion_body.push('\n'); + } else { + companion_body.push_str(line); + companion_body.push('\n'); + } + } + } + + let native_handle_class = ext.mangle_harness("NativeHandle"); + let native_handle_fqn = if ext.package.is_empty() { + native_handle_class.clone() + } else { + format!("{}.{}", ext.package, native_handle_class) + }; + // Typed-handle classes emitted into subpackages still extend and call + // helpers on the base-package NativeHandle and JNINative objects. + if package != ext.package { + imports.insert(native_handle_fqn); + if !instance_methods.is_empty() || !companion_methods.is_empty() { + imports.insert(format!("{}.{}", ext.package, ext.jni_native_class_name())); + } + } + + // Imports filtered the same way as render_kotlin_interface — drop + // entries whose package matches our own (no need to import locals). + let mut import_list: Vec = imports + .iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != package + }) + .cloned() + .collect(); + import_list.sort(); + import_list.dedup(); + + let mut s = String::new(); + s.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !package.is_empty() { + s.push_str(&format!("package {}\n\n", package)); + } + if !import_list.is_empty() { + // Exception and cross-package helper imports are included in + // `import_list`; emit them even when this class has no promoted + // methods (e.g. a pure typed handle shell in a subpackage). + for imp in &import_list { + s.push_str(&format!("import {}\n", imp)); + } + s.push('\n'); + } + let free_extern = ext.mangle_fun("freePtr"); + s.push_str(&format!( + "/** Typed [{native_handle_class}] for a native Zenoh `{}`. */\n", + rust_doc_name + )); + // The concrete subclass owns its own lifecycle: it is `AutoCloseable`, + // registers a `Cleaner` action, and that action calls its own + // `@JvmStatic external freePtr` directly. The base class stays minimal + // (pointer + lock only) and knows nothing about freeing. The cleanup + // `Cleanup` references only the detached `state` holder + the static + // `freePtr`, never `this`, so it can't pin the handle (which would + // stop the cleaner from ever firing). + s.push_str(&format!( + "public class {class_name}(initialPtr: Long) : \ + {native_handle_class}(initialPtr), AutoCloseable {{\n", + )); + s.push_str(&format!( + " private val cleanable: java.lang.ref.Cleaner.Cleanable =\n \ + {native_handle_class}.CLEANER.register(this, Cleanup(state))\n\n" + )); + // `Cleaner.Cleanable.clean()` runs the action exactly once — whether + // invoked here or by the cleaner thread on GC — then deregisters, so + // explicit close() and GC cleanup can't double-free. + s.push_str(" override fun close() = cleanable.clean()\n\n"); + s.push_str(&format!( + " private class Cleanup(private val state: {native_handle_class}.State) : Runnable {{\n \ + override fun run() = state.freeOnce {{ {class_name}.{free_extern}(it) }}\n }}\n" + )); + if !instance_body.is_empty() { + s.push('\n'); + for line in instance_body.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + } + // Companion object always exists — at minimum it carries the + // `@JvmStatic external fun freePtr(ptr: Long)` called by `Cleanup` + // above. Promoted-method bodies (e.g. typed factory functions) follow. + s.push('\n'); + s.push_str(" public companion object {\n"); + s.push_str(&format!( + " @JvmStatic\n external fun {free_extern}(ptr: Long)\n", + )); + if !companion_body.is_empty() { + s.push('\n'); + for line in companion_body.lines() { + if line.is_empty() { + s.push('\n'); + } else { + s.push_str(" "); + s.push_str(line); + s.push('\n'); + } + } + } + s.push_str(" }\n"); + s.push_str("}\n"); + s +} + +/// Emit the package-level wrapper file: one safe top-level wrapper per +/// `#[prebindgen]` fn whose name is in `promoted` (i.e. listed in +/// `package_methods.methods`). Each wrapper delegates to the centralized +/// Native object's matching `external fun`. Opaque-handle parameters +/// (detected via the input converter returning `OwnedObject`) become +/// `NativeHandle`; the wrapper body nests `withPtr` / `consume` per the +/// syntactic shape. Non-opaque parameters pass through with the Kotlin +/// type from `kotlin_types`. +fn render_jni_package_source( + ext: &JniExt, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + functions: &[MethodEntry], + package: &str, +) -> String { + // Start with the auto-derived callback FQNs and let user-provided + // entries WIN — the user (build.rs) may need to override e.g. + // `impl Fn(Query)` to point at a hand-written + // `JNIQueryCallback` instead of the auto-derived default. + let callback_fqns = ext.collect_kotlin_callback_fqns(registry); + let mut merged_types = KotlinTypeMap::new(); + for (k, v) in callback_fqns.iter() { + merged_types = merged_types.add(k, v.clone()); + } + for (k, v) in kotlin_types.iter() { + merged_types = merged_types.add(k, v.clone()); + } + + let mut imports: BTreeSet = BTreeSet::new(); + let mut body = String::new(); + + for entry in functions { + let (item_fn, _loc) = registry.functions.get(&entry.rust_ident).unwrap_or_else(|| { + panic!( + "render_jni_package_source: function `{}` registered via .function(...) is \ + not in the prebindgen registry — check the spelling against the matching \ + `#[prebindgen]` Rust fn name.", + entry.rust_ident, + ) + }); + // Top-level wrappers never carry a `promoted_handle`, so the + // returned [`MethodKind`] is always `Instance` and can be + // discarded — there is no companion-object emission here. + if let Some((block, _kind)) = render_wrapper_fn( + ext, + item_fn, + registry, + &merged_types, + &mut imports, + None, + entry.kotlin_name_override.as_deref(), + ) { + body.push_str(&block); + body.push('\n'); + } + } + + let mut out = String::new(); + out.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !package.is_empty() { + out.push_str(&format!("package {}\n\n", package)); + } + // Exception imports (if any) are added to `imports` by the per-wrapper + // `@Throws` emission, so no error class is hardcoded here. + for imp in &imports { + out.push_str(&format!("import {}\n", imp)); + } + if !ext.package.is_empty() { + out.push_str(&format!("import {}.{}\n", ext.package, ext.jni_native_class_name())); + } + out.push('\n'); + out.push_str(&body); + out +} + +/// Render the centralized `internal object ` holder: +/// one `external fun` per `#[prebindgen]` function, at the JNI **wire** +/// level. Parameter and return types match what the Rust extern +/// receives: +/// * opaque-handle (Borrow/Consume) → jlong → `Long` +/// * `enum_class` → jint → `Int` (call passes `.value`) +/// * `Any` (impl-Into Dispatch) → JObject → `Any` +/// * everything else → entry's high-level Kotlin name +/// Opaque returns become `Long`; every other return uses +/// [`classify_return`]'s `kt_return` (Unit is empty string). No `init` +/// block is emitted — the holder stays free of any wrapper-layer +/// reference; the wrapper-layer call sites are responsible for +/// triggering `System.load` before invoking any extern. +fn render_jni_native_source( + ext: &JniExt, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + declared: &HashSet, + class_name: &str, +) -> String { + let callback_fqns = ext.collect_kotlin_callback_fqns(registry); + let mut merged_types = KotlinTypeMap::new(); + for (k, v) in callback_fqns.iter() { + merged_types = merged_types.add(k, v.clone()); + } + for (k, v) in kotlin_types.iter() { + merged_types = merged_types.add(k, v.clone()); + } + + let mut imports: BTreeSet = BTreeSet::new(); + let mut body = String::new(); + + let mut idents: Vec<&syn::Ident> = registry.functions.keys().collect(); + idents.sort(); + for ident in idents { + if !declared.contains(ident) { + continue; + } + let (item_fn, _loc) = ®istry.functions[ident]; + if let Some(line) = render_extern_decl(ext, item_fn, registry, &mut imports) { + body.push_str(&line); + body.push('\n'); + } + } + + let mut out = String::new(); + out.push_str("// Auto-generated by JniExt — do not edit by hand.\n"); + if !ext.package.is_empty() { + out.push_str(&format!("package {}\n\n", ext.package)); + } + for imp in &imports { + out.push_str(&format!("import {}\n", imp)); + } + out.push('\n'); + out.push_str(&format!("internal object {} {{\n", class_name)); + for line in body.lines() { + if line.is_empty() { + out.push('\n'); + } else { + out.push_str(" "); + out.push_str(line); + out.push('\n'); + } + } + out.push_str("}\n"); + out +} + +/// Render one `external fun (…): ` line +/// at the JNI **wire** level (matches what the Rust extern receives): +/// * opaque-handle (Borrow/Consume) → jlong → `Long` +/// * `enum_class` → jint → `Int` (call passes `.value`) +/// * `Any` (impl-Into Dispatch) → JObject → `Any` +/// * everything else → entry's high-level Kotlin name +/// Opaque returns become `Long`; every other return uses +/// [`classify_return`]'s `kt_return` (Unit is empty string). +/// Returns `None` if any parameter's input converter isn't resolved. +pub(crate) fn render_extern_decl( + ext: &JniExt, + f: &syn::ItemFn, + registry: &Registry, + imports: &mut BTreeSet, +) -> Option { + use std::fmt::Write; + + let rust_name = f.sig.ident.to_string(); + let kt_name = snake_to_camel(&rust_name); + let jni_call = ext.mangle_fun(&kt_name); + + let mut params: Vec<(String, String)> = Vec::new(); + for input in &f.sig.inputs { + let syn::FnArg::Typed(pt) = input else { continue }; + let syn::Pat::Ident(pid) = &*pt.pat else { continue }; + let name = snake_to_camel(&pid.ident.to_string()); + let arg_ty = &*pt.ty; + + let entry = registry.input_entry(arg_ty)?; + + let is_opaque = converter_returns_owned_object(&entry.function.sig.output); + let arg_no_ref: syn::Type = match arg_ty { + syn::Type::Reference(r) => (*r.elem).clone(), + _ => arg_ty.clone(), + }; + // `Option<&Opaque>` crosses the JNI wire as a primitive `jlong` + // with `0` encoding `None`; nullability lives in the safe wrapper + // (`withPtrOrZero`) not the JNI extern. Strip the `?` here so the + // extern signature matches what the JVM will look up. Detection + // uses `metadata.handle.is_some()` because the `Option>` + // converter doesn't return `OwnedObject` directly so the local + // `is_opaque` flag (which checks the return shape) misses it. + let is_opt_ref_opaque = + entry.metadata.handle.is_some() && is_option_ref(arg_ty); + let optional = is_option_type(arg_ty) && !is_opt_ref_opaque; + + let kt_type_raw = if is_opaque || is_opt_ref_opaque { + "Long".to_string() + } else if ext.is_kotlin_enum(&arg_no_ref) { + "Int".to_string() + } else { + entry.metadata.kotlin_name.clone()? + }; + let short = register_fqn(&kt_type_raw, imports); + let suffix = if optional { "?" } else { "" }; + params.push((name, format!("{short}{suffix}"))); + } + + let (kt_return, opaque_ctor) = + classify_return(ext, &f.sig.output, registry, imports)?; + let wire_return = if opaque_ctor.is_some() { + "Long".to_string() + } else { + kt_return + }; + + let formals = params + .iter() + .map(|(n, t)| format!("{n}: {t}")) + .collect::>() + .join(", "); + + let mut line = String::new(); + if wire_return.is_empty() { + write!(&mut line, "external fun {jni_call}({formals})").ok()?; + } else { + write!(&mut line, "external fun {jni_call}({formals}): {wire_return}").ok()?; + } + Some(line) +} + +/// Whether a typed-handle-promoted wrapper is emitted as an instance +/// method on the handle class (the first parameter matched the promoted +/// Rust type-key as a literal `&T` / `T`), or inside the class's +/// `companion object` (no param matched, or the candidate was an +/// `impl Into` Dispatch param — those are not eligible for instance +/// promotion even when the inner `T` matches). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum MethodKind { + Instance, + Companion, +} + +/// Emit a single wrapper function. Returns `None` if the function has +/// a parameter whose Kotlin type isn't registered (in that case we +/// skip the function rather than panicking — the legacy `JNINative.kt` +/// retains the unwrapped external fun so callers still have an +/// escape hatch). +/// +/// When `promoted_handle` is `Some(rust_key)`, the wrapper is emitted +/// as either an instance method or a companion-object method, depending +/// on whether any parameter matches `rust_key`: +/// +/// * **Instance** — the first parameter whose Rust type matches +/// `rust_key` (modulo `&T` borrow) is dropped from the signature, and +/// its `withPtr` / `consume` wrapper uses the inherited +/// [`NativeHandle`] scope (no `.` prefix) so the captured +/// `_ptr` is bound in `this`. Every other parameter is emitted +/// exactly as the `JNIWrappers` top-level form. +/// * **Companion** — no parameter matched (e.g. the fn takes no opaque +/// handle of this type, or it takes an `impl Into` Dispatch param +/// whose inner `T` matches the key — those are intentionally **not** +/// promoted to instance methods). The body is emitted exactly as the +/// `JNIWrappers` top-level form (all params, full Dispatch arm tree, +/// no `this` rewrite); the caller is expected to wrap it inside a +/// `companion object { ... }` block on the typed-handle class. +/// +/// When `promoted_handle` is `None` (top-level `JNIWrappers` emission), +/// the returned kind is always [`MethodKind::Instance`] (no +/// promotion-shape decision is made) and the caller can ignore it. +fn render_wrapper_fn( + ext: &JniExt, + f: &syn::ItemFn, + registry: &Registry, + kotlin_types: &KotlinTypeMap, + imports: &mut BTreeSet, + promoted_handle: Option<&str>, + kotlin_name_override: Option<&str>, +) -> Option<(String, MethodKind)> { + use std::fmt::Write; + + let rust_name = f.sig.ident.to_string(); + // The Kotlin extern in `JNINative` is keyed on the Rust ident + // (`snake_to_camel(rust_name)` → `ext.mangle_fun`). The per-entry + // `.name("...")` override only changes the *user-facing* Kotlin + // wrapper name; the JNI call still has to hit the one extern that + // the Rust extern actually emits. + let default_kt_name = snake_to_camel(&rust_name); + let kt_name = match kotlin_name_override { + Some(n) => n.to_string(), + None => default_kt_name.clone(), + }; + let jni_call = ext.mangle_fun(&default_kt_name); + + // Pre-parse the promoted Rust type-key (if any) so per-param matching + // is whitespace-normalised against the canonical form. + let promoted_key: Option = + promoted_handle.map(|s| TypeKey::parse(s)); + + // Classify each parameter. + struct Param { + kt_name: String, + kt_type: String, + mode: ParamMode, + /// `true` when the param's Rust type is a `enum_class`-declared + /// enum: the high-level Kotlin signature uses the typed enum + /// (`Priority`), but the underlying JNI `external fun` declares + /// the param as `Int` (jint wire). The wrapper bridges the two + /// by passing `.value` at the call site. + as_enum_value: bool, + } + enum ParamMode { + Borrow, // &T opaque-handle → withPtr + Consume, // T opaque-handle → consume + /// `Option<&T>` / `Option<&mut T>` opaque-handle → `withPtrOrZero`. + /// Nullable typed-handle param; the wrapper runs the body under + /// the read lock when the handle is non-null and with `0L` when + /// null. The Rust converter materializes `Option>` + /// and the call site uses `.as_deref()` / `.as_deref_mut()`. + BorrowNullable, + /// `impl Into` (Kotlin `Any`). At runtime the parameter + /// fans out into one arm per declared + /// [`IntoSource`] in `arms`. See + /// [`DispatchArm`] for the arm shape. + Dispatch { arms: Vec }, + PassThrough, + /// Promoted opaque param: identical lock semantics to + /// `Borrow` / `Consume` (the inner bool flag chooses), but the + /// wrapper uses inherited [`NativeHandle`] scope (no + /// `.` prefix) and the param is omitted from the + /// Kotlin signature. Set when `promoted_handle` matches. + PromotedBorrow, + PromotedConsume, + /// Promoted non-opaque param (e.g. `&Hello` on a `data_class` + /// instance method). The Kotlin call site substitutes `this` for + /// the param name — no lock wrapping needed, and the param is + /// dropped from the wrapper signature. Set when `promoted_handle` + /// matches a non-opaque type. + PromotedPassThrough, + } + + // Tracks whether we've already consumed the promoted-handle slot — + // only the first matching param is promoted; any later param of the + // same Rust type stays as a normal Borrow/Consume. + let mut promoted_taken = false; + + let mut params: Vec = Vec::new(); + for input in &f.sig.inputs { + let syn::FnArg::Typed(pt) = input else { continue }; + let syn::Pat::Ident(pid) = &*pt.pat else { continue }; + let name = snake_to_camel(&pid.ident.to_string()); + let arg_ty = &*pt.ty; + + // Strip leading reference for the type-map lookup; the registry's + // input entry is keyed by the param as-written. + let entry = registry.input_entry(arg_ty)?; + // Opaque-handle params surface as the base `JNINativeHandle` (the + // withPtr/consume lock contract lives there). Detection flows from + // the folded `HandleInfo` — present for both `&T` and by-value `T` + // (the `owned` flag is orthogonal to presence) — so it's the same + // source of truth the typed-surface emitters use. + let is_opaque = entry.metadata.handle.is_some(); + + // `Option<&T>` / `Option<&mut T>` for opaque T uses the typed + // handle subclass (not the bare `NativeHandle` base) with a `?` + // suffix, so the call site can call `withPtrOrZero` on the + // nullable receiver. The typed FQN comes from + // `ext.kotlin_type_fqns` (set by `ptr_class`), keyed by + // the handle's `leaf_key`. The `KotlinMeta.kotlin_name` is + // intentionally the value-context name (`"Long"`) for opaque, so + // it can't be used here. + let is_opt_ref_opaque = is_opaque && is_option_ref(arg_ty); + let (kt_type_raw, optional) = if is_opt_ref_opaque { + let h = entry.metadata.handle.as_ref()?; + let fqn = ext + .kotlin_type_fqns + .iter() + .find(|(k, _)| k == &h.leaf_key) + .map(|(_, v)| v.clone())?; + (fqn, true) + } else if is_opaque { + (ext.mangle_harness("NativeHandle"), false) + } else { + // Read the Kotlin name straight off the resolved entry's + // metadata — the rank-N handler that built this converter + // is also the one that derived the Kotlin name (primitives + // from `kotlin_for_wire`, wrappers inherit from inner, + // user-declared decoders from `with_kotlin_name`). + let kt = entry.metadata.kotlin_name.clone()?; + let opt = is_option_type(arg_ty); + (kt, opt) + }; + + // Does this param match the promoted handle's Rust type? + // Strip a leading `&` before comparing; the registered type-key + // is the bare-name form (e.g. `Publisher < 'static >`). + let matches_promoted = if !promoted_taken { + if let Some(pk) = &promoted_key { + let arg_no_ref: syn::Type = match arg_ty { + syn::Type::Reference(r) => (*r.elem).clone(), + _ => arg_ty.clone(), + }; + TypeKey::from_type(&arg_no_ref) == *pk + } else { + false + } + } else { + false + }; + + // Mode: opaque → Borrow/Consume by Rust syntactic shape. + // Non-opaque + `Any` triggers Dispatch — one arm per declared + // `IntoSource`. Everything else (primitives, callbacks, data + // classes) passes through. Promoted variants kick in when this + // param is the matched-and-not-yet-consumed handle slot. + let mode = if is_opaque { + let borrow = matches!(arg_ty, syn::Type::Reference(_)); + if is_opt_ref_opaque { + if !ext.package.is_empty() { + imports.insert(format!("{}.withPtrOrZero", ext.package)); + } + // Nullable borrow — promoted form not supported here (no + // typed-handle subclass to promote against when the param + // type is `T?`). + ParamMode::BorrowNullable + } else if matches_promoted { + promoted_taken = true; + if borrow { ParamMode::PromotedBorrow } else { ParamMode::PromotedConsume } + } else if borrow { + ParamMode::Borrow + } else { + ParamMode::Consume + } + } else if matches_promoted { + // Non-opaque (data/value/enum class) instance-method param: + // drop from the Kotlin signature, substitute `this` at the + // JNI call site. No lock semantics — the JNI side decodes the + // Kotlin instance directly (struct decoder via jobject field + // reflection, value-class projection, enum `.value` etc.). + promoted_taken = true; + ParamMode::PromotedPassThrough + } else if kt_type_raw == "Any" { + let sources = entry.into_sources.as_deref().unwrap_or(&[]); + ParamMode::Dispatch { + arms: build_dispatch_arms(sources, registry, kotlin_types, imports), + } + } else { + ParamMode::PassThrough + }; + + let short = register_fqn(&kt_type_raw, imports); + let suffix = if optional { "?" } else { "" }; + // Strip a leading `&` before the enum check — the `&Priority` + // input converter shares Priority's converter (see the rank-1 + // `& _` arm), and the same `.value` projection applies either + // way at the call site. + let arg_no_ref: syn::Type = match arg_ty { + syn::Type::Reference(r) => (*r.elem).clone(), + _ => arg_ty.clone(), + }; + let as_enum_value = ext.is_kotlin_enum(&arg_no_ref); + params.push(Param { + kt_name: name, + kt_type: format!("{short}{suffix}"), + mode, + as_enum_value, + }); + } + + // A promoted-handle was requested but never matched any param — + // emit as a companion-object method instead of panicking. `.method(...)` + // is a namespace declaration ("this fn lives on the typed-handle + // class"), and the generator chooses between an instance method and + // a companion-object method based on whether any param matched. + let kind = if promoted_handle.is_some() && !promoted_taken { + MethodKind::Companion + } else { + MethodKind::Instance + }; + + // Return type: peel ZResult<...>; detect opaque-handle return. + // `opaque_ctor` is the constructor name to wrap the JNI return + // in (typed FQN short name when registered, else `NativeHandle`). + let (kt_return, opaque_ctor) = classify_return(ext, &f.sig.output, registry, imports)?; + + // Indices of Dispatch-mode params. + let dispatch_indices: Vec = params + .iter() + .enumerate() + .filter_map(|(i, p)| matches!(p.mode, ParamMode::Dispatch { .. }).then_some(i)) + .collect(); + + // Build the JNINative call for a given per-Dispatch arm selection. + // `arm_choice[k]` is the index into the arms list for + // `dispatch_indices[k]`; values are interpreted by the arm itself + // — `Unwrap` arms pass `_ptr`, every other arm passes the + // raw `` (typed handle or non-handle value, untouched). + let build_call = |arm_choice: &[usize]| -> String { + let mut args: Vec = Vec::with_capacity(params.len()); + for (i, p) in params.iter().enumerate() { + let arg = match &p.mode { + ParamMode::Borrow + | ParamMode::Consume + | ParamMode::BorrowNullable + | ParamMode::PromotedBorrow + | ParamMode::PromotedConsume => format!("{}_ptr", p.kt_name), + ParamMode::PromotedPassThrough => { + if p.as_enum_value { + "this.value".to_string() + } else { + "this".to_string() + } + } + ParamMode::Dispatch { arms } => { + let pos = dispatch_indices.iter().position(|&di| di == i).unwrap(); + let arm = &arms[arm_choice[pos]]; + if arm.unwrap_to_ptr { + format!("{}_ptr", p.kt_name) + } else { + p.kt_name.clone() + } + } + ParamMode::PassThrough => { + if p.as_enum_value { + format!("{}.value", p.kt_name) + } else { + p.kt_name.clone() + } + } + }; + args.push(arg); + } + let mut call = format!("{}.{jni_call}({})", ext.jni_native_class_name(), args.join(", ")); + if let Some(ctor) = &opaque_ctor { + call = format!("{ctor}({call})"); + } + call + }; + + // Recurse over Dispatch params; at each level enumerate arms. + fn build_tree( + level: usize, + choice: &mut Vec, + dispatch_indices: &[usize], + params: &[Param], + build_call: &dyn Fn(&[usize]) -> String, + ) -> String { + if level == dispatch_indices.len() { + return build_call(choice); + } + let pi = dispatch_indices[level]; + let arms = match ¶ms[pi].mode { + ParamMode::Dispatch { arms } => arms, + _ => unreachable!("dispatch_indices points only at Dispatch params"), + }; + let name = ¶ms[pi].kt_name; + + // Emit the if/else-if chain over arms, with the final + // `else` carrying the unconditional-pass-through branch. + let mut out = String::new(); + for (k, arm) in arms.iter().enumerate() { + choice.push(k); + let inner = build_tree(level + 1, choice, dispatch_indices, params, build_call); + choice.pop(); + // The lock-scope wrapper (`.withPtr` / `.consume`) lives + // around each NativeHandle-typed arm; non-handle arms + // (`String`, no-runtime-check else) just inline `inner`. + // Lambda capture: `_ptr` for unwrap arms (the inner + // call references it), `_` for typed-handle arms (the + // inner call passes the typed handle directly to JNI). + let capture = if arm.unwrap_to_ptr { + format!("{name}_ptr") + } else { + "_".to_string() + }; + let arm_body = match (&arm.runtime_check, &arm.lock_qual) { + (Some(check), Some(qual)) => format!( + "{prefix} ({name} is {check}) {name}.{qual} {{ {capture} ->\n {inner}\n}}", + prefix = if k == 0 { "if" } else { " else if" }, + ), + (Some(check), None) => format!( + "{prefix} ({name} is {check}) {{\n {inner}\n}}", + prefix = if k == 0 { "if" } else { " else if" }, + ), + (None, _) => { + // Catch-all else branch (no runtime check). + if k == 0 { + // Single unconditional arm (no opaque sources + // declared) — skip the if/else scaffolding. + inner.clone() + } else { + format!(" else {{\n {inner}\n}}") + } + } + }; + out.push_str(&arm_body); + } + out + } + + let mut choice: Vec = Vec::with_capacity(dispatch_indices.len()); + let mut body_expr = build_tree(0, &mut choice, &dispatch_indices, ¶ms, &build_call); + + // Wrap with nested withPtr/consume from innermost to outermost + // for the syntactic-opaque params. Promoted variants drop the + // `.` prefix to use the inherited `NativeHandle` scope. + for p in params.iter().rev() { + match p.mode { + ParamMode::Borrow => { + body_expr = format!( + "{name}.withPtr {{ {name}_ptr ->\n {expr}\n}}", + name = p.kt_name, + expr = body_expr, + ); + } + ParamMode::Consume => { + body_expr = format!( + "{name}.consume {{ {name}_ptr ->\n {expr}\n}}", + name = p.kt_name, + expr = body_expr, + ); + } + ParamMode::BorrowNullable => { + // Nullable typed-handle receiver — `withPtrOrZero` runs + // the block under the read lock when non-null and with + // `0L` when null. + body_expr = format!( + "{name}.withPtrOrZero {{ {name}_ptr ->\n {expr}\n}}", + name = p.kt_name, + expr = body_expr, + ); + } + ParamMode::PromotedBorrow => { + body_expr = format!( + "withPtr {{ {name}_ptr ->\n {expr}\n}}", + name = p.kt_name, + expr = body_expr, + ); + } + ParamMode::PromotedConsume => { + body_expr = format!( + "consume {{ {name}_ptr ->\n {expr}\n}}", + name = p.kt_name, + expr = body_expr, + ); + } + ParamMode::Dispatch { .. } | ParamMode::PassThrough | ParamMode::PromotedPassThrough => {} + } + } + + let _ = ext; // ext no longer needed here — throws comes from registry metadata + let mut out = String::new(); + // `@Throws` is the UNION of every stage every converter the wrapper + // drives can raise: + // * each input parameter's wire-facing converter (its `?` failure + // raises the metadata `throws` exception — framework + // `JniBindingError` by default, or a custom one bound via + // `Some(parse_quote!())` in the input wrapper's + // closure); + // * each pre_stage on that input's chain (value-inspecting throw + // stages — an `input_wrapper` / `output_wrapper` whose closure + // returns a rust type with `Some(parse_quote!())` + // and gets composed onto that type's converter); + // * the return type's output converter and its pre_stages + // (likewise). + // Collected into a `BTreeSet` so the emitted annotation is sorted and + // deterministic; stages/converters with no `throws` metadata + // contribute nothing. + let mut throws_fqns: BTreeSet = BTreeSet::new(); + for input in &f.sig.inputs { + let syn::FnArg::Typed(pt) = input else { continue }; + let arg_ty = &*pt.ty; + if let Some(entry) = registry.input_entry(arg_ty) { + if let Some(fqn) = entry.metadata.throws.clone() { + throws_fqns.insert(fqn); + } + for stage in &entry.pre_stages { + throws_fqns.insert(stage.throws_fqn.clone()); + } + } + } + let return_ty: syn::Type = match &f.sig.output { + syn::ReturnType::Default => syn::parse_quote!(()), + syn::ReturnType::Type(_, ty) => (**ty).clone(), + }; + if let Some(entry) = registry.output_entry(&return_ty) { + if let Some(fqn) = entry.metadata.throws.clone() { + throws_fqns.insert(fqn); + } + for stage in &entry.pre_stages { + throws_fqns.insert(stage.throws_fqn.clone()); + } + } + if !throws_fqns.is_empty() { + let parts: Vec = throws_fqns + .iter() + .map(|fqn| format!("{}::class", register_fqn(fqn, imports))) + .collect(); + let _ = writeln!(out, "@Throws({})", parts.join(", ")); + } + let param_list: Vec = params + .iter() + .filter(|p| !matches!(p.mode, ParamMode::PromotedBorrow | ParamMode::PromotedConsume | ParamMode::PromotedPassThrough)) + .map(|p| format!("{}: {}", p.kt_name, p.kt_type)) + .collect(); + let _ = write!(out, "public fun {kt_name}({})", param_list.join(", ")); + if !kt_return.is_empty() { + let _ = write!(out, ": {kt_return}"); + } + let _ = writeln!(out, " ="); + let _ = writeln!(out, " {body_expr}"); + Some((out, kind)) +} + +/// One arm of an `impl Into` parameter's Java-side dispatch tree. +/// Produced by [`build_dispatch_arms`] from the +/// [`IntoSource`] list the resolver stored on +/// `TypeEntry::into_sources`. +struct DispatchArm { + /// `is ` check, or `None` for the unconditional + /// catch-all arm placed last. Examples: + /// * `Some("JNISession")` — typed-FQN arm; the JNI dispatcher's + /// matching arm does `instanceof io/zenoh/jni/JNISession` and + /// reads the pointer via `.peek()`. Kotlin holds the lock via + /// `.` and passes the typed handle to JNI unchanged. + /// * `Some("NativeHandle")` — generic opaque catch-all for + /// sources whose Kotlin class isn't registered as a typed FQN. + /// The JNI dispatcher's matching arm does `instanceof + /// java.lang.Long` and reads the autoboxed long via + /// `longValue()`; Kotlin unwraps to `Long` via + /// `. { ptr -> ... }` and passes `ptr` (autoboxed). + /// * `None` — final else; emits the JNI call unconditionally on + /// the raw `Any` parameter. Covers non-opaque source kinds + /// (e.g. `String`, `Int`) whose JNI side does its own + /// per-class `instanceof` checks downstream of the wire. + runtime_check: Option, + /// `withPtr` / `consume` — scope qualifier on the typed handle + /// (`is NativeHandle` arms only). `None` for the non-handle + /// catch-all (no lock to acquire). + lock_qual: Option<&'static str>, + /// `true` → JNI receives `_ptr` (the `Long` extracted by + /// `.withPtr`/`.consume`, autoboxed to `java.lang.Long`). + /// `false` → JNI receives the parameter as-is (typed handle for + /// typed-FQN arms, raw value for the catch-all). The two cases + /// pair with the JNI-side `instanceof` shape — `java.lang.Long` + /// vs typed FQN vs whatever non-opaque source class. + unwrap_to_ptr: bool, +} + +/// Translate the resolver-recorded `IntoSource` list into the Kotlin +/// emit's per-arm dispatch shape. Arm ordering matters: typed-FQN +/// opaque arms come first (so they aren't swallowed by the catch-all +/// else), then the final unconditional `else` branch handling every +/// non-opaque source class (`String`, primitives, etc.) — the JNI +/// dispatcher does its own `instanceof` chain on the wire side for +/// those. +/// +/// Opaque sources without a typed FQN are a build-time error in +/// `jobject_to_wire_adapter` (it panics with a registration hint), +/// so this helper never has to emit a generic `is NativeHandle` +/// fallback arm — every opaque source is either typed or rejected. +fn build_dispatch_arms( + sources: &[IntoSource], + registry: &Registry, + kotlin_types: &KotlinTypeMap, + imports: &mut BTreeSet, +) -> Vec { + let mut typed: Vec = Vec::new(); + for src in sources { + let canon = TypeKey::from_type(&src.source_type).as_str().to_string(); + // Only opaque sources need an `is ` arm; the rest + // (String, primitives) fall through to the catch-all else + // where the JNI dispatcher's own per-class `instanceof` chain + // takes over. + let is_opaque = registry + .input_entry(&src.source_type) + .map(|e| converter_returns_owned_object(&e.function.sig.output)) + .unwrap_or(false); + if !is_opaque { + continue; + } + let qual: &'static str = match src.mode { + IntoSourceMode::Borrow => "withPtr", + IntoSourceMode::Consume => "consume", + }; + let fqn = kotlin_types.lookup(&canon).unwrap_or_else(|| { + panic!( + "build_dispatch_arms: opaque source `{}` has no Kotlin FQN registered \ + — register one via `JniExt::kotlin_type_fqn(...)` and ensure the \ + corresponding Kotlin class exists.", + canon + ) + }); + let short = register_fqn(fqn, imports); + typed.push(DispatchArm { + runtime_check: Some(short), + lock_qual: Some(qual), + unwrap_to_ptr: false, + }); + } + + let mut arms = typed; + // Final unconditional else — JNI dispatcher's own `instanceof` + // chain handles non-opaque source classes (String, etc.). + arms.push(DispatchArm { + runtime_check: None, + lock_qual: None, + unwrap_to_ptr: false, + }); + arms +} + + +/// Fall-back Kotlin type derived directly from the JNI wire type. +/// Returns the **non-nullable** Kotlin base name — the use site adds +/// a `?` suffix when the entry's Rust type is `Option<…>` (via +/// [`is_option_type`]), so this helper must not double up. +pub(crate) fn kotlin_for_wire(wire: &syn::Type) -> Option { + if let syn::Type::Path(tp) = wire { + if let Some(last) = tp.path.segments.last() { + let name = last.ident.to_string(); + let kt = match name.as_str() { + "jboolean" => "Boolean", + "jbyte" => "Byte", + "jchar" => "Char", + "jshort" => "Short", + "jint" => "Int", + "jlong" => "Long", + "jfloat" => "Float", + "jdouble" => "Double", + "JString" | "jstring" => "String", + "JByteArray" | "jbyteArray" => "ByteArray", + "JObject" | "jobject" => "Any", + "JClass" => "Any", + _ => return None, + }; + return Some(kt.to_string()); + } + } + None +} + +/// Returns `(kt_return, opaque_ctor)` where: +/// * `kt_return` is the declared Kotlin return type written in the +/// wrapper's signature (empty for `Unit`). +/// * `opaque_ctor` is `Some()` when the return is an +/// opaque-handle type (jlong wire) — `` is the registered +/// typed FQN's short name (e.g. `JNIKeyExpr`) when one is mapped, +/// else `NativeHandle`. The wrapper body uses this to construct +/// the returned object so its runtime class survives downstream +/// `instanceof` checks at the JNI boundary. `None` for non-opaque +/// returns. The declared `kt_return` deliberately stays at the +/// base `NativeHandle` for opaque returns even when a typed FQN +/// exists — minimises caller-side churn (typed instance, upcast +/// declared type). +fn classify_return( + ext: &JniExt, + output: &syn::ReturnType, + registry: &Registry, + imports: &mut BTreeSet, +) -> Option<(String, Option)> { + let ty = match output { + syn::ReturnType::Default => return Some((String::new(), None)), + syn::ReturnType::Type(_, t) => &**t, + }; + let outer_meta = registry.output_entry(ty).map(|e| e.metadata.clone()); + // Unit returns (incl. `ZResult<()>`, whose inner identity rides + // `value_rust_key`) declare no Kotlin return type. + let inner_canon = outer_meta + .as_ref() + .and_then(|m| m.value_rust_key.clone()) + .unwrap_or_else(|| ty.to_token_stream().to_string()); + let inner: syn::Type = syn::parse_str(&inner_canon).unwrap_or_else(|_| ty.clone()); + if crate::util::is_unit(&inner) { + return Some((String::new(), None)); + } + // Opaque-handle return: read the folded `HandleInfo` the type-unfolding + // mechanism propagated onto this return type's converter metadata — + // one source of truth, no shape-specific peeling. The declared return + // type is the concrete typed handle (`AutoCloseable`, so the caller can + // `close()` / `use {}`); `opaque_ctor` is the typed short name the + // wrapper body uses to wrap the jlong so the runtime class survives + // downstream `instanceof` checks. + if let Some(h) = outer_meta.as_ref().and_then(|m| m.handle.clone()) { + let fqn = ext + .kotlin_type_fqns + .iter() + .find(|(k, _)| k == &h.leaf_key) + .map(|(_, v)| v.clone()); + return Some(match fqn { + Some(fqn) => { + let short = register_fqn(&fqn, imports); + (render_handle_type(&h.strategy, &short), Some(short)) + } + // No typed FQN registered — fall back to the base harness class. + None => { + let base = ext.mangle_harness("NativeHandle"); + (base.clone(), Some(base)) + } + }); + } + // Non-opaque: read the Kotlin name straight off the resolved + // output entry's metadata — the rank-N handler propagates + // `ZResult` / `Option` / `Vec` derivations alongside the + // wire, so no peel-and-fallback chain is needed at the use site. + if let Some(out_entry) = registry.output_entry(ty) { + if let Some(kt) = out_entry.metadata.kotlin_name.clone() { + return Some((register_fqn(&kt, imports), None)); + } + } + None +} + +fn snake_to_camel(s: &str) -> String { + let mut out = String::new(); + let mut upper = false; + for c in s.chars() { + if c == '_' { + upper = true; + } else if upper { + out.push(c.to_ascii_uppercase()); + upper = false; + } else { + out.push(c); + } + } + out +} diff --git a/prebindgen-ext/src/jni/mod.rs b/prebindgen-ext/src/jni/mod.rs new file mode 100644 index 00000000..823f5dea --- /dev/null +++ b/prebindgen-ext/src/jni/mod.rs @@ -0,0 +1,20 @@ +//! JNI back-end for the Registry pipeline. +//! +//! [`JniExt`] implements [`crate::core::prebindgen_ext::PrebindgenExt`] +//! (Rust-side conversion bodies) and provides an inherent +//! [`JniExt::write_kotlin`] for emitting all Kotlin output (per-callback +//! fun-interface files, `NativeHandle.kt`, typed-handle classes, +//! `JNIWrappers.kt`). + +pub mod byte_array_helpers; +pub mod jni_binding_error; +pub mod jni_ext; +pub(crate) mod jni_kotlin_ext; +pub(crate) mod templates; +pub mod string_helpers; +pub(crate) mod wire_access; + +pub use byte_array_helpers::{decode_byte_array, encode_byte_array, null_byte_array}; +pub use jni_binding_error::JniBindingError; +pub use jni_ext::JniExt; +pub use string_helpers::{decode_string, encode_string, null_string}; diff --git a/prebindgen-ext/src/jni/string_helpers.rs b/prebindgen-ext/src/jni/string_helpers.rs new file mode 100644 index 00000000..df891520 --- /dev/null +++ b/prebindgen-ext/src/jni/string_helpers.rs @@ -0,0 +1,31 @@ +//! JNI String conversion helpers for use in type converters and runtime code. + +use jni::objects::{JObject, JString}; +use jni::JNIEnv; + +/// Converts a JString into a Rust String. +/// +/// Used by the String <-> JString converter in primitive_builtins and at runtime. +pub fn decode_string(env: &mut JNIEnv, string: &JString) -> Result { + let binding = env + .get_string(string) + .map_err(|err| format!("Error while retrieving JString: {}", err))?; + let value = binding + .to_str() + .map_err(|err| format!("Error decoding JString: {}", err))?; + Ok(value.to_string()) +} + +/// Converts a Rust string-like value into a JNI `JString`. +pub fn encode_string<'local, S: AsRef>( + env: &mut JNIEnv<'local>, + string: S, +) -> Result, String> { + env.new_string(string.as_ref()) + .map_err(|err| format!("Error while encoding JString: {}", err)) +} + +/// Returns a null JNI string handle. +pub fn null_string() -> JString<'static> { + JString::from(JObject::null()) +} diff --git a/prebindgen-ext/src/jni/templates/callback.rs b/prebindgen-ext/src/jni/templates/callback.rs new file mode 100644 index 00000000..16bfcc1e --- /dev/null +++ b/prebindgen-ext/src/jni/templates/callback.rs @@ -0,0 +1,44 @@ +use std::collections::BTreeSet; + +pub(crate) fn render_kotlin_interface( + package: &str, + class_name: &str, + params: &[String], + used_fqns: &BTreeSet, +) -> String { + let mut imports: Vec = used_fqns + .iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != package + }) + .cloned() + .collect(); + imports.sort(); + imports.dedup(); + + let mut out = String::new(); + out.push_str("// Auto-generated by JniExt - do not edit by hand.\n"); + if !package.is_empty() { + out.push_str(&format!("package {}\n\n", package)); + } + for imp in &imports { + out.push_str(&format!("import {}\n", imp)); + } + if !imports.is_empty() { + out.push('\n'); + } + out.push_str(&format!("public fun interface {} {{\n", class_name)); + if params.is_empty() { + out.push_str(" fun run()\n"); + } else { + out.push_str(" fun run(\n"); + for p in params { + out.push_str(p); + out.push('\n'); + } + out.push_str(" )\n"); + } + out.push_str("}\n"); + out +} diff --git a/prebindgen-ext/src/jni/templates/exception.rs b/prebindgen-ext/src/jni/templates/exception.rs new file mode 100644 index 00000000..b245d209 --- /dev/null +++ b/prebindgen-ext/src/jni/templates/exception.rs @@ -0,0 +1,26 @@ +use crate::kotlin::kotlin_ext::KotlinFile; + +pub(crate) fn emit_exception(package: &str, class_name: &str, rust_doc_name: &str) -> KotlinFile { + KotlinFile { + contents: render_exception_source(package, class_name, rust_doc_name), + package: package.to_string(), + class_name: class_name.to_string(), + } +} + +fn render_exception_source(package: &str, class_name: &str, rust_doc_name: &str) -> String { + let mut s = String::new(); + s.push_str("// Auto-generated by JniExt - do not edit by hand.\n"); + if !package.is_empty() { + s.push_str(&format!("package {}\n\n", package)); + } + s.push_str(&format!( + "/** JVM-side surface for the native Rust `{}` error. */\n", + rust_doc_name + )); + s.push_str(&format!( + "public class {}(override val message: String? = null) : Exception()\n", + class_name + )); + s +} diff --git a/prebindgen-ext/src/jni/templates/mod.rs b/prebindgen-ext/src/jni/templates/mod.rs new file mode 100644 index 00000000..54d707d8 --- /dev/null +++ b/prebindgen-ext/src/jni/templates/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod callback; +pub(crate) mod exception; +pub(crate) mod native_handle; diff --git a/prebindgen-ext/src/jni/templates/native_handle.rs b/prebindgen-ext/src/jni/templates/native_handle.rs new file mode 100644 index 00000000..ad2fa21d --- /dev/null +++ b/prebindgen-ext/src/jni/templates/native_handle.rs @@ -0,0 +1,164 @@ +use crate::kotlin::kotlin_ext::KotlinFile; + +pub(crate) fn emit_native_handle( + package: &str, + class_name: &str, + exception_fqn: &str, +) -> KotlinFile { + KotlinFile { + package: package.to_string(), + class_name: class_name.to_string(), + contents: render_native_handle_source(package, class_name, exception_fqn), + } +} + +fn render_native_handle_source(package: &str, class_name: &str, exception_fqn: &str) -> String { + let exc_short = exception_fqn.rsplit('.').next().unwrap_or(exception_fqn); + let exception_pkg = exception_fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + let import_line = if exception_fqn.contains('.') && exception_pkg != package { + format!("import {}\n", exception_fqn) + } else { + String::new() + }; + let body = r#" +__EXCEPTION_IMPORT__ +import java.lang.ref.Cleaner +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * Minimal race-free wrapper around a raw `Box` pointer obtained from + * native code via `Box::into_raw(Box::new(v))`. Pairs the pointer with a + * `ReentrantReadWriteLock` so borrow-style JNI calls run in parallel + * under the read lock and consume/free serialise under the write lock. + * + * The base deliberately knows **nothing** about how to free the pointer: + * `freePtr` and the `AutoCloseable` / `Cleaner` lifecycle live on the + * concrete generator-emitted subclasses (e.g. `ZKeyExpr`), each of which + * statically knows its own destructor. The base is just the pointer + + * lock + the borrow/consume contract used polymorphically by generated + * wrappers (params typed as this base class). + * + * The mutable pointer lives in a detached [State] holder so a subclass's + * `Cleaner` action can reference the state **without** referencing the + * handle instance — a back-reference would pin the handle and the cleaner + * would never fire. + * + * Marked `open` so the generator-emitted typed-handle classes can + * subclass for type safety while inheriting the lock contract. + */ +public open class __CLASS_NAME__(initial: Long) { + /** + * Detached pointer + lock holder. Kept separate from the enclosing + * handle so a subclass cleaner can capture only this (plus its own + * static `freePtr`), never the handle instance. + */ + internal class State(@Volatile var ptr: Long) { + val lock = ReentrantReadWriteLock() + + /** Run [freePtr] exactly once under the write lock if still live. */ + fun freeOnce(freePtr: (Long) -> Unit) = lock.write { + val p = ptr + if (p != 0L) { + ptr = 0L + freePtr(p) + } + } + } + + internal val state = State(initial) + + /** + * Run [block] with the live pointer under the read lock. Throws + * [ZError] if the handle has already been released. Multiple + * concurrent invocations run in parallel; only consume/close are + * serialised against them. + */ + @Throws(ZError::class) + public fun withPtr(block: (Long) -> T): T = state.lock.read { + val p = state.ptr + if (p == 0L) throw ZError("Operation on a closed native handle.") + block(p) + } + + /** + * Consume the pointer: take it under the write lock, run [action] + * with the captured pointer, then null the slot - even if [action] + * throws. Used by the generator-emitted wrappers whose Rust side + * runs `*Box::from_raw(...)` - i.e. by-value `T` opaque-handle + * parameters and `impl Into` arms with `IntoSourceMode::Consume`. + * + * The slot stays valid during [action] so the wrapper can pass the + * typed handle to JNI and have JNI extract the pointer via [peek] + * - symmetric with [withPtr] for the Borrow path. Unique-ownership + * is still guaranteed: the write lock excludes every other + * [withPtr] / [consume], and the `finally` clause unconditionally + * nulls the slot before the lock is released. + * + * After consuming, the subclass cleaner (if any) sees `ptr == 0` and + * no-ops on GC - no double-free. + * + * Throws if the handle has already been closed/consumed. + */ + @Throws(ZError::class) + public fun consume(action: (Long) -> R): R = state.lock.write { + val p = state.ptr + if (p == 0L) throw ZError("Operation on a closed native handle.") + try { + action(p) + } finally { + state.ptr = 0L + } + } + + /** True iff the handle has been released (closed/consumed/cleaned). */ + public fun isClosed(): Boolean = state.ptr == 0L + + /** + * Read the current pointer value without holding the lock. + * Returns `0L` if the handle has been closed/consumed. + */ + public fun peek(): Long = state.ptr + + public companion object { + /** One shared cleaner thread for every typed-handle subclass. */ + @JvmStatic + internal val CLEANER: Cleaner = Cleaner.create() + } +} + +/** + * Nullable borrow: when the receiver is non-null, run [block] under its + * read lock with the live pointer; when null, run [block] with `0L`. + * Used by generated wrappers for `Option<&T>` opaque-handle parameters, + * where the FFI convention is `0` = `None`, nonzero = borrow of the + * native value. The lock contract is identical to [__CLASS_NAME__.withPtr] + * for the non-null case; the null case takes no lock. + */ +@Throws(ZError::class) +public fun __CLASS_NAME__?.withPtrOrZero(block: (Long) -> T): T = + if (this == null) block(0L) else withPtr(block) +"#; + let import_replacement = if import_line.is_empty() { + String::new() + } else { + import_line.trim_end_matches('\n').to_string() + }; + let body = body + .replace("__CLASS_NAME__", class_name) + .replace("__EXCEPTION_IMPORT__\n", &if import_replacement.is_empty() { + String::new() + } else { + format!("{}\n", import_replacement) + }) + .replace("ZError", exc_short); + + let mut out = String::new(); + out.push_str("// Auto-generated by JniExt - do not edit by hand.\n"); + if !package.is_empty() { + out.push_str(&format!("package {}\n", package)); + } + out.push_str(&body); + out +} diff --git a/prebindgen-ext/src/jni/wire_access.rs b/prebindgen-ext/src/jni/wire_access.rs new file mode 100644 index 00000000..7d6f5995 --- /dev/null +++ b/prebindgen-ext/src/jni/wire_access.rs @@ -0,0 +1,34 @@ +//! Map a JNI wire type to its `(jvm_signature_chunk, JValue accessor, +//! is_object)` triple — shared between the struct-strategy decoder/encoder +//! and the callback strategy. + +use quote::format_ident; + +/// Map a JNI wire type to `(jvm_field_descriptor, JValue_accessor_ident, is_object)`. +/// +/// Primitive types (`jlong`, `jint`, …) set `is_object = false` and the +/// accessor names the `.j()` / `.i()` / … `JValue` variant. +/// +/// Object types (`JString`, `JByteArray`, …) set `is_object = true`; the +/// caller uses `.l()` to get a `JObject` and then `.into()` to cast to the +/// wire type. +pub(crate) fn jni_field_access(jni_type: &syn::Type) -> Option<(&'static str, syn::Ident, bool)> { + let syn::Type::Path(tp) = jni_type else { + return None; + }; + let last = tp.path.segments.last()?; + let (sig, accessor, is_obj) = match last.ident.to_string().as_str() { + "jboolean" => ("Z", "z", false), + "jbyte" => ("B", "b", false), + "jchar" => ("C", "c", false), + "jshort" => ("S", "s", false), + "jint" => ("I", "i", false), + "jlong" => ("J", "j", false), + "jfloat" => ("F", "f", false), + "jdouble" => ("D", "d", false), + "JString" => ("Ljava/lang/String;", "l", true), + "JByteArray" => ("[B", "l", true), + _ => return None, + }; + Some((sig, format_ident!("{}", accessor), is_obj)) +} diff --git a/prebindgen-ext/src/kotlin/kotlin_ext.rs b/prebindgen-ext/src/kotlin/kotlin_ext.rs new file mode 100644 index 00000000..4be383a6 --- /dev/null +++ b/prebindgen-ext/src/kotlin/kotlin_ext.rs @@ -0,0 +1,71 @@ +//! Kotlin emission primitives — `KotlinFile` value and `WriteKotlinError` +//! shared by the JNI back-end's emitters in `crate::jni::jni_kotlin_ext`. +//! +//! Public surface (`KotlinFile`, `WriteKotlinError`) is re-exported by the +//! `jni` module for back-ends; the trait-based dispatch is gone — Kotlin +//! emission is JniExt-inherent. + +use std::path::{Path, PathBuf}; + +/// One Kotlin file's contents. +#[derive(Clone, Debug)] +pub struct KotlinFile { + /// Java/Kotlin package (`io.zenoh.jni.callbacks`). Empty for default + /// package. + pub package: String, + /// Class/interface name without `.kt` extension. Becomes the file name + /// (e.g. `JNISampleCallback` → `JNISampleCallback.kt`). + pub class_name: String, + /// Full file contents — package line and any imports must already be + /// included by the ext. + pub contents: String, +} + +impl KotlinFile { + /// Resolve the on-disk path for this file under `output_dir`. The + /// `package` is translated to a directory path (`.` → `/`). + pub fn path_under(&self, output_dir: &Path) -> PathBuf { + let dir = if self.package.is_empty() { + output_dir.to_path_buf() + } else { + output_dir.join(self.package.replace('.', "/")) + }; + dir.join(format!("{}.kt", self.class_name)) + } + + /// Write this file to its `path_under(output_dir)`, creating parent + /// directories as needed. + pub fn write(&self, output_dir: &Path) -> Result { + let path = self.path_under(output_dir); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&path, &self.contents)?; + Ok(path) + } +} + +/// Errors surfaced by Kotlin emission. +#[derive(Debug)] +pub enum WriteKotlinError { + Io(std::io::Error), + /// Bubbled from the ext-specific implementation. + Other(String), +} + +impl std::fmt::Display for WriteKotlinError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WriteKotlinError::Io(e) => write!(f, "I/O error writing Kotlin file: {}", e), + WriteKotlinError::Other(s) => write!(f, "Kotlin emission error: {}", s), + } + } +} + +impl std::error::Error for WriteKotlinError {} + +impl From for WriteKotlinError { + fn from(e: std::io::Error) -> Self { + WriteKotlinError::Io(e) + } +} diff --git a/prebindgen-ext/src/kotlin/mod.rs b/prebindgen-ext/src/kotlin/mod.rs new file mode 100644 index 00000000..81527679 --- /dev/null +++ b/prebindgen-ext/src/kotlin/mod.rs @@ -0,0 +1,10 @@ +//! Kotlin emission support for back-ends that produce Kotlin output. +//! +//! `kotlin_ext` exposes the shared `KotlinFile` / `WriteKotlinError` +//! types; `type_map` is the internal `KotlinTypeMap` (Rust → Kotlin +//! name lookup) consumed by `crate::jni::jni_kotlin_ext`. Both modules +//! are `pub(crate)` — Kotlin emission is JniExt-inherent and not +//! exposed at the crate boundary. + +pub(crate) mod kotlin_ext; +pub(crate) mod type_map; diff --git a/prebindgen-ext/src/kotlin/type_map.rs b/prebindgen-ext/src/kotlin/type_map.rs new file mode 100644 index 00000000..dc77a2cd --- /dev/null +++ b/prebindgen-ext/src/kotlin/type_map.rs @@ -0,0 +1,56 @@ +//! Per-type Rust → Kotlin name mapping. Internal helper consumed by the +//! Kotlin emitters in `jni_kotlin_ext.rs`; not part of the public API. + +use std::collections::HashMap; + +use quote::ToTokens; + +/// Whitespace-normalise a Rust type string by parsing it as a +/// `syn::Type` and re-serialising. Ensures lookup keys match the +/// canonical form used elsewhere in the pipeline. Falls back to the +/// raw input if parse fails (e.g. legacy `&Session` patterns). +pub(crate) fn canon_type(s: &str) -> String { + match syn::parse_str::(s) { + Ok(ty) => ty.to_token_stream().to_string(), + Err(_) => s.to_string(), + } +} + +/// Mapping from canonical Rust type-shape to its Kotlin parameter / +/// return type. Values may be either bare Kotlin names (`"Boolean"`, +/// `"String"`) or fully-qualified paths (`"io.zenoh.jni.JNIKeyExpr"`); +/// the generator emits the short name and adds the matching `import` +/// for FQN-shaped values. +#[derive(Default, Clone)] +pub(crate) struct KotlinTypeMap { + pub(crate) map: HashMap, +} + +impl KotlinTypeMap { + pub fn new() -> Self { + Self::default() + } + + pub fn add(mut self, rust_type: impl AsRef, kotlin_type: impl Into) -> Self { + self.map + .insert(canon_type(rust_type.as_ref()), kotlin_type.into()); + self + } + + pub fn lookup(&self, rust_type: &str) -> Option<&str> { + self.map.get(&canon_type(rust_type)).map(String::as_str) + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.map.iter() + } + + /// Pre-fill primitive language types whose Kotlin name is fixed. + pub fn with_primitive_builtins(mut self) -> Self { + self.map.insert(canon_type("bool"), "Boolean".into()); + self.map.insert(canon_type("i64"), "Long".into()); + self.map.insert(canon_type("f64"), "Double".into()); + self.map.insert(canon_type("Duration"), "Long".into()); + self + } +} diff --git a/prebindgen-ext/src/lib.rs b/prebindgen-ext/src/lib.rs new file mode 100644 index 00000000..5069a8c5 --- /dev/null +++ b/prebindgen-ext/src/lib.rs @@ -0,0 +1,20 @@ +//! Registry-based universal converter pipeline for `#[prebindgen]` items. +//! +//! Pipeline: +//! 1. [`core::Registry::from_items`] scans a stream of +//! `(syn::Item, SourceLocation)` (typically `source.items_all()`, +//! with optional upstream filtering). +//! 2. [`core::Registry::write_rust`] resolves every required type via +//! a configured [`core::prebindgen_ext::PrebindgenExt`] and writes +//! the generated Rust bindings file. Language-agnostic — any +//! [`PrebindgenExt`] back-end (JNI, future C/cbindgen, etc.) is +//! accepted. +//! 3. Back-end-specific emitters (e.g. [`jni::JniExt::write_kotlin`]) +//! walk the resolved registry to emit secondary artifacts. +//! +//! The [`jni::JniExt`] back-end is the reference language module. + +pub mod core; +pub mod jni; +pub mod kotlin; +mod util; diff --git a/prebindgen-ext/src/util.rs b/prebindgen-ext/src/util.rs new file mode 100644 index 00000000..5b3fd8a6 --- /dev/null +++ b/prebindgen-ext/src/util.rs @@ -0,0 +1,153 @@ +//! Shared internal utilities used by multiple modules. + +/// Convert a `snake_case` Rust identifier name to `camelCase`. +pub(crate) fn snake_to_camel(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper_next = false; + for (i, c) in s.chars().enumerate() { + if c == '_' { + upper_next = true; + } else if upper_next { + out.extend(c.to_uppercase()); + upper_next = false; + } else if i == 0 { + out.extend(c.to_lowercase()); + } else { + out.push(c); + } + } + out +} + +/// Convert a `CamelCase` Rust identifier to `SCREAMING_SNAKE_CASE`. Used to +/// project Rust enum variant idents into Kotlin enum constant names. +pub(crate) fn camel_to_screaming_snake(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() && i > 0 { + out.push('_'); + } + out.extend(c.to_uppercase()); + } + out +} + +/// True iff `ty` is the unit type `()`. +pub(crate) fn is_unit(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) +} + +/// Pull a signed integer out of a `syn::Expr` literal (`5`, `-3`, +/// `0x07`). Returns `None` for anything else (constants, paths, +/// arithmetic). +pub(crate) fn extract_int_literal(expr: &syn::Expr) -> Option { + match expr { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Int(int) => int.base10_parse::().ok(), + _ => None, + }, + syn::Expr::Unary(syn::ExprUnary { + op: syn::UnOp::Neg(_), + expr, + .. + }) => extract_int_literal(expr).map(|v| -v), + _ => None, + } +} + +/// Resolve each enum variant to its discriminant value following Rust's +/// own assignment rule: an explicit `= N` sets the value, an implicit +/// variant takes the previous value plus one (starting at 0). This is +/// the single source of truth for both the Kotlin `value(N)` constants +/// and the generated Rust `jint → variant` decode — keeping the two +/// from drifting and removing the need for a hand-written +/// `TryFrom` on the flat enum. Non-literal discriminants are +/// rejected because prebindgen-ext cannot reliably evaluate arbitrary +/// expressions at codegen time. +pub(crate) fn enum_discriminant_values(e: &syn::ItemEnum) -> Vec<(syn::Ident, i64)> { + let mut out = Vec::with_capacity(e.variants.len()); + let mut next: i64 = 0; + for variant in &e.variants { + let value = match variant.discriminant.as_ref() { + Some((_, expr)) => extract_int_literal(expr).unwrap_or_else(|| { + panic!( + "enum `{}` variant `{}` has a non-literal discriminant; use a literal integer value (e.g. `= 1`) or an implicit discriminant", + e.ident, + variant.ident + ) + }), + None => next, + }; + out.push((variant.ident.clone(), value)); + next = value + 1; + } + out +} + +#[cfg(test)] +mod tests { + use super::{camel_to_screaming_snake, enum_discriminant_values}; + + #[test] + fn camel_to_screaming_snake_basics() { + assert_eq!(camel_to_screaming_snake("RealTime"), "REAL_TIME"); + assert_eq!(camel_to_screaming_snake("InteractiveHigh"), "INTERACTIVE_HIGH"); + assert_eq!(camel_to_screaming_snake("Data"), "DATA"); + assert_eq!(camel_to_screaming_snake("Background"), "BACKGROUND"); + } + + fn discriminants(e: syn::ItemEnum) -> Vec<(String, i64)> { + enum_discriminant_values(&e) + .into_iter() + .map(|(ident, value)| (ident.to_string(), value)) + .collect() + } + + #[test] + fn discriminants_no_explicit_values() { + // Implicit C-like enum: 0, 1, 2 — matches Rust's default repr, + // which is also what the `as jint` output cast produces. + let e: syn::ItemEnum = syn::parse_quote! { enum E { A, B, C } }; + assert_eq!( + discriminants(e), + vec![("A".into(), 0), ("B".into(), 1), ("C".into(), 2)] + ); + } + + #[test] + fn discriminants_all_explicit() { + let e: syn::ItemEnum = syn::parse_quote! { + enum E { A = 1, B = 2, C = 7 } + }; + assert_eq!( + discriminants(e), + vec![("A".into(), 1), ("B".into(), 2), ("C".into(), 7)] + ); + } + + #[test] + fn discriminants_mixed_follow_rust_rule() { + // Explicit sets the value; the next implicit variant is prev + 1. + let e: syn::ItemEnum = syn::parse_quote! { + enum E { A = 5, B, C = 1, D } + }; + assert_eq!( + discriminants(e), + vec![ + ("A".into(), 5), + ("B".into(), 6), + ("C".into(), 1), + ("D".into(), 2), + ] + ); + } + + #[test] + #[should_panic(expected = "non-literal discriminant")] + fn discriminants_non_literal_rejected() { + let e: syn::ItemEnum = syn::parse_quote! { + enum E { A = OTHER, B } + }; + let _ = discriminants(e); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2d17a6a4..20b5515b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ rootProject.name = "zenoh-java" include(":zenoh-java") include(":examples") include(":zenoh-jni") +include(":zenoh-flat-jni") plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0") diff --git a/zenoh-flat-jni/Cargo.lock b/zenoh-flat-jni/Cargo.lock new file mode 100644 index 00000000..54b32ec9 --- /dev/null +++ b/zenoh-flat-jni/Cargo.lock @@ -0,0 +1,4533 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-logd-logger" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0483169d5fac0887f85c2fa8fecfe08669791712d8260de1a6ec30630a62932f" +dependencies = [ + "bytes", + "env_logger", + "lazy_static", + "libc", + "log", + "parking_lot", + "redox_syscall 0.4.1", + "thiserror 1.0.69", + "time", + "winapi", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst 0.2.20", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.4", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.8", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d255a6b6ecd77bb93ce91de984d7039bff7503f500eb4851a1269732f22baf" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst 0.3.17", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.0.0" +dependencies = [ + "itertools 0.12.1", + "jni", + "prebindgen", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" +dependencies = [ + "prebindgen", + "proc-macro2", + "quote", + "rand 0.9.4", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3e7aa0a681b232e7cd7f856a53b10603df88ca74b79a8d8088845185492e35" +dependencies = [ + "array-init", + "crossbeam", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags 2.11.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "either", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stabby" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "rand 0.8.6", + "syn 1.0.109", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls-listener" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1461056cc1ef47003f7ee16e4cef3741068d4c7f6b627bfce49b7c00c120a530" +dependencies = [ + "futures-util", + "pin-project-lite", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "token-cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb48920ae769b58126c8c93269805011c793201f95fde28b479b81a9a531bbde" +dependencies = [ + "paste", + "portable-atomic", + "rustversion", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.6", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uhlc" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unzip-n" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5bb2756c16fb66f80cfbf5fb0e0c09a7001e739f453c9ec241b9c8b1556fda" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "validated_struct" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869a93e8a7286e339e1128630051d82babbcd75d585975af07b9f3327220e60e" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c44ce98e7227a04eeb4cf9c784109a5c9710e54849ceb4f09f8597247897f1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unzip-n", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zenoh" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "ahash", + "arc-swap", + "async-trait", + "bytes", + "const_format", + "flate2", + "flume 0.11.1", + "futures", + "git-version", + "itertools 0.14.0", + "json5", + "lazy_static", + "nonempty-collections", + "once_cell", + "petgraph", + "phf", + "rand 0.8.6", + "rustc_version", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "uhlc", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-link", + "zenoh-link-commons", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-buffers" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "tracing", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "ahash", +] + +[[package]] +name = "zenoh-config" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "json5", + "nonempty-collections", + "num_cpus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "toml", + "tracing", + "uhlc", + "validated_struct", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "lazy_static", + "tokio", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-crypto" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "aes", + "hmac", + "rand 0.8.6", + "rand_chacha 0.3.1", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-ext" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "bincode", + "flume 0.11.1", + "futures", + "leb128", + "serde", + "tokio", + "tracing", + "uhlc", + "zenoh", + "zenoh-macros", + "zenoh-util", +] + +[[package]] +name = "zenoh-flat" +version = "1.9.0" +dependencies = [ + "android-logd-logger", + "json5", + "prebindgen", + "prebindgen-proc-macro", + "serde_yaml", + "tracing", + "zenoh", +] + +[[package]] +name = "zenoh-flat-jni" +version = "0.1.0" +dependencies = [ + "jni", + "konst 0.3.17", + "prebindgen", + "prebindgen-ext", + "syn 2.0.117", + "tracing", + "zenoh-flat", + "zenoh_jni", +] + +[[package]] +name = "zenoh-keyexpr" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "getrandom 0.2.17", + "hashbrown 0.16.1", + "keyed-set", + "rand 0.8.6", + "schemars 1.2.1", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "zenoh-config", + "zenoh-link-commons", + "zenoh-link-quic", + "zenoh-link-quic_datagram", + "zenoh-link-tcp", + "zenoh-link-tls", + "zenoh-link-udp", + "zenoh-link-unixsock_stream", + "zenoh-link-ws", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "base64", + "bytes", + "flume 0.11.1", + "futures", + "quinn", + "quinn-proto", + "rcgen", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "rustls-webpki", + "secrecy", + "serde", + "socket2 0.5.10", + "time", + "tokio", + "tokio-util", + "tracing", + "webpki-roots", + "x509-parser", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-quic" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "rustls-webpki", + "time", + "tracing", + "zenoh-core", + "zenoh-link-commons", + "zenoh-link-quic_datagram", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-quic_datagram" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "rustls-webpki", + "time", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-tcp" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-tls" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "base64", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "rustls-webpki", + "secrecy", + "socket2 0.5.10", + "time", + "tls-listener", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "webpki-roots", + "x509-parser", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-link-udp" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "libc", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "windows-sys 0.61.2", + "zenoh-buffers", + "zenoh-core", + "zenoh-link-commons", + "zenoh-link-quic_datagram", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-unixsock_stream" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "nix", + "tokio", + "tokio-util", + "tracing", + "uuid", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-link-ws" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "futures-util", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", + "url", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "git-version", + "libloading", + "serde", + "stabby", + "tracing", + "zenoh-config", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "const_format", + "rand 0.8.6", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-runtime" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "lazy_static", + "ron", + "serde", + "tokio", + "tracing", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-sync" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "arc-swap", + "event-listener", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-task" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "futures", + "tokio", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-transport" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "crossbeam-utils", + "flume 0.11.1", + "futures", + "lazy_static", + "lz4_flex", + "rand 0.8.6", + "ringbuffer-spsc", + "rsa", + "serde", + "sha3", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "const_format", + "flume 0.11.1", + "home", + "humantime", + "lazy_static", + "libc", + "libloading", + "pnet_datalink", + "schemars 1.2.1", + "serde", + "serde_json", + "shellexpand", + "tokio", + "tracing", + "tracing-subscriber", + "winapi", + "zenoh-core", + "zenoh-result", +] + +[[package]] +name = "zenoh_jni" +version = "1.9.0" +dependencies = [ + "android-logd-logger", + "async-std", + "clap", + "flume 0.10.14", + "jni", + "json5", + "rustc_version", + "serde_yaml", + "tracing", + "uhlc", + "zenoh", + "zenoh-ext", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/zenoh-flat-jni/Cargo.toml b/zenoh-flat-jni/Cargo.toml new file mode 100644 index 00000000..4b11ebe1 --- /dev/null +++ b/zenoh-flat-jni/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "zenoh-flat-jni" +version = "0.1.0" +edition = "2021" + +[features] +default = ["zenoh-ext"] +# Propagated to zenoh-jni so the legacy zbytes JNI surface stays exported +# through the outdated/ shim. +zenoh-ext = ["zenoh_jni/zenoh-ext"] + +[dependencies] +zenoh-flat = { version = "1.9.0", path = "../zenoh-flat" } +prebindgen-ext = { version = "1.0.0", path = "../prebindgen-ext" } +# zenoh_jni is consumed here as a plain rlib. default-features = false drops +# the export_jni_symbols feature so the same Java_… fns become regular Rust +# pubs — only outdated/ wrappers below carry #[no_mangle] for the JNI ABI. +zenoh_jni = { path = "../zenoh-jni", default-features = false, features = ["zenoh-default"] } +jni = "0.21.1" +konst = "0.3.17" +tracing = { version = "0.1", features = ["log"] } + +[lib] +name = "zenoh_flat_jni" +crate-type = ["staticlib", "dylib"] + +[build-dependencies] +zenoh-flat = { version = "1.9.0", path = "../zenoh-flat" } +prebindgen = "0.4.1" +prebindgen-ext = { version = "1.0.0", path = "../prebindgen-ext" } +syn = "2" + +# Mirror of zenoh-jni's old release profile. Critical: panic = "abort" — a +# Rust panic crossing the JNI boundary under unwind is UB. The setting was +# previously on zenoh-jni's dylib build; now that zenoh-jni is an rlib it +# inherits the consuming crate's profile, so the gating must live here. +[profile.release] +debug = false +lto = "fat" +codegen-units = 1 +opt-level = 3 +panic = "abort" \ No newline at end of file diff --git a/zenoh-flat-jni/build.rs b/zenoh-flat-jni/build.rs new file mode 100644 index 00000000..113dda24 --- /dev/null +++ b/zenoh-flat-jni/build.rs @@ -0,0 +1,110 @@ +use prebindgen_ext::core::prebindgen_ext::IntoSource; +use prebindgen_ext::core::registry::Registry; +use prebindgen_ext::jni::JniExt; +use syn::parse_quote as pq; + +fn fail(context: &str, err: impl std::fmt::Display) -> ! { + eprintln!("error: prebindgen-ext {context}: {err}"); + std::process::exit(1); +} + +fn main() { + let jni = JniExt::new() + .source_module(pq!(zenoh_flat)) // how to prefix prebindgen-marked items (functions, types + .package_prefix("io.zenoh.jni") // the package of the generated JNI bindings + .data_class(pq!(Error)) // structured Kotlin data class for Error + .throwable() // …also throwable; JniExt's built-in + // rank-2 Result<_, _> wrapper routes + // Err(Error) through it on the JVM side. + .package("keyexpr") + .ptr_class(pq!(ZKeyExpr)) + .class_object_fun(pq!(z_keyexpr_try_from)) + .class_object_fun(pq!(z_keyexpr_autocanonize)) + .class_fun(pq!(z_keyexpr_intersects)) + .class_fun(pq!(z_keyexpr_includes)) + .class_fun(pq!(z_keyexpr_relation_to)) + .class_fun(pq!(z_keyexpr_join)) + .class_fun(pq!(z_keyexpr_concat)) + .enum_class(pq!(SetIntersectionLevel)) + .data_class(pq!(KeyExpr)) + .class_object_fun(pq!(keyexpr_try_from)) + .class_object_fun(pq!(keyexpr_autocanonize)) + .class_object_fun(pq!(keyexpr_intersects)) + .class_object_fun(pq!(keyexpr_includes)) + .class_object_fun(pq!(keyexpr_relation_to)) + .class_object_fun(pq!(keyexpr_join)) + .class_object_fun(pq!(keyexpr_concat)) + .into_sources( + pq!(KeyExpr), + [ + IntoSource::borrow(pq!(KeyExpr)), + IntoSource::borrow(pq!(ZKeyExpr)), + IntoSource::borrow(pq!(String)), + ], + ) + .package("config") + .ptr_class(pq!(ZConfig)) + .class_object_fun(pq!(z_config_default)) + .class_object_fun(pq!(z_config_from_file)) + .class_object_fun(pq!(z_config_from_json)) + .class_object_fun(pq!(z_config_from_json5)) + .class_object_fun(pq!(z_config_from_yaml)) + .class_fun(pq!(z_config_get_json)) + .class_fun(pq!(z_config_insert_json5)) + .enum_class(pq!(WhatAmI)) + .ptr_class(pq!(ZZenohId)) + .class_fun(pq!(z_zenoh_id_to_bytes)) + .class_fun(pq!(z_zenoh_id_to_string)) + .value_class(pq!(ZenohId)) + .package("scouting") + .ptr_class(pq!(ZHello)) + .class_fun(pq!(z_hello_whatami)) + .class_fun(pq!(z_hello_zid)) + .class_fun(pq!(z_hello_locators)) + .data_class(pq!(Hello)) + .ptr_class(pq!(ZScout)) + .package_fun(pq!(z_scout)) + .package_fun(pq!(scout)) + .package("logger") + .package_fun(pq!(init_android_logs)) + .package_fun(pq!(try_init_zenoh_logs_from_env)) + .package_fun(pq!(init_zenoh_logs_from_env_or)) + .package("qos") + .enum_class(pq!(Reliability)) + .enum_class(pq!(Priority)) + .enum_class(pq!(CongestionControl)) + ; + + let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); + let mut registry = match Registry::from_items(source.items_all()) { + Ok(registry) => registry, + Err(err) => fail("scan failed", err), + }; + let rust_path = match registry.write_rust(&jni, "zenoh_flat_jni.rs") { + Ok(path) => path, + Err(err) => fail("write_rust failed", err), + }; + println!( + "cargo:warning=Generated bindings at: {}", + rust_path.display() + ); + + // ── Write Kotlin output ─────────────────────────────────────────── + // All generated Kotlin lives under `generated-kotlin/`; the runtime + // module's Gradle source set picks it up via + // `kotlin.srcDir("$rootDir/zenoh-jni/generated-kotlin")`. + let kotlin_root = std::path::Path::new("generated-kotlin"); + // Remove stale generated files so package moves don't leave old classes + // behind (e.g. io/zenoh/jni/* and io/zenoh/jni//* side-by-side). + if let Err(err) = std::fs::remove_dir_all(kotlin_root) { + if err.kind() != std::io::ErrorKind::NotFound { + fail("cleanup generated-kotlin failed", err); + } + } + for path in match jni.write_kotlin(®istry, kotlin_root) { + Ok(paths) => paths, + Err(err) => fail("write_kotlin failed", err), + } { + println!("cargo:warning=Wrote {}", path.display()); + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/Error.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/Error.kt new file mode 100644 index 00000000..d1330082 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/Error.kt @@ -0,0 +1,9 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni + +public data class Error( + override val message: String, +) : Exception(message) { + public companion object { + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINative.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINative.kt new file mode 100644 index 00000000..f351917e --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINative.kt @@ -0,0 +1,47 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni + +import io.zenoh.jni.callbacks.Callback +import io.zenoh.jni.callbacks.HelloCallback +import io.zenoh.jni.callbacks.ZHelloCallback +import io.zenoh.jni.config.WhatAmI +import io.zenoh.jni.config.ZConfig +import io.zenoh.jni.config.ZZenohId +import io.zenoh.jni.keyexpr.KeyExpr +import io.zenoh.jni.keyexpr.SetIntersectionLevel +import io.zenoh.jni.keyexpr.ZKeyExpr +import io.zenoh.jni.scouting.ZScout + +internal object JNINative { + external fun initAndroidLogs(filter: String) + external fun initZenohLogsFromEnvOr(fallbackFilter: String) + external fun keyexprAutocanonize(s: String): KeyExpr + external fun keyexprConcat(a: Any, b: String): KeyExpr + external fun keyexprIncludes(a: Any, b: Any): Boolean + external fun keyexprIntersects(a: Any, b: Any): Boolean + external fun keyexprJoin(a: Any, b: String): KeyExpr + external fun keyexprRelationTo(a: Any, b: Any): SetIntersectionLevel + external fun keyexprTryFrom(s: String): KeyExpr + external fun scout(whatami: Int, config: Long, callback: HelloCallback, onClose: Callback): Long + external fun tryInitZenohLogsFromEnv() + external fun zConfigDefault(): Long + external fun zConfigFromFile(path: String): Long + external fun zConfigFromJson(s: String): Long + external fun zConfigFromJson5(s: String): Long + external fun zConfigFromYaml(s: String): Long + external fun zConfigGetJson(c: Long, key: String): String + external fun zConfigInsertJson5(c: Long, key: String, value: String) + external fun zHelloLocators(h: Long): List + external fun zHelloWhatami(h: Long): WhatAmI + external fun zHelloZid(h: Long): Long + external fun zKeyexprAutocanonize(s: String): Long + external fun zKeyexprConcat(a: Long, b: String): Long + external fun zKeyexprIncludes(a: Long, b: Long): Boolean + external fun zKeyexprIntersects(a: Long, b: Long): Boolean + external fun zKeyexprJoin(a: Long, b: String): Long + external fun zKeyexprRelationTo(a: Long, b: Long): SetIntersectionLevel + external fun zKeyexprTryFrom(s: String): Long + external fun zScout(whatami: Int, config: Long, callback: ZHelloCallback, onClose: Callback): Long + external fun zZenohIdToBytes(z: Long): ByteArray + external fun zZenohIdToString(z: Long): String +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINativeHandle.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINativeHandle.kt new file mode 100644 index 00000000..9a2fc2ec --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JNINativeHandle.kt @@ -0,0 +1,120 @@ +// Auto-generated by JniExt - do not edit by hand. +package io.zenoh.jni + +import java.lang.ref.Cleaner +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * Minimal race-free wrapper around a raw `Box` pointer obtained from + * native code via `Box::into_raw(Box::new(v))`. Pairs the pointer with a + * `ReentrantReadWriteLock` so borrow-style JNI calls run in parallel + * under the read lock and consume/free serialise under the write lock. + * + * The base deliberately knows **nothing** about how to free the pointer: + * `freePtr` and the `AutoCloseable` / `Cleaner` lifecycle live on the + * concrete generator-emitted subclasses (e.g. `ZKeyExpr`), each of which + * statically knows its own destructor. The base is just the pointer + + * lock + the borrow/consume contract used polymorphically by generated + * wrappers (params typed as this base class). + * + * The mutable pointer lives in a detached [State] holder so a subclass's + * `Cleaner` action can reference the state **without** referencing the + * handle instance — a back-reference would pin the handle and the cleaner + * would never fire. + * + * Marked `open` so the generator-emitted typed-handle classes can + * subclass for type safety while inheriting the lock contract. + */ +public open class JNINativeHandle(initial: Long) { + /** + * Detached pointer + lock holder. Kept separate from the enclosing + * handle so a subclass cleaner can capture only this (plus its own + * static `freePtr`), never the handle instance. + */ + internal class State(@Volatile var ptr: Long) { + val lock = ReentrantReadWriteLock() + + /** Run [freePtr] exactly once under the write lock if still live. */ + fun freeOnce(freePtr: (Long) -> Unit) = lock.write { + val p = ptr + if (p != 0L) { + ptr = 0L + freePtr(p) + } + } + } + + internal val state = State(initial) + + /** + * Run [block] with the live pointer under the read lock. Throws + * [JniBindingError] if the handle has already been released. Multiple + * concurrent invocations run in parallel; only consume/close are + * serialised against them. + */ + @Throws(JniBindingError::class) + public fun withPtr(block: (Long) -> T): T = state.lock.read { + val p = state.ptr + if (p == 0L) throw JniBindingError("Operation on a closed native handle.") + block(p) + } + + /** + * Consume the pointer: take it under the write lock, run [action] + * with the captured pointer, then null the slot - even if [action] + * throws. Used by the generator-emitted wrappers whose Rust side + * runs `*Box::from_raw(...)` - i.e. by-value `T` opaque-handle + * parameters and `impl Into` arms with `IntoSourceMode::Consume`. + * + * The slot stays valid during [action] so the wrapper can pass the + * typed handle to JNI and have JNI extract the pointer via [peek] + * - symmetric with [withPtr] for the Borrow path. Unique-ownership + * is still guaranteed: the write lock excludes every other + * [withPtr] / [consume], and the `finally` clause unconditionally + * nulls the slot before the lock is released. + * + * After consuming, the subclass cleaner (if any) sees `ptr == 0` and + * no-ops on GC - no double-free. + * + * Throws if the handle has already been closed/consumed. + */ + @Throws(JniBindingError::class) + public fun consume(action: (Long) -> R): R = state.lock.write { + val p = state.ptr + if (p == 0L) throw JniBindingError("Operation on a closed native handle.") + try { + action(p) + } finally { + state.ptr = 0L + } + } + + /** True iff the handle has been released (closed/consumed/cleaned). */ + public fun isClosed(): Boolean = state.ptr == 0L + + /** + * Read the current pointer value without holding the lock. + * Returns `0L` if the handle has been closed/consumed. + */ + public fun peek(): Long = state.ptr + + public companion object { + /** One shared cleaner thread for every typed-handle subclass. */ + @JvmStatic + internal val CLEANER: Cleaner = Cleaner.create() + } +} + +/** + * Nullable borrow: when the receiver is non-null, run [block] under its + * read lock with the live pointer; when null, run [block] with `0L`. + * Used by generated wrappers for `Option<&T>` opaque-handle parameters, + * where the FFI convention is `0` = `None`, nonzero = borrow of the + * native value. The lock contract is identical to [JNINativeHandle.withPtr] + * for the non-null case; the null case takes no lock. + */ +@Throws(JniBindingError::class) +public fun JNINativeHandle?.withPtrOrZero(block: (Long) -> T): T = + if (this == null) block(0L) else withPtr(block) diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JniBindingError.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JniBindingError.kt new file mode 100644 index 00000000..ef16716a --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/JniBindingError.kt @@ -0,0 +1,5 @@ +// Auto-generated by JniExt - do not edit by hand. +package io.zenoh.jni + +/** JVM-side surface for the native Rust `JniBindingError` error. */ +public class JniBindingError(override val message: String? = null) : Exception() diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/Callback.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/Callback.kt new file mode 100644 index 00000000..1fa6e6f5 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/Callback.kt @@ -0,0 +1,6 @@ +// Auto-generated by JniExt - do not edit by hand. +package io.zenoh.jni.callbacks + +public fun interface Callback { + fun run() +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/HelloCallback.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/HelloCallback.kt new file mode 100644 index 00000000..a8bcfb21 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/HelloCallback.kt @@ -0,0 +1,10 @@ +// Auto-generated by JniExt - do not edit by hand. +package io.zenoh.jni.callbacks + +import io.zenoh.jni.scouting.Hello + +public fun interface HelloCallback { + fun run( + p0: Hello, + ) +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/ZHelloCallback.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/ZHelloCallback.kt new file mode 100644 index 00000000..bf8a4a4d --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/callbacks/ZHelloCallback.kt @@ -0,0 +1,8 @@ +// Auto-generated by JniExt - do not edit by hand. +package io.zenoh.jni.callbacks + +public fun interface ZHelloCallback { + fun run( + p0: Long, + ) +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/WhatAmI.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/WhatAmI.kt new file mode 100644 index 00000000..c0f510bd --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/WhatAmI.kt @@ -0,0 +1,14 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.config + +/** JVM-side surface for the native Rust `WhatAmI` enum. */ +public enum class WhatAmI(public val value: Int) { + ROUTER(1), + PEER(2), + CLIENT(4); + + public companion object { + @JvmStatic + public fun fromInt(value: Int): WhatAmI = entries.first { it.value == value } + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZConfig.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZConfig.kt new file mode 100644 index 00000000..8b5471e4 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZConfig.kt @@ -0,0 +1,56 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.config + +import io.zenoh.jni.Error +import io.zenoh.jni.JNINative +import io.zenoh.jni.JNINativeHandle +import io.zenoh.jni.JniBindingError + +/** Typed [JNINativeHandle] for a native Zenoh `ZConfig`. */ +public class ZConfig(initialPtr: Long) : JNINativeHandle(initialPtr), AutoCloseable { + private val cleanable: java.lang.ref.Cleaner.Cleanable = + JNINativeHandle.CLEANER.register(this, Cleanup(state)) + + override fun close() = cleanable.clean() + + private class Cleanup(private val state: JNINativeHandle.State) : Runnable { + override fun run() = state.freeOnce { ZConfig.freePtr(it) } + } + + @Throws(Error::class, JniBindingError::class) + public fun zConfigGetJson(key: String): String = + withPtr { c_ptr -> + JNINative.zConfigGetJson(c_ptr, key) + } + + @Throws(Error::class, JniBindingError::class) + public fun zConfigInsertJson5(key: String, value: String) = + withPtr { c_ptr -> + JNINative.zConfigInsertJson5(c_ptr, key, value) + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + + @Throws(JniBindingError::class) + public fun zConfigDefault(): ZConfig = + ZConfig(JNINative.zConfigDefault()) + + @Throws(Error::class, JniBindingError::class) + public fun zConfigFromFile(path: String): ZConfig = + ZConfig(JNINative.zConfigFromFile(path)) + + @Throws(Error::class, JniBindingError::class) + public fun zConfigFromJson(s: String): ZConfig = + ZConfig(JNINative.zConfigFromJson(s)) + + @Throws(Error::class, JniBindingError::class) + public fun zConfigFromJson5(s: String): ZConfig = + ZConfig(JNINative.zConfigFromJson5(s)) + + @Throws(Error::class, JniBindingError::class) + public fun zConfigFromYaml(s: String): ZConfig = + ZConfig(JNINative.zConfigFromYaml(s)) + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZZenohId.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZZenohId.kt new file mode 100644 index 00000000..02f14b79 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZZenohId.kt @@ -0,0 +1,35 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.config + +import io.zenoh.jni.JNINative +import io.zenoh.jni.JNINativeHandle +import io.zenoh.jni.JniBindingError + +/** Typed [JNINativeHandle] for a native Zenoh `ZZenohId`. */ +public class ZZenohId(initialPtr: Long) : JNINativeHandle(initialPtr), AutoCloseable { + private val cleanable: java.lang.ref.Cleaner.Cleanable = + JNINativeHandle.CLEANER.register(this, Cleanup(state)) + + override fun close() = cleanable.clean() + + private class Cleanup(private val state: JNINativeHandle.State) : Runnable { + override fun run() = state.freeOnce { ZZenohId.freePtr(it) } + } + + @Throws(JniBindingError::class) + public fun zZenohIdToBytes(): ByteArray = + withPtr { z_ptr -> + JNINative.zZenohIdToBytes(z_ptr) + } + + @Throws(JniBindingError::class) + public fun zZenohIdToString(): String = + withPtr { z_ptr -> + JNINative.zZenohIdToString(z_ptr) + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZenohId.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZenohId.kt new file mode 100644 index 00000000..a2d8e3cb --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/config/ZenohId.kt @@ -0,0 +1,5 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.config + +@JvmInline +public value class ZenohId(val bytes: ByteArray) diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/KeyExpr.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/KeyExpr.kt new file mode 100644 index 00000000..1933d2ce --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/KeyExpr.kt @@ -0,0 +1,96 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.keyexpr + +import io.zenoh.jni.Error +import io.zenoh.jni.JNINative +import io.zenoh.jni.JniBindingError + +public data class KeyExpr( + val keyExprString: String, + val keyExprNative: ZKeyExpr?, +) : AutoCloseable { + override fun close() { + keyExprNative?.close() + } + + public companion object { + @Throws(Error::class, JniBindingError::class) + public fun keyexprTryFrom(s: String): KeyExpr = + JNINative.keyexprTryFrom(s) + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprAutocanonize(s: String): KeyExpr = + JNINative.keyexprAutocanonize(s) + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprIntersects(a: Any, b: Any): Boolean = + if (a is ZKeyExpr) a.withPtr { _ -> + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprIntersects(a, b) + } else { + JNINative.keyexprIntersects(a, b) + } + } else { + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprIntersects(a, b) + } else { + JNINative.keyexprIntersects(a, b) + } + } + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprIncludes(a: Any, b: Any): Boolean = + if (a is ZKeyExpr) a.withPtr { _ -> + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprIncludes(a, b) + } else { + JNINative.keyexprIncludes(a, b) + } + } else { + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprIncludes(a, b) + } else { + JNINative.keyexprIncludes(a, b) + } + } + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprRelationTo(a: Any, b: Any): SetIntersectionLevel = + if (a is ZKeyExpr) a.withPtr { _ -> + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprRelationTo(a, b) + } else { + JNINative.keyexprRelationTo(a, b) + } + } else { + if (b is ZKeyExpr) b.withPtr { _ -> + JNINative.keyexprRelationTo(a, b) + } else { + JNINative.keyexprRelationTo(a, b) + } + } + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprJoin(a: Any, b: String): KeyExpr = + if (a is ZKeyExpr) a.withPtr { _ -> + JNINative.keyexprJoin(a, b) + } else { + JNINative.keyexprJoin(a, b) + } + + + @Throws(Error::class, JniBindingError::class) + public fun keyexprConcat(a: Any, b: String): KeyExpr = + if (a is ZKeyExpr) a.withPtr { _ -> + JNINative.keyexprConcat(a, b) + } else { + JNINative.keyexprConcat(a, b) + } + + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/SetIntersectionLevel.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/SetIntersectionLevel.kt new file mode 100644 index 00000000..a48a2ee2 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/SetIntersectionLevel.kt @@ -0,0 +1,15 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.keyexpr + +/** JVM-side surface for the native Rust `SetIntersectionLevel` enum. */ +public enum class SetIntersectionLevel(public val value: Int) { + DISJOINT(0), + INTERSECTS(1), + INCLUDES(2), + EQUALS(3); + + public companion object { + @JvmStatic + public fun fromInt(value: Int): SetIntersectionLevel = entries.first { it.value == value } + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/ZKeyExpr.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/ZKeyExpr.kt new file mode 100644 index 00000000..90c232a8 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/keyexpr/ZKeyExpr.kt @@ -0,0 +1,68 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.keyexpr + +import io.zenoh.jni.Error +import io.zenoh.jni.JNINative +import io.zenoh.jni.JNINativeHandle +import io.zenoh.jni.JniBindingError + +/** Typed [JNINativeHandle] for a native Zenoh `ZKeyExpr`. */ +public class ZKeyExpr(initialPtr: Long) : JNINativeHandle(initialPtr), AutoCloseable { + private val cleanable: java.lang.ref.Cleaner.Cleanable = + JNINativeHandle.CLEANER.register(this, Cleanup(state)) + + override fun close() = cleanable.clean() + + private class Cleanup(private val state: JNINativeHandle.State) : Runnable { + override fun run() = state.freeOnce { ZKeyExpr.freePtr(it) } + } + + @Throws(JniBindingError::class) + public fun zKeyexprIntersects(b: JNINativeHandle): Boolean = + withPtr { a_ptr -> + b.withPtr { b_ptr -> + JNINative.zKeyexprIntersects(a_ptr, b_ptr) + } + } + + @Throws(JniBindingError::class) + public fun zKeyexprIncludes(b: JNINativeHandle): Boolean = + withPtr { a_ptr -> + b.withPtr { b_ptr -> + JNINative.zKeyexprIncludes(a_ptr, b_ptr) + } + } + + @Throws(JniBindingError::class) + public fun zKeyexprRelationTo(b: JNINativeHandle): SetIntersectionLevel = + withPtr { a_ptr -> + b.withPtr { b_ptr -> + JNINative.zKeyexprRelationTo(a_ptr, b_ptr) + } + } + + @Throws(Error::class, JniBindingError::class) + public fun zKeyexprJoin(b: String): ZKeyExpr = + withPtr { a_ptr -> + ZKeyExpr(JNINative.zKeyexprJoin(a_ptr, b)) + } + + @Throws(Error::class, JniBindingError::class) + public fun zKeyexprConcat(b: String): ZKeyExpr = + withPtr { a_ptr -> + ZKeyExpr(JNINative.zKeyexprConcat(a_ptr, b)) + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + + @Throws(Error::class, JniBindingError::class) + public fun zKeyexprTryFrom(s: String): ZKeyExpr = + ZKeyExpr(JNINative.zKeyexprTryFrom(s)) + + @Throws(Error::class, JniBindingError::class) + public fun zKeyexprAutocanonize(s: String): ZKeyExpr = + ZKeyExpr(JNINative.zKeyexprAutocanonize(s)) + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/logger/JNIlogger.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/logger/JNIlogger.kt new file mode 100644 index 00000000..4d78049b --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/logger/JNIlogger.kt @@ -0,0 +1,17 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.logger + +import io.zenoh.jni.JniBindingError +import io.zenoh.jni.JNINative + +@Throws(JniBindingError::class) +public fun initAndroidLogs(filter: String) = + JNINative.initAndroidLogs(filter) + +public fun tryInitZenohLogsFromEnv() = + JNINative.tryInitZenohLogsFromEnv() + +@Throws(JniBindingError::class) +public fun initZenohLogsFromEnvOr(fallbackFilter: String) = + JNINative.initZenohLogsFromEnvOr(fallbackFilter) + diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/CongestionControl.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/CongestionControl.kt new file mode 100644 index 00000000..642146d0 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/CongestionControl.kt @@ -0,0 +1,14 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.qos + +/** JVM-side surface for the native Rust `CongestionControl` enum. */ +public enum class CongestionControl(public val value: Int) { + DROP(0), + BLOCK(1), + BLOCK_FIRST(2); + + public companion object { + @JvmStatic + public fun fromInt(value: Int): CongestionControl = entries.first { it.value == value } + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Priority.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Priority.kt new file mode 100644 index 00000000..62a80783 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Priority.kt @@ -0,0 +1,18 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.qos + +/** JVM-side surface for the native Rust `Priority` enum. */ +public enum class Priority(public val value: Int) { + REAL_TIME(1), + INTERACTIVE_HIGH(2), + INTERACTIVE_LOW(3), + DATA_HIGH(4), + DATA(5), + DATA_LOW(6), + BACKGROUND(7); + + public companion object { + @JvmStatic + public fun fromInt(value: Int): Priority = entries.first { it.value == value } + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Reliability.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Reliability.kt new file mode 100644 index 00000000..6ef44201 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/qos/Reliability.kt @@ -0,0 +1,13 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.qos + +/** JVM-side surface for the native Rust `Reliability` enum. */ +public enum class Reliability(public val value: Int) { + BEST_EFFORT(0), + RELIABLE(1); + + public companion object { + @JvmStatic + public fun fromInt(value: Int): Reliability = entries.first { it.value == value } + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/Hello.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/Hello.kt new file mode 100644 index 00000000..591235b4 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/Hello.kt @@ -0,0 +1,14 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.scouting + +import io.zenoh.jni.config.WhatAmI +import io.zenoh.jni.config.ZenohId + +public data class Hello( + val whatami: WhatAmI, + val zid: ZenohId, + val locators: List, +) { + public companion object { + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/JNIscouting.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/JNIscouting.kt new file mode 100644 index 00000000..ee0119fe --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/JNIscouting.kt @@ -0,0 +1,25 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.scouting + +import io.zenoh.jni.Error +import io.zenoh.jni.JniBindingError +import io.zenoh.jni.callbacks.Callback +import io.zenoh.jni.callbacks.HelloCallback +import io.zenoh.jni.callbacks.ZHelloCallback +import io.zenoh.jni.config.ZConfig +import io.zenoh.jni.scouting.ZScout +import io.zenoh.jni.withPtrOrZero +import io.zenoh.jni.JNINative + +@Throws(Error::class, JniBindingError::class) +public fun zScout(whatami: Int, config: ZConfig?, callback: ZHelloCallback, onClose: Callback): ZScout = + config.withPtrOrZero { config_ptr -> + ZScout(JNINative.zScout(whatami, config_ptr, callback, onClose)) +} + +@Throws(Error::class, JniBindingError::class) +public fun scout(whatami: Int, config: ZConfig?, callback: HelloCallback, onClose: Callback): ZScout = + config.withPtrOrZero { config_ptr -> + ZScout(JNINative.scout(whatami, config_ptr, callback, onClose)) +} + diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZHello.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZHello.kt new file mode 100644 index 00000000..26502415 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZHello.kt @@ -0,0 +1,43 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.scouting + +import io.zenoh.jni.JNINative +import io.zenoh.jni.JNINativeHandle +import io.zenoh.jni.JniBindingError +import io.zenoh.jni.config.WhatAmI +import io.zenoh.jni.config.ZZenohId + +/** Typed [JNINativeHandle] for a native Zenoh `ZHello`. */ +public class ZHello(initialPtr: Long) : JNINativeHandle(initialPtr), AutoCloseable { + private val cleanable: java.lang.ref.Cleaner.Cleanable = + JNINativeHandle.CLEANER.register(this, Cleanup(state)) + + override fun close() = cleanable.clean() + + private class Cleanup(private val state: JNINativeHandle.State) : Runnable { + override fun run() = state.freeOnce { ZHello.freePtr(it) } + } + + @Throws(JniBindingError::class) + public fun zHelloWhatami(): WhatAmI = + withPtr { h_ptr -> + JNINative.zHelloWhatami(h_ptr) + } + + @Throws(JniBindingError::class) + public fun zHelloZid(): ZZenohId = + withPtr { h_ptr -> + ZZenohId(JNINative.zHelloZid(h_ptr)) + } + + @Throws(JniBindingError::class) + public fun zHelloLocators(): List = + withPtr { h_ptr -> + JNINative.zHelloLocators(h_ptr) + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + } +} diff --git a/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZScout.kt b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZScout.kt new file mode 100644 index 00000000..41b89527 --- /dev/null +++ b/zenoh-flat-jni/generated-kotlin/io/zenoh/jni/scouting/ZScout.kt @@ -0,0 +1,21 @@ +// Auto-generated by JniExt — do not edit by hand. +package io.zenoh.jni.scouting + +import io.zenoh.jni.JNINativeHandle + +/** Typed [JNINativeHandle] for a native Zenoh `ZScout`. */ +public class ZScout(initialPtr: Long) : JNINativeHandle(initialPtr), AutoCloseable { + private val cleanable: java.lang.ref.Cleaner.Cleanable = + JNINativeHandle.CLEANER.register(this, Cleanup(state)) + + override fun close() = cleanable.clean() + + private class Cleanup(private val state: JNINativeHandle.State) : Runnable { + override fun run() = state.freeOnce { ZScout.freePtr(it) } + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + } +} diff --git a/zenoh-flat-jni/src/lib.rs b/zenoh-flat-jni/src/lib.rs new file mode 100644 index 00000000..6017d786 --- /dev/null +++ b/zenoh-flat-jni/src/lib.rs @@ -0,0 +1,16 @@ +// Generated `kotlin_enum` decoders use bare type idents (e.g. `WhatAmI`) +// matching the wrapper fn's `v: ` signature. Bring upstream +// enums into scope so those decoders resolve at the include site. +use zenoh_flat::WhatAmI; +use zenoh_flat::SetIntersectionLevel; +use zenoh_flat::CongestionControl; +use zenoh_flat::Priority; +use zenoh_flat::Reliability; + +// Generated by build.rs into OUT_DIR. +include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); + +// Mechanical shim re-exporting the legacy zenoh-jni Java_… symbols through +// thin wrappers that forward to zenoh_jni::::Java_…. Delete entries +// as their callers migrate to autogenerated wrappers over zenoh-flat. +pub mod outdated; diff --git a/zenoh-flat-jni/src/outdated/liveliness.rs b/zenoh-flat-jni/src/outdated/liveliness.rs new file mode 100644 index 00000000..a72632a2 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/liveliness.rs @@ -0,0 +1,57 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JClass, JObject, JString}, sys::{jboolean, jlong}, JNIEnv}; +use zenoh_jni::zenoh::{key_expr::KeyExpr, liveliness::LivelinessToken, pubsub::Subscriber, session::Session}; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + callback: JObject, + timeout_ms: jlong, + on_close: JObject, +) { + zenoh_jni::liveliness::Java_io_zenoh_jni_JNILiveliness_getViaJNI(env, _class, session_ptr, key_expr_ptr, key_expr_str, callback, timeout_ms, on_close) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, +) -> *const LivelinessToken { + zenoh_jni::liveliness::Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI(env, _class, session_ptr, key_expr_ptr, key_expr_str) +} + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareViaJNI( + _env: JNIEnv, + _class: JClass, + token_ptr: *const LivelinessToken, +) { + zenoh_jni::liveliness::Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareViaJNI(_env, _class, token_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + callback: JObject, + history: jboolean, + on_close: JObject, +) -> *const Subscriber<()> { + zenoh_jni::liveliness::Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI(env, _class, session_ptr, key_expr_ptr, key_expr_str, callback, history, on_close) +} diff --git a/zenoh-flat-jni/src/outdated/logger.rs b/zenoh-flat-jni/src/outdated/logger.rs new file mode 100644 index 00000000..8521f105 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/logger.rs @@ -0,0 +1,17 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JClass, JString}, JNIEnv}; + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_Logger_00024Companion_startLogsViaJNI( + env: JNIEnv, + _class: JClass, + filter: JString, +) { + zenoh_jni::logger::Java_io_zenoh_Logger_00024Companion_startLogsViaJNI(env, _class, filter) +} diff --git a/zenoh-flat-jni/src/outdated/mod.rs b/zenoh-flat-jni/src/outdated/mod.rs new file mode 100644 index 00000000..10ea2d82 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/mod.rs @@ -0,0 +1,14 @@ +// Auto-generated. Mechanical re-export of legacy zenoh-jni JNI symbols +// through wrappers that forward to zenoh_jni::::Java_… + +pub mod liveliness; +pub mod logger; +pub mod publisher; +pub mod querier; +pub mod query; +pub mod queryable; +pub mod session; +pub mod subscriber; +pub mod zenoh_id; +#[cfg(feature = "zenoh-ext")] +pub mod zbytes; diff --git a/zenoh-flat-jni/src/outdated/publisher.rs b/zenoh-flat-jni/src/outdated/publisher.rs new file mode 100644 index 00000000..a60d4f86 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/publisher.rs @@ -0,0 +1,41 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JByteArray, JClass, JString}, sys::jint, JNIEnv}; +use zenoh_jni::zenoh::pubsub::Publisher; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( + env: JNIEnv, + _class: JClass, + payload: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const Publisher<'static>, +) { + zenoh_jni::publisher::Java_io_zenoh_jni_JNIPublisher_putViaJNI(env, _class, payload, encoding_id, encoding_schema, attachment, publisher_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( + env: JNIEnv, + _class: JClass, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const Publisher<'static>, +) { + zenoh_jni::publisher::Java_io_zenoh_jni_JNIPublisher_deleteViaJNI(env, _class, attachment, publisher_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI( + _env: JNIEnv, + _class: JClass, + publisher_ptr: *const Publisher, +) { + zenoh_jni::publisher::Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI(_env, _class, publisher_ptr) +} diff --git a/zenoh-flat-jni/src/outdated/querier.rs b/zenoh-flat-jni/src/outdated/querier.rs new file mode 100644 index 00000000..4fc5d370 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/querier.rs @@ -0,0 +1,36 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JByteArray, JClass, JObject, JString}, sys::jint, JNIEnv}; +use zenoh_jni::zenoh::{key_expr::KeyExpr, query::Querier}; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( + env: JNIEnv, + _class: JClass, + querier_ptr: *const Querier, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + selector_params: /*nullable*/ JString, + callback: JObject, + on_close: JObject, + attachment: /*nullable*/ JByteArray, + payload: /*nullable*/ JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, +) { + zenoh_jni::querier::Java_io_zenoh_jni_JNIQuerier_getViaJNI(env, _class, querier_ptr, key_expr_ptr, key_expr_str, selector_params, callback, on_close, attachment, payload, encoding_id, encoding_schema) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_freePtrViaJNI( + _env: JNIEnv, + _class: JClass, + querier_ptr: *const Querier<'static>, +) { + zenoh_jni::querier::Java_io_zenoh_jni_JNIQuerier_freePtrViaJNI(_env, _class, querier_ptr) +} diff --git a/zenoh-flat-jni/src/outdated/query.rs b/zenoh-flat-jni/src/outdated/query.rs new file mode 100644 index 00000000..8d216632 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/query.rs @@ -0,0 +1,63 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JByteArray, JClass, JString}, sys::{jboolean, jint, jlong}, JNIEnv}; +use zenoh_jni::zenoh::{key_expr::KeyExpr, query::Query}; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( + env: JNIEnv, + _class: JClass, + query_ptr: *const Query, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + payload: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + timestamp_enabled: jboolean, + timestamp_ntp_64: jlong, + attachment: /*nullable*/ JByteArray, + qos_express: jboolean, +) { + zenoh_jni::query::Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI(env, _class, query_ptr, key_expr_ptr, key_expr_str, payload, encoding_id, encoding_schema, timestamp_enabled, timestamp_ntp_64, attachment, qos_express) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( + env: JNIEnv, + _class: JClass, + query_ptr: *const Query, + payload: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, +) { + zenoh_jni::query::Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI(env, _class, query_ptr, payload, encoding_id, encoding_schema) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( + env: JNIEnv, + _class: JClass, + query_ptr: *const Query, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + timestamp_enabled: jboolean, + timestamp_ntp_64: jlong, + attachment: /*nullable*/ JByteArray, + qos_express: jboolean, +) { + zenoh_jni::query::Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI(env, _class, query_ptr, key_expr_ptr, key_expr_str, timestamp_enabled, timestamp_ntp_64, attachment, qos_express) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_freePtrViaJNI( + _env: JNIEnv, + _class: JClass, + query_ptr: *const Query, +) { + zenoh_jni::query::Java_io_zenoh_jni_JNIQuery_freePtrViaJNI(_env, _class, query_ptr) +} diff --git a/zenoh-flat-jni/src/outdated/queryable.rs b/zenoh-flat-jni/src/outdated/queryable.rs new file mode 100644 index 00000000..10e0bd8e --- /dev/null +++ b/zenoh-flat-jni/src/outdated/queryable.rs @@ -0,0 +1,22 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::JClass, JNIEnv}; +use zenoh_jni::zenoh::query::Queryable; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI( + _env: JNIEnv, + _class: JClass, + queryable_ptr: *const Queryable<()>, +) { + zenoh_jni::queryable::Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI( + _env, + _class, + queryable_ptr, + ) +} diff --git a/zenoh-flat-jni/src/outdated/session.rs b/zenoh-flat-jni/src/outdated/session.rs new file mode 100644 index 00000000..be09b83f --- /dev/null +++ b/zenoh-flat-jni/src/outdated/session.rs @@ -0,0 +1,212 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JByteArray, JClass, JObject, JString}, sys::{jboolean, jbyteArray, jint, jlong, jobject}, JNIEnv}; +use zenoh_jni::zenoh::{config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, query::{Querier, Queryable}, session::Session}; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI( + env: JNIEnv, + _class: JClass, + config_ptr: *const Config, +) -> *const Session { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI(env, _class, config_ptr) +} + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( + env: JNIEnv, + _class: JClass, + json_config: JString, +) -> *const Session { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI(env, _class, json_config) +} + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( + env: JNIEnv, + _class: JClass, + yaml_config: JString, +) -> *const Session { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI(env, _class, yaml_config) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, +) { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_closeSessionViaJNI(env, _class, session_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + congestion_control: jint, + priority: jint, + is_express: jboolean, + reliability: jint, +) -> *const Publisher<'static> { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_declarePublisherViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, congestion_control, priority, is_express, reliability) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + payload: JByteArray, + encoding_id: jint, + encoding_schema: JString, + congestion_control: jint, + priority: jint, + is_express: jboolean, + attachment: JByteArray, + reliability: jint, +) { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_putViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, payload, encoding_id, encoding_schema, congestion_control, priority, is_express, attachment, reliability) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + congestion_control: jint, + priority: jint, + is_express: jboolean, + attachment: JByteArray, + reliability: jint, +) { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_deleteViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, congestion_control, priority, is_express, attachment, reliability) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + callback: JObject, + on_close: JObject, +) -> *const Subscriber<()> { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, callback, on_close) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + target: jint, + consolidation: jint, + congestion_control: jint, + priority: jint, + is_express: jboolean, + timeout_ms: jlong, + accept_replies: jint, +) -> *const Querier<'static> { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_declareQuerierViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, target, consolidation, congestion_control, priority, is_express, timeout_ms, accept_replies) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + callback: JObject, + on_close: JObject, + complete: jboolean, +) -> *const Queryable<()> { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_declareQueryableViaJNI(env, _class, key_expr_ptr, key_expr_str, session_ptr, callback, on_close, complete) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, + key_expr_str: JString, +) -> *const KeyExpr<'static> { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI(env, _class, session_ptr, key_expr_str) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, + key_expr_ptr: *const KeyExpr<'static>, +) { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI(env, _class, session_ptr, key_expr_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( + env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + selector_params: /*nullable*/ JString, + session_ptr: *const Session, + callback: JObject, + on_close: JObject, + timeout_ms: jlong, + target: jint, + consolidation: jint, + attachment: /*nullable*/ JByteArray, + payload: /*nullable*/ JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + congestion_control: jint, + priority: jint, + is_express: jboolean, + accept_replies: jint, +) { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_getViaJNI(env, _class, key_expr_ptr, key_expr_str, selector_params, session_ptr, callback, on_close, timeout_ms, target, consolidation, attachment, payload, encoding_id, encoding_schema, congestion_control, priority, is_express, accept_replies) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, +) -> jobject { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_getPeersZidViaJNI(env, _class, session_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, +) -> jobject { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI(env, _class, session_ptr) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( + env: JNIEnv, + _class: JClass, + session_ptr: *const Session, +) -> jbyteArray { + zenoh_jni::session::Java_io_zenoh_jni_JNISession_getZidViaJNI(env, _class, session_ptr) +} diff --git a/zenoh-flat-jni/src/outdated/subscriber.rs b/zenoh-flat-jni/src/outdated/subscriber.rs new file mode 100644 index 00000000..8ddd73f3 --- /dev/null +++ b/zenoh-flat-jni/src/outdated/subscriber.rs @@ -0,0 +1,22 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::JClass, JNIEnv}; +use zenoh_jni::zenoh::pubsub::Subscriber; + +#[no_mangle] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI( + _env: JNIEnv, + _class: JClass, + subscriber_ptr: *const Subscriber<()>, +) { + zenoh_jni::subscriber::Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI( + _env, + _class, + subscriber_ptr, + ) +} diff --git a/zenoh-flat-jni/src/outdated/zbytes.rs b/zenoh-flat-jni/src/outdated/zbytes.rs new file mode 100644 index 00000000..1cdbf77a --- /dev/null +++ b/zenoh-flat-jni/src/outdated/zbytes.rs @@ -0,0 +1,28 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JClass, JObject}, sys::jobject, JNIEnv}; + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeViaJNI( + env: JNIEnv, + _class: JClass, + any: JObject, + token_type: JObject, +) -> jobject { + zenoh_jni::zbytes::Java_io_zenoh_jni_JNIZBytes_serializeViaJNI(env, _class, any, token_type) +} + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( + env: JNIEnv, + _class: JClass, + zbytes: JObject, + jtype: JObject, +) -> jobject { + zenoh_jni::zbytes::Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI(env, _class, zbytes, jtype) +} diff --git a/zenoh-flat-jni/src/outdated/zenoh_id.rs b/zenoh-flat-jni/src/outdated/zenoh_id.rs new file mode 100644 index 00000000..bc0a799e --- /dev/null +++ b/zenoh-flat-jni/src/outdated/zenoh_id.rs @@ -0,0 +1,17 @@ +// Auto-generated mechanical JNI shim. Each wrapper exports the legacy +// JNI symbol and forwards to the same-named Rust fn in zenoh_jni. +// Delete entries here as their callers move to autogenerated wrappers. + +#![allow(non_snake_case)] +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use jni::{objects::{JByteArray, JClass}, sys::jstring, JNIEnv}; + +#[no_mangle] +pub extern "C" fn Java_io_zenoh_jni_JNIZenohId_toStringViaJNI( + env: JNIEnv, + _class: JClass, + zenoh_id: JByteArray, +) -> jstring { + zenoh_jni::zenoh_id::Java_io_zenoh_jni_JNIZenohId_toStringViaJNI(env, _class, zenoh_id) +} diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock new file mode 100644 index 00000000..fc47e724 --- /dev/null +++ b/zenoh-flat/Cargo.lock @@ -0,0 +1,2930 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-logd-logger" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0483169d5fac0887f85c2fa8fecfe08669791712d8260de1a6ec30630a62932f" +dependencies = [ + "bytes", + "env_logger", + "lazy_static", + "libc", + "log", + "parking_lot", + "redox_syscall 0.4.1", + "thiserror 1.0.69", + "time", + "winapi", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst 0.2.20", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d255a6b6ecd77bb93ce91de984d7039bff7503f500eb4851a1269732f22baf" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools", + "konst 0.3.17", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" +dependencies = [ + "prebindgen", + "proc-macro2", + "quote", + "rand 0.9.4", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3e7aa0a681b232e7cd7f856a53b10603df88ca74b79a8d8088845185492e35" +dependencies = [ + "array-init", + "crossbeam", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags 2.11.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "either", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "stabby" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "rand 0.8.6", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "token-cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb48920ae769b58126c8c93269805011c793201f95fde28b479b81a9a531bbde" +dependencies = [ + "paste", + "portable-atomic", + "rustversion", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uhlc" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unzip-n" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5bb2756c16fb66f80cfbf5fb0e0c09a7001e739f453c9ec241b9c8b1556fda" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "validated_struct" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869a93e8a7286e339e1128630051d82babbcd75d585975af07b9f3327220e60e" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c44ce98e7227a04eeb4cf9c784109a5c9710e54849ceb4f09f8597247897f1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unzip-n", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zenoh" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "ahash", + "arc-swap", + "async-trait", + "bytes", + "const_format", + "flate2", + "flume", + "futures", + "git-version", + "itertools", + "json5", + "lazy_static", + "nonempty-collections", + "once_cell", + "petgraph", + "phf", + "rand 0.8.6", + "rustc_version", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "uhlc", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-link", + "zenoh-link-commons", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-buffers" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "tracing", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "ahash", +] + +[[package]] +name = "zenoh-config" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "json5", + "nonempty-collections", + "num_cpus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "toml", + "tracing", + "uhlc", + "validated_struct", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "lazy_static", + "tokio", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-crypto" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "aes", + "hmac", + "rand 0.8.6", + "rand_chacha 0.3.1", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-ext" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "bincode", + "flume", + "futures", + "leb128", + "serde", + "tokio", + "tracing", + "uhlc", + "zenoh", + "zenoh-macros", + "zenoh-util", +] + +[[package]] +name = "zenoh-flat" +version = "1.9.0" +dependencies = [ + "android-logd-logger", + "json5", + "prebindgen", + "prebindgen-proc-macro", + "serde_yaml", + "tracing", + "zenoh", + "zenoh-ext", +] + +[[package]] +name = "zenoh-keyexpr" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "getrandom 0.2.17", + "hashbrown 0.16.1", + "keyed-set", + "rand 0.8.6", + "schemars 1.2.1", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "zenoh-config", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "flume", + "futures", + "serde", + "socket2 0.5.10", + "time", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "git-version", + "libloading", + "serde", + "stabby", + "tracing", + "zenoh-config", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "const_format", + "rand 0.8.6", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-runtime" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "lazy_static", + "ron", + "serde", + "tokio", + "tracing", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-sync" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "arc-swap", + "event-listener", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-task" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "futures", + "tokio", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-transport" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "crossbeam-utils", + "flume", + "futures", + "lazy_static", + "lz4_flex", + "rand 0.8.6", + "ringbuffer-spsc", + "serde", + "sha3", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#aa4767626d5893bb1e931482aa99f7b6307cc9c2" +dependencies = [ + "async-trait", + "const_format", + "flume", + "home", + "humantime", + "lazy_static", + "libc", + "libloading", + "pnet_datalink", + "schemars 1.2.1", + "serde", + "serde_json", + "shellexpand", + "tokio", + "tracing", + "tracing-subscriber", + "winapi", + "zenoh-core", + "zenoh-result", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml new file mode 100644 index 00000000..eb6fa25e --- /dev/null +++ b/zenoh-flat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "zenoh-flat" +version = "1.9.0" +edition = "2024" + +[dependencies] +zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } +zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["internal"], default-features = false, optional = true } +android-logd-logger = "0.4.0" +tracing = { version = "0.1" , features = ["log"] } +prebindgen = "0.4.1" +prebindgen-proc-macro = "0.4.1" +json5 = "0.4.1" +serde_yaml = "0.9.19" + +[build-dependencies] +prebindgen = "0.4.1" diff --git a/zenoh-flat/build.rs b/zenoh-flat/build.rs new file mode 100644 index 00000000..e279017f --- /dev/null +++ b/zenoh-flat/build.rs @@ -0,0 +1,3 @@ +fn main() { + prebindgen::init_prebindgen_out_dir(); +} \ No newline at end of file diff --git a/zenoh-flat/src/config/mod.rs b/zenoh-flat/src/config/mod.rs new file mode 100644 index 00000000..6f4ad2bb --- /dev/null +++ b/zenoh-flat/src/config/mod.rs @@ -0,0 +1,9 @@ +pub(crate) mod whatami; +pub(crate) mod z_config; +pub(crate) mod z_zenoh_id; +pub(crate) mod zenoh_id; + +pub use whatami::*; +pub use z_config::*; +pub use z_zenoh_id::*; +pub use zenoh_id::*; diff --git a/zenoh-flat/src/config/whatami.rs b/zenoh-flat/src/config/whatami.rs new file mode 100644 index 00000000..a416b7c3 --- /dev/null +++ b/zenoh-flat/src/config/whatami.rs @@ -0,0 +1,23 @@ +use prebindgen_proc_macro::prebindgen; + +/// Node type in a Zenoh network: router, peer, or client. The discriminant +/// values match upstream `zenoh::config::WhatAmI` (1, 2, 4) so an OR'd +/// bitfield of variants encodes a `WhatAmIMatcher` directly as `u8`. +#[prebindgen] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WhatAmI { + Router = 1, + Peer = 2, + Client = 4, +} + +impl From for WhatAmI { + fn from(w: zenoh::config::WhatAmI) -> Self { + match w { + zenoh::config::WhatAmI::Router => WhatAmI::Router, + zenoh::config::WhatAmI::Peer => WhatAmI::Peer, + zenoh::config::WhatAmI::Client => WhatAmI::Client, + } + } +} diff --git a/zenoh-flat/src/config/z_config.rs b/zenoh-flat/src/config/z_config.rs new file mode 100644 index 00000000..7c9295f2 --- /dev/null +++ b/zenoh-flat/src/config/z_config.rs @@ -0,0 +1,66 @@ +use crate::Error; +use crate::ZConfig; +use prebindgen_proc_macro::prebindgen; + +/// Build a default configuration. +#[prebindgen] +pub fn z_config_default() -> ZConfig { + ZConfig::default() +} + +/// Load a configuration from a file path. The file extension determines +/// the format (JSON, JSON5, or YAML). +#[prebindgen] +pub fn z_config_from_file(path: &str) -> Result { + Ok(ZConfig::from_file(path)?) +} + +/// Parse a configuration from a JSON-formatted string. JSON is a subset +/// of JSON5, so routing through the JSON5 deserializer is sufficient. +#[prebindgen] +pub fn z_config_from_json(s: &str) -> Result { + z_config_from_json5(s) +} + +/// Parse a configuration from a JSON5-formatted string. +#[prebindgen] +pub fn z_config_from_json5(s: &str) -> Result { + let mut deserializer = json5::Deserializer::from_str(s).map_err(|e| Error { + message: format!("JSON error: {e}"), + })?; + ZConfig::from_deserializer(&mut deserializer).map_err(|err| match err { + Ok(c) => Error { + message: format!("Invalid configuration: {c}"), + }, + Err(e) => Error { + message: format!("JSON error: {e}"), + }, + }) +} + +/// Parse a configuration from a YAML-formatted string. +#[prebindgen] +pub fn z_config_from_yaml(s: &str) -> Result { + let deserializer = serde_yaml::Deserializer::from_str(s); + ZConfig::from_deserializer(deserializer).map_err(|err| match err { + Ok(c) => Error { + message: format!("Invalid configuration: {c}"), + }, + Err(e) => Error { + message: format!("YAML error: {e}"), + }, + }) +} + +/// Return the JSON value associated with `key` in the configuration. +#[prebindgen] +pub fn z_config_get_json(c: &ZConfig, key: &str) -> Result { + Ok(c.get_json(key)?) +} + +/// Insert a JSON5-formatted value at `key` in the configuration. +#[prebindgen] +pub fn z_config_insert_json5(c: &mut ZConfig, key: &str, value: &str) -> Result<(), Error> { + c.insert_json5(key, value)?; + Ok(()) +} diff --git a/zenoh-flat/src/config/z_zenoh_id.rs b/zenoh-flat/src/config/z_zenoh_id.rs new file mode 100644 index 00000000..479160f3 --- /dev/null +++ b/zenoh-flat/src/config/z_zenoh_id.rs @@ -0,0 +1,14 @@ +use crate::ZZenohId; +use prebindgen_proc_macro::prebindgen; + +/// Serialize a Zenoh node identifier as raw bytes (16 bytes, little-endian). +#[prebindgen] +pub fn z_zenoh_id_to_bytes(z: &ZZenohId) -> Vec { + z.to_le_bytes().to_vec() +} + +/// Format a Zenoh node identifier as its standard string form. +#[prebindgen] +pub fn z_zenoh_id_to_string(z: &ZZenohId) -> String { + z.to_string() +} diff --git a/zenoh-flat/src/config/zenoh_id.rs b/zenoh-flat/src/config/zenoh_id.rs new file mode 100644 index 00000000..f0f09875 --- /dev/null +++ b/zenoh-flat/src/config/zenoh_id.rs @@ -0,0 +1,26 @@ +use crate::ZZenohId; +use super::z_zenoh_id::z_zenoh_id_to_bytes; +use prebindgen_proc_macro::prebindgen; + +/// Data-class twin of [`ZZenohId`]. Carries the raw bytes so bindings with +/// expensive FFI boundaries can read the id without round-tripping into +/// native code. +#[prebindgen] +#[derive(Clone)] +pub struct ZenohId { + pub bytes: Vec, +} + +impl From<&ZZenohId> for ZenohId { + fn from(z: &ZZenohId) -> Self { + Self { + bytes: z_zenoh_id_to_bytes(z), + } + } +} + +impl From for ZenohId { + fn from(z: ZZenohId) -> Self { + Self::from(&z) + } +} diff --git a/zenoh-flat/src/error.rs b/zenoh-flat/src/error.rs new file mode 100644 index 00000000..73173454 --- /dev/null +++ b/zenoh-flat/src/error.rs @@ -0,0 +1,21 @@ +use prebindgen_proc_macro::prebindgen; + +#[prebindgen] +#[derive(Clone)] +pub struct Error { + pub message: String, +} + +impl From> for Error { + fn from(err: Box) -> Self { + Self { + message: err.to_string(), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} diff --git a/zenoh-flat/src/keyexpr/keyexpr.rs b/zenoh-flat/src/keyexpr/keyexpr.rs new file mode 100644 index 00000000..26454d5d --- /dev/null +++ b/zenoh-flat/src/keyexpr/keyexpr.rs @@ -0,0 +1,136 @@ +use crate::Error; +use crate::ZKeyExpr; +use super::z_keyexpr::z_keyexpr_autocanonize; +use super::z_keyexpr::z_keyexpr_concat; +use super::z_keyexpr::z_keyexpr_includes; +use super::z_keyexpr::z_keyexpr_intersects; +use super::z_keyexpr::z_keyexpr_join; +use super::z_keyexpr::z_keyexpr_relation_to; +use prebindgen_proc_macro::prebindgen; + +/// Mirrors `zenoh::key_expr::SetIntersectionLevel` with a stable FFI surface. +#[prebindgen] +pub enum SetIntersectionLevel { + Disjoint = 0, + Intersects = 1, + Includes = 2, + Equals = 3, +} + +impl From for SetIntersectionLevel { + fn from(value: zenoh::key_expr::SetIntersectionLevel) -> Self { + match value { + zenoh::key_expr::SetIntersectionLevel::Disjoint => SetIntersectionLevel::Disjoint, + zenoh::key_expr::SetIntersectionLevel::Intersects => SetIntersectionLevel::Intersects, + zenoh::key_expr::SetIntersectionLevel::Includes => SetIntersectionLevel::Includes, + zenoh::key_expr::SetIntersectionLevel::Equals => SetIntersectionLevel::Equals, + } + } +} + +#[prebindgen] +pub struct KeyExpr { + pub key_expr_string: String, + pub key_expr_native: Option, +} + +impl From for KeyExpr { + fn from(s: String) -> Self { + KeyExpr { + key_expr_string: s, + key_expr_native: None, + } + } +} + +impl From<&str> for KeyExpr { + fn from(s: &str) -> Self { + KeyExpr { + key_expr_string: s.to_string(), + key_expr_native: None, + } + } +} + +impl From for KeyExpr { + fn from(ke: ZKeyExpr) -> Self { + KeyExpr { + key_expr_string: ke.to_string(), + key_expr_native: Some(ke), + } + } +} + +/// Validate that string `s` is a syntactically valid Zenoh key expression +#[prebindgen] +pub fn keyexpr_try_from(s: String) -> Result { + let ke = ZKeyExpr::try_from(s)?; + Ok(KeyExpr::from(ke)) +} + +/// Convert a key expression string into it's canonical form +#[prebindgen] +pub fn keyexpr_autocanonize(s: String) -> Result { + let ke = z_keyexpr_autocanonize(s)?; + Ok(KeyExpr::from(ke)) +} + +/// Returns true if keyexpr a and b intersect, false otherwise +#[prebindgen] +pub fn keyexpr_intersects( + a: impl Into + Send + 'static, + b: impl Into + Send + 'static, +) -> Result { + let a_ke = into_native(a.into())?; + let b_ke = into_native(b.into())?; + Ok(z_keyexpr_intersects(&a_ke, &b_ke)) +} + +/// Returns true if keyexpr a includes keyexpr b (every key matched by b is matched by a). +#[prebindgen] +pub fn keyexpr_includes( + a: impl Into + Send + 'static, + b: impl Into + Send + 'static, +) -> Result { + let a_ke = into_native(a.into())?; + let b_ke = into_native(b.into())?; + Ok(z_keyexpr_includes(&a_ke, &b_ke)) +} + +/// Returns `SetIntersectionLevel` describing the relation of `a` to `b`. +#[prebindgen] +pub fn keyexpr_relation_to( + a: impl Into + Send + 'static, + b: impl Into + Send + 'static, +) -> Result { + let a_ke = into_native(a.into())?; + let b_ke = into_native(b.into())?; + Ok(z_keyexpr_relation_to(&a_ke, &b_ke)) +} + +/// Joins `a` and `b` with a `/` separator and returns the resulting KeyExpr. +#[prebindgen] +pub fn keyexpr_join( + a: impl Into + Send + 'static, + b: String, +) -> Result { + let a_ke = into_native(a.into())?; + Ok(KeyExpr::from(z_keyexpr_join(&a_ke, b)?)) +} + +/// Concatenates `b` onto `a` and returns the resulting KeyExpr if valid. +#[prebindgen] +pub fn keyexpr_concat( + a: impl Into + Send + 'static, + b: String, +) -> Result { + let a_ke = into_native(a.into())?; + Ok(KeyExpr::from(z_keyexpr_concat(&a_ke, b)?)) +} + +fn into_native(k: KeyExpr) -> Result { + match k.key_expr_native { + Some(ke) => Ok(ke), + None => Ok(ZKeyExpr::try_from(k.key_expr_string)?), + } +} diff --git a/zenoh-flat/src/keyexpr/mod.rs b/zenoh-flat/src/keyexpr/mod.rs new file mode 100644 index 00000000..5d0657c3 --- /dev/null +++ b/zenoh-flat/src/keyexpr/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod keyexpr; +pub(crate) mod z_keyexpr; + +pub use keyexpr::*; +pub use z_keyexpr::*; diff --git a/zenoh-flat/src/keyexpr/z_keyexpr.rs b/zenoh-flat/src/keyexpr/z_keyexpr.rs new file mode 100644 index 00000000..b4c93548 --- /dev/null +++ b/zenoh-flat/src/keyexpr/z_keyexpr.rs @@ -0,0 +1,41 @@ +use prebindgen_proc_macro::prebindgen; +use crate::ZKeyExpr; +use crate::Error; +use super::keyexpr::SetIntersectionLevel; + +#[prebindgen] +pub fn z_keyexpr_try_from(s: String) -> Result { + let ke = ZKeyExpr::try_from(s)?; + Ok(ke) +} + +#[prebindgen] +pub fn z_keyexpr_autocanonize(s: String) -> Result { + let ke = ZKeyExpr::autocanonize(s)?; + Ok(ke) +} + +#[prebindgen] +pub fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { + a.intersects(b) +} + +#[prebindgen] +pub fn z_keyexpr_includes(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { + a.includes(b) +} + +#[prebindgen] +pub fn z_keyexpr_relation_to(a: &ZKeyExpr, b: &ZKeyExpr) -> SetIntersectionLevel { + a.relation_to(b).into() +} + +#[prebindgen] +pub fn z_keyexpr_join(a: &ZKeyExpr, b: String) -> Result { + Ok(a.join(&b)?) +} + +#[prebindgen] +pub fn z_keyexpr_concat(a: &ZKeyExpr, b: String) -> Result { + Ok(a.concat(&b)?) +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs new file mode 100644 index 00000000..8ab88789 --- /dev/null +++ b/zenoh-flat/src/lib.rs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +pub const PREBINDGEN_OUT_DIR: &str = prebindgen_proc_macro::prebindgen_out_dir!(); +pub const FEATURES: &str = prebindgen_proc_macro::features!(); + +pub(crate) mod keyexpr; +pub(crate) mod config; +pub(crate) mod scouting; +pub(crate) mod qos; +pub(crate) mod error; +pub(crate) mod logger; + +// reexports to make all zenoh-flat API really flat +pub use keyexpr::*; +pub use config::*; +pub use scouting::*; +pub use qos::*; +pub use error::*; +pub use logger::*; + +// reexports of zenoh types with Z prefix to distiguish them from zenoh-flat types +pub type ZKeyExpr = zenoh::key_expr::KeyExpr<'static>; +pub type ZConfig = zenoh::Config; +pub type ZZenohId = zenoh::session::ZenohId; +pub type ZHello = zenoh::scouting::Hello; +pub type ZScout = zenoh::scouting::Scout<()>; \ No newline at end of file diff --git a/zenoh-flat/src/logger/mod.rs b/zenoh-flat/src/logger/mod.rs new file mode 100644 index 00000000..cd576577 --- /dev/null +++ b/zenoh-flat/src/logger/mod.rs @@ -0,0 +1,31 @@ +use prebindgen_proc_macro::prebindgen; + +/// Initialize Android-style logging (logcat on Android, standard +/// output on non-Android systems) with an explicit filter. +/// +/// If the logger was already initialized, this call is a no-op. +/// +/// See for +/// accepted filter format. +#[prebindgen] +pub fn init_android_logs(filter: &str) { + android_logd_logger::builder() + .parse_filters(filter) + .tag_target_strip() + .prepend_module(true) + .try_init() + .ok(); +} + +/// Try to initialize zenoh logging from environment variables. +#[prebindgen] +pub fn try_init_zenoh_logs_from_env() { + zenoh::try_init_log_from_env(); +} + +/// Initialize zenoh logging from environment variables, falling back to +/// `fallback_filter` when the environment is unset. +#[prebindgen] +pub fn init_zenoh_logs_from_env_or(fallback_filter: &str) { + zenoh::init_log_from_env_or(fallback_filter); +} diff --git a/zenoh-flat/src/qos/congestion_control.rs b/zenoh-flat/src/qos/congestion_control.rs new file mode 100644 index 00000000..ee47d079 --- /dev/null +++ b/zenoh-flat/src/qos/congestion_control.rs @@ -0,0 +1,21 @@ +use prebindgen_proc_macro::prebindgen; + +/// Congestion control policy used when routing data. +#[prebindgen] +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CongestionControl { + Drop = 0, + Block = 1, + BlockFirst = 2, +} + +impl From for CongestionControl { + fn from(value: zenoh::qos::CongestionControl) -> Self { + match value { + zenoh::qos::CongestionControl::Drop => CongestionControl::Drop, + zenoh::qos::CongestionControl::Block => CongestionControl::Block, + zenoh::qos::CongestionControl::BlockFirst => CongestionControl::BlockFirst, + } + } +} diff --git a/zenoh-flat/src/qos/mod.rs b/zenoh-flat/src/qos/mod.rs new file mode 100644 index 00000000..bc673bb2 --- /dev/null +++ b/zenoh-flat/src/qos/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod congestion_control; +pub(crate) mod priority; +pub(crate) mod reliability; + +pub use congestion_control::*; +pub use priority::*; +pub use reliability::*; diff --git a/zenoh-flat/src/qos/priority.rs b/zenoh-flat/src/qos/priority.rs new file mode 100644 index 00000000..766d4d67 --- /dev/null +++ b/zenoh-flat/src/qos/priority.rs @@ -0,0 +1,29 @@ +use prebindgen_proc_macro::prebindgen; + +/// Message priority policy. Lower numeric value means higher priority. +#[prebindgen] +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Priority { + RealTime = 1, + InteractiveHigh = 2, + InteractiveLow = 3, + DataHigh = 4, + Data = 5, + DataLow = 6, + Background = 7, +} + +impl From for Priority { + fn from(value: zenoh::qos::Priority) -> Self { + match value { + zenoh::qos::Priority::RealTime => Priority::RealTime, + zenoh::qos::Priority::InteractiveHigh => Priority::InteractiveHigh, + zenoh::qos::Priority::InteractiveLow => Priority::InteractiveLow, + zenoh::qos::Priority::DataHigh => Priority::DataHigh, + zenoh::qos::Priority::Data => Priority::Data, + zenoh::qos::Priority::DataLow => Priority::DataLow, + zenoh::qos::Priority::Background => Priority::Background, + } + } +} diff --git a/zenoh-flat/src/qos/reliability.rs b/zenoh-flat/src/qos/reliability.rs new file mode 100644 index 00000000..69d9b6cd --- /dev/null +++ b/zenoh-flat/src/qos/reliability.rs @@ -0,0 +1,19 @@ +use prebindgen_proc_macro::prebindgen; + +/// Reliability policy for publications/subscriptions. +#[prebindgen] +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Reliability { + BestEffort = 0, + Reliable = 1, +} + +impl From for Reliability { + fn from(value: zenoh::qos::Reliability) -> Self { + match value { + zenoh::qos::Reliability::BestEffort => Reliability::BestEffort, + zenoh::qos::Reliability::Reliable => Reliability::Reliable, + } + } +} diff --git a/zenoh-flat/src/scouting/hello.rs b/zenoh-flat/src/scouting/hello.rs new file mode 100644 index 00000000..97b216a8 --- /dev/null +++ b/zenoh-flat/src/scouting/hello.rs @@ -0,0 +1,29 @@ +use crate::{WhatAmI, ZHello, ZenohId}; +use super::z_hello::{z_hello_locators, z_hello_whatami, z_hello_zid}; +use prebindgen_proc_macro::prebindgen; + +/// Data-class twin of [`ZHello`]. Carries decomposed fields so bindings +/// with expensive FFI boundaries can read the hello payload without a +/// round-trip per field. +#[prebindgen] +pub struct Hello { + pub whatami: WhatAmI, + pub zid: ZenohId, + pub locators: Vec, +} + +impl From<&ZHello> for Hello { + fn from(h: &ZHello) -> Self { + Self { + whatami: z_hello_whatami(h), + zid: ZenohId::from(z_hello_zid(h)), + locators: z_hello_locators(h), + } + } +} + +impl From for Hello { + fn from(h: ZHello) -> Self { + Self::from(&h) + } +} diff --git a/zenoh-flat/src/scouting/mod.rs b/zenoh-flat/src/scouting/mod.rs new file mode 100644 index 00000000..b89e2791 --- /dev/null +++ b/zenoh-flat/src/scouting/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod hello; +pub(crate) mod z_hello; +pub(crate) mod z_scout; + +pub use hello::*; +pub use z_hello::*; +pub use z_scout::*; diff --git a/zenoh-flat/src/scouting/z_hello.rs b/zenoh-flat/src/scouting/z_hello.rs new file mode 100644 index 00000000..87b78a9d --- /dev/null +++ b/zenoh-flat/src/scouting/z_hello.rs @@ -0,0 +1,20 @@ +use crate::{WhatAmI, ZHello, ZZenohId}; +use prebindgen_proc_macro::prebindgen; + +/// Node type that emitted this hello message. +#[prebindgen] +pub fn z_hello_whatami(h: &ZHello) -> WhatAmI { + h.whatami().into() +} + +/// Zenoh id of the node that emitted this hello message. +#[prebindgen] +pub fn z_hello_zid(h: &ZHello) -> ZZenohId { + h.zid() +} + +/// Locators advertised in this hello message. +#[prebindgen] +pub fn z_hello_locators(h: &ZHello) -> Vec { + h.locators().iter().map(|l| l.to_string()).collect() +} diff --git a/zenoh-flat/src/scouting/z_scout.rs b/zenoh-flat/src/scouting/z_scout.rs new file mode 100644 index 00000000..4e6f5b45 --- /dev/null +++ b/zenoh-flat/src/scouting/z_scout.rs @@ -0,0 +1,77 @@ +use crate::{Error, ZConfig, ZHello, ZScout}; +use super::hello::Hello; +use prebindgen_proc_macro::prebindgen; +use zenoh::Wait; +use zenoh::config::WhatAmIMatcher; + +/// Start a scout, invoking `callback` for each hello message received. +/// +/// `whatami` is a bitfield over the [`crate::WhatAmI`] variants +/// (`Router=1 | Peer=2 | Client=4`); only the low 3 bits are +/// significant, the wider type matches the JVM/`Int` matcher +/// representation. When `config` is `None` the default configuration is +/// used. +/// +/// `on_close` is dropped — and therefore invoked — when the returned +/// [`ZScout`] is dropped: callers wanting to be notified of scout +/// teardown should attach behavior to that drop. +/// +/// Returns an opaque scout handle whose lifetime owns the running scout; +/// dropping it stops the scout and triggers `on_close`. +#[prebindgen] +pub fn z_scout( + whatami: i32, + config: Option<&ZConfig>, + callback: impl Fn(ZHello) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, +) -> Result { + let bits = u8::try_from(whatami) + .map_err(|_| Error { message: format!("invalid whatami bitfield: {whatami}") })?; + let matcher: WhatAmIMatcher = bits + .try_into() + .map_err(|_| Error { message: format!("invalid whatami bitfield: 0b{bits:b}") })?; + let config = config.cloned().unwrap_or_default(); + let on_close = OnceDrop::new(on_close); + zenoh::scout(matcher, config) + .callback(move |hello| { + let _ = &on_close; + callback(hello); + }) + .wait() + .map_err(Error::from) +} + +/// Start a scout, delivering each hello as a serialized [`Hello`] data +/// class. See [`z_scout`] for parameter semantics; this variant pays one +/// extra Rust-side `Hello::from(ZHello)` copy per message in exchange for +/// a single FFI hop on the binding side. +#[prebindgen] +pub fn scout( + whatami: i32, + config: Option<&ZConfig>, + callback: impl Fn(Hello) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, +) -> Result { + z_scout(whatami, config, move |zh| callback(Hello::from(zh)), on_close) +} + +/// Calls a no-arg closure on drop. Lets a long-lived callback closure +/// signal teardown by capturing one of these alongside its main +/// callback — when the outer holder is dropped, this fires. +struct OnceDrop { + f: Option, +} + +impl OnceDrop { + fn new(f: F) -> Self { + Self { f: Some(f) } + } +} + +impl Drop for OnceDrop { + fn drop(&mut self) { + if let Some(f) = self.f.take() { + f(); + } + } +} diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index 123d7499..168242d3 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -31,6 +31,10 @@ val release = project.findProperty("release")?.toString()?.toBoolean() == true val isRemotePublication = project.findProperty("remotePublication")?.toString()?.toBoolean() == true var buildMode = if (release) BuildMode.RELEASE else BuildMode.DEBUG +// zenoh-jni is now a plain Rust rlib statically linked into zenoh-flat-jni. +// Only one dylib is produced and loaded. +val zenohFlatJniTargetDir = "../zenoh-flat-jni/target/$buildMode" +val nativeLibraryPath = zenohFlatJniTargetDir if (androidEnabled) { apply(plugin = "com.android.library") @@ -47,8 +51,7 @@ kotlin { kotlinOptions.jvmTarget = "11" } testRuns["test"].executionTask.configure { - val zenohPaths = "../zenoh-jni/target/$buildMode" - jvmArgs("-Djava.library.path=$zenohPaths") + jvmArgs("-Djava.library.path=$nativeLibraryPath") } if (!androidEnabled) { withJava() // Adding java to a kotlin lib targeting android is incompatible @@ -65,6 +68,7 @@ kotlin { @Suppress("Unused") sourceSets { val commonMain by getting { + kotlin.srcDir("../zenoh-flat-jni/generated-kotlin") dependencies { implementation("commons-net:commons-net:3.9.0") implementation("com.google.guava:guava:33.3.1-jre") @@ -87,12 +91,12 @@ kotlin { // The line below is intended to load the native libraries that are crosscompiled on GitHub actions when publishing a JVM package. resources.srcDir("../jni-libs").include("*/**") } else { - resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) + resources.srcDir(zenohFlatJniTargetDir).include(arrayListOf("*.dylib", "*.so", "*.dll")) } } val jvmTest by getting { - resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) + resources.srcDir(zenohFlatJniTargetDir).include(arrayListOf("*.dylib", "*.so", "*.dll")) } } @@ -157,7 +161,7 @@ tasks.withType { doFirst { // The line below is added for the Android Unit tests which are equivalent to the JVM tests. // For them to work we need to specify the path to the native library as a system property and not as a jvmArg. - systemProperty("java.library.path", "../zenoh-jni/target/$buildMode") + systemProperty("java.library.path", nativeLibraryPath) } } @@ -168,20 +172,21 @@ tasks.whenObjectAdded { } tasks.named("compileKotlinJvm") { - dependsOn("buildZenohJni") + // Building zenoh-flat-jni transitively builds zenoh-jni as a Rust dep. + dependsOn("buildZenohFlatJni") } -tasks.register("buildZenohJni") { +tasks.register("buildZenohFlatJni") { doLast { if (!isRemotePublication) { // This is intended for local publications. For publications done through GitHub workflows, - // the zenoh-jni build is achieved and loaded differently from the CI - buildZenohJNI(buildMode) + // the zenoh-flat-jni build is achieved and loaded differently from the CI + buildZenohFlatJNI(buildMode) } } } -fun buildZenohJNI(mode: BuildMode = BuildMode.DEBUG) { +fun buildZenohFlatJNI(mode: BuildMode = BuildMode.DEBUG) { val cargoCommand = mutableListOf("cargo", "build") if (mode == BuildMode.RELEASE) { @@ -189,11 +194,11 @@ fun buildZenohJNI(mode: BuildMode = BuildMode.DEBUG) { } val result = project.exec { - commandLine(*(cargoCommand.toTypedArray()), "--manifest-path", "../zenoh-jni/Cargo.toml") + commandLine(*(cargoCommand.toTypedArray()), "--manifest-path", "../zenoh-flat-jni/Cargo.toml") } if (result.exitValue != 0) { - throw GradleException("Failed to build Zenoh-JNI.") + throw GradleException("Failed to build Zenoh-Flat-JNI.") } Thread.sleep(1000) @@ -253,10 +258,10 @@ fun Project.configureAndroid() { fun Project.configureCargo() { extensions.configure("cargo") { pythonCommand = "python3" - module = "../zenoh-jni" - libname = "zenoh-jni" - targetIncludes = arrayOf("libzenoh_jni.so") - targetDirectory = "../zenoh-jni/target/" + module = "../zenoh-flat-jni" + libname = "zenoh-flat-jni" + targetIncludes = arrayOf("libzenoh_flat_jni.so") + targetDirectory = "../zenoh-flat-jni/target/" profile = "release" targets = arrayListOf( "arm", diff --git a/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt b/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt index 74d42e51..3368ab93 100644 --- a/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt +++ b/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt @@ -19,7 +19,7 @@ package io.zenoh * log level configuration. */ internal actual object ZenohLoad { - private const val ZENOH_LIB_NAME = "zenoh_jni" + private const val ZENOH_LIB_NAME = "zenoh_flat_jni" init { System.loadLibrary(ZENOH_LIB_NAME) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt index 91bc0e01..21de74e9 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt @@ -14,8 +14,10 @@ package io.zenoh +import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -import io.zenoh.jni.JNIConfig +import io.zenoh.exceptions.wrapJNIExceptionAsZError +import io.zenoh.jni.config.ZConfig import java.io.File import java.nio.file.Path @@ -35,18 +37,22 @@ import java.nio.file.Path * Visit the [default configuration](https://github.com/eclipse-zenoh/zenoh/blob/main/DEFAULT_CONFIG.json5) for more * information on the Zenoh config parameters. */ -class Config internal constructor(internal val jniConfig: JNIConfig) { +class Config internal constructor(internal val zConfig: ZConfig) { companion object { + init { + ZenohLoad + } + private const val CONFIG_ENV = "ZENOH_CONFIG" /** * Returns the default config. */ @JvmStatic - fun loadDefault(): Config { - return JNIConfig.loadDefaultConfig() + fun loadDefault(): Config = wrapJNIExceptionAsZError { + Config(ZConfig.zConfigDefault()) } /** @@ -58,9 +64,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic @Throws(ZError::class) - fun fromFile(file: File): Config { - return JNIConfig.loadConfigFile(file) - } + fun fromFile(file: File): Config = fromFile(file.toPath()) /** * Loads the configuration from the [Path] specified. @@ -71,8 +75,8 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic @Throws(ZError::class) - fun fromFile(path: Path): Config { - return JNIConfig.loadConfigFile(path) + fun fromFile(path: Path): Config = wrapJNIExceptionAsZError { + Config(ZConfig.zConfigFromFile(path.toString())) } /** @@ -86,8 +90,8 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic @Throws(ZError::class) - fun fromJson(config: String): Config { - return JNIConfig.loadJsonConfig(config) + fun fromJson(config: String): Config = wrapJNIExceptionAsZError { + Config(ZConfig.zConfigFromJson(config)) } /** @@ -101,8 +105,8 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic @Throws(ZError::class) - fun fromJson5(config: String): Config { - return JNIConfig.loadJson5Config(config) + fun fromJson5(config: String): Config = wrapJNIExceptionAsZError { + Config(ZConfig.zConfigFromJson5(config)) } /** @@ -116,8 +120,8 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic @Throws(ZError::class) - fun fromYaml(config: String): Config { - return JNIConfig.loadYamlConfig(config) + fun fromYaml(config: String): Config = wrapJNIExceptionAsZError { + Config(ZConfig.zConfigFromYaml(config)) } /** @@ -141,19 +145,15 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { * The json value associated to the [key]. */ @Throws(ZError::class) - fun getJson(key: String): String { - return jniConfig.getJson(key) + fun getJson(key: String): String = wrapJNIExceptionAsZError { + zConfig.zConfigGetJson(key) } /** * Inserts a json5 value associated to the [key] into the Config. */ @Throws(ZError::class) - fun insertJson5(key: String, value: String) { - jniConfig.insertJson5(key, value) - } - - protected fun finalize() { - jniConfig.close() + fun insertJson5(key: String, value: String) = wrapJNIExceptionAsZError { + zConfig.zConfigInsertJson5(key, value) } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt index 7a5931f8..65506eb8 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt @@ -19,8 +19,11 @@ import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler -import io.zenoh.jni.JNIScout -import io.zenoh.scouting.* +import io.zenoh.scouting.CallbackScout +import io.zenoh.scouting.HandlerScout +import io.zenoh.scouting.Hello +import io.zenoh.scouting.Scout +import io.zenoh.scouting.ScoutOptions import java.util.* import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingDeque @@ -53,7 +56,7 @@ object Zenoh { @Throws(ZError::class) fun scout(scoutOptions: ScoutOptions = ScoutOptions()): HandlerScout>> { val handler = BlockingQueueHandler(LinkedBlockingDeque>()) - return JNIScout.scoutWithHandler( + return Scout.scoutWithHandler( scoutOptions.whatAmI, handler::handle, fun() { handler.onClose() }, receiver = handler.receiver(), config = scoutOptions.config ) @@ -74,7 +77,7 @@ object Zenoh { @JvmStatic @Throws(ZError::class) fun scout(handler: Handler, scoutOptions: ScoutOptions = ScoutOptions()): HandlerScout { - return JNIScout.scoutWithHandler( + return Scout.scoutWithHandler( scoutOptions.whatAmI, handler::handle, fun() { handler.onClose() }, receiver = handler.receiver(), config = scoutOptions.config ) @@ -94,7 +97,7 @@ object Zenoh { @JvmStatic @Throws(ZError::class) fun scout(callback: Callback, scoutOptions: ScoutOptions = ScoutOptions()): CallbackScout { - return JNIScout.scoutWithCallback( + return Scout.scoutWithCallback( scoutOptions.whatAmI, callback, config = scoutOptions.config ) } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/WhatAmI.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/WhatAmI.kt index 983c9221..c6d3161c 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/WhatAmI.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/WhatAmI.kt @@ -19,12 +19,12 @@ package io.zenoh.config * * The role of the node sending the `hello` message. */ -enum class WhatAmI(internal val value: Int) { - Router(1), - Peer(2), - Client(4); +enum class WhatAmI(internal val jni: io.zenoh.jni.config.WhatAmI) { + Router(io.zenoh.jni.config.WhatAmI.ROUTER), + Peer(io.zenoh.jni.config.WhatAmI.PEER), + Client(io.zenoh.jni.config.WhatAmI.CLIENT); companion object { - internal fun fromInt(value: Int) = entries.first { value == it.value } + internal fun fromJni(jni: io.zenoh.jni.config.WhatAmI): WhatAmI = entries.first { it.jni == jni } } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt index 9787fced..23f07b15 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt @@ -20,10 +20,10 @@ import kotlin.math.absoluteValue /** * The global unique id of a Zenoh peer. */ -data class ZenohId internal constructor(internal val bytes: ByteArray) { +data class ZenohId internal constructor(internal val inner: io.zenoh.jni.config.ZenohId) { override fun toString(): String { - return JNIZenohId.toStringViaJNI(bytes) + return JNIZenohId.toStringViaJNI(inner.bytes) } override fun equals(other: Any?): Boolean { @@ -32,11 +32,11 @@ data class ZenohId internal constructor(internal val bytes: ByteArray) { other as ZenohId - return bytes.contentEquals(other.bytes) + return inner.bytes.contentEquals(other.inner.bytes) } override fun hashCode(): Int { - return bytes.contentHashCode() + return inner.bytes.contentHashCode() } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/JNIExceptionWrapping.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/JNIExceptionWrapping.kt new file mode 100644 index 00000000..e40c34e8 --- /dev/null +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/JNIExceptionWrapping.kt @@ -0,0 +1,29 @@ +// +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.exceptions + +/** + * Run [block], translating flat-jni exceptions into [ZError] so callers keep + * the zenoh-java exception surface stable during migration. + */ +internal inline fun wrapJNIExceptionAsZError(block: () -> T): T { + return try { + block() + } catch (e: io.zenoh.jni.Error) { + throw ZError(e.message) + } catch (e: io.zenoh.jni.JniBindingError) { + throw ZError(e.message) + } +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt deleted file mode 100644 index ea278988..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni - -import io.zenoh.Config -import io.zenoh.ZenohLoad -import io.zenoh.exceptions.ZError -import java.io.File -import java.nio.file.Path - -internal class JNIConfig(internal val ptr: Long) { - - companion object { - - init { - ZenohLoad - } - - fun loadDefaultConfig(): Config { - val cfgPtr = loadDefaultConfigViaJNI() - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - fun loadConfigFile(path: Path): Config { - val cfgPtr = loadConfigFileViaJNI(path.toString()) - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - fun loadConfigFile(file: File): Config = loadConfigFile(file.toPath()) - - @Throws(ZError::class) - fun loadJsonConfig(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - fun loadJson5Config(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - fun loadYamlConfig(rawConfig: String): Config { - val cfgPtr = loadYamlConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - private external fun loadDefaultConfigViaJNI(): Long - - @Throws(ZError::class) - private external fun loadConfigFileViaJNI(path: String): Long - - @Throws(ZError::class) - private external fun loadJsonConfigViaJNI(rawConfig: String): Long - - @Throws(ZError::class) - private external fun loadYamlConfigViaJNI(rawConfig: String): Long - - @Throws(ZError::class) - private external fun getIdViaJNI(ptr: Long): ByteArray - - @Throws(ZError::class) - private external fun insertJson5ViaJNI(ptr: Long, key: String, value: String): Long - - /** Frees the underlying native config. */ - private external fun freePtrViaJNI(ptr: Long) - - @Throws(ZError::class) - private external fun getJsonViaJNI(ptr: Long, key: String): String - } - - fun close() { - freePtrViaJNI(ptr) - } - - @Throws(ZError::class) - fun getJson(key: String): String { - return getJsonViaJNI(ptr, key) - } - - @Throws(ZError::class) - fun insertJson5(key: String, value: String) { - insertJson5ViaJNI(this.ptr, key, value) - } -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt deleted file mode 100644 index 29e419e3..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ /dev/null @@ -1,104 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni - -import io.zenoh.ZenohLoad -import io.zenoh.exceptions.ZError -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.keyexpr.SetIntersectionLevel - -internal class JNIKeyExpr(internal val ptr: Long) { - - companion object { - init { - ZenohLoad - } - - @Throws(ZError::class) - fun tryFrom(keyExpr: String): KeyExpr { - return KeyExpr(tryFromViaJNI(keyExpr)) - } - - @Throws(ZError::class) - fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(autocanonizeViaJNI(keyExpr)) - } - - @Throws(ZError::class) - fun intersects(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = intersectsViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) - - @Throws(ZError::class) - fun includes(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = includesViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) - - @Throws(ZError::class) - fun relationTo(keyExpr: KeyExpr, other: KeyExpr): SetIntersectionLevel { - val intersection = relationToViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - other.jniKeyExpr?.ptr ?: 0, - other.keyExpr - ) - return SetIntersectionLevel.fromInt(intersection) - } - - @Throws(ZError::class) - fun joinViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(joinViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } - - @Throws(ZError::class) - fun concatViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(concatViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } - - @Throws(ZError::class) - private external fun tryFromViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun autocanonizeViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int - - @Throws(ZError::class) - private external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String - - @Throws(ZError::class) - private external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String - } - - fun close() { - freePtrViaJNI(ptr) - } - - /** Frees the underlying native KeyExpr. */ - private external fun freePtrViaJNI(ptr: Long) -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt index 0225c32b..5ef10d01 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt @@ -67,7 +67,7 @@ internal object JNILiveliness { if (success) { val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr2!!, null), + KeyExpr(keyExpr2!!), payload.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -75,10 +75,10 @@ internal object JNILiveliness { QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), attachmentBytes?.into() ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) + reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, sample) } else { reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema) ) @@ -87,8 +87,8 @@ internal object JNILiveliness { } getViaJNI( jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, getCallback, timeout.toMillis(), onClose::run @@ -97,7 +97,7 @@ internal object JNILiveliness { } fun declareToken(jniSession: JNISession, keyExpr: KeyExpr): LivelinessToken { - val ptr = declareTokenViaJNI(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr) + val ptr = declareTokenViaJNI(jniSession.sessionPtr, keyExpr.flat.keyExprNative?.peek() ?: 0L, keyExpr.flat.keyExprString) return LivelinessToken(JNILivelinessToken(ptr)) } @@ -112,7 +112,7 @@ internal object JNILiveliness { JNISubscriberCallback { keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr2, null), + KeyExpr(keyExpr2), payload.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -124,8 +124,8 @@ internal object JNILiveliness { } val ptr = declareSubscriberViaJNI( jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, subCallback, history, onClose @@ -145,7 +145,7 @@ internal object JNILiveliness { JNISubscriberCallback { keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr2, null), + KeyExpr(keyExpr2), payload.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -157,8 +157,8 @@ internal object JNILiveliness { } val ptr = declareSubscriberViaJNI( jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, subCallback, history, onClose diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index c026ed1c..58f7c33e 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -81,7 +81,7 @@ internal class JNIQuerier(val ptr: Long) { if (success) { val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr2!!, null), + KeyExpr(keyExpr2!!), payload2.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -89,10 +89,10 @@ internal class JNIQuerier(val ptr: Long) { QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), attachmentBytes?.into() ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) + reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, sample) } else { reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, payload2.into(), Encoding(encodingId, schema = encodingSchema) ) @@ -101,8 +101,8 @@ internal class JNIQuerier(val ptr: Long) { } getViaJNI(this.ptr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, parameters?.toString(), getCallback, onClose, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt index 0b1683f0..583c7a1b 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -35,8 +35,8 @@ internal class JNIQuery(private val ptr: Long) { val timestampEnabled = sample.timestamp != null replySuccessViaJNI( ptr, - sample.keyExpr.jniKeyExpr?.ptr ?: 0, - sample.keyExpr.keyExpr, + sample.keyExpr.flat.keyExprNative?.peek() ?: 0L, + sample.keyExpr.flat.keyExprString, sample.payload.bytes, sample.encoding.id, sample.encoding.schema, @@ -55,8 +55,8 @@ internal class JNIQuery(private val ptr: Long) { val timestampEnabled = timestamp != null replyDeleteViaJNI( ptr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, timestampEnabled, if (timestampEnabled) timestamp!!.ntpValue() else 0, attachment?.into()?.bytes, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt deleted file mode 100644 index f2e1c49c..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni - -import io.zenoh.Config -import io.zenoh.ZenohLoad -import io.zenoh.exceptions.ZError -import io.zenoh.handlers.Callback -import io.zenoh.jni.callbacks.JNIScoutCallback -import io.zenoh.config.ZenohId -import io.zenoh.scouting.Hello -import io.zenoh.config.WhatAmI -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.scouting.CallbackScout -import io.zenoh.scouting.HandlerScout - -/** - * Adapter class to handle the interactions with Zenoh through JNI for a [io.zenoh.scouting.Scout] - * - * @property ptr: raw pointer to the underlying native scout. - */ -internal class JNIScout(private val ptr: Long) { - - companion object { - - init { - ZenohLoad - } - - @Throws(ZError::class) - fun scoutWithHandler( - whatAmI: Set, - callback: Callback, - onClose: () -> Unit, - config: Config?, - receiver: R - ): HandlerScout { - val scoutCallback = JNIScoutCallback { whatAmI2: Int, id: ByteArray, locators: List -> - callback.run(Hello(WhatAmI.fromInt(whatAmI2), ZenohId(id), locators)) - } - val binaryWhatAmI: Int = whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = scoutViaJNI(binaryWhatAmI, scoutCallback, onClose,config?.jniConfig?.ptr ?: 0) - return HandlerScout(JNIScout(ptr), receiver) - } - - @Throws(ZError::class) - fun scoutWithCallback( - whatAmI: Set, - callback: Callback, - config: Config?, - ): CallbackScout { - val scoutCallback = JNIScoutCallback { whatAmI2: Int, id: ByteArray, locators: List -> - callback.run(Hello(WhatAmI.fromInt(whatAmI2), ZenohId(id), locators)) - } - val binaryWhatAmI: Int = whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = scoutViaJNI(binaryWhatAmI, scoutCallback, fun() {},config?.jniConfig?.ptr ?: 0) - return CallbackScout(JNIScout(ptr)) - } - - @Throws(ZError::class) - private external fun scoutViaJNI( - whatAmI: Int, - callback: JNIScoutCallback, - onClose: JNIOnCloseCallback, - configPtr: Long, - ): Long - - @Throws(ZError::class) - external fun freePtrViaJNI(ptr: Long) - } - - fun close() { - freePtrViaJNI(ptr) - } -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 096a6c59..99d025b2 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -49,7 +49,7 @@ internal class JNISession(val sessionPtr: Long) { @Throws(ZError::class) fun open(config: Config): JNISession { - val sessionPtr = openSessionViaJNI(config.jniConfig.ptr) + val sessionPtr = config.zConfig.withPtr { openSessionViaJNI(it) } return JNISession(sessionPtr) } @@ -64,13 +64,13 @@ internal class JNISession(val sessionPtr: Long) { @Throws(ZError::class) fun declarePublisher(keyExpr: KeyExpr, publisherOptions: PublisherOptions): Publisher { val publisherRawPtr = declarePublisherViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, publisherOptions.congestionControl.value, publisherOptions.priority.value, publisherOptions.express, - publisherOptions.reliability.ordinal + publisherOptions.reliability.jni.value ) return Publisher( keyExpr, @@ -89,7 +89,7 @@ internal class JNISession(val sessionPtr: Long) { JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr1, null), + KeyExpr(keyExpr1), payload.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -100,7 +100,7 @@ internal class JNISession(val sessionPtr: Long) { handler.handle(sample) } val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, handler::onClose + keyExpr.flat.keyExprNative?.peek() ?: 0L, keyExpr.flat.keyExprString, sessionPtr, subCallback, handler::onClose ) return HandlerSubscriber(keyExpr, JNISubscriber(subscriberRawPtr), handler.receiver()) } @@ -113,7 +113,7 @@ internal class JNISession(val sessionPtr: Long) { JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr1, null), + KeyExpr(keyExpr1), payload.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -124,8 +124,8 @@ internal class JNISession(val sessionPtr: Long) { callback.run(sample) } val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, subCallback, fun() {} @@ -140,7 +140,7 @@ internal class JNISession(val sessionPtr: Long) { val queryCallback = JNIQueryableCallback { keyExpr1: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long, acceptReplies: Int -> val jniQuery = JNIQuery(queryPtr) - val keyExpr2 = KeyExpr(keyExpr1, null) + val keyExpr2 = KeyExpr(keyExpr1) val selector = if (selectorParams.isEmpty()) { Selector(keyExpr2) } else { @@ -159,8 +159,8 @@ internal class JNISession(val sessionPtr: Long) { callback.run(query) } val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, queryCallback, fun() {}, @@ -176,7 +176,7 @@ internal class JNISession(val sessionPtr: Long) { val queryCallback = JNIQueryableCallback { keyExpr1: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long, acceptReplies: Int -> val jniQuery = JNIQuery(queryPtr) - val keyExpr2 = KeyExpr(keyExpr1, null) + val keyExpr2 = KeyExpr(keyExpr1) val selector = if (selectorParams.isEmpty()) { Selector(keyExpr2) } else { @@ -195,8 +195,8 @@ internal class JNISession(val sessionPtr: Long) { handler.handle(query) } val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, queryCallback, handler::onClose, @@ -211,8 +211,8 @@ internal class JNISession(val sessionPtr: Long) { options: QuerierOptions ): Querier { val querierRawPtr = declareQuerierViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, options.target.ordinal, options.consolidationMode.ordinal, @@ -259,7 +259,7 @@ internal class JNISession(val sessionPtr: Long) { if (success) { val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr!!, null), + KeyExpr(keyExpr!!), payload1.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -267,10 +267,10 @@ internal class JNISession(val sessionPtr: Long) { QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), attachmentBytes?.into() ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) + reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, sample) } else { reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, payload1.into(), Encoding(encodingId, schema = encodingSchema) ) @@ -280,8 +280,8 @@ internal class JNISession(val sessionPtr: Long) { val selector = intoSelector.into() getViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, + selector.keyExpr.flat.keyExprNative?.peek() ?: 0L, + selector.keyExpr.flat.keyExprString, selector.parameters?.toString(), sessionPtr, getCallback, @@ -326,7 +326,7 @@ internal class JNISession(val sessionPtr: Long) { if (success) { val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null val sample = Sample( - KeyExpr(keyExpr!!, null), + KeyExpr(keyExpr!!), payload1.into(), Encoding(encodingId, schema = encodingSchema), SampleKind.fromInt(kind), @@ -334,10 +334,10 @@ internal class JNISession(val sessionPtr: Long) { QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), attachmentBytes?.into() ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) + reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, sample) } else { reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + replierZid?.let { EntityGlobalId(ZenohId(io.zenoh.jni.config.ZenohId(it)), replierEid.toUInt()) }, payload1.into(), Encoding(encodingId, schema = encodingSchema) ) @@ -347,8 +347,8 @@ internal class JNISession(val sessionPtr: Long) { val selector = intoSelector.into() getViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, + selector.keyExpr.flat.keyExprNative?.peek() ?: 0L, + selector.keyExpr.flat.keyExprString, selector.parameters?.toString(), sessionPtr, getCallback, @@ -371,15 +371,19 @@ internal class JNISession(val sessionPtr: Long) { @Throws(ZError::class) fun declareKeyExpr(keyExpr: String): KeyExpr { val ptr = declareKeyExprViaJNI(sessionPtr, keyExpr) - return KeyExpr(keyExpr, JNIKeyExpr(ptr)) + return KeyExpr(keyExpr, ptr) } @Throws(ZError::class) fun undeclareKeyExpr(keyExpr: KeyExpr) { - keyExpr.jniKeyExpr?.run { - undeclareKeyExprViaJNI(sessionPtr, this.ptr) - keyExpr.jniKeyExpr = null - } ?: throw ZError("Attempting to undeclare a non declared key expression.") + val handle = keyExpr.flat.keyExprNative + if (handle == null || handle.isClosed()) { + throw ZError("Attempting to undeclare a non declared key expression.") + } + // `undeclareKeyExprViaJNI` consumes the box on the Rust side, so + // take the pointer under the write lock and null the slot — the + // handle's cleaner will then no-op on GC. + handle.consume { ptr -> undeclareKeyExprViaJNI(sessionPtr, ptr) } } @Throws(ZError::class) @@ -390,8 +394,8 @@ internal class JNISession(val sessionPtr: Long) { ) { val encoding = options.encoding ?: Encoding.defaultEncoding() putViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, payload.into().bytes, encoding.id, @@ -410,30 +414,30 @@ internal class JNISession(val sessionPtr: Long) { options: DeleteOptions, ) { deleteViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, + keyExpr.flat.keyExprNative?.peek() ?: 0L, + keyExpr.flat.keyExprString, sessionPtr, options.congestionControl.value, options.priority.value, options.express, options.attachment?.into()?.bytes, - options.reliability.ordinal + options.reliability.jni.value ) } @Throws(ZError::class) fun zid(): ZenohId { - return ZenohId(getZidViaJNI(sessionPtr)) + return ZenohId(io.zenoh.jni.config.ZenohId(getZidViaJNI(sessionPtr))) } @Throws(ZError::class) fun peersZid(): List { - return getPeersZidViaJNI(sessionPtr).map { ZenohId(it) } + return getPeersZidViaJNI(sessionPtr).map { ZenohId(io.zenoh.jni.config.ZenohId(it)) } } @Throws(ZError::class) fun routersZid(): List { - return getRoutersZidViaJNI(sessionPtr).map { ZenohId(it) } + return getRoutersZidViaJNI(sessionPtr).map { ZenohId(io.zenoh.jni.config.ZenohId(it)) } } @Throws(ZError::class) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt deleted file mode 100644 index 0a8b20e9..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni.callbacks - -internal fun interface JNIScoutCallback { - - fun run(whatAmI: Int, zid: ByteArray, locators: List) -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index b5fe6110..2b3dd700 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -15,11 +15,14 @@ package io.zenoh.keyexpr import io.zenoh.Session +import io.zenoh.ZenohLoad import io.zenoh.session.SessionDeclaration import io.zenoh.exceptions.ZError -import io.zenoh.jni.JNIKeyExpr +import io.zenoh.exceptions.wrapJNIExceptionAsZError +import io.zenoh.jni.keyexpr.ZKeyExpr import io.zenoh.query.IntoSelector import io.zenoh.query.Selector +import io.zenoh.jni.keyexpr.KeyExpr as JniKeyExpr /** * # Address space @@ -59,10 +62,16 @@ import io.zenoh.query.Selector * ensures that [close] is automatically called, safely managing the lifecycle of the [KeyExpr] instance. * */ -class KeyExpr internal constructor(internal val keyExpr: String, internal var jniKeyExpr: JNIKeyExpr? = null): AutoCloseable, IntoSelector, +class KeyExpr internal constructor(internal val flat: JniKeyExpr) : AutoCloseable, IntoSelector, SessionDeclaration { + internal constructor(keyExpr: String, ptr: Long = 0L) : + this(JniKeyExpr(keyExpr, ptr.takeIf { it != 0L }?.let { ZKeyExpr(it) })) + companion object { + init { + ZenohLoad + } /** * Try from. @@ -78,8 +87,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @JvmStatic @Throws(ZError::class) - fun tryFrom(keyExpr: String): KeyExpr { - return JNIKeyExpr.tryFrom(keyExpr) + fun tryFrom(keyExpr: String): KeyExpr = wrapJNIExceptionAsZError { + KeyExpr(JniKeyExpr.keyexprTryFrom(keyExpr)) } /** @@ -94,8 +103,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @JvmStatic @Throws(ZError::class) - fun autocanonize(keyExpr: String): KeyExpr { - return JNIKeyExpr.autocanonize(keyExpr) + fun autocanonize(keyExpr: String): KeyExpr = wrapJNIExceptionAsZError { + KeyExpr(JniKeyExpr.keyexprAutocanonize(keyExpr)) } } @@ -105,8 +114,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * Will return false as well if the key expression is not valid anymore. */ @Throws(ZError::class) - fun intersects(other: KeyExpr): Boolean { - return JNIKeyExpr.intersects(this, other) + fun intersects(other: KeyExpr): Boolean = wrapJNIExceptionAsZError { + JniKeyExpr.keyexprIntersects(this.flat, other.flat) } /** @@ -115,8 +124,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * Will return false as well if the key expression is not valid anymore. */ @Throws(ZError::class) - fun includes(other: KeyExpr): Boolean { - return JNIKeyExpr.includes(this, other) + fun includes(other: KeyExpr): Boolean = wrapJNIExceptionAsZError { + JniKeyExpr.keyexprIncludes(this.flat, other.flat) } /** @@ -125,8 +134,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * so you should favor these methods for most applications. */ @Throws(ZError::class) - fun relationTo(other: KeyExpr): SetIntersectionLevel { - return JNIKeyExpr.relationTo(this, other) + fun relationTo(other: KeyExpr): SetIntersectionLevel = wrapJNIExceptionAsZError { + SetIntersectionLevel.fromJni(JniKeyExpr.keyexprRelationTo(this.flat, other.flat)) } /** @@ -134,8 +143,8 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * This should be your preferred method when concatenating path segments. */ @Throws(ZError::class) - fun join(other: String): KeyExpr { - return JNIKeyExpr.joinViaJNI(this, other) + fun join(other: String): KeyExpr = wrapJNIExceptionAsZError { + KeyExpr(JniKeyExpr.keyexprJoin(this.flat, other)) } /** @@ -143,13 +152,11 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * You should probably prefer [join] as Zenoh may then take advantage of the hierarchical separation it inserts. */ @Throws(ZError::class) - fun concat(other: String): KeyExpr { - return JNIKeyExpr.concatViaJNI(this, other) + fun concat(other: String): KeyExpr = wrapJNIExceptionAsZError { + KeyExpr(JniKeyExpr.keyexprConcat(this.flat, other)) } - override fun toString(): String { - return keyExpr - } + override fun toString(): String = flat.keyExprString /** * Equivalent to [undeclare]. This function is automatically called when using try with resources. @@ -166,8 +173,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * operations on it, but without the inner optimizations. */ override fun undeclare() { - jniKeyExpr?.close() - jniKeyExpr = null + flat.close() } override fun into(): Selector = Selector(this) @@ -178,10 +184,10 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn other as KeyExpr - return keyExpr == other.keyExpr + return flat.keyExprString == other.flat.keyExprString } override fun hashCode(): Int { - return keyExpr.hashCode() + return flat.keyExprString.hashCode() } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/SetIntersectionLevel.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/SetIntersectionLevel.kt index 981cc4b1..56c7e3ed 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/SetIntersectionLevel.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/SetIntersectionLevel.kt @@ -19,13 +19,14 @@ package io.zenoh.keyexpr * * Note that [EQUALS] implies [INCLUDES], which itself implies [INTERSECTS]. */ -enum class SetIntersectionLevel(internal val value: Int) { - DISJOINT(0), - INTERSECTS(1), - INCLUDES(2), - EQUALS(3); +enum class SetIntersectionLevel(internal val jni: io.zenoh.jni.keyexpr.SetIntersectionLevel) { + DISJOINT(io.zenoh.jni.keyexpr.SetIntersectionLevel.DISJOINT), + INTERSECTS(io.zenoh.jni.keyexpr.SetIntersectionLevel.INTERSECTS), + INCLUDES(io.zenoh.jni.keyexpr.SetIntersectionLevel.INCLUDES), + EQUALS(io.zenoh.jni.keyexpr.SetIntersectionLevel.EQUALS); companion object { - internal fun fromInt(value: Int) = SetIntersectionLevel.entries.first { it.value == value } + internal fun fromJni(jni: io.zenoh.jni.keyexpr.SetIntersectionLevel): SetIntersectionLevel = + entries.first { it.jni == jni } } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/CongestionControl.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/CongestionControl.kt index 82b3463a..0b8326a5 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/CongestionControl.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/CongestionControl.kt @@ -26,9 +26,19 @@ enum class CongestionControl (val value: Int) { * Prevents the message from being dropped at all cost. * In the face of heavy congestion on a part of the network, this could result in your publisher node blocking. */ - BLOCK(1); + BLOCK(1), + + /** + * Blocks low-priority traffic first, then drops when needed. + */ + BLOCK_FIRST(2); + + internal val jni: io.zenoh.jni.qos.CongestionControl + get() = io.zenoh.jni.qos.CongestionControl.fromInt(value) companion object { fun fromInt(value: Int) = entries.first { it.value == value } + + internal fun fromJni(jni: io.zenoh.jni.qos.CongestionControl): CongestionControl = fromInt(jni.value) } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Priority.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Priority.kt index 0e27780e..22126ee6 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Priority.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Priority.kt @@ -31,7 +31,12 @@ enum class Priority(val value: Int) { DATA_LOW(6), BACKGROUND(7); + internal val jni: io.zenoh.jni.qos.Priority + get() = io.zenoh.jni.qos.Priority.fromInt(value) + companion object { fun fromInt(value: Int) = entries.first { it.value == value } + + internal fun fromJni(jni: io.zenoh.jni.qos.Priority): Priority = fromInt(jni.value) } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Reliability.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Reliability.kt index d574f27c..fe009d62 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Reliability.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/qos/Reliability.kt @@ -36,4 +36,17 @@ enum class Reliability { * this reliability requirement may be infringed to prevent slow readers from blocking the network. */ RELIABLE, + + ; + + internal val jni: io.zenoh.jni.qos.Reliability + get() = when (this) { + BEST_EFFORT -> io.zenoh.jni.qos.Reliability.BEST_EFFORT + RELIABLE -> io.zenoh.jni.qos.Reliability.RELIABLE + } + + companion object { + internal fun fromJni(jni: io.zenoh.jni.qos.Reliability): Reliability = + entries.first { it.jni == jni } + } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/scouting/Scout.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/scouting/Scout.kt index 8034a851..49a3315b 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/scouting/Scout.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/scouting/Scout.kt @@ -14,7 +14,15 @@ package io.zenoh.scouting -import io.zenoh.jni.JNIScout +import io.zenoh.Config +import io.zenoh.ZenohLoad +import io.zenoh.config.WhatAmI +import io.zenoh.config.ZenohId +import io.zenoh.exceptions.ZError +import io.zenoh.exceptions.wrapJNIExceptionAsZError +import io.zenoh.handlers.Callback +import io.zenoh.jni.scouting.ZScout +import io.zenoh.jni.callbacks.HelloCallback /** * Scout for routers and/or peers. @@ -62,15 +70,62 @@ import io.zenoh.jni.JNIScout * @see HandlerScout */ sealed class Scout ( - private var jniScout: JNIScout? + private var zScout: ZScout? ) : AutoCloseable { + companion object { + + init { + ZenohLoad + } + + @Throws(ZError::class) + internal fun scoutWithHandler( + whatAmI: Set, + callback: Callback, + onClose: () -> Unit, + config: Config?, + receiver: R, + ): HandlerScout = wrapJNIExceptionAsZError { + HandlerScout(runScout(whatAmI, config, callback, onClose), receiver) + } + + @Throws(ZError::class) + internal fun scoutWithCallback( + whatAmI: Set, + callback: Callback, + config: Config?, + ): CallbackScout = wrapJNIExceptionAsZError { + CallbackScout(runScout(whatAmI, config, callback) {}) + } + + private fun runScout( + whatAmI: Set, + config: Config?, + callback: Callback, + onClose: () -> Unit, + ): ZScout { + val bitfield = whatAmI.map { it.jni.value }.reduce { acc, v -> acc or v } + val helloCallback = HelloCallback { jniHello -> + callback.run( + Hello( + whatAmI = WhatAmI.fromJni(jniHello.whatami), + zid = ZenohId(jniHello.zid), + locators = jniHello.locators, + ) + ) + } + val onCloseCallback = io.zenoh.jni.callbacks.Callback { onClose() } + return io.zenoh.jni.scouting.scout(bitfield, config?.zConfig, helloCallback, onCloseCallback) + } + } + /** * Stops the scouting. */ fun stop() { - jniScout?.close() - jniScout = null + zScout?.close() + zScout = null } /** @@ -93,7 +148,7 @@ sealed class Scout ( * CallbackScout scout = Zenoh.scout(hello -> {...}); * ``` */ -class CallbackScout internal constructor(jniScout: JNIScout?) : Scout(jniScout) +class CallbackScout internal constructor(zScout: ZScout) : Scout(zScout) /** * Scout using a handler to handle incoming [Hello] messages. @@ -106,4 +161,4 @@ class CallbackScout internal constructor(jniScout: JNIScout?) : Scout(jniScout) * @param R The type of the receiver. * @param receiver The receiver of the scout's handler. */ -class HandlerScout internal constructor(jniScout: JNIScout?, val receiver: R) : Scout(jniScout) +class HandlerScout internal constructor(zScout: ZScout, val receiver: R) : Scout(zScout) diff --git a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt index 8da47656..b7b9e76b 100644 --- a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt @@ -25,7 +25,7 @@ import java.util.zip.ZipInputStream * log level configuration. */ internal actual object ZenohLoad { - private const val ZENOH_LIB_NAME = "zenoh_jni" + private const val ZENOH_LIB_NAME = "zenoh_flat_jni" init { // Try first to load the local native library for cases in which the module was built locally, diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index a88e31ec..5d4e3cdb 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -24,7 +24,16 @@ description = "Zenoh: Zero Overhead Pub/sub, Store/Query and Compute." name = "zenoh_jni" [features] -default = ["zenoh/default", "zenoh-ext"] +default = ["zenoh-default", "zenoh-ext"] +# Re-export of zenoh's "default" features under a name that dependents can +# enable through this crate's [features] table (Cargo disallows raw `zenoh/…` +# in a dependent's `features = […]` list, so we need a passthrough feature). +zenoh-default = ["zenoh/default"] +# Vestigial gating left in place so the standalone dylib build can be +# re-enabled by hand (turn this feature on AND add `dylib` to crate-type). +# In the normal build path, zenoh-flat-jni's `outdated/` shim re-exports +# every Java_… symbol, so this is off by default. +export_jni_symbols = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -41,7 +50,8 @@ zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.g tracing = { version = "0.1" , features = ["log"] } [lib] name = "zenoh_jni" -crate_type = ["staticlib", "dylib"] +# Plain rlib: the JNI ABI is now exported through zenoh-flat-jni's outdated/ +# shim, which links zenoh-jni as a regular Rust library. No standalone dylib. [build-dependencies] rustc_version = "0.4.0" diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs deleted file mode 100644 index 0ada1340..00000000 --- a/zenoh-jni/src/config.rs +++ /dev/null @@ -1,185 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use std::{ptr::null, sync::Arc}; - -use jni::{ - objects::{JClass, JString}, - sys::jstring, - JNIEnv, -}; -use zenoh::Config; - -use crate::{errors::ZResult, zerror}; -use crate::{throw_exception, utils::decode_string}; - -/// Loads the default configuration, returning a raw pointer to it. -/// -/// The pointer to the config is expected to be freed later on upon the destruction of the -/// Kotlin Config instance. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadDefaultConfigViaJNI( - _env: JNIEnv, - _class: JClass, -) -> *const Config { - let config = Config::default(); - Arc::into_raw(Arc::new(config)) -} - -/// Loads the config from a file, returning a pointer to the loaded config in case of success. -/// In case of failure, an exception is thrown via JNI. -/// -/// The pointer to the config is expected to be freed later on upon the destruction of the -/// Kotlin Config instance. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadConfigFileViaJNI( - mut env: JNIEnv, - _class: JClass, - config_path: JString, -) -> *const Config { - || -> ZResult<*const Config> { - let config_file_path = decode_string(&mut env, &config_path)?; - let config = Config::from_file(config_file_path).map_err(|err| zerror!(err))?; - Ok(Arc::into_raw(Arc::new(config))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Loads the config from a json/json5 formatted string, returning a pointer to the loaded config -/// in case of success. In case of failure, an exception is thrown via JNI. -/// -/// The pointer to the config is expected to be freed later on upon the destruction of the -/// Kotlin Config instance. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadJsonConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - json_config: JString, -) -> *const Config { - || -> ZResult<*const Config> { - let json_config = decode_string(&mut env, &json_config)?; - let mut deserializer = - json5::Deserializer::from_str(&json_config).map_err(|err| zerror!(err))?; - let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("JSON error: {}", e), - })?; - Ok(Arc::into_raw(Arc::new(config))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Loads the config from a yaml-formatted string, returning a pointer to the loaded config -/// in case of success. In case of failure, an exception is thrown via JNI. -/// -/// The pointer to the config is expected to be freed later on upon the destruction of the -/// Kotlin Config instance. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadYamlConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - yaml_config: JString, -) -> *const Config { - || -> ZResult<*const Config> { - let yaml_config = decode_string(&mut env, &yaml_config)?; - let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); - let config = Config::from_deserializer(deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("YAML error: {}", e), - })?; - Ok(Arc::into_raw(Arc::new(config))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Returns the json value associated to the provided [key]. May throw an exception in case of failure, which must be handled -/// on the kotlin layer. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_getJsonViaJNI( - mut env: JNIEnv, - _class: JClass, - cfg_ptr: *const Config, - key: JString, -) -> jstring { - let arc_cfg: Arc = Arc::from_raw(cfg_ptr); - let result = || -> ZResult { - let key = decode_string(&mut env, &key)?; - let json = arc_cfg.get_json(&key).map_err(|err| zerror!(err))?; - let java_json = env.new_string(json).map_err(|err| zerror!(err))?; - Ok(java_json.as_raw()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JString::default().as_raw() - }); - std::mem::forget(arc_cfg); - result -} - -/// Inserts a json5 value associated to the provided [key]. May throw an exception in case of failure, which must be handled -/// on the kotlin layer. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_insertJson5ViaJNI( - mut env: JNIEnv, - _class: JClass, - cfg_ptr: *const Config, - key: JString, - value: JString, -) { - || -> ZResult<()> { - let key = decode_string(&mut env, &key)?; - let value = decode_string(&mut env, &value)?; - let mut config = core::ptr::read(cfg_ptr); - let insert_result = config - .insert_json5(&key, &value) - .map_err(|err| zerror!(err)); - core::ptr::write(cfg_ptr as *mut _, config); - insert_result - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - }) -} - -/// Frees the pointer to the config. The pointer should be valid and should have been obtained through -/// one of the preceding `load` functions. This function should be called upon destruction of the kotlin -/// Config instance. -#[no_mangle] -#[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_freePtrViaJNI( - _env: JNIEnv, - _: JClass, - config_ptr: *const Config, -) { - Arc::from_raw(config_ptr); -} diff --git a/zenoh-jni/src/errors.rs b/zenoh-jni/src/errors.rs index 23687d4c..a7af450b 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -35,10 +35,10 @@ macro_rules! zerror { }; } -pub(crate) type ZResult = core::result::Result; +pub type ZResult = core::result::Result; #[derive(Debug)] -pub(crate) struct ZError(pub String); +pub struct ZError(pub String); impl fmt::Display for ZError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index 11d39d47..d1fff399 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -12,309 +12,24 @@ // ZettaScale Zenoh Team, // -use std::ops::Deref; -use std::sync::Arc; - -use jni::objects::JClass; -use jni::sys::{jboolean, jint, jstring}; use jni::{objects::JString, JNIEnv}; use zenoh::key_expr::KeyExpr; use crate::errors::ZResult; use crate::utils::decode_string; -use crate::{throw_exception, zerror}; - -/// Validates the provided `key_expr` to be a valid key expression, returning it back -/// in case of success or throwing an exception in case of failure. -/// -/// # Parameters: -/// `env`: The JNI environment. -/// `_class`: the Java class (unused). -/// `key_expr`: Java string representation of the intended key expression. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_tryFromViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr: JString, -) -> jstring { - validate_key_expr(&mut env, &key_expr) - .map(|_| **key_expr) - .unwrap_or_else(|err| { - throw_exception!(env, err); - JString::default().as_raw() - }) -} +use crate::zerror; -/// Returns a java string representation of the autocanonized version of the provided `key_expr`. -/// In case of failure and exception will be thrown. -/// -/// # Parameters: -/// `env`: The JNI environment. -/// `_class`: the Java class (unused). -/// `key_expr`: Java string representation of the intended key expression. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_autocanonizeViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr: JString, -) -> jstring { - autocanonize_key_expr(&mut env, &key_expr) - .and_then(|key_expr| { - env.new_string(key_expr.to_string()) - .map(|kexp| kexp.as_raw()) - .map_err(|err| zerror!(err)) - }) - .unwrap_or_else(|err| { - throw_exception!(env, err); - JString::default().as_raw() - }) -} - -/// Returns true in case key_expr_1 intersects key_expr_2. -/// -/// # Params: -/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. -/// - `key_expr_str_1`: String representation of the key expression 1. -/// - `key_expr_ptr_2`: Pointer to the key expression 2, differs from null only if it's a declared key expr. -/// - `key_expr_str_2`: String representation of the key expression 2. +/// Bridge a Kotlin-side `(KeyExpr.flat.keyExprNative, KeyExpr.flat.keyExprString)` +/// pair into a `zenoh::KeyExpr<'static>`. When the pointer is non-null, +/// the layout matches `Box>` (compatible across +/// zenoh-jni and zenoh-flat-jni), so cloning the pointee yields a fresh +/// owned key expression; otherwise the string is parsed fresh. /// /// # Safety -/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing -/// key expressions that were declared from a session (in that case the key expression has a pointer associated). -/// -/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers -/// remain valid after the call to this function. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_intersectsViaJNI( - mut env: JNIEnv, - _: JClass, - key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_1: JString, - key_expr_ptr_2: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_2: JString, -) -> jboolean { - || -> ZResult { - let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; - let key_expr_2 = process_kotlin_key_expr(&mut env, &key_expr_str_2, key_expr_ptr_2)?; - Ok(key_expr_1.intersects(&key_expr_2) as jboolean) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - false as jboolean - }) -} - -/// Returns true in case key_expr_1 includes key_expr_2. -/// -/// # Params: -/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. -/// - `key_expr_str_1`: String representation of the key expression 1. -/// - `key_expr_ptr_2`: Pointer to the key expression 2, differs from null only if it's a declared key expr. -/// - `key_expr_str_2`: String representation of the key expression 2. -/// -/// # Safety -/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing -/// key expressions that were declared from a session (in that case the key expression has a pointer associated). -/// -/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers -/// remain valid after the call to this function. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_includesViaJNI( - mut env: JNIEnv, - _: JClass, - key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_1: JString, - key_expr_ptr_2: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_2: JString, -) -> jboolean { - || -> ZResult { - let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; - let key_expr_2 = process_kotlin_key_expr(&mut env, &key_expr_str_2, key_expr_ptr_2)?; - Ok(key_expr_1.includes(&key_expr_2) as jboolean) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - false as jboolean - }) -} - -/// Returns the integer representation of the intersection level of the key expression 1 and key expression 2, -/// from the perspective of key expression 1. -/// -/// # Params: -/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. -/// - `key_expr_str_1`: String representation of the key expression 1. -/// - `key_expr_ptr_2`: Pointer to the key expression 2, differs from null only if it's a declared key expr. -/// - `key_expr_str_2`: String representation of the key expression 2. -/// -/// # Safety -/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing -/// key expressions that were declared from a session (in that case the key expression has a pointer associated). -/// -/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers -/// remain valid after the call to this function. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_relationToViaJNI( - mut env: JNIEnv, - _: JClass, - key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_1: JString, - key_expr_ptr_2: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_2: JString, -) -> jint { - || -> ZResult { - let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; - let key_expr_2 = process_kotlin_key_expr(&mut env, &key_expr_str_2, key_expr_ptr_2)?; - Ok(key_expr_1.relation_to(&key_expr_2) as jint) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - -1 as jint - }) -} - -/// Joins key expression 1 with key expression 2, where key_expr_2 is a string. Returns the string representation -/// of the result, or throws an exception in case of failure. -/// -/// # Params: -/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. -/// - `key_expr_ptr_1`: String representation of the key expression 1. -/// - `key_expr_2`: String representation of the key expression 2. -/// -/// # Safety -/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing -/// key expressions that were declared from a session (in that case the key expression has a pointer associated). -/// -/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers -/// remain valid after the call to this function. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_joinViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_1: JString, - key_expr_2: JString, -) -> jstring { - || -> ZResult { - let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; - let key_expr_2_str = decode_string(&mut env, &key_expr_2)?; - let result = key_expr_1 - .join(key_expr_2_str.as_str()) - .map_err(|err| zerror!(err))?; - env.new_string(result.to_string()) - .map(|kexp| kexp.as_raw()) - .map_err(|err| zerror!(err)) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JString::default().as_raw() - }) -} - -/// Concats key_expr_1 with key_expr_2, where key_expr_2 is a string. Returns the string representation -/// of the result, or throws an exception in case of failure. -/// -/// # Params: -/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. -/// - `key_expr_ptr_1`: String representation of the key expression 1. -/// - `key_expr_2`: String representation of the key expression 2. -/// -/// # Safety -/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing -/// key expressions that were declared from a session (in that case the key expression has a pointer associated). -/// -/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers -/// remain valid after the call to this function. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_concatViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, - key_expr_str_1: JString, - key_expr_2: JString, -) -> jstring { - || -> ZResult { - let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; - let key_expr_2_str = decode_string(&mut env, &key_expr_2)?; - let result = key_expr_1 - .concat(key_expr_2_str.as_str()) - .map_err(|err| zerror!(err))?; - env.new_string(result.to_string()) - .map(|kexp| kexp.as_raw()) - .map_err(|err| zerror!(err)) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - JString::default().as_raw() - }) -} - -/// Frees a declared key expression. -/// -/// # Parameters -/// - `_env`: Unused. The JNI environment. -/// - `_class`: Unused. The java class from which the function was called. -/// - `key_expr_ptr`: the pointer to the key expression. -/// -/// # Safety -/// - This function assumes the provided pointer is valid and points to a native key expression. -/// - The memory associated to the pointer is freed after returning from this call, turning the -/// pointer invalid after that. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_freePtrViaJNI( - _env: JNIEnv, - _: JClass, - key_expr_ptr: *const KeyExpr<'static>, -) { - Arc::from_raw(key_expr_ptr); -} - -fn validate_key_expr(env: &mut JNIEnv, key_expr: &JString) -> ZResult> { - let key_expr_str = decode_string(env, key_expr) - .map_err(|err| zerror!("Unable to get key expression string value: '{}'.", err))?; - - KeyExpr::try_from(key_expr_str) - .map_err(|err| zerror!("Unable to create key expression: '{}'.", err)) -} - -fn autocanonize_key_expr(env: &mut JNIEnv, key_expr: &JString) -> ZResult> { - decode_string(env, key_expr) - .map_err(|err| zerror!("Unable to get key expression string value: '{}'.", err)) - .and_then(|key_expr_str| { - KeyExpr::autocanonize(key_expr_str) - .map_err(|err| zerror!("Unable to create key expression: '{}'", err)) - }) -} - -/// Processes a kotlin key expression. -/// -/// Receives the Java/Kotlin string representation of the key expression as well as a pointer. -/// The pointer is only valid in cases where the key expression is associated to a native pointer -/// (when the key expression was declared from a session). -/// If the pointer is valid, the key expression returned is the key expression the pointer pointed to. -/// Otherwise, a key expression created from the string representation of the key expression is returned. -/// -/// # Safety: -/// -/// The key_expr_str argument provided should already have been validated upon creation of the -/// KeyExpr instance on Kotlin. /// +/// `key_expr_ptr` must either be null or point to a live +/// `KeyExpr<'static>` produced by either side of the JNI boundary. The +/// string is assumed already validated by the Kotlin wrapper. pub(crate) unsafe fn process_kotlin_key_expr( env: &mut JNIEnv, key_expr_str: &JString, @@ -325,9 +40,6 @@ pub(crate) unsafe fn process_kotlin_key_expr( .map_err(|err| zerror!("Unable to get key expression string value: '{}'.", err))?; Ok(KeyExpr::from_string_unchecked(key_expr)) } else { - let key_expr = Arc::from_raw(key_expr_ptr); - let key_expr_clone = key_expr.deref().clone(); - std::mem::forget(key_expr); - Ok(key_expr_clone) + Ok((*key_expr_ptr).clone()) } } diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index ff3981a4..f058076d 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -12,22 +12,26 @@ // ZettaScale Zenoh Team, // -mod config; -mod errors; -mod key_expr; -mod liveliness; -mod logger; -mod publisher; -mod querier; -mod query; -mod queryable; -mod scouting; -mod session; -mod subscriber; -mod utils; +pub mod errors; +pub mod key_expr; +pub mod liveliness; +pub mod logger; +pub mod publisher; +pub mod querier; +pub mod query; +pub mod queryable; +pub mod session; +pub mod subscriber; +pub mod utils; #[cfg(feature = "zenoh-ext")] -mod zbytes; -mod zenoh_id; +pub mod zbytes; +pub mod zenoh_id; + +pub use errors::{ZError, ZResult}; + +// Re-export upstream zenoh so dependents (outdated/ shim in zenoh-flat-jni) +// don't need a separate `zenoh` Cargo dep to name handle types. +pub use zenoh; // Test should be runned with `cargo test --no-default-features` #[test] diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 8b05c925..dec2a077 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -37,9 +37,9 @@ use crate::{ zerror, }; -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -49,7 +49,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( timeout_ms: jlong, on_close: JObject, ) { - let session = unsafe { Arc::from_raw(session_ptr) }; + let session: &Session = unsafe { &*session_ptr }; let _ = || -> ZResult<()> { let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -98,20 +98,19 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( .map_err(|err| { throw_exception!(env, err); }); - std::mem::forget(session); } -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, ) -> *const LivelinessToken { - let session = unsafe { Arc::from_raw(session_ptr) }; - let ptr = || -> ZResult<*const LivelinessToken> { + let session: &Session = unsafe { &*session_ptr }; + || -> ZResult<*const LivelinessToken> { let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; tracing::trace!("Declaring liveliness token on '{key_expr}'."); let token = session @@ -119,29 +118,27 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( .declare_token(key_expr) .wait() .map_err(|err| zerror!(err))?; - Ok(Arc::into_raw(Arc::new(token))) + Ok(Box::into_raw(Box::new(token)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - std::mem::forget(session); - ptr + }) } -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareViaJNI( _env: JNIEnv, _: JClass, token_ptr: *const LivelinessToken, ) { - unsafe { Arc::from_raw(token_ptr) }; + let _ = unsafe { Box::from_raw(token_ptr as *mut LivelinessToken) }; } -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -151,7 +148,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( history: jboolean, on_close: JObject, ) -> *const Subscriber<()> { - let session = unsafe { Arc::from_raw(session_ptr) }; + let session: &Session = unsafe { &*session_ptr }; || -> ZResult<*const Subscriber<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; @@ -232,8 +229,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( result.map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); - std::mem::forget(session); - Ok(Arc::into_raw(Arc::new(subscriber))) + Ok(Box::into_raw(Box::new(subscriber)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); diff --git a/zenoh-jni/src/logger.rs b/zenoh-jni/src/logger.rs index 785a6cd1..8491969d 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -35,7 +35,7 @@ use crate::{errors::ZResult, throw_exception, zerror}; /// # Errors: /// - If there is an error parsing the log level string, a `JNIException` is thrown on the JVM. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_Logger_00024Companion_startLogsViaJNI( mut env: JNIEnv, diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index ead60c3f..1801d0b8 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; - use jni::{ objects::{JByteArray, JClass, JString}, sys::jint, @@ -45,7 +43,7 @@ use crate::{ /// - The publisher pointer remains valid after this function call. /// - May throw an exception in case of failure, which must be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( mut env: JNIEnv, @@ -56,7 +54,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( attachment: /*nullable*/ JByteArray, publisher_ptr: *const Publisher<'static>, ) { - let publisher = Arc::from_raw(publisher_ptr); + let publisher: &Publisher<'static> = &*publisher_ptr; let _ = || -> ZResult<()> { let payload = decode_byte_array(&env, payload)?; let mut publication = publisher.put(payload); @@ -69,7 +67,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( publication.wait().map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(publisher); } /// Performs a DELETE operation on a Zenoh publisher via JNI. @@ -86,7 +83,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( /// - The publisher pointer remains valid after this function call. /// - May throw an exception in case of failure, which must be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( mut env: JNIEnv, @@ -94,7 +91,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( attachment: /*nullable*/ JByteArray, publisher_ptr: *const Publisher<'static>, ) { - let publisher = Arc::from_raw(publisher_ptr); + let publisher: &Publisher<'static> = &*publisher_ptr; let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { @@ -104,7 +101,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( delete.wait().map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(publisher) } /// Frees the publisher. @@ -119,12 +115,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( /// - It assumes that the provided publisher pointer is valid and has not been modified or freed. /// - After calling this function, the publisher pointer becomes invalid and should not be used anymore. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI( _env: JNIEnv, _: JClass, publisher_ptr: *const Publisher, ) { - Arc::from_raw(publisher_ptr); + let _ = Box::from_raw(publisher_ptr as *mut Publisher); } diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 8c239498..92b65a55 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -53,7 +53,7 @@ use crate::{ /// - `encoding_id`: Encoding id of the payload provided. /// - `encoding_schema`: Encoding schema of the payload provided. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( mut env: JNIEnv, @@ -69,7 +69,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( encoding_id: jint, encoding_schema: /*nullable*/ JString, ) { - let querier = Arc::from_raw(querier_ptr); + let querier: &Querier = &*querier_ptr; let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -118,7 +118,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(querier); } /// @@ -126,12 +125,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( /// /// After a call to this function, no further jni operations should be performed using the querier associated to the raw pointer provided. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_freePtrViaJNI( _env: JNIEnv, _: JClass, querier_ptr: *const Querier<'static>, ) { - Arc::from_raw(querier_ptr); + let _ = Box::from_raw(querier_ptr as *mut Querier<'static>); } diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index 9c843892..cbed7537 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; - use crate::utils::{decode_byte_array, decode_encoding}; use crate::zerror; use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception}; @@ -55,9 +53,9 @@ use zenoh::{ /// therefore the query isn't valid anymore after that. /// - May throw a JNI exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, @@ -72,7 +70,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( qos_express: jboolean, ) { let _ = || -> ZResult<()> { - let query = Arc::from_raw(query_ptr); + let query: Query = *Box::from_raw(query_ptr as *mut Query); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let payload = decode_byte_array(&env, payload)?; let mut reply_builder = query.reply(key_expr, payload); @@ -108,9 +106,9 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( /// - The query pointer is freed after calling this function (queries shouldn't be replied more than once), /// therefore the query isn't valid anymore after that. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, @@ -119,7 +117,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( encoding_schema: /*nullable*/ JString, ) { let _ = || -> ZResult<()> { - let query = Arc::from_raw(query_ptr); + let query: Query = *Box::from_raw(query_ptr as *mut Query); let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; query .reply_err(decode_byte_array(&env, payload)?) @@ -152,9 +150,9 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( /// - The query pointer is freed after calling this function (queries shouldn't be replied more than once), /// therefore the query isn't valid anymore after that. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, @@ -166,7 +164,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( qos_express: jboolean, ) { let _ = || -> ZResult<()> { - let query = Arc::from_raw(query_ptr); + let query: Query = *Box::from_raw(query_ptr as *mut Query); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let mut reply_builder = query.reply_del(key_expr); if timestamp_enabled != 0 { @@ -195,12 +193,12 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( /// - The function takes ownership of the raw pointer and releases the associated memory. /// - After calling this function, the query pointer becomes invalid and should not be used anymore. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_freePtrViaJNI( _env: JNIEnv, _: JClass, query_ptr: *const Query, ) { - Arc::from_raw(query_ptr); + let _ = Box::from_raw(query_ptr as *mut Query); } diff --git a/zenoh-jni/src/queryable.rs b/zenoh-jni/src/queryable.rs index 5d2ddb1d..b854769c 100644 --- a/zenoh-jni/src/queryable.rs +++ b/zenoh-jni/src/queryable.rs @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; - use jni::{objects::JClass, JNIEnv}; use zenoh::query::Queryable; @@ -30,12 +28,12 @@ use zenoh::query::Queryable; /// - The function takes ownership of the raw pointer and releases the associated memory. /// - After calling this function, the queryable pointer becomes invalid and should not be used anymore. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI( _env: JNIEnv, _: JClass, queryable_ptr: *const Queryable<()>, ) { - Arc::from_raw(queryable_ptr); + let _ = Box::from_raw(queryable_ptr as *mut Queryable<()>); } diff --git a/zenoh-jni/src/scouting.rs b/zenoh-jni/src/scouting.rs deleted file mode 100644 index b0a665c1..00000000 --- a/zenoh-jni/src/scouting.rs +++ /dev/null @@ -1,113 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use std::{ptr::null, sync::Arc}; - -use jni::{ - objects::{GlobalRef, JClass, JList, JObject, JValue}, - sys::jint, - JNIEnv, -}; -use zenoh::{config::WhatAmIMatcher, Wait}; -use zenoh::{scouting::Scout, Config}; - -use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; -use crate::{errors::ZResult, throw_exception, zerror}; - -/// Start a scout. -/// -/// # Params -/// - `whatAmI`: Ordinal value of the WhatAmI enum. -/// - `callback`: Callback to be executed whenever a hello message is received. -/// - `config_ptr`: Optional config pointer. -/// -/// Returns a pointer to the scout, which must be freed afterwards. -/// If starting the scout fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIScout_00024Companion_scoutViaJNI( - mut env: JNIEnv, - _class: JClass, - whatAmI: jint, - callback: JObject, - on_close: JObject, - config_ptr: /*nullable=*/ *const Config, -) -> *const Scout<()> { - || -> ZResult<*const Scout<()>> { - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let java_vm = Arc::new(get_java_vm(&mut env)?); - let on_close_global_ref: GlobalRef = get_callback_global_ref(&mut env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - let whatAmIMatcher: WhatAmIMatcher = (whatAmI as u8).try_into().unwrap(); // The validity of the operation is guaranteed on the kotlin layer. - let config = if config_ptr.is_null() { - Config::default() - } else { - let arc_cfg = Arc::from_raw(config_ptr); - let config_clone = arc_cfg.as_ref().clone(); - std::mem::forget(arc_cfg); - config_clone - }; - zenoh::scout(whatAmIMatcher, config) - .callback(move |hello| { - on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure - tracing::debug!("Received hello: {hello}"); - let _ = || -> jni::errors::Result<()> { - let mut env = java_vm.attach_current_thread_as_daemon()?; - let whatami = hello.whatami() as jint; - let zenoh_id = env - .byte_array_from_slice(&hello.zid().to_le_bytes()) - .map(|it| env.auto_local(it))?; - let locators = env - .new_object("java/util/ArrayList", "()V", &[]) - .map(|it| env.auto_local(it))?; - let jlist = JList::from_env(&mut env, &locators)?; - for value in hello.locators() { - let locator = env.new_string(value.as_str())?; - jlist.add(&mut env, &locator)?; - } - env.call_method( - &callback_global_ref, - "run", - "(I[BLjava/util/List;)V", - &[ - JValue::from(whatami), - JValue::from(&zenoh_id), - JValue::from(&locators), - ], - )?; - Ok(()) - }() - .map_err(|err| tracing::error!("Error while scouting: ${err}")); - }) - .wait() - .map(|scout| Arc::into_raw(Arc::new(scout))) - .map_err(|err| zerror!(err)) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Frees the scout. -#[no_mangle] -#[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIScout_00024Companion_freePtrViaJNI( - _env: JNIEnv, - _: JClass, - scout_ptr: *const Scout<()>, -) { - Arc::from_raw(scout_ptr); -} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 5f0848b2..80c66c4a 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -use std::{mem, ops::Deref, ptr::null, sync::Arc, time::Duration}; +use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ objects::{GlobalRef, JByteArray, JClass, JList, JObject, JString, JValue}, @@ -46,7 +46,7 @@ use crate::{ /// - `_class`: The JNI class (parameter required by the JNI interface but unused). /// - `config_path`: Nullable path to the Zenoh config file. If null, the default configuration will be loaded. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI( mut env: JNIEnv, @@ -55,7 +55,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSession ) -> *const Session { let session = open_session(config_ptr); match session { - Ok(session) => Arc::into_raw(Arc::new(session)), + Ok(session) => Box::into_raw(Box::new(session)) as *const _, Err(err) => { tracing::error!("Unable to open session: {}", err); throw_exception!(env, zerror!(err)); @@ -69,12 +69,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSession /// If the config path provided is null then the default configuration is loaded. /// unsafe fn open_session(config_ptr: *const Config) -> ZResult { - let config = Arc::from_raw(config_ptr); - let result = zenoh::open(config.as_ref().clone()) + zenoh::open((*config_ptr).clone()) .wait() - .map_err(|err| zerror!(err)); - mem::forget(config); - result + .map_err(|err| zerror!(err)) } /// Open a Zenoh session with a JSON configuration. @@ -90,7 +87,7 @@ unsafe fn open_session(config_ptr: *const Config) -> ZResult { /// - `_class`: The JNI class (parameter required by the JNI interface but unused). /// - `json_config`: Configuration as a JSON string. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( mut env: JNIEnv, @@ -99,7 +96,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( ) -> *const Session { let session = open_session_with_json_config(&mut env, json_config); match session { - Ok(session) => Arc::into_raw(Arc::new(session)), + Ok(session) => Box::into_raw(Box::new(session)) as *const _, Err(err) => { tracing::error!("Unable to open session: {}", err); throw_exception!(env, zerror!(err)); @@ -134,7 +131,7 @@ fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> ZRes /// - `_class`: The JNI class (parameter required by the JNI interface but unused). /// - `yaml_config`: Configuration as a YAML string. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( mut env: JNIEnv, @@ -143,7 +140,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( ) -> *const Session { let session = open_session_with_yaml_config(&mut env, yaml_config); match session { - Ok(session) => Arc::into_raw(Arc::new(session)), + Ok(session) => Box::into_raw(Box::new(session)) as *const _, Err(err) => { tracing::error!("Unable to open session: {}", err); throw_exception!(env, zerror!(err)); @@ -177,14 +174,14 @@ fn open_session_with_yaml_config(env: &mut JNIEnv, yaml_config: JString) -> ZRes /// - The function may throw a JNI exception in case of failure, which should be handled by the caller. /// - After the session is closed, the provided pointer is no more valid. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case, unused)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, ) { - Arc::from_raw(session_ptr); + let _ = Box::from_raw(session_ptr as *mut Session); } /// Declare a Zenoh publisher via JNI. @@ -212,7 +209,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( /// after this function call so it is safe to use it after this call. /// - The function may throw an exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( mut env: JNIEnv, @@ -225,7 +222,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( is_express: jboolean, reliability: jint, ) -> *const Publisher<'static> { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let publisher_ptr = || -> ZResult<*const Publisher<'static>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; @@ -239,7 +236,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( .reliability(reliability) .wait(); match result { - Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), + Ok(publisher) => Ok(Box::into_raw(Box::new(publisher)) as *const _), Err(err) => Err(zerror!(err)), } }() @@ -247,7 +244,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( throw_exception!(env, err); null() }); - std::mem::forget(session); publisher_ptr } @@ -277,7 +273,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( /// allowing safe usage of the session after this function call. /// - The function may throw an exception in case of failure, which should be handled by the Java/Kotlin caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( mut env: JNIEnv, @@ -294,7 +290,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let payload = decode_byte_array(&env, payload)?; @@ -322,7 +318,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } /// Performs a `delete` operation in the Zenoh session via JNI. @@ -349,7 +344,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( /// - The function may throw a JNI exception or a Session exception in case of failure, which /// should be handled by the Java/Kotlin caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( mut env: JNIEnv, @@ -363,7 +358,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; @@ -388,7 +383,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } /// Declare a Zenoh subscriber via JNI. @@ -416,7 +410,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( /// in Java/Kotlin, matching the specified signature. /// - The function may throw a JNI exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( mut env: JNIEnv, @@ -427,7 +421,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( callback: JObject, on_close: JObject, ) -> *const Subscriber<()> { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; || -> ZResult<*const Subscriber<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; @@ -505,8 +499,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( let subscriber = result.map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); - std::mem::forget(session); - Ok(Arc::into_raw(Arc::new(subscriber))) + Ok(Box::into_raw(Box::new(subscriber)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); @@ -531,7 +524,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( /// - `priority`: The ordinal value of the priority enum value. /// - `is_express`: The boolean express value of the QoS provided. /// - `timeout_ms`: The timeout in milliseconds. -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( mut env: JNIEnv, @@ -547,7 +540,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( timeout_ms: jlong, accept_replies: jint, ) -> *const Querier<'static> { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; || -> ZResult<*const Querier<'static>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let query_target = decode_query_target(target)?; @@ -571,8 +564,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( .map_err(|err| zerror!(err))?; tracing::debug!("Querier declared on '{}'.", key_expr); - std::mem::forget(session); - Ok(Arc::into_raw(Arc::new(querier))) + Ok(Box::into_raw(Box::new(querier)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); @@ -608,7 +600,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( /// in Java/Kotlin, matching the specified signature. /// - The function may throw a JNI exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( mut env: JNIEnv, @@ -620,7 +612,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( on_close: JObject, complete: jboolean, ) -> *const Queryable<()> { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let query_ptr = || -> ZResult<*const Queryable<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; @@ -652,13 +644,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( let queryable = builder .wait() .map_err(|err| zerror!("Error declaring queryable: {}", err))?; - Ok(Arc::into_raw(Arc::new(queryable))) + Ok(Box::into_raw(Box::new(queryable)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); null() }); - std::mem::forget(session); query_ptr } @@ -717,7 +708,7 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> Z ReplyKeyExpr::Any => 1, }; - let query_ptr = Arc::into_raw(Arc::new(query)); + let query_ptr = Box::into_raw(Box::new(query)) as *const Query; let result = env .call_method( @@ -742,7 +733,7 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> Z // and remains unaltered, it is safe to reclaim ownership of the memory by converting // the raw pointers back into an `Arc` and freeing the memory. unsafe { - Arc::from_raw(query_ptr); + let _ = Box::from_raw(query_ptr as *mut Query); }; _ = env.exception_describe(); zerror!(err) @@ -768,7 +759,7 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> Z /// allowing safe usage of the session after this function call. /// - The function may throw an exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( mut env: JNIEnv, @@ -776,7 +767,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( session_ptr: *const Session, key_expr_str: JString, ) -> *const KeyExpr<'static> { - let session: Arc = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let key_expr_ptr = || -> ZResult<*const KeyExpr<'static>> { let key_expr_str = decode_string(&mut env, &key_expr_str)?; let key_expr = session @@ -789,13 +780,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( err ) })?; - Ok(Arc::into_raw(Arc::new(key_expr))) + Ok(Box::into_raw(Box::new(key_expr)) as *const _) }() .unwrap_or_else(|err| { throw_exception!(env, err); null() }); - mem::forget(session); key_expr_ptr } @@ -819,7 +809,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( /// - The key expression pointer is voided after this function call. /// - The function may throw an exception in case of failure, which should be handled by the caller. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( mut env: JNIEnv, @@ -827,9 +817,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( session_ptr: *const Session, key_expr_ptr: *const KeyExpr<'static>, ) { - let session = Arc::from_raw(session_ptr); - let key_expr = Arc::from_raw(key_expr_ptr); - let key_expr_clone = key_expr.deref().clone(); + let session: &Session = &*session_ptr; + let key_expr = Box::from_raw(key_expr_ptr as *mut KeyExpr<'static>); + let key_expr_clone = (*key_expr).clone(); match session.undeclare(key_expr_clone).wait() { Ok(_) => {} Err(err) => { @@ -839,8 +829,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( ); } } - std::mem::forget(session); - // `key_expr` is intentionally left to be freed by Rust + // `key_expr` Box is dropped here, freeing the KeyExpr } /// Performs a `get` operation in the Zenoh session via JNI with Value. @@ -877,7 +866,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( /// Throws: /// - An exception in case of failure handling the query. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( mut env: JNIEnv, @@ -900,7 +889,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( is_express: jboolean, accept_replies: jint, ) { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -971,7 +960,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } pub(crate) fn on_reply_success( @@ -1123,14 +1111,14 @@ pub(crate) fn on_reply_error( /// Returns a list of zenoh ids as byte arrays corresponding to the peers connected to the session provided. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, ) -> jobject { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let ids = { let peers_zid = session.info().peers_zid().wait(); let ids = peers_zid.collect::>(); @@ -1140,20 +1128,19 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( throw_exception!(env, err); JObject::default().as_raw() }); - std::mem::forget(session); ids } /// Returns a list of zenoh ids as byte arrays corresponding to the routers connected to the session provided. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, ) -> jobject { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let ids = { let peers_zid = session.info().routers_zid().wait(); let ids = peers_zid.collect::>(); @@ -1163,19 +1150,18 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( throw_exception!(env, err); JObject::default().as_raw() }); - std::mem::forget(session); ids } /// Returns the Zenoh ID as a byte array of the session. -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, ) -> jbyteArray { - let session = Arc::from_raw(session_ptr); + let session: &Session = &*session_ptr; let ids = { let zid = session.info().zid().wait(); env.byte_array_from_slice(&zid.to_le_bytes()) @@ -1186,7 +1172,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( throw_exception!(env, err); JByteArray::default().as_raw() }); - std::mem::forget(session); ids } diff --git a/zenoh-jni/src/subscriber.rs b/zenoh-jni/src/subscriber.rs index 3462ec7e..fc283ff9 100644 --- a/zenoh-jni/src/subscriber.rs +++ b/zenoh-jni/src/subscriber.rs @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; - use jni::{objects::JClass, JNIEnv}; use zenoh::pubsub::Subscriber; @@ -30,12 +28,12 @@ use zenoh::pubsub::Subscriber; /// - The function takes ownership of the raw pointer and releases the associated memory. /// - After calling this function, the subscriber pointer becomes invalid and should not be used anymore. /// -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI( _env: JNIEnv, _: JClass, subscriber_ptr: *const Subscriber<()>, ) { - Arc::from_raw(subscriber_ptr); + let _ = Box::from_raw(subscriber_ptr as *mut Subscriber<()>); } diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index 627bb8b0..29862541 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -156,7 +156,7 @@ fn decode_token_type(env: &mut JNIEnv, type_obj: JObject) -> ZResult { } } -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeViaJNI( mut env: JNIEnv, @@ -291,7 +291,7 @@ fn serialize( Ok(()) } -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( mut env: JNIEnv, diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index 6647f86f..7e8e09ca 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -21,7 +21,7 @@ use jni::{ use zenoh::session::ZenohId; /// Returns the string representation of a ZenohID. -#[no_mangle] +#[cfg_attr(feature = "export_jni_symbols", no_mangle)] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNIZenohId_toStringViaJNI( mut env: JNIEnv,