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