@@ -107,6 +107,47 @@ impl JniExt {
107107 Ok ( file. write ( output_dir) ?)
108108 }
109109
110+ /// Emit one Kotlin file per entry in `handles` — each becomes a
111+ /// `public class <ClassName>(initialPtr: Long) : NativeHandle(initialPtr)`
112+ /// with the standard `close()` + `private external fun freePtrViaJNI(ptr: Long)`
113+ /// destructor pair. The emitted class is exactly what hand-written
114+ /// shell-only typed handles used to be; pulling them through the
115+ /// generator removes the per-type copy-paste.
116+ ///
117+ /// `handles` is `&[(rust_doc_name, kotlin_fqn)]`:
118+ /// * `rust_doc_name` — short Rust name shown in the class doc comment
119+ /// (e.g. `"Subscriber"`). Pure documentation, doesn't have to match
120+ /// anything in the Registry.
121+ /// * `kotlin_fqn` — package-qualified Kotlin class name
122+ /// (e.g. `"io.zenoh.jni.JNISubscriber"`). The package and class
123+ /// name are split on the last `.`.
124+ ///
125+ /// Only the "standard `freePtrViaJNI` destructor" shape is emitted.
126+ /// Typed handles with non-standard destructors
127+ /// (e.g. `JNILivelinessToken::undeclare`) or with helper methods
128+ /// (e.g. `JNISession::declarePublisher`) stay hand-written — pass
129+ /// only the shell-only types here.
130+ pub fn write_typed_handles (
131+ & self ,
132+ handles : & [ ( & str , & str ) ] ,
133+ output_dir : & Path ,
134+ ) -> Result < Vec < PathBuf > , WriteKotlinError > {
135+ let mut written = Vec :: new ( ) ;
136+ for ( rust_doc_name, fqn) in handles {
137+ let ( package, class_name) = match fqn. rsplit_once ( '.' ) {
138+ Some ( ( p, c) ) => ( p. to_string ( ) , c. to_string ( ) ) ,
139+ None => ( String :: new ( ) , ( * fqn) . to_string ( ) ) ,
140+ } ;
141+ let file = KotlinFile {
142+ contents : render_typed_handle_source ( & package, & class_name, rust_doc_name) ,
143+ package,
144+ class_name,
145+ } ;
146+ written. push ( file. write ( output_dir) ?) ;
147+ }
148+ Ok ( written)
149+ }
150+
110151 /// Return the `<rust-type-key> → <kotlin FQN>` map for every
111152 /// `impl Fn(args)` type the Registry has resolved. Use this to merge
112153 /// into a `KotlinTypeMap` consumed by the aggregated-interface
@@ -377,6 +418,39 @@ public open class NativeHandle(initial: Long) {
377418 . to_string ( )
378419}
379420
421+ /// Render one typed-handle Kotlin source file. Matches the shape of
422+ /// the hand-written shell-only `JNI*.kt` files this replaces:
423+ ///
424+ /// ```kotlin
425+ /// public class JNIFoo(initialPtr: Long) : NativeHandle(initialPtr) {
426+ /// fun close() = close { freePtrViaJNI(it) }
427+ /// private external fun freePtrViaJNI(ptr: Long)
428+ /// }
429+ /// ```
430+ ///
431+ /// The Kotlin/JVM JNI name mangler binds `freePtrViaJNI` to the
432+ /// `Java_<pkg>_<class>_freePtrViaJNI` extern emitted on the Rust
433+ /// side — same convention as before, no Rust-side change needed.
434+ fn render_typed_handle_source ( package : & str , class_name : & str , rust_doc_name : & str ) -> String {
435+ let mut s = String :: new ( ) ;
436+ s. push_str ( "// Auto-generated by JniExt — do not edit by hand.\n " ) ;
437+ if !package. is_empty ( ) {
438+ s. push_str ( & format ! ( "package {}\n \n " , package) ) ;
439+ }
440+ s. push_str ( & format ! (
441+ "/** Typed [NativeHandle] for a native Zenoh `{}`. */\n " ,
442+ rust_doc_name
443+ ) ) ;
444+ s. push_str ( & format ! (
445+ "public class {}(initialPtr: Long) : NativeHandle(initialPtr) {{\n " ,
446+ class_name
447+ ) ) ;
448+ s. push_str ( " public fun close() = close { freePtrViaJNI(it) }\n " ) ;
449+ s. push_str ( " private external fun freePtrViaJNI(ptr: Long)\n " ) ;
450+ s. push_str ( "}\n " ) ;
451+ s
452+ }
453+
380454/// Emit one safe top-level wrapper function per `#[prebindgen]` fn in
381455/// `registry.functions`. Opaque-handle parameters (detected via the
382456/// input converter returning `OwnedObject<T>`) become `NativeHandle`;
0 commit comments