1use kotlin_codegen::{
16 escape_kotlin_ident, is_kotlin_hard_keyword, is_valid_kotlin_ident, is_valid_kotlin_package,
17 mangle_kotlin_ident, mangle_kotlin_package, merge_files, merged_file_path, KtClass,
18 KtClassModifier, KtCode, KtCompanion, KtCtorParam, KtDecl, KtEnumEntry, KtFile, KtFun,
19 KtFunInterface, KtFunSig, KtParam, KtProperty, KtType, KtVis,
20};
21
22fn main() {
23 let fragments = vec![
24 types_fragment(),
25 session_fragment(),
26 natives_fragment(),
27 root_fragment(),
28 ];
29
30 let files = merge_files(fragments).expect("the showcase model is valid");
31
32 println!("=== generated files ===");
33 for file in &files {
34 let path = merged_file_path(std::path::Path::new("kotlin"), file, "Generated");
35 println!("\n--- {} ---", path.display());
36 print!("{}", file.render());
37 }
38
39 println!("\n=== identifier helpers ===");
40 for raw in ["myValue", "my-field", "2fast", "object", "data", ""] {
41 let quoted = format!("{raw:?}");
42 let mangled = format!("{:?}", mangle_kotlin_ident(raw));
43 let escaped = format!("{:?}", escape_kotlin_ident(raw));
44 let valid = is_valid_kotlin_ident(raw);
45 let keyword = is_kotlin_hard_keyword(raw);
46 println!(
47 "{quoted:>10} -> valid={valid:<5} keyword={keyword:<5} \
48 mangled={mangled:<10} escaped={escaped}"
49 );
50 }
51 for raw in ["io.example.api", "fun.my-pkg", "io..jni."] {
52 let quoted = format!("{raw:?}");
53 let mangled = format!("{:?}", mangle_kotlin_package(raw));
54 let valid = is_valid_kotlin_package(raw);
55 println!("{quoted:>16} -> valid={valid:<5} mangled={mangled}");
56 }
57}
58
59fn types_fragment() -> KtFile {
62 let priority = KtClass::enum_("Priority")
65 .vis(KtVis::Public)
66 .kdoc("Delivery priority.\n\nMirrors the native `z_priority_t`.")
67 .ctor_param(
68 KtCtorParam::new("code", KtType::int())
69 .val()
70 .vis(KtVis::Public),
71 )
72 .entry(KtEnumEntry::with_args("REAL_TIME", "1"))
73 .entry(KtEnumEntry::with_args("INTERACTIVE", "4"))
74 .entry(KtEnumEntry::with_args("DATA", "5"))
75 .companion(
76 KtCompanion::named("Codes")
77 .vis(KtVis::Public)
78 .kdoc("Lookup helpers keyed by the native code.")
79 .member(
80 KtFun::new("fromInt")
81 .vis(KtVis::Public)
82 .annotation("JvmStatic")
83 .param(KtParam::new("value", KtType::int()))
84 .returns(KtType::cls("Priority"))
85 .body(
86 KtCode::new()
87 .blk("return when (value) {", |c| {
88 c.line("1 -> REAL_TIME")
89 .line("4 -> INTERACTIVE")
90 .line("5 -> DATA")
91 .line("else -> throw IllegalArgumentException(\"bad priority: $value\")")
92 }),
93 ),
94 ),
95 );
96
97 let zid = KtClass::value(
99 "ZenohId",
100 KtCtorParam::new("bytes", KtType::byte_array())
101 .val()
102 .vis(KtVis::Public),
103 )
104 .vis(KtVis::Public)
105 .kdoc("A 16-byte peer identifier, carried by value.");
106
107 let sample = KtClass::data(
109 "Sample",
110 KtCtorParam::new("keyExpr", KtType::string())
111 .val()
112 .vis(KtVis::Public),
113 )
114 .vis(KtVis::Public)
115 .ctor_param(
116 KtCtorParam::new("payload", KtType::byte_array())
117 .val()
118 .vis(KtVis::Public),
119 )
120 .ctor_param(
121 KtCtorParam::new("priority", KtType::cls("Priority"))
122 .val()
123 .vis(KtVis::Public)
124 .default("Priority.DATA"),
125 )
126 .ctor_param(
127 KtCtorParam::new("attachment", KtType::byte_array().nullable())
128 .var()
129 .vis(KtVis::Public)
130 .annotation("JvmField")
131 .default("null"),
132 );
133
134 let reply = KtClass::sealed_interface("Reply")
137 .vis(KtVis::Public)
138 .kdoc("Either a sample or the end of the stream.")
139 .member(
140 KtClass::data(
141 "Value",
142 KtCtorParam::new("sample", KtType::cls("Sample"))
143 .val()
144 .vis(KtVis::Public),
145 )
146 .vis(KtVis::Public)
147 .implements(KtType::cls("Reply")),
148 )
149 .member(
150 KtClass::data_object("Done")
151 .vis(KtVis::Public)
152 .implements(KtType::cls("Reply")),
153 );
154
155 KtFile::new("io.example.api")
156 .decl(priority)
157 .decl(zid)
158 .decl(sample)
159 .decl(reply)
160}
161
162fn session_fragment() -> KtFile {
165 let base = KtClass::class_with(KtClassModifier::Abstract, "NativeHandle")
168 .vis(KtVis::Public)
169 .kdoc("Owns a raw pointer into Rust and frees it exactly once.")
170 .ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
171 .implements(KtType::cls("AutoCloseable"))
172 .member(
173 KtProperty::var("ptr")
174 .ty(KtType::long())
175 .vis(KtVis::Internal)
176 .annotation("Volatile")
177 .kdoc("Zero once closed.")
178 .initializer("initialPtr"),
179 )
180 .member(
181 KtProperty::val("isClosed")
182 .ty(KtType::boolean())
183 .vis(KtVis::Public)
184 .accessors(KtCode::new().line("get() = ptr == 0L")),
185 )
186 .member(
187 KtFun::new("freePtr")
188 .vis(KtVis::Public)
189 .modifier("abstract")
190 .kdoc("Release the native allocation. Called once, under the lock.")
191 .param(KtParam::new("ptr", KtType::long())),
192 );
193
194 let session = KtClass::class_with(KtClassModifier::Open, "Session")
196 .vis(KtVis::Public)
197 .annotation("Suppress(\"unused\")")
198 .ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
199 .extends(KtType::cls("NativeHandle"), Some("initialPtr"))
200 .implements(KtType::cls("io.example.api.Describable"))
201 .member(
202 KtFun::new("freePtr")
203 .vis(KtVis::Public)
204 .modifier("override")
205 .param(KtParam::new("ptr", KtType::long()))
206 .body(KtCode::new().line("JNINative.sessionFree(ptr)")),
207 )
208 .member(
209 KtFun::new("describe")
210 .vis(KtVis::Public)
211 .modifier("override")
212 .returns(KtType::string())
213 .expr_body(KtCode::new().line("\"Session(0x${ptr.toString(16)})\"")),
214 )
215 .member(
217 KtFun::new("get")
218 .vis(KtVis::Public)
219 .generic("R")
220 .kdoc("Run a query, folding each reply into an accumulator.")
221 .param(KtParam::new("selector", KtType::string()))
222 .param(KtParam::new(
223 "onReply",
224 KtType::lambda(
225 [("reply".to_string(), KtType::cls("io.example.api.Reply"))],
226 KtType::var_r(),
227 ),
228 ))
229 .param(KtParam::new("timeoutMs", KtType::long()).default("10_000L"))
230 .returns(KtType::generic("List", [KtType::var_r()]))
231 .body(
232 KtCode::new()
233 .line("val acc = ArrayList<R>()")
234 .line("val guard = Guard.acquire(this)")
235 .import("io.example.internal.Guard")
236 .try_finally(
237 "",
238 KtCode::new().blk("JNINative.sessionGet(ptr, selector, timeoutMs) { raw ->", |c| {
239 c.line("acc.add(onReply(raw))")
240 }),
241 KtCode::new().line("guard.release()"),
242 )
243 .line("")
244 .wline("reportQueryOutcome(selector, timeoutMs, acc.size, acc.isNotEmpty(), \"query finished\", System.nanoTime())")
247 .line("return acc"),
248 ),
249 )
250 .member(
252 KtFun::new("label")
253 .vis(KtVis::Public)
254 .modifier("override")
255 .receiver(KtType::cls("Sample"))
256 .returns(KtType::string())
257 .expr_body(KtCode::new().line("\"${keyExpr}@${describe()}\"")),
258 )
259 .member(
261 KtFun::new("toSample")
262 .vis(KtVis::Public)
263 .receiver(KtType::byte_array())
264 .returns(KtType::cls("Sample"))
265 .expr_body(KtCode::new().line("Sample(describe(), this)")),
266 )
267 .member(
269 KtClass::class_("Config")
270 .vis(KtVis::Public)
271 .member(KtProperty::val("describe").initializer("\"config\"")),
272 )
273 .companion(
274 KtCompanion::new()
275 .vis(KtVis::Public)
276 .member(
277 KtFun::new("open")
278 .vis(KtVis::Public)
279 .annotation("JvmStatic")
280 .param(KtParam::new("config", KtType::string()).default("\"{}\""))
281 .returns(KtType::cls("Session"))
282 .expr_body(KtCode::new().line("Session(JNINative.sessionOpen(config))")),
283 ),
284 );
285
286 let describable = KtClass::interface_("Describable")
289 .vis(KtVis::Public)
290 .member(KtFunSig::new("describe").returns(KtType::string()))
291 .member(
295 KtFunSig::new("label")
296 .receiver(KtType::cls("Sample"))
297 .returns(KtType::string()),
298 );
299
300 let handler = KtFunInterface::new(
302 "ReplyHandler",
303 KtFunSig::new("onReply")
304 .param(KtParam::new("reply", KtType::cls("Reply")))
305 .returns(KtType::var_r()),
306 )
307 .vis(KtVis::Public)
308 .type_param("out R")
309 .kdoc("Invoked from the native thread for each reply.");
310
311 let summary = KtFun::new("summary")
315 .vis(KtVis::Public)
316 .receiver(KtType::cls("Sample"))
317 .returns(KtType::string())
318 .expr_body(KtCode::new().line("\"$keyExpr (${payload.size} bytes)\""));
319
320 let map_replies = KtFun::new("mapValues")
322 .vis(KtVis::Public)
323 .generic("R")
324 .receiver(KtType::generic("List", [KtType::cls("Reply")]))
325 .param(KtParam::new(
326 "transform",
327 KtType::lambda(
328 [("sample".to_string(), KtType::cls("Sample"))],
329 KtType::var_r(),
330 ),
331 ))
332 .returns(KtType::generic("List", [KtType::var_r()]))
333 .expr_body(
334 KtCode::new().line("filterIsInstance<Reply.Value>().map { transform(it.sample) }"),
335 );
336
337 let as_raw = KtFun::new("asRaw")
340 .vis(KtVis::Internal)
341 .receiver(KtType::lambda(
342 [("sample".to_string(), KtType::cls("Sample"))],
343 KtType::unit(),
344 ))
345 .returns(KtType::cls("io.example.api.internal.RawSink"))
346 .expr_body(
349 KtCode::new().line("RawSink { raw -> if (raw is Reply.Value) this(raw.sample) }"),
350 );
351
352 KtFile::new("io.example.api")
353 .imports(["io.example.api.internal.JNINative".to_string()])
355 .decl(base)
356 .decl(describable)
357 .decl(handler)
358 .decl(session)
359 .decl(summary)
360 .decl(map_replies)
361 .decl(as_raw)
362 .decl(KtDecl::TypeAlias {
363 vis: KtVis::Public,
364 name: "SampleList".to_string(),
365 target: KtType::generic("List", [KtType::cls("Sample")]),
366 })
367}
368
369fn natives_fragment() -> KtFile {
371 let natives = KtClass::object_("JNINative")
372 .vis(KtVis::Internal)
373 .kdoc("One-to-one with the exported Rust symbols.")
374 .member(
375 KtFun::new("sessionOpen")
376 .vis(KtVis::Internal)
377 .param(KtParam::new("config", KtType::string()))
378 .returns(KtType::long())
379 .external(),
380 )
381 .member(
382 KtFun::new("sessionFree")
383 .vis(KtVis::Internal)
384 .param(KtParam::new("ptr", KtType::long()))
385 .external(),
386 )
387 .member(
388 KtFun::new("sessionGet")
389 .vis(KtVis::Internal)
390 .param(KtParam::new("ptr", KtType::long()))
391 .param(KtParam::new("selector", KtType::string()))
392 .param(KtParam::new("timeoutMs", KtType::long()))
393 .param(KtParam::new(
394 "sink",
395 KtType::lambda(
396 [("reply".to_string(), KtType::cls("io.example.api.Reply"))],
397 KtType::unit(),
398 ),
399 ))
400 .external(),
401 );
402
403 let loader = KtDecl::Raw {
406 name: "__loadNative".to_string(),
407 code: KtCode::raw_reindent(
408 "internal val __loaded: Boolean = run {\n\
409 System.loadLibrary(\"example_jni\")\n\
410 true\n\
411 }",
412 ),
413 };
414
415 let raw_sink = KtFunInterface::new(
416 "RawSink",
417 KtFunSig::new("accept").param(KtParam::new("reply", KtType::cls("io.example.api.Reply"))),
418 )
419 .vis(KtVis::Internal);
420
421 KtFile::new("io.example.api.internal")
422 .decl(raw_sink)
423 .decl(natives)
424 .decl(loader)
425 .import("io.example.api.Reply")
427}
428
429fn root_fragment() -> KtFile {
432 KtFile::new("")
433 .banner("// Hand-tuned banner for the root package.")
434 .decl(
435 KtProperty::val("LIBRARY_VERSION")
436 .ty(KtType::string())
437 .vis(KtVis::Public)
438 .kdoc("Version this binding was generated against.")
439 .initializer("\"1.9.0\""),
440 )
441 .decl(
443 KtProperty::val("PROTOCOL")
444 .ty(KtType::string())
445 .vis(KtVis::Public)
446 .modifier("const")
447 .initializer("\"tcp\""),
448 )
449 .decl(
451 KtProperty::val("defaultTimeout")
452 .vis(KtVis::Internal)
453 .ty(KtType::cls("java.time.Duration"))
454 .delegate("lazy { Duration.ofSeconds(10) }"),
455 )
456 .decl(
457 KtFun::new("describeAll")
458 .vis(KtVis::Public)
459 .generic("T : io.example.api.Describable")
462 .param(KtParam::new(
463 "items",
464 KtType::generic("List", [KtType::var_("T")]),
465 ))
466 .returns(KtType::string())
467 .expr_body(KtCode::new().line("items.joinToString { it.describe() }")),
468 )
469}