1use std::collections::BTreeMap;
25use std::fmt::Write as _;
26use std::path::{Path, PathBuf};
27
28#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
30pub enum Lang {
31 Rust,
32 Swift,
33 Kotlin,
34 Java,
35 ArkTs,
36 Js,
37 C,
38 Cpp,
39}
40
41impl Lang {
42 fn parse(s: &str) -> Option<Lang> {
43 Some(match s {
44 "rust" => Lang::Rust,
45 "swift" => Lang::Swift,
46 "kotlin" => Lang::Kotlin,
47 "java" => Lang::Java,
48 "arkts" => Lang::ArkTs,
49 "js" => Lang::Js,
50 "c" => Lang::C,
51 "cpp" => Lang::Cpp,
52 _ => return None,
53 })
54 }
55
56 pub fn key(self) -> &'static str {
58 match self {
59 Lang::Rust => "rust",
60 Lang::Swift => "swift",
61 Lang::Kotlin => "kotlin",
62 Lang::Java => "java",
63 Lang::ArkTs => "arkts",
64 Lang::Js => "js",
65 Lang::C => "c",
66 Lang::Cpp => "cpp",
67 }
68 }
69}
70
71const PLATFORMS: &[&str] = &[
73 "ios", "macos", "android", "ohos", "web", "linux", "windows", "other",
74];
75
76const ARM_OPTIONS: &[&str] = &["src", "link", "pkg_config", "encoding", "support"];
79
80fn cfg_for(platform: &str) -> &'static str {
83 match platform {
84 "ios" => "target_os = \"ios\"",
85 "macos" => "target_os = \"macos\"",
86 "android" => "target_os = \"android\"",
87 "windows" => "target_os = \"windows\"",
88 "web" => "target_arch = \"wasm32\"",
89 "linux" => "all(target_os = \"linux\", not(target_env = \"ohos\"))",
90 "ohos" => "all(target_os = \"linux\", target_env = \"ohos\")",
91 _ => "",
92 }
93}
94
95const SCALARS: &[&str] = &["bool", "i32", "i64", "f32", "f64"];
98
99#[derive(Clone, Debug)]
101pub struct Decl {
102 pub name: String,
103 pub args: Vec<(String, String)>,
105 pub ret: String,
107 pub line: usize,
109}
110
111#[derive(Clone, Debug)]
113pub struct Arm {
114 pub lang: Lang,
115 pub platforms: Vec<String>,
116 pub body: Option<String>,
118 pub prelude: Option<String>,
123 pub src: Option<String>,
125 pub options: BTreeMap<String, String>,
127 pub source: Option<String>,
129 pub line: usize,
131 pub body_line: usize,
134}
135
136#[derive(Default, Debug)]
138pub struct Bridge {
139 pub decls: Vec<Decl>,
140 pub arms: Vec<Arm>,
141}
142
143pub fn generate() -> Result<(), String> {
148 let root = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR unset")?;
149 let out = std::env::var("OUT_DIR").map_err(|_| "OUT_DIR unset")?;
150 let crate_name = std::env::var("CARGO_PKG_NAME").map_err(|_| "CARGO_PKG_NAME unset")?;
151 generate_in(Path::new(&root), Path::new(&out), &crate_name)
152}
153
154pub fn parse_crate(root: &Path) -> Result<Bridge, String> {
158 let bridge = scan(root)?;
159 validate(&bridge)?;
160 Ok(bridge)
161}
162
163pub fn is_bridged(root: &Path) -> bool {
165 let mut sources: Vec<PathBuf> = Vec::new();
166 collect_rs(&root.join("src"), &mut sources);
167 sources.iter().any(|p| {
168 std::fs::read_to_string(p)
169 .map(|t| {
170 t.lines()
171 .any(|l| !l.trim_start().starts_with("//") && l.contains("bridge!"))
172 })
173 .unwrap_or(false)
174 })
175}
176
177pub fn swift_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
179 render_swift(bridge, arm, crate_name)
180}
181
182pub fn jvm_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
186 match arm.lang {
187 Lang::Java => render_java(arm, crate_name),
188 _ => render_kotlin(bridge, arm, crate_name),
189 }
190}
191
192pub fn js_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
194 render_js(bridge, arm, crate_name)
195}
196
197pub fn arkts_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
199 render_arkts(bridge, arm, crate_name)
200}
201
202pub fn kotlin_package_of(crate_name: &str) -> String {
204 kotlin_package(crate_name)
205}
206
207pub fn adapter_name(arm: &Arm, crate_name: &str) -> String {
209 generated_name(arm, crate_name)
210}
211
212fn scan(root: &Path) -> Result<Bridge, String> {
213 let mut sources: Vec<PathBuf> = Vec::new();
214 collect_rs(&root.join("src"), &mut sources);
215 sources.sort(); let mut bridge = Bridge::default();
218 for path in &sources {
219 let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
220 if !text.contains("bridge!") {
221 continue;
222 }
223 let rel = path
224 .strip_prefix(root)
225 .unwrap_or(path)
226 .display()
227 .to_string();
228 #[cfg(windows)]
234 let rel = rel.replace('\\', "/");
235 parse_into(&text, &rel, &mut bridge).map_err(|e| format!("{rel}: {e}"))?;
236 }
237 Ok(bridge)
238}
239
240pub fn generate_in(root: &Path, out_dir: &Path, crate_name: &str) -> Result<(), String> {
242 let mut sources: Vec<PathBuf> = Vec::new();
246 collect_rs(&root.join("src"), &mut sources);
247 sources.sort();
248 for path in &sources {
249 println!("cargo:rerun-if-changed={}", path.display());
250 }
251
252 let bridge = parse_crate(root)?;
253
254 let dir = out_dir.join("day-bridge");
255 std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
256 write_if_changed(&dir.join("mod.rs"), &render_rust(&bridge, crate_name))?;
257 emit_c(&bridge, &dir, crate_name)?;
258 Ok(())
259}
260
261fn active_platform() -> Option<String> {
264 let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?;
265 let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
266 let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
267 Some(
268 match (os.as_str(), env.as_str(), arch.as_str()) {
269 (_, _, "wasm32") => "web",
270 ("linux", "ohos", _) => "ohos",
271 ("linux", _, _) => "linux",
272 ("ios", _, _) => "ios",
273 ("macos", _, _) => "macos",
274 ("android", _, _) => "android",
275 ("windows", _, _) => "windows",
276 _ => return None,
277 }
278 .to_string(),
279 )
280}
281
282fn emit_c(bridge: &Bridge, dir: &Path, crate_name: &str) -> Result<(), String> {
289 let active = active_platform();
290
291 println!("cargo:rustc-check-cfg=cfg({STAGED_CFG})");
294 println!("cargo:rerun-if-env-changed=DAY_BRIDGE_STAGED");
295 let staged_here = std::env::var("DAY_BRIDGE_STAGED").is_ok()
296 && bridge.arms.iter().any(|a| {
297 staged_by_cli(a.lang)
298 && active
299 .as_deref()
300 .is_some_and(|p| a.platforms.iter().any(|x| x == p))
301 });
302 if staged_here {
303 println!("cargo:rustc-cfg={STAGED_CFG}");
304 }
305
306 for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Swift) {
310 let file = dir.join(format!("{}-{}.swift", crate_name, arm.platforms.join("-")));
311 write_if_changed(&file, &render_swift(bridge, arm, crate_name))?;
312 }
313
314 for arm in bridge
315 .arms
316 .iter()
317 .filter(|a| matches!(a.lang, Lang::C | Lang::Cpp))
318 {
319 let cpp = arm.lang == Lang::Cpp;
320 let file = dir.join(format!(
321 "{}-{}.{}",
322 crate_name,
323 arm.platforms.join("-"),
324 if cpp { "cpp" } else { "c" }
325 ));
326 write_if_changed(&file, &render_c(bridge, arm, crate_name))?;
327
328 let selected = active
329 .as_deref()
330 .is_some_and(|p| arm.platforms.iter().any(|a| a == p));
331 if !selected {
332 continue;
333 }
334 let mut build = cc::Build::new();
335 build.file(&file).cpp(cpp).warnings(false);
336 if cpp {
337 build.std("c++17");
338 }
339 build.compile(&format!("day_bridge_{}", crate_name.replace('-', "_")));
340 for lib in arm
341 .options
342 .get("link")
343 .map(|v| v.trim_matches(['[', ']']).to_string())
344 .unwrap_or_default()
345 .split(',')
346 .map(|l| l.trim().trim_matches('"'))
347 .filter(|l| !l.is_empty())
348 {
349 println!("cargo:rustc-link-lib={lib}");
350 }
351 if let Some(pkg) = arm.options.get("pkg_config") {
352 println!("cargo:rustc-link-lib={pkg}");
353 }
354 }
355 Ok(())
356}
357
358fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
359 let Ok(rd) = std::fs::read_dir(dir) else {
360 return;
361 };
362 for entry in rd.flatten() {
363 let path = entry.path();
364 if path.is_dir() {
365 collect_rs(&path, out);
366 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
367 out.push(path);
368 }
369 }
370}
371
372fn parse_into(text: &str, source: &str, bridge: &mut Bridge) -> Result<(), String> {
378 let mut at = 0;
379 while let Some(found) = text[at..].find("bridge!") {
380 let start = at + found;
381 let line_start = text[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
384 if text[line_start..start].trim_start().starts_with("//") {
385 at = start + "bridge!".len();
386 continue;
387 }
388 let after = start + "bridge!".len();
390 let Some(brace) = text[after..]
391 .find(|c: char| !c.is_whitespace())
392 .map(|i| after + i)
393 else {
394 break;
395 };
396 if text.as_bytes().get(brace) != Some(&b'{') {
397 at = after;
398 continue;
399 }
400 let end = match_delim(text, brace, b'{', b'}')
401 .ok_or_else(|| "unterminated `bridge! {` block".to_string())?;
402 let first = bridge.arms.len();
403 parse_body(&text[brace + 1..end], line_of(text, brace), bridge)?;
404 for arm in &mut bridge.arms[first..] {
405 arm.source = Some(source.to_string());
406 }
407 at = end + 1;
408 }
409 Ok(())
410}
411
412fn parse_body(body: &str, base_line: usize, bridge: &mut Bridge) -> Result<(), String> {
414 let mut at = 0;
415 while let Some(found) = body[at..].find("#[day_bridge::") {
416 let start = at + found;
417 let open = start + "#[".len() - 1; let close = match_delim(body, open, b'[', b']')
419 .ok_or_else(|| "unterminated bridge attribute".to_string())?;
420 let attr = &body[start + 2..close];
421 let line = base_line + line_of(body, start) - 1;
424 let rest = &body[close + 1..];
425
426 let kind = attr
427 .trim_start_matches("day_bridge::")
428 .split(['(', ' '])
429 .next()
430 .unwrap_or("")
431 .trim();
432 let consumed = match kind {
433 "declare" => parse_declare(rest, line, bridge)?,
434 "prelude" => {
435 return Err(format!(
436 "line {line}: a standalone `prelude` attribute no longer exists — write it as \
437 `lang!(prelude = r#\" … \"#, body = r#\" … \"#)` on the arm it belongs to \
438 (docs/bridge.md \"The file\")"
439 ));
440 }
441 "impl" => parse_impl(attr, rest, line, bridge)?,
442 "data" => 0, other => return Err(format!("line {line}: unknown bridge attribute `{other}`")),
444 };
445 at = close + 1 + consumed;
446 }
447 Ok(())
448}
449
450fn parse_declare(rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
452 let open = rest
453 .find('{')
454 .ok_or_else(|| format!("line {line}: `declare` needs an `extern \"day\" {{ … }}` block"))?;
455 let close = match_delim(rest, open, b'{', b'}')
456 .ok_or_else(|| format!("line {line}: unterminated `extern \"day\"` block"))?;
457 let block = strip_comments(&rest[open + 1..close]);
460 for raw in block.split(';') {
461 let sig = raw.trim();
462 if sig.is_empty() {
463 continue;
464 }
465 let sig = sig
466 .strip_prefix("fn ")
467 .ok_or_else(|| format!("line {line}: `{sig}` is not a `fn` declaration"))?;
468 let name_end = sig
469 .find('(')
470 .ok_or_else(|| format!("line {line}: `{sig}` has no argument list"))?;
471 let name = sig[..name_end].trim().to_string();
472 let args_end = match_delim(sig, name_end, b'(', b')')
473 .ok_or_else(|| format!("line {line}: `{name}` has an unterminated argument list"))?;
474 let mut args = Vec::new();
475 for arg in split_top(&sig[name_end + 1..args_end], ',') {
476 let arg = arg.trim();
477 if arg.is_empty() {
478 continue;
479 }
480 let (n, t) = arg
481 .split_once(':')
482 .ok_or_else(|| format!("line {line}: argument `{arg}` needs a type"))?;
483 args.push((n.trim().to_string(), t.trim().to_string()));
484 }
485 let ret = sig[args_end + 1..]
486 .trim()
487 .strip_prefix("->")
488 .map(|r| r.trim().to_string())
489 .unwrap_or_default();
490 bridge.decls.push(Decl {
491 name,
492 args,
493 ret,
494 line,
495 });
496 }
497 Ok(close + 1)
498}
499
500fn parse_impl(attr: &str, rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
501 let lang = attr_lang(attr, line)?;
502 let inner = attr
503 .split_once('(')
504 .map(|(_, v)| v.trim_end().trim_end_matches(')'))
505 .unwrap_or("");
506 let mut platforms = Vec::new();
507 let mut options = BTreeMap::new();
508 for part in split_top(inner, ',') {
509 let part = part.trim();
510 if part.is_empty() || Lang::parse(part).is_some() {
511 continue;
512 }
513 let Some((key, value)) = part.split_once('=') else {
514 return Err(format!("line {line}: `{part}` is not `key = value`"));
515 };
516 let key = key.trim();
517 let value = value.trim().trim_matches('"');
518 if key == "platforms" {
519 for p in value.trim_matches(['[', ']']).split(',') {
520 let p = p.trim();
521 if p.is_empty() {
522 continue;
523 }
524 if !PLATFORMS.contains(&p) {
525 return Err(format!(
526 "line {line}: unknown platform `{p}` (expected one of {})",
527 PLATFORMS.join(", ")
528 ));
529 }
530 platforms.push(p.to_string());
531 }
532 } else {
533 if !ARM_OPTIONS.contains(&key) {
537 return Err(format!(
538 "line {line}: unknown arm option `{key}` (expected one of {})",
539 ARM_OPTIONS.join(", ")
540 ));
541 }
542 if key == "encoding" && value != "utf8" && value != "utf16" {
543 return Err(format!(
544 "line {line}: `encoding = \"{value}\"` — expected \"utf8\" or \"utf16\""
545 ));
546 }
547 if key == "support" && value != "native" && value != "emulated" {
548 return Err(format!(
549 "line {line}: `support = \"{value}\"` — expected \"native\" or \"emulated\""
550 ));
551 }
552 options.insert(key.to_string(), value.to_string());
553 }
554 }
555 if platforms.is_empty() {
556 return Err(format!("line {line}: an arm must name `platforms = [ … ]`"));
557 }
558
559 let (prelude, body, consumed, body_line) = if lang == Lang::Rust {
562 let (body, consumed) = rust_item_after(rest, line)?;
563 (None, Some(body), consumed, line)
564 } else if options.contains_key("src") {
565 (None, None, 0, line)
566 } else {
567 let call = macro_call_after(rest, line)?;
568 (
569 call.prelude,
570 Some(call.body),
571 call.consumed,
572 line + call.body_skipped + 1,
573 )
574 };
575 if let Some(text) = &prelude {
576 for bad in ["package ", "namespace ", "module "] {
577 if text.lines().any(|l| l.trim_start().starts_with(bad)) {
578 return Err(format!(
579 "line {line}: a `{}` line belongs to the generator, not a prelude — daybridge \
580 derives it from the crate name (docs/bridge.md \"Names\")",
581 bad.trim()
582 ));
583 }
584 }
585 }
586
587 bridge.arms.push(Arm {
588 lang,
589 platforms,
590 body,
591 prelude,
592 src: options.get("src").cloned(),
593 options,
594 source: None,
595 line,
596 body_line,
597 });
598 Ok(consumed)
599}
600
601fn attr_lang(attr: &str, line: usize) -> Result<Lang, String> {
602 let inner = attr.split_once('(').map(|(_, v)| v).unwrap_or("");
603 let first = inner.split([',', ')']).next().unwrap_or("").trim();
604 Lang::parse(first).ok_or_else(|| format!("line {line}: unknown bridge language `{first}`"))
605}
606
607struct MacroCall {
609 prelude: Option<String>,
611 body: String,
613 consumed: usize,
615 body_skipped: usize,
617}
618
619fn macro_call_after(rest: &str, line: usize) -> Result<MacroCall, String> {
633 let open = rest.find('(').ok_or_else(|| {
634 format!("line {line}: expected a language macro, e.g. `java!(r#\" … \"#)`")
635 })?;
636 let close = match_delim(rest, open, b'(', b')')
637 .ok_or_else(|| format!("line {line}: unterminated language macro"))?;
638
639 let mut prelude: Option<String> = None;
640 let mut body: Option<(String, usize)> = None;
641 let mut at = open + 1;
642 while let Some(rel) = find_raw_open(&rest[at..close]) {
643 let r = at + rel;
644 let hashes = rest[r + 1..].bytes().take_while(|b| *b == b'#').count();
645 let start = r + 1 + hashes + 1; let terminator = format!("\"{}", "#".repeat(hashes));
647 let end = rest[start..close]
648 .find(&terminator)
649 .map(|i| start + i)
650 .ok_or_else(|| format!("line {line}: unterminated raw string"))?;
651 let key = rest[at..r]
653 .trim()
654 .trim_start_matches(',')
655 .trim()
656 .trim_end_matches('=')
657 .trim()
658 .to_string();
659 let text = dedent(&rest[start..end]);
660 match key.as_str() {
661 "prelude" => prelude = Some(text),
662 "body" | "" => body = Some((text, rest[..start].matches('\n').count())),
663 other => {
664 return Err(format!(
665 "line {line}: unknown argument `{other}` — a language macro takes `prelude` \
666 and `body` (docs/bridge.md \"The file\")"
667 ));
668 }
669 }
670 at = end + terminator.len();
671 }
672
673 let (body, body_skipped) = body.ok_or_else(|| {
674 format!("line {line}: expected a raw-string body, e.g. `java!(r#\" … \"#)`")
675 })?;
676 Ok(MacroCall {
677 prelude,
678 body,
679 consumed: close + 1,
680 body_skipped,
681 })
682}
683
684fn find_raw_open(text: &str) -> Option<usize> {
686 let b = text.as_bytes();
687 let mut i = 0;
688 while i < b.len() {
689 if b[i] == b'r' {
690 let mut j = i + 1;
691 while j < b.len() && b[j] == b'#' {
692 j += 1;
693 }
694 if b.get(j) == Some(&b'"') {
695 return Some(i);
696 }
697 }
698 i += 1;
699 }
700 None
701}
702
703fn rust_item_after(rest: &str, line: usize) -> Result<(String, usize), String> {
705 let open = rest
706 .find('{')
707 .ok_or_else(|| format!("line {line}: expected a Rust `fn` body"))?;
708 let close = match_delim(rest, open, b'{', b'}')
709 .ok_or_else(|| format!("line {line}: unterminated Rust body"))?;
710 Ok((dedent_item(rest[..=close].trim()), close + 1))
711}
712
713fn match_delim(text: &str, from: usize, open: u8, close: u8) -> Option<usize> {
720 let b = text.as_bytes();
721 let mut depth = 0usize;
722 let mut i = from;
723 while i < b.len() {
724 match b[i] {
725 b'r' if raw_open_hashes(b, i).is_some() => {
728 let hashes = raw_open_hashes(b, i).unwrap_or(0);
729 let terminator: Vec<u8> = std::iter::once(b'"')
730 .chain(std::iter::repeat_n(b'#', hashes))
731 .collect();
732 i += 1 + hashes + 1;
733 while i < b.len() && !b[i..].starts_with(&terminator) {
734 i += 1;
735 }
736 i += terminator.len();
737 continue;
738 }
739 b'"' => {
740 i += 1;
741 while i < b.len() && b[i] != b'"' {
742 i += if b[i] == b'\\' { 2 } else { 1 };
743 }
744 }
745 b'/' if b.get(i + 1) == Some(&b'/') => {
746 while i < b.len() && b[i] != b'\n' {
747 i += 1;
748 }
749 }
750 c if c == open => depth += 1,
751 c if c == close => {
752 depth = depth.checked_sub(1)?;
753 if depth == 0 {
754 return Some(i);
755 }
756 }
757 _ => {}
758 }
759 i += 1;
760 }
761 None
762}
763
764fn raw_open_hashes(b: &[u8], i: usize) -> Option<usize> {
767 if b.get(i) != Some(&b'r') {
768 return None;
769 }
770 let mut j = i + 1;
771 while b.get(j) == Some(&b'#') {
772 j += 1;
773 }
774 (b.get(j) == Some(&b'"')).then_some(j - i - 1)
775}
776
777fn split_top(text: &str, sep: char) -> Vec<String> {
779 let mut out = Vec::new();
780 let mut depth = 0i32;
781 let mut cur = String::new();
782 for c in text.chars() {
783 match c {
784 '(' | '[' | '<' | '{' => depth += 1,
785 ')' | ']' | '>' | '}' => depth -= 1,
786 _ => {}
787 }
788 if c == sep && depth == 0 {
789 out.push(std::mem::take(&mut cur));
790 } else {
791 cur.push(c);
792 }
793 }
794 out.push(cur);
795 out
796}
797
798fn strip_comments(text: &str) -> String {
799 text.lines()
800 .map(|l| l.split_once("//").map(|(a, _)| a).unwrap_or(l))
801 .collect::<Vec<_>>()
802 .join("\n")
803}
804
805fn line_of(text: &str, at: usize) -> usize {
806 text[..at].matches('\n').count() + 1
807}
808
809fn dedent(body: &str) -> String {
812 let indent = body
813 .lines()
814 .filter(|l| !l.trim().is_empty())
815 .map(|l| l.len() - l.trim_start().len())
816 .min()
817 .unwrap_or(0);
818 body.lines()
819 .map(|l| {
822 if l.len() >= indent && l.is_char_boundary(indent) {
823 &l[indent..]
824 } else {
825 l.trim_start()
826 }
827 })
828 .collect::<Vec<_>>()
829 .join("\n")
830 .trim_matches('\n')
831 .to_string()
832}
833
834fn dedent_item(item: &str) -> String {
837 let mut lines = item.lines();
838 let Some(first) = lines.next() else {
839 return String::new();
840 };
841 let rest: Vec<&str> = lines.collect();
842 let indent = rest
843 .iter()
844 .filter(|l| !l.trim().is_empty())
845 .map(|l| l.len() - l.trim_start().len())
846 .min()
847 .unwrap_or(0);
848 let mut out = String::from(first);
849 for line in rest {
850 out.push('\n');
851 out.push_str(if line.len() >= indent && line.is_char_boundary(indent) {
852 &line[indent..]
853 } else {
854 line.trim_start()
855 });
856 }
857 out
858}
859
860fn validate(bridge: &Bridge) -> Result<(), String> {
865 if bridge.decls.is_empty() && bridge.arms.is_empty() {
866 return Ok(());
867 }
868
869 for decl in &bridge.decls {
871 for (arg, ty) in &decl.args {
872 check_type(ty, true)
873 .map_err(|e| format!("line {}: `{}`'s `{arg}`: {e}", decl.line, decl.name))?;
874 }
875 if !decl.ret.is_empty() {
876 let inner = decl
877 .ret
878 .strip_prefix("Result<")
879 .and_then(|r| r.strip_suffix('>'))
880 .map(|r| split_top(r, ',').first().cloned().unwrap_or_default())
881 .unwrap_or_else(|| decl.ret.clone());
882 let inner = inner.trim();
883 if !inner.is_empty() && inner != "()" {
884 check_type(inner, false)
885 .map_err(|e| format!("line {}: `{}`'s return: {e}", decl.line, decl.name))?;
886 }
887 }
888 }
889
890 for arm in &bridge.arms {
894 for decl in &bridge.decls {
895 for (arg, ty) in &decl.args {
896 if !implemented(arm.lang, ty, true) {
897 return Err(format!(
898 "line {}: `{}`'s `{arg}: {ty}` is in the type table but the {} generator \
899 does not marshal it yet (docs/bridge.md \"Types\")",
900 arm.line,
901 decl.name,
902 arm.lang.key()
903 ));
904 }
905 }
906 let Some(ty) = result_value(&decl.ret) else {
907 continue;
908 };
909 if !implemented(arm.lang, &ty, false) {
910 return Err(match arm.lang {
911 Lang::C | Lang::Cpp | Lang::Swift => format!(
914 "line {}: `{}` returns a value, which the {} arm cannot express yet — \
915 return `Result<(), day_bridge::Error>` there, or split the value into \
916 its own function",
917 arm.line,
918 decl.name,
919 arm.lang.key()
920 ),
921 _ => format!(
922 "line {}: `{}` returns `{ty}`, which the {} generator does not marshal \
923 yet (docs/bridge.md \"Types\")",
924 arm.line,
925 decl.name,
926 arm.lang.key()
927 ),
928 });
929 }
930 }
931 }
932
933 let mut claimed: BTreeMap<&str, (Lang, usize)> = BTreeMap::new();
938 for arm in &bridge.arms {
939 for p in &arm.platforms {
940 match claimed.get(p.as_str()) {
941 Some(&(lang, first)) if lang != arm.lang => {
942 return Err(format!(
943 "line {}: platform `{p}` is already claimed by the {} arm on line {first}",
944 arm.line,
945 lang.key()
946 ));
947 }
948 _ => {
949 claimed.insert(p, (arm.lang, arm.line));
950 }
951 }
952 }
953 }
954 if !bridge.decls.is_empty() && !claimed.contains_key("other") {
955 return Err(
956 "no `other` arm: a bridged crate must compile under day-mock on any host \
957 (docs/bridge.md \"Platform selection\")"
958 .into(),
959 );
960 }
961
962 let rust: Vec<&str> = bridge
965 .arms
966 .iter()
967 .filter(|a| a.lang == Lang::Rust)
968 .filter_map(|a| a.body.as_deref())
969 .collect();
970 if !rust.is_empty() {
971 for decl in &bridge.decls {
972 let wanted = format!("fn {}", decl.name);
973 if !rust.iter().any(|body| body.contains(&wanted)) {
974 return Err(format!(
975 "line {}: no rust arm implements `{}`",
976 decl.line, decl.name
977 ));
978 }
979 }
980 }
981 Ok(())
982}
983
984fn implemented(lang: Lang, ty: &str, argument: bool) -> bool {
988 let ty = ty.trim();
989 if lang == Lang::Rust {
990 return true;
991 }
992 if argument {
993 return SCALARS.contains(&ty) || ty == "&str";
994 }
995 match lang {
996 Lang::Kotlin | Lang::Java => SCALARS.contains(&ty) || ty == "String",
998 Lang::Js | Lang::ArkTs => SCALARS.contains(&ty),
999 _ => false,
1001 }
1002}
1003
1004fn check_type(ty: &str, argument: bool) -> Result<(), String> {
1005 let ty = ty.trim();
1006 if SCALARS.contains(&ty) {
1007 return Ok(());
1008 }
1009 if argument && (ty == "&str" || ty == "&[u8]") {
1010 return Ok(());
1011 }
1012 if !argument && (ty == "String" || ty == "Vec<u8>") {
1013 return Ok(());
1014 }
1015 if ty.starts_with("Option<") {
1016 return Err(format!(
1017 "`{ty}` does not cross a bridge — model absence in the value, or return `Result` \
1018 (docs/bridge.md \"Types\")"
1019 ));
1020 }
1021 if ty.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
1023 return Ok(());
1024 }
1025 Err(format!(
1026 "`{ty}` is outside the v1 type table (docs/bridge.md \"Types\")"
1027 ))
1028}
1029
1030fn symbol(crate_name: &str, decl: &Decl) -> String {
1036 format!("day_bridge_{}_{}", crate_name.replace('-', "_"), decl.name)
1037}
1038
1039fn c_type(ty: &str, utf16: bool) -> &'static str {
1042 match ty.trim() {
1043 "bool" | "i32" => "int32_t",
1044 "i64" => "int64_t",
1045 "f32" => "float",
1046 "f64" => "double",
1047 "&str" if utf16 => "const char16_t*",
1048 "&str" => "const char*",
1049 _ => "const void*",
1050 }
1051}
1052
1053fn rust_c_type(ty: &str, utf16: bool) -> &'static str {
1054 match ty.trim() {
1055 "bool" | "i32" => "i32",
1056 "i64" => "i64",
1057 "f32" => "f32",
1058 "f64" => "f64",
1059 "&str" if utf16 => "*const u16",
1060 "&str" => "*const std::ffi::c_char",
1061 _ => "*const std::ffi::c_void",
1062 }
1063}
1064
1065fn render_c(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1070 let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
1071 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1072 let mut out = String::new();
1073 let _ = writeln!(
1074 out,
1075 "/* @generated by day-build from {source}:{} — edit the arm, never this file. */",
1076 arm.line
1077 );
1078 let _ = writeln!(out, "#include <stdint.h>");
1079 if let Some(prelude) = &arm.prelude {
1080 let _ = writeln!(out, "{}", prelude);
1081 }
1082 let _ = writeln!(out, "\n#line {} {}", arm.body_line, quote(source));
1083 let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
1084 let _ = writeln!(out, "#line 1 {}", quote("<day-bridge adapters>"));
1085 if arm.lang == Lang::Cpp {
1088 let _ = writeln!(out, "extern \"C\" {{");
1089 }
1090 for decl in &bridge.decls {
1091 let params: Vec<String> = decl
1092 .args
1093 .iter()
1094 .map(|(n, t)| format!("{} {n}", c_type(t, utf16)))
1095 .collect();
1096 let names: Vec<&str> = decl.args.iter().map(|(n, _)| n.as_str()).collect();
1097 let params = if params.is_empty() {
1098 "void".to_string()
1099 } else {
1100 params.join(", ")
1101 };
1102 if decl.ret.is_empty() {
1103 let _ = writeln!(
1104 out,
1105 "void {}({params}) {{ {}({}); }}",
1106 symbol(crate_name, decl),
1107 decl.name,
1108 names.join(", ")
1109 );
1110 } else {
1111 let _ = writeln!(
1112 out,
1113 "int32_t {}({params}) {{ return {}({}); }}",
1114 symbol(crate_name, decl),
1115 decl.name,
1116 names.join(", ")
1117 );
1118 }
1119 }
1120 if arm.lang == Lang::Cpp {
1121 let _ = writeln!(out, "}}");
1122 }
1123 out
1124}
1125
1126fn render_swift(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1130 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1131 let mut out = String::new();
1132 let _ = writeln!(
1133 out,
1134 "// @generated by day-build from {source}:{} — edit the arm, never this file.",
1135 arm.line
1136 );
1137 let _ = writeln!(out, "import Foundation");
1138 if let Some(prelude) = &arm.prelude {
1139 let _ = writeln!(out, "{}", prelude);
1140 }
1141 let _ = writeln!(
1144 out,
1145 "\n#sourceLocation(file: {}, line: {})",
1146 quote(source),
1147 arm.body_line
1148 );
1149 let _ = writeln!(out, "{}", arm.body.as_deref().unwrap_or(""));
1150 let _ = writeln!(out, "#sourceLocation()\n");
1151
1152 for decl in &bridge.decls {
1153 let params: Vec<String> = decl
1154 .args
1155 .iter()
1156 .map(|(n, t)| format!("{n}: {}", swift_abi_type(t)))
1157 .collect();
1158 let ret = if decl.ret.is_empty() { "" } else { " -> Int32" };
1159 let _ = writeln!(out, "@_cdecl({})", quote(&symbol(crate_name, decl)));
1160 let _ = writeln!(
1161 out,
1162 "public func {}({}){ret} {{",
1163 symbol(crate_name, decl),
1164 params.join(", ")
1165 );
1166 let mut passed: Vec<String> = Vec::new();
1168 for (n, t) in &decl.args {
1169 match t.trim() {
1170 "&str" => {
1171 let _ = writeln!(out, " let {n}_s = String(cString: {n})");
1172 passed.push(format!("{n}: {n}_s"));
1173 }
1174 "bool" => {
1175 let _ = writeln!(out, " let {n}_b = {n} != 0");
1176 passed.push(format!("{n}: {n}_b"));
1177 }
1178 _ => passed.push(format!("{n}: {n}")),
1179 }
1180 }
1181 let call = format!("{}({})", decl.name, passed.join(", "));
1182 if decl.ret.is_empty() {
1183 let _ = writeln!(out, " {call}");
1184 } else {
1185 let _ = writeln!(out, " do {{");
1187 let _ = writeln!(out, " try {call}");
1188 let _ = writeln!(out, " return 0");
1189 let _ = writeln!(out, " }} catch {{");
1190 let _ = writeln!(
1191 out,
1192 " FileHandle.standardError.write(\"day-bridge: \\(error)\\n\".data(using: .utf8)!)"
1193 );
1194 let _ = writeln!(out, " return 1");
1195 let _ = writeln!(out, " }}");
1196 }
1197 let _ = writeln!(out, "}}\n");
1198 }
1199 out
1200}
1201
1202fn render_js(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1209 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1210 let mut out = String::new();
1211 let _ = writeln!(
1212 out,
1213 "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1214 //# sourceURL={source}",
1215 arm.line
1216 );
1217 if let Some(prelude) = &arm.prelude {
1218 let _ = writeln!(out, "{}", prelude);
1219 }
1220 let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1221
1222 let _ = writeln!(
1223 out,
1224 "// The shim calls this once at boot and spreads the result into the wasm import object."
1225 );
1226 let _ = writeln!(out, "export function register(rt) {{");
1227 let _ = writeln!(out, " return {{");
1228 for decl in &bridge.decls {
1229 let mut params: Vec<String> = Vec::new();
1230 let mut passed: Vec<String> = Vec::new();
1231 for (n, t) in &decl.args {
1232 if t.trim() == "&str" {
1233 params.push(format!("{n}_ptr"));
1234 params.push(format!("{n}_len"));
1235 passed.push(format!("rt.str({n}_ptr, {n}_len)"));
1236 } else {
1237 params.push(n.clone());
1238 passed.push(n.clone());
1239 }
1240 }
1241 let call = format!("{}({})", decl.name, passed.join(", "));
1242 let _ = writeln!(
1243 out,
1244 " {}({}) {{",
1245 symbol(crate_name, decl),
1246 params.join(", ")
1247 );
1248 match (decl.ret.is_empty(), result_value(&decl.ret)) {
1249 (true, _) => {
1250 let _ = writeln!(out, " {call};");
1251 }
1252 (false, None) => {
1253 let _ = writeln!(out, " try {{");
1255 let _ = writeln!(out, " {call};");
1256 let _ = writeln!(out, " return 0;");
1257 let _ = writeln!(out, " }} catch (e) {{");
1258 let _ = writeln!(
1259 out,
1260 " console.error('day-bridge: {}', e);",
1261 decl.name
1262 );
1263 let _ = writeln!(out, " return 1;");
1264 let _ = writeln!(out, " }}");
1265 }
1266 (false, Some(_)) => {
1267 let _ = writeln!(out, " return {call};");
1268 }
1269 }
1270 let _ = writeln!(out, " }},");
1271 }
1272 let _ = writeln!(out, " }};");
1273 let _ = writeln!(out, "}}");
1274 out
1275}
1276
1277fn render_arkts(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1281 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1282 let mut out = String::new();
1283 let _ = writeln!(
1284 out,
1285 "// @generated by day-build from {source}:{} — edit the arm, never this file.",
1286 arm.line
1287 );
1288 if let Some(prelude) = &arm.prelude {
1289 let _ = writeln!(out, "{}", prelude);
1290 }
1291 let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1292 let _ = writeln!(
1293 out,
1294 "// The host calls this once at startup; the returned record is registered with the napi\n\
1295 // module so the Rust side can reach each arm by name."
1296 );
1297 let _ = writeln!(
1298 out,
1299 "export function register(): Record<string, Function> {{"
1300 );
1301 let _ = writeln!(out, " return {{");
1302 for decl in &bridge.decls {
1303 let _ = writeln!(out, " '{}': {},", symbol(crate_name, decl), decl.name);
1304 }
1305 let _ = writeln!(out, " }};");
1306 let _ = writeln!(out, "}}");
1307 out
1308}
1309
1310fn render_js_rust(bridge: &Bridge, crate_name: &str) -> String {
1313 let mut out = String::new();
1314 let _ = writeln!(out, "#[link(wasm_import_module = \"env\")]");
1318 let _ = writeln!(out, "unsafe extern \"C\" {{");
1319 for decl in &bridge.decls {
1320 let mut params: Vec<String> = Vec::new();
1321 for (n, t) in &decl.args {
1322 if t.trim() == "&str" {
1323 params.push(format!("{n}_ptr: *const u8"));
1324 params.push(format!("{n}_len: usize"));
1325 } else {
1326 params.push(format!("{n}: {}", rust_c_type(t, false)));
1327 }
1328 }
1329 let ret = match (decl.ret.is_empty(), result_value(&decl.ret)) {
1330 (true, _) => String::new(),
1331 (false, None) => " -> i32".to_string(),
1332 (false, Some(ty)) => format!(" -> {ty}"),
1333 };
1334 let _ = writeln!(
1335 out,
1336 " fn {}({}){ret};",
1337 symbol(crate_name, decl),
1338 params.join(", ")
1339 );
1340 }
1341 let _ = writeln!(out, "}}\n");
1342
1343 for decl in &bridge.decls {
1344 let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1345 let ret = if decl.ret.is_empty() {
1346 String::new()
1347 } else {
1348 format!(" -> {}", decl.ret)
1349 };
1350 let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1351 let mut passed: Vec<String> = Vec::new();
1352 for (n, t) in &decl.args {
1353 if t.trim() == "&str" {
1354 passed.push(format!("{n}.as_ptr()"));
1355 passed.push(format!("{n}.len()"));
1356 } else if t.trim() == "bool" {
1357 passed.push(format!("{n} as i32"));
1358 } else {
1359 passed.push(n.clone());
1360 }
1361 }
1362 let call = format!(
1363 "unsafe {{ {}({}) }}",
1364 symbol(crate_name, decl),
1365 passed.join(", ")
1366 );
1367 match (decl.ret.is_empty(), result_value(&decl.ret)) {
1368 (true, _) => {
1369 let _ = writeln!(out, " {call};");
1370 }
1371 (false, None) => {
1372 let _ = writeln!(out, " if {call} == 0 {{");
1373 let _ = writeln!(out, " Ok(())");
1374 let _ = writeln!(out, " }} else {{");
1375 let _ = writeln!(
1376 out,
1377 " Err(day_bridge::Error::Foreign(\"{}\".into()))",
1378 decl.name
1379 );
1380 let _ = writeln!(out, " }}");
1381 }
1382 (false, Some(_)) => {
1383 let _ = writeln!(out, " Ok({call})");
1384 }
1385 }
1386 let _ = writeln!(out, "}}\n");
1387 }
1388 out
1389}
1390
1391fn render_kotlin(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1403 let pkg = kotlin_package(crate_name);
1404 let object = kotlin_object(crate_name);
1405 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1406 let mut out = String::new();
1407 let _ = writeln!(
1408 out,
1409 "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1410 // Kotlin carries no line directive: an error below is at {source}:{} plus the offset.",
1411 arm.line, arm.body_line
1412 );
1413 let _ = writeln!(out, "package {pkg}\n");
1414 if let Some(prelude) = &arm.prelude {
1415 let _ = writeln!(out, "{}", prelude);
1416 }
1417 let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1418
1419 let _ = writeln!(out, "object {object} {{");
1420 for decl in &bridge.decls {
1421 let params: Vec<String> = decl
1422 .args
1423 .iter()
1424 .map(|(n, t)| format!("{n}: {}", kotlin_type(t)))
1425 .collect();
1426 let call = format!(
1427 "{pkg}.{}({})",
1430 decl.name,
1431 decl.args
1432 .iter()
1433 .map(|(n, _)| format!("{n} = {n}"))
1434 .collect::<Vec<_>>()
1435 .join(", ")
1436 );
1437 let value = result_value(&decl.ret);
1441 let ret = match value.as_deref() {
1442 None => String::new(),
1443 Some(ty) => format!(": {}", kotlin_type(ty)),
1444 };
1445 let _ = writeln!(out, " @JvmStatic");
1446 let _ = writeln!(out, " fun {}({}){ret} {{", decl.name, params.join(", "));
1447 if value.is_some() {
1448 let _ = writeln!(out, " return {call}");
1449 } else {
1450 let _ = writeln!(out, " {call}");
1451 }
1452 let _ = writeln!(out, " }}");
1453 }
1454 let _ = writeln!(out, "}}");
1455 out
1456}
1457
1458fn render_java(arm: &Arm, crate_name: &str) -> String {
1462 let pkg = kotlin_package(crate_name);
1463 let class = kotlin_object(crate_name);
1464 let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1465 let mut out = String::new();
1466 let _ = writeln!(
1467 out,
1468 "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1469 // Java carries no line directive: an error below is at {source}:{} plus the offset.",
1470 arm.line, arm.body_line
1471 );
1472 let _ = writeln!(out, "package {pkg};\n");
1473 if let Some(prelude) = &arm.prelude {
1474 let _ = writeln!(out, "{}", prelude);
1475 }
1476 let _ = writeln!(out, "\npublic final class {class} {{");
1477 let _ = writeln!(out, " private {class}() {{}}\n");
1478 for line in arm.body.as_deref().unwrap_or("").lines() {
1481 if line.trim().is_empty() {
1482 let _ = writeln!(out);
1483 } else {
1484 let _ = writeln!(out, " {line}");
1485 }
1486 }
1487 let _ = writeln!(out, "}}");
1488 out
1489}
1490
1491fn render_jvm_rust(bridge: &Bridge, crate_name: &str) -> String {
1494 let class = kotlin_package(crate_name).replace('.', "/") + "/" + &kotlin_object(crate_name);
1495 let mut out = String::new();
1496 for decl in &bridge.decls {
1497 let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1498 let ret = if decl.ret.is_empty() {
1499 String::new()
1500 } else {
1501 format!(" -> {}", decl.ret)
1502 };
1503 let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1504 let _ = writeln!(out, " use day_android::{{DayEnv, with_env}};");
1505 let _ = writeln!(out, " if !day_android::vm_ready() {{");
1509 let _ = writeln!(
1510 out,
1511 " return{};",
1512 if decl.ret.is_empty() {
1513 String::new()
1514 } else {
1515 " Err(day_bridge::Error::Runtime)".to_string()
1516 }
1517 );
1518 let _ = writeln!(out, " }}");
1519 let _ = writeln!(out, " let called = with_env(|env| {{");
1520 let mut jvalues: Vec<String> = Vec::new();
1522 for (n, t) in &decl.args {
1523 match t.trim() {
1524 "&str" => {
1525 let _ = writeln!(out, " let {n}_j = env.new_string({n}).ok()?;");
1526 jvalues.push(format!("(&{n}_j).into()"));
1527 }
1528 "bool" => jvalues.push(format!(
1529 "day_android::jni::objects::JValue::Bool({n} as u8)"
1530 )),
1531 "i32" => jvalues.push(format!("day_android::jni::objects::JValue::Int({n})")),
1532 "i64" => jvalues.push(format!("day_android::jni::objects::JValue::Long({n})")),
1533 "f32" => jvalues.push(format!("day_android::jni::objects::JValue::Float({n})")),
1534 "f64" => jvalues.push(format!("day_android::jni::objects::JValue::Double({n})")),
1535 _ => jvalues.push(n.clone()),
1536 }
1537 }
1538 let _ = writeln!(
1539 out,
1540 " let outcome = env.dcall_static({}, {}, {}, &[{}]);",
1541 quote(&class),
1542 quote(&decl.name),
1543 quote(&jni_signature(decl)),
1544 jvalues.join(", ")
1545 );
1546 let _ = writeln!(out, " if env.exception_check() {{");
1551 let _ = writeln!(out, " env.exception_describe(); // → logcat");
1552 let _ = writeln!(out, " env.exception_clear();");
1553 let _ = writeln!(out, " }}");
1554 match (decl.ret.is_empty(), result_value(&decl.ret)) {
1557 (true, _) => {
1558 let _ = writeln!(out, " outcome.ok()?;");
1559 let _ = writeln!(out, " Some(())");
1560 let _ = writeln!(out, " }});");
1561 let _ = writeln!(out, " let _ = called;");
1562 }
1563 (false, None) => {
1564 let _ = writeln!(out, " outcome.ok()?;");
1565 let _ = writeln!(out, " Some(())");
1566 let _ = writeln!(out, " }});");
1567 let _ = writeln!(
1568 out,
1569 " // A Java exception fails `dcall_static`, so a throwing"
1570 );
1571 let _ = writeln!(out, " // arm arrives here as `None`.");
1572 let _ = writeln!(out, " match called {{");
1573 let _ = writeln!(out, " Some(()) => Ok(()),");
1574 let _ = writeln!(
1575 out,
1576 " None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
1577 decl.name
1578 );
1579 let _ = writeln!(out, " }}");
1580 }
1581 (false, Some(ty)) => {
1582 if ty == "String" {
1583 let _ = writeln!(out, " let obj = outcome.ok()?.l().ok()?;");
1587 let _ = writeln!(out, " if obj.is_null() {{");
1588 let _ = writeln!(out, " return Some(String::new());");
1589 let _ = writeln!(out, " }}");
1590 let _ = writeln!(out, " env.dstr(&day_android::as_jstring(obj)).ok()");
1591 } else {
1592 let _ = writeln!(out, " outcome.ok()?.{}().ok()", jvalue_accessor(&ty));
1593 }
1594 let _ = writeln!(out, " }});");
1595 let _ = writeln!(
1596 out,
1597 " // A Java exception fails `dcall_static`, so a throwing"
1598 );
1599 let _ = writeln!(out, " // arm arrives here as `None`.");
1600 let _ = writeln!(out, " match called {{");
1601 let _ = writeln!(out, " Some(v) => Ok(v),");
1602 let _ = writeln!(
1603 out,
1604 " None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
1605 decl.name
1606 );
1607 let _ = writeln!(out, " }}");
1608 }
1609 }
1610 let _ = writeln!(out, "}}\n");
1611 }
1612 out
1613}
1614
1615fn result_value(ret: &str) -> Option<String> {
1617 let inner = ret
1618 .trim()
1619 .strip_prefix("Result<")
1620 .and_then(|r| r.strip_suffix('>'))?;
1621 let value = split_top(inner, ',').first()?.trim().to_string();
1622 (!value.is_empty() && value != "()").then_some(value)
1623}
1624
1625fn jvalue_accessor(ty: &str) -> &'static str {
1627 match ty.trim() {
1628 "bool" => "z",
1629 "i32" => "i",
1630 "i64" => "j",
1631 "f32" => "f",
1632 "f64" => "d",
1633 _ => "i",
1634 }
1635}
1636
1637fn jni_signature(decl: &Decl) -> String {
1639 let args: String = decl
1640 .args
1641 .iter()
1642 .map(|(_, t)| match t.trim() {
1643 "bool" => "Z",
1644 "i32" => "I",
1645 "i64" => "J",
1646 "f32" => "F",
1647 "f64" => "D",
1648 "&str" => "Ljava/lang/String;",
1649 _ => "Ljava/lang/Object;",
1650 })
1651 .collect();
1652 let ret = match result_value(&decl.ret).as_deref() {
1653 None => "V",
1654 Some("bool") => "Z",
1655 Some("i32") => "I",
1656 Some("i64") => "J",
1657 Some("f32") => "F",
1658 Some("f64") => "D",
1659 Some("String") => "Ljava/lang/String;",
1660 Some(_) => "Ljava/lang/Object;",
1661 };
1662 format!("({args}){ret}")
1663}
1664
1665fn kotlin_type(ty: &str) -> &'static str {
1666 match ty.trim() {
1667 "bool" => "Boolean",
1668 "i32" => "Int",
1669 "i64" => "Long",
1670 "f32" => "Float",
1671 "f64" => "Double",
1672 "&str" | "String" => "String",
1673 _ => "Any",
1674 }
1675}
1676
1677fn kotlin_package(crate_name: &str) -> String {
1679 format!("dev.daybrite.day.bridge.{}", crate_name.replace('-', "_"))
1680}
1681
1682fn kotlin_object(crate_name: &str) -> String {
1684 let mut out = String::new();
1685 for part in crate_name.split('-') {
1686 let mut chars = part.chars();
1687 if let Some(first) = chars.next() {
1688 out.extend(first.to_uppercase());
1689 out.push_str(chars.as_str());
1690 }
1691 }
1692 format!("{out}Bridge")
1693}
1694
1695fn swift_abi_type(ty: &str) -> &'static str {
1697 match ty.trim() {
1698 "bool" | "i32" => "Int32",
1699 "i64" => "Int64",
1700 "f32" => "Float",
1701 "f64" => "Double",
1702 "&str" => "UnsafePointer<CChar>",
1703 _ => "UnsafeRawPointer",
1704 }
1705}
1706
1707fn render_c_rust(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1710 let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
1711 let mut out = String::new();
1712 let _ = writeln!(out, "unsafe extern \"C\" {{");
1713 for decl in &bridge.decls {
1714 let args: Vec<String> = decl
1715 .args
1716 .iter()
1717 .map(|(n, t)| format!("{n}: {}", rust_c_type(t, utf16)))
1718 .collect();
1719 let ret = if decl.ret.is_empty() { "" } else { " -> i32" };
1720 let _ = writeln!(
1721 out,
1722 " fn {}({}){ret};",
1723 symbol(crate_name, decl),
1724 args.join(", ")
1725 );
1726 }
1727 let _ = writeln!(out, "}}\n");
1728
1729 for decl in &bridge.decls {
1730 let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1731 let ret = if decl.ret.is_empty() {
1732 String::new()
1733 } else {
1734 format!(" -> {}", decl.ret)
1735 };
1736 let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1737 let mut passed: Vec<String> = Vec::new();
1738 for (n, t) in &decl.args {
1739 match t.trim() {
1740 "&str" if utf16 => {
1741 let _ = writeln!(
1742 out,
1743 " let mut {n}_w: Vec<u16> = {n}.encode_utf16().collect();"
1744 );
1745 let _ = writeln!(out, " {n}_w.push(0);");
1746 passed.push(format!("{n}_w.as_ptr()"));
1747 }
1748 "&str" => {
1749 let _ = writeln!(
1750 out,
1751 " let Ok({n}_c) = std::ffi::CString::new({n}) else {{"
1752 );
1753 let _ = writeln!(
1754 out,
1755 " return {};",
1756 if decl.ret.is_empty() {
1757 "".to_string()
1758 } else {
1759 "Err(day_bridge::Error::Encoding)".to_string()
1760 }
1761 );
1762 let _ = writeln!(out, " }};");
1763 passed.push(format!("{n}_c.as_ptr()"));
1764 }
1765 "bool" => passed.push(format!("{n} as i32")),
1766 _ => passed.push(n.clone()),
1767 }
1768 }
1769 let call = format!(
1770 "unsafe {{ {}({}) }}",
1771 symbol(crate_name, decl),
1772 passed.join(", ")
1773 );
1774 if decl.ret.is_empty() {
1775 let _ = writeln!(out, " {call};");
1776 } else {
1777 let _ = writeln!(out, " if {call} == 0 {{");
1778 let _ = writeln!(out, " Ok(())");
1779 let _ = writeln!(out, " }} else {{");
1780 let _ = writeln!(
1781 out,
1782 " Err(day_bridge::Error::Foreign(\"{} failed\".into()))",
1783 decl.name
1784 );
1785 let _ = writeln!(out, " }}");
1786 }
1787 let _ = writeln!(out, "}}\n");
1788 }
1789 out
1790}
1791
1792fn staged_by_cli(lang: Lang) -> bool {
1796 matches!(
1797 lang,
1798 Lang::Swift | Lang::Kotlin | Lang::Java | Lang::ArkTs | Lang::Js
1799 )
1800}
1801
1802const STAGED_CFG: &str = "day_bridge_staged";
1804
1805fn arm_cfg(arm: &Arm, bridge: &Bridge) -> String {
1808 if arm.platforms.iter().any(|p| p == "other") {
1809 let mut claimed: Vec<&str> = bridge
1810 .arms
1811 .iter()
1812 .flat_map(|a| a.platforms.iter())
1813 .filter(|p| p.as_str() != "other")
1814 .map(|p| cfg_for(p))
1815 .collect();
1816 claimed.sort_unstable();
1817 claimed.dedup();
1818 return format!("not(any({}))", claimed.join(", "));
1819 }
1820 let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
1821 list.sort_unstable();
1822 list.dedup();
1823 let platform = if list.len() == 1 {
1824 list[0].to_string()
1825 } else {
1826 format!("any({})", list.join(", "))
1827 };
1828 if staged_by_cli(arm.lang) {
1829 format!("all({platform}, {STAGED_CFG})")
1830 } else {
1831 platform
1832 }
1833}
1834
1835fn unstaged_cfg(arm: &Arm) -> String {
1839 let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
1840 list.sort_unstable();
1841 list.dedup();
1842 let platform = if list.len() == 1 {
1843 list[0].to_string()
1844 } else {
1845 format!("any({})", list.join(", "))
1846 };
1847 format!("all({platform}, not({STAGED_CFG}))")
1848}
1849
1850fn cfg_attr(pred: &str) -> Option<String> {
1853 (pred != "not(any())").then(|| format!("#[cfg({pred})]"))
1854}
1855
1856fn render_rust(bridge: &Bridge, crate_name: &str) -> String {
1857 let mut out = String::from(
1858 "// @generated by day-build from this crate's `day_bridge::bridge!` block.\n\
1859 // Edit the arms in the crate source, never this file (docs/bridge.md).\n\n",
1860 );
1861 if bridge.decls.is_empty() {
1862 return out;
1863 }
1864
1865 for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Rust) {
1866 if let Some(cfg) = cfg_attr(&arm_cfg(arm, bridge)) {
1867 let _ = writeln!(out, "{cfg}");
1868 }
1869 let _ = writeln!(out, "#[allow(dead_code)]");
1870 let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
1871 }
1872
1873 let fallback: Vec<&Arm> = bridge
1876 .arms
1877 .iter()
1878 .filter(|a| a.lang == Lang::Rust && a.platforms.iter().any(|p| p == "other"))
1879 .collect();
1880 for arm in bridge.arms.iter().filter(|a| staged_by_cli(a.lang)) {
1881 for fb in &fallback {
1882 let _ = writeln!(out, "#[cfg({})]", unstaged_cfg(arm));
1883 let _ = writeln!(out, "#[allow(dead_code)]");
1884 let _ = writeln!(out, "{}\n", fb.body.as_deref().unwrap_or(""));
1885 }
1886 }
1887
1888 for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Js) {
1890 let block = render_js_rust(bridge, crate_name);
1891 let cfg = arm_cfg(arm, bridge);
1892 for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1893 let _ = writeln!(
1894 out,
1895 "#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
1896 item.trim_end()
1897 );
1898 }
1899 }
1900
1901 for arm in bridge
1904 .arms
1905 .iter()
1906 .filter(|a| matches!(a.lang, Lang::Kotlin | Lang::Java))
1907 {
1908 let block = render_jvm_rust(bridge, crate_name);
1909 let cfg = arm_cfg(arm, bridge);
1910 for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1911 let _ = writeln!(
1912 out,
1913 "#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
1914 item.trim_end()
1915 );
1916 }
1917 }
1918
1919 for arm in bridge
1923 .arms
1924 .iter()
1925 .filter(|a| matches!(a.lang, Lang::C | Lang::Cpp | Lang::Swift))
1926 {
1927 let block = render_c_rust(bridge, arm, crate_name);
1928 match cfg_attr(&arm_cfg(arm, bridge)) {
1929 Some(cfg) => {
1930 for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1932 let _ = writeln!(out, "{cfg}\n#[allow(dead_code)]\n{}\n", item.trim_end());
1933 }
1934 }
1935 None => {
1936 let _ = writeln!(out, "{block}");
1937 }
1938 }
1939 }
1940
1941 let mut levels: Vec<(String, &'static str)> = Vec::new();
1945 for arm in &bridge.arms {
1946 let support = if arm.lang == Lang::Rust && arm.platforms.iter().any(|p| p == "other") {
1947 "Unsupported"
1948 } else if arm.options.get("support").map(String::as_str) == Some("emulated") {
1949 "Emulated"
1950 } else {
1951 "Native"
1952 };
1953 let cfg = arm_cfg(arm, bridge);
1954 if !levels.iter().any(|(seen, _)| seen == &cfg) {
1955 levels.push((cfg, support));
1956 }
1957 if staged_by_cli(arm.lang) {
1958 let cfg = unstaged_cfg(arm);
1959 if !levels.iter().any(|(seen, _)| seen == &cfg) {
1960 levels.push((cfg, "Unsupported"));
1961 }
1962 }
1963 }
1964 for decl in &bridge.decls {
1965 for (cfg, support) in &levels {
1966 if let Some(attr) = cfg_attr(cfg) {
1967 let _ = writeln!(out, "{attr}");
1968 }
1969 let _ = writeln!(
1970 out,
1971 "#[allow(dead_code)]\npub(crate) fn {}_support() -> day_bridge::Support {{\n \
1972 day_bridge::Support::{support}\n}}\n",
1973 decl.name
1974 );
1975 }
1976 }
1977 out
1978}
1979
1980fn generated_name(arm: &Arm, crate_name: &str) -> String {
1983 if arm.lang == Lang::Java {
1987 return format!("{}.java", kotlin_object(crate_name));
1988 }
1989 let ext = match arm.lang {
1990 Lang::Swift => "swift",
1991 Lang::Kotlin => "kt",
1992 Lang::Java => "java",
1993 Lang::ArkTs => "ets",
1994 Lang::Js => "js",
1995 Lang::Cpp => "cpp",
1996 Lang::C => "c",
1997 Lang::Rust => "rs",
1998 };
1999 format!("{crate_name}-{}.{ext}", arm.platforms.join("-"))
2000}
2001
2002fn quote(s: &str) -> String {
2003 let mut out = String::with_capacity(s.len() + 2);
2004 out.push('"');
2005 for c in s.chars() {
2006 match c {
2007 '"' => out.push_str("\\\""),
2008 '\\' => out.push_str("\\\\"),
2009 '\n' => out.push_str("\\n"),
2010 '\r' => out.push_str("\\r"),
2011 '\t' => out.push_str("\\t"),
2012 c if (c as u32) < 0x20 => {
2013 let _ = write!(out, "\\u{:04x}", c as u32);
2014 }
2015 c => out.push(c),
2016 }
2017 }
2018 out.push('"');
2019 out
2020}
2021
2022fn write_if_changed(path: &Path, content: &str) -> Result<(), String> {
2025 if std::fs::read(path).is_ok_and(|cur| cur == content.as_bytes()) {
2026 return Ok(());
2027 }
2028 std::fs::write(path, content).map_err(|e| format!("{}: {e}", path.display()))
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 const SPEECH: &str = r###"
2036day_bridge::bridge! {
2037 #[day_bridge::declare]
2038 extern "day" {
2039 fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
2040 fn stop_native();
2041 }
2042
2043 #[day_bridge::impl(kotlin, platforms = [android])]
2044 kotlin!(
2045 prelude = r#"
2046 import android.speech.tts.TextToSpeech
2047 "#,
2048 body = r#"
2049 fun speak_native(text: String) { engine?.speak(text) }
2050 "#,
2051 );
2052
2053 #[day_bridge::impl(rust, platforms = [other])]
2054 fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
2055 Err(day_bridge::Error::Unsupported)
2056 }
2057
2058 #[day_bridge::impl(rust, platforms = [other])]
2059 fn stop_native() {}
2060}
2061"###;
2062
2063 fn parse(src: &str) -> Bridge {
2064 let mut b = Bridge::default();
2065 parse_into(src, "src/lib.rs", &mut b).expect("parse");
2066 b
2067 }
2068
2069 fn parse_err(src: &str) -> String {
2070 let mut b = Bridge::default();
2071 parse_into(src, "src/lib.rs", &mut b).expect_err("should not parse")
2072 }
2073
2074 #[test]
2075 fn parses_declarations_and_arms() {
2076 let b = parse(SPEECH);
2077 assert_eq!(b.decls.len(), 2);
2078 assert_eq!(b.decls[0].name, "speak_native");
2079 assert_eq!(b.decls[0].args, vec![("text".into(), "&str".into())]);
2080 assert_eq!(b.decls[0].ret, "Result<(), day_bridge::Error>");
2081 assert_eq!(b.decls[1].name, "stop_native");
2082 assert!(b.decls[1].args.is_empty());
2083 assert_eq!(b.arms.len(), 3);
2084 assert_eq!(
2085 b.arms[0].prelude.as_deref(),
2086 Some("import android.speech.tts.TextToSpeech"),
2087 "the prelude belongs to the arm that declared it"
2088 );
2089 assert_eq!(b.arms[0].lang, Lang::Kotlin);
2090 assert_eq!(b.arms[0].platforms, vec!["android".to_string()]);
2091 assert!(
2092 b.arms[0]
2093 .body
2094 .as_deref()
2095 .unwrap()
2096 .starts_with("fun speak_native")
2097 );
2098 }
2099
2100 #[test]
2101 fn validates_and_renders_the_rust_arm() {
2102 let b = parse(SPEECH);
2103 validate(&b).expect("valid");
2104 let rust = render_rust(&b, "day-part-speech");
2105 assert!(
2107 rust.contains("#[cfg(not(any(target_os = \"android\")))]"),
2108 "{rust}"
2109 );
2110 assert!(rust.contains("fn speak_native(_text: &str)"));
2111 assert!(rust.contains("pub(crate) fn speak_native_support()"));
2112 assert!(
2113 rust.contains("Support::Native"),
2114 "the android arm reports Native"
2115 );
2116 assert!(
2117 rust.contains("Support::Unsupported"),
2118 "the fallback reports Unsupported"
2119 );
2120 }
2121
2122 #[test]
2123 fn rejects_a_type_outside_the_table() {
2124 let b = parse(
2125 r###"
2126 day_bridge::bridge! {
2127 #[day_bridge::declare]
2128 extern "day" { fn f(x: Option<i32>); }
2129 #[day_bridge::impl(rust, platforms = [other])]
2130 fn f(_x: Option<i32>) {}
2131 }
2132 "###,
2133 );
2134 let err = validate(&b).unwrap_err();
2135 assert!(err.contains("does not cross a bridge"), "{err}");
2136 }
2137
2138 #[test]
2139 fn rejects_a_missing_fallback() {
2140 let b = parse(
2141 r###"
2142 day_bridge::bridge! {
2143 #[day_bridge::declare]
2144 extern "day" { fn f(); }
2145 #[day_bridge::impl(kotlin, platforms = [android])]
2146 kotlin!(r#" fun f() {} "#);
2147 }
2148 "###,
2149 );
2150 assert!(validate(&b).unwrap_err().contains("no `other` arm"));
2151 }
2152
2153 #[test]
2154 fn rejects_two_arms_claiming_one_platform() {
2155 let b = parse(
2156 r###"
2157 day_bridge::bridge! {
2158 #[day_bridge::declare]
2159 extern "day" { fn f(); }
2160 #[day_bridge::impl(kotlin, platforms = [android])]
2161 kotlin!(r#" fun f() {} "#);
2162 #[day_bridge::impl(js, platforms = [android])]
2163 js!(r#" export function f() {} "#);
2164 #[day_bridge::impl(rust, platforms = [other])]
2165 fn f() {}
2166 }
2167 "###,
2168 );
2169 assert!(validate(&b).unwrap_err().contains("already claimed"));
2170 }
2171
2172 #[test]
2176 fn a_body_containing_a_quote_hash_needs_more_hashes() {
2177 let b = parse(
2178 r####"
2179 day_bridge::bridge! {
2180 #[day_bridge::declare]
2181 extern "day" { fn focus_native(); }
2182 #[day_bridge::impl(js, platforms = [web])]
2183 js!(r##"
2184 export function focus_native() { document.querySelector("#speech").focus(); }
2185 "##);
2186 #[day_bridge::impl(rust, platforms = [other])]
2187 fn focus_native() {}
2188 }
2189 "####,
2190 );
2191 let arm = b.arms.iter().find(|a| a.lang == Lang::Js).unwrap();
2192 let body = arm.body.as_deref().unwrap();
2193 assert!(
2194 body.contains("querySelector(\"#speech\")") && body.ends_with("}"),
2195 "the whole body survives:\n{body}"
2196 );
2197 assert!(b.arms.iter().any(|a| a.lang == Lang::Rust), "{:?}", b.arms);
2199 }
2200
2201 #[test]
2203 fn rejects_unknown_arm_options_and_values() {
2204 let bad_key = parse_err(
2205 r###"
2206 day_bridge::bridge! {
2207 #[day_bridge::impl(c, platforms = [linux], linkk = ["speechd"])]
2208 c!(r#" void f(void) {} "#);
2209 }
2210 "###,
2211 );
2212 assert!(bad_key.contains("unknown arm option `linkk`"), "{bad_key}");
2213
2214 let bad_encoding = parse_err(
2215 r###"
2216 day_bridge::bridge! {
2217 #[day_bridge::impl(cpp, platforms = [windows], encoding = "utf-16")]
2218 cpp!(r#" void f(void) {} "#);
2219 }
2220 "###,
2221 );
2222 assert!(
2223 bad_encoding.contains("expected \"utf8\" or \"utf16\""),
2224 "{bad_encoding}"
2225 );
2226
2227 let bad_support = parse_err(
2228 r###"
2229 day_bridge::bridge! {
2230 #[day_bridge::impl(arkts, platforms = [ohos], support = "partial")]
2231 arkts!(r#" export function f() {} "#);
2232 }
2233 "###,
2234 );
2235 assert!(
2236 bad_support.contains("expected \"native\" or \"emulated\""),
2237 "{bad_support}"
2238 );
2239 }
2240
2241 #[test]
2242 fn rejects_a_package_line_in_a_prelude() {
2243 let err = parse_err(
2244 r###"
2245 day_bridge::bridge! {
2246 #[day_bridge::impl(kotlin, platforms = [android])]
2247 kotlin!(
2248 prelude = r#"
2249 package dev.example.mine
2250 "#,
2251 body = r#"
2252 fun f() {}
2253 "#,
2254 );
2255 }
2256 "###,
2257 );
2258 assert!(err.contains("belongs to the generator"), "{err}");
2259 }
2260
2261 #[test]
2264 fn a_prelude_reaches_only_its_own_arm() {
2265 let b = parse(
2266 r###"
2267 day_bridge::bridge! {
2268 #[day_bridge::declare]
2269 extern "day" { fn f(); }
2270
2271 #[day_bridge::impl(c, platforms = [linux])]
2272 c!(
2273 prelude = r#"
2274 #include <linux_only.h>
2275 "#,
2276 body = r#" void f(void) {} "#,
2277 );
2278
2279 #[day_bridge::impl(c, platforms = [windows])]
2280 c!(
2281 prelude = r#"
2282 #include <windows.h>
2283 "#,
2284 body = r#" void f(void) {} "#,
2285 );
2286
2287 #[day_bridge::impl(rust, platforms = [other])]
2288 fn f() {}
2289 }
2290 "###,
2291 );
2292 let windows = b
2293 .arms
2294 .iter()
2295 .find(|a| a.platforms.iter().any(|p| p == "windows"))
2296 .unwrap();
2297 let c = render_c(&b, windows, "day-part-demo");
2298 assert!(c.contains("#include <windows.h>"), "{c}");
2299 assert!(
2300 !c.contains("linux_only.h"),
2301 "no leak from the other arm:\n{c}"
2302 );
2303 }
2304
2305 #[test]
2307 fn a_standalone_prelude_attribute_says_what_replaced_it() {
2308 let err = parse_err(
2309 r###"
2310 day_bridge::bridge! {
2311 #[day_bridge::prelude(swift)]
2312 swift!(r#" import AVFoundation "#);
2313 }
2314 "###,
2315 );
2316 assert!(err.contains("no longer exists"), "{err}");
2317 assert!(err.contains("prelude = r#"), "{err}");
2318 }
2319
2320 #[test]
2321 fn renders_the_kotlin_adapter_and_its_jni_side() {
2322 let b = parse(SPEECH);
2323 let arm = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
2324 let kt = render_kotlin(&b, arm, "day-part-speech");
2325 assert!(
2326 kt.contains("package dev.daybrite.day.bridge.day_part_speech"),
2327 "{kt}"
2328 );
2329 assert!(
2330 kt.contains("import android.speech.tts.TextToSpeech"),
2331 "prelude hoisted:\n{kt}"
2332 );
2333 assert!(kt.contains("object DayPartSpeechBridge {"), "{kt}");
2334 assert!(
2337 kt.contains("dev.daybrite.day.bridge.day_part_speech.speak_native(text = text)"),
2338 "the declared name is used verbatim, not camel-cased:\n{kt}"
2339 );
2340 assert!(
2343 !kt.contains("catch ("),
2344 "the arm's exceptions cross as-is:\n{kt}"
2345 );
2346
2347 let rust = render_jvm_rust(&b, "day-part-speech");
2348 assert!(
2349 rust.contains(
2350 "env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
2351 ),
2352 "{rust}"
2353 );
2354 assert!(
2355 rust.contains("let text_j = env.new_string(text).ok()?;"),
2356 "{rust}"
2357 );
2358 assert!(
2359 rust.contains("Err(day_bridge::Error::Foreign(\"speak_native\".into()))"),
2360 "a failed call becomes Foreign:\n{rust}"
2361 );
2362 }
2363
2364 #[test]
2365 fn renders_the_java_adapter_the_jvm_side_shares() {
2366 let b = parse(&SPEECH.replace("kotlin", "java"));
2369 let arm = b.arms.iter().find(|a| a.lang == Lang::Java).unwrap();
2370 let java = render_java(arm, "day-part-speech");
2371 assert!(
2372 java.contains("package dev.daybrite.day.bridge.day_part_speech;"),
2373 "{java}"
2374 );
2375 assert!(
2376 java.contains("import android.speech.tts.TextToSpeech"),
2377 "prelude hoisted:\n{java}"
2378 );
2379 assert!(
2380 java.contains("public final class DayPartSpeechBridge {"),
2381 "{java}"
2382 );
2383 assert!(
2384 java.contains("speak_native"),
2385 "the declared name is used verbatim:\n{java}"
2386 );
2387 assert_eq!(
2390 adapter_name(arm, "day-part-speech"),
2391 "DayPartSpeechBridge.java"
2392 );
2393
2394 let rust = render_jvm_rust(&b, "day-part-speech");
2396 assert!(
2397 rust.contains(
2398 "env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
2399 ),
2400 "{rust}"
2401 );
2402 }
2403
2404 #[test]
2407 fn a_cpp_arm_exports_unmangled_utf16_adapters() {
2408 let b = parse(
2409 r###"
2410 day_bridge::bridge! {
2411 #[day_bridge::declare]
2412 extern "day" {
2413 fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
2414 fn stop_native();
2415 }
2416 #[day_bridge::impl(cpp, platforms = [windows], encoding = "utf16", link = ["ole32", "sapi"])]
2417 cpp!(r#" int32_t speak_native(const char16_t* t) { return 0; } "#);
2418 #[day_bridge::impl(rust, platforms = [other])]
2419 fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
2420 Err(day_bridge::Error::Unsupported)
2421 }
2422 #[day_bridge::impl(rust, platforms = [other])]
2423 fn stop_native() {}
2424 }
2425 "###,
2426 );
2427 validate(&b).expect("valid");
2428 let arm = b.arms.iter().find(|a| a.lang == Lang::Cpp).unwrap();
2429 let cpp = render_c(&b, arm, "day-part-speech");
2430 assert!(cpp.contains("extern \"C\" {"), "{cpp}");
2431 assert!(
2432 cpp.contains(
2433 "int32_t day_bridge_day_part_speech_speak_native(const char16_t* text) { return speak_native(text); }"
2434 ),
2435 "{cpp}"
2436 );
2437
2438 let rust = render_c_rust(&b, arm, "day-part-speech");
2439 assert!(
2440 rust.contains("fn day_bridge_day_part_speech_speak_native(text: *const u16) -> i32;"),
2441 "{rust}"
2442 );
2443 assert!(
2444 rust.contains("let mut text_w: Vec<u16> = text.encode_utf16().collect();")
2445 && rust.contains("text_w.push(0);"),
2446 "the wide string is built and NUL-terminated in Rust:\n{rust}"
2447 );
2448 }
2449
2450 #[test]
2453 fn a_c_arm_takes_no_extern_c_wrapper() {
2454 let b = parse(
2455 r###"
2456 day_bridge::bridge! {
2457 #[day_bridge::declare]
2458 extern "day" { fn f(text: &str); }
2459 #[day_bridge::impl(c, platforms = [linux])]
2460 c!(r#" void f(const char* t) {} "#);
2461 #[day_bridge::impl(rust, platforms = [other])]
2462 fn f(_text: &str) {}
2463 }
2464 "###,
2465 );
2466 let arm = b.arms.iter().find(|a| a.lang == Lang::C).unwrap();
2467 let c = render_c(&b, arm, "day-part-demo");
2468 assert!(!c.contains("extern \"C\""), "{c}");
2469 assert!(c.contains("(const char* text)"), "{c}");
2470 }
2471
2472 #[test]
2473 fn jni_descriptors_match_the_declaration() {
2474 let unit = Decl {
2475 name: "stop".into(),
2476 args: vec![],
2477 ret: String::new(),
2478 line: 1,
2479 };
2480 assert_eq!(jni_signature(&unit), "()V");
2481 let mixed = Decl {
2482 name: "f".into(),
2483 args: vec![
2484 ("a".into(), "&str".into()),
2485 ("b".into(), "i64".into()),
2486 ("c".into(), "bool".into()),
2487 ],
2488 ret: "Result<(), day_bridge::Error>".into(),
2489 line: 1,
2490 };
2491 assert_eq!(jni_signature(&mixed), "(Ljava/lang/String;JZ)V");
2493
2494 let valued = Decl {
2496 name: "level".into(),
2497 args: vec![],
2498 ret: "Result<i32, day_bridge::Error>".into(),
2499 line: 1,
2500 };
2501 assert_eq!(jni_signature(&valued), "()I");
2502 assert_eq!(result_value(&valued.ret).as_deref(), Some("i32"));
2503 }
2504
2505 #[test]
2506 fn the_cli_can_parse_a_crate_without_cargo() {
2507 let b = parse(SPEECH);
2510 let kotlin = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
2511 assert_eq!(
2512 adapter_name(kotlin, "day-part-speech"),
2513 "day-part-speech-android.kt"
2514 );
2515 }
2516}