use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Lang {
Rust,
Swift,
Kotlin,
Java,
ArkTs,
Js,
C,
Cpp,
}
impl Lang {
fn parse(s: &str) -> Option<Lang> {
Some(match s {
"rust" => Lang::Rust,
"swift" => Lang::Swift,
"kotlin" => Lang::Kotlin,
"java" => Lang::Java,
"arkts" => Lang::ArkTs,
"js" => Lang::Js,
"c" => Lang::C,
"cpp" => Lang::Cpp,
_ => return None,
})
}
pub fn key(self) -> &'static str {
match self {
Lang::Rust => "rust",
Lang::Swift => "swift",
Lang::Kotlin => "kotlin",
Lang::Java => "java",
Lang::ArkTs => "arkts",
Lang::Js => "js",
Lang::C => "c",
Lang::Cpp => "cpp",
}
}
}
const PLATFORMS: &[&str] = &[
"ios", "macos", "android", "ohos", "web", "linux", "windows", "other",
];
const ARM_OPTIONS: &[&str] = &["src", "link", "pkg_config", "encoding", "support"];
fn cfg_for(platform: &str) -> &'static str {
match platform {
"ios" => "target_os = \"ios\"",
"macos" => "target_os = \"macos\"",
"android" => "target_os = \"android\"",
"windows" => "target_os = \"windows\"",
"web" => "target_arch = \"wasm32\"",
"linux" => "all(target_os = \"linux\", not(target_env = \"ohos\"))",
"ohos" => "all(target_os = \"linux\", target_env = \"ohos\")",
_ => "",
}
}
const SCALARS: &[&str] = &["bool", "i32", "i64", "f32", "f64"];
#[derive(Clone, Debug)]
pub struct Decl {
pub name: String,
pub args: Vec<(String, String)>,
pub ret: String,
pub line: usize,
}
#[derive(Clone, Debug)]
pub struct Arm {
pub lang: Lang,
pub platforms: Vec<String>,
pub body: Option<String>,
pub prelude: Option<String>,
pub src: Option<String>,
pub options: BTreeMap<String, String>,
pub source: Option<String>,
pub line: usize,
pub body_line: usize,
}
#[derive(Default, Debug)]
pub struct Bridge {
pub decls: Vec<Decl>,
pub arms: Vec<Arm>,
}
pub fn generate() -> Result<(), String> {
let root = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR unset")?;
let out = std::env::var("OUT_DIR").map_err(|_| "OUT_DIR unset")?;
let crate_name = std::env::var("CARGO_PKG_NAME").map_err(|_| "CARGO_PKG_NAME unset")?;
generate_in(Path::new(&root), Path::new(&out), &crate_name)
}
pub fn parse_crate(root: &Path) -> Result<Bridge, String> {
let bridge = scan(root)?;
validate(&bridge)?;
Ok(bridge)
}
pub fn is_bridged(root: &Path) -> bool {
let mut sources: Vec<PathBuf> = Vec::new();
collect_rs(&root.join("src"), &mut sources);
sources.iter().any(|p| {
std::fs::read_to_string(p)
.map(|t| {
t.lines()
.any(|l| !l.trim_start().starts_with("//") && l.contains("bridge!"))
})
.unwrap_or(false)
})
}
pub fn swift_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
render_swift(bridge, arm, crate_name)
}
pub fn jvm_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
match arm.lang {
Lang::Java => render_java(arm, crate_name),
_ => render_kotlin(bridge, arm, crate_name),
}
}
pub fn js_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
render_js(bridge, arm, crate_name)
}
pub fn arkts_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
render_arkts(bridge, arm, crate_name)
}
pub fn kotlin_package_of(crate_name: &str) -> String {
kotlin_package(crate_name)
}
pub fn adapter_name(arm: &Arm, crate_name: &str) -> String {
generated_name(arm, crate_name)
}
fn scan(root: &Path) -> Result<Bridge, String> {
let mut sources: Vec<PathBuf> = Vec::new();
collect_rs(&root.join("src"), &mut sources);
sources.sort();
let mut bridge = Bridge::default();
for path in &sources {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
if !text.contains("bridge!") {
continue;
}
let rel = path
.strip_prefix(root)
.unwrap_or(path)
.display()
.to_string();
#[cfg(windows)]
let rel = rel.replace('\\', "/");
parse_into(&text, &rel, &mut bridge).map_err(|e| format!("{rel}: {e}"))?;
}
Ok(bridge)
}
pub fn generate_in(root: &Path, out_dir: &Path, crate_name: &str) -> Result<(), String> {
let mut sources: Vec<PathBuf> = Vec::new();
collect_rs(&root.join("src"), &mut sources);
sources.sort();
for path in &sources {
println!("cargo:rerun-if-changed={}", path.display());
}
let bridge = parse_crate(root)?;
let dir = out_dir.join("day-bridge");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
write_if_changed(&dir.join("mod.rs"), &render_rust(&bridge, crate_name))?;
emit_c(&bridge, &dir, crate_name)?;
Ok(())
}
fn active_platform() -> Option<String> {
let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?;
let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
Some(
match (os.as_str(), env.as_str(), arch.as_str()) {
(_, _, "wasm32") => "web",
("linux", "ohos", _) => "ohos",
("linux", _, _) => "linux",
("ios", _, _) => "ios",
("macos", _, _) => "macos",
("android", _, _) => "android",
("windows", _, _) => "windows",
_ => return None,
}
.to_string(),
)
}
fn emit_c(bridge: &Bridge, dir: &Path, crate_name: &str) -> Result<(), String> {
let active = active_platform();
println!("cargo:rustc-check-cfg=cfg({STAGED_CFG})");
println!("cargo:rerun-if-env-changed=DAY_BRIDGE_STAGED");
let staged_here = std::env::var("DAY_BRIDGE_STAGED").is_ok()
&& bridge.arms.iter().any(|a| {
staged_by_cli(a.lang)
&& active
.as_deref()
.is_some_and(|p| a.platforms.iter().any(|x| x == p))
});
if staged_here {
println!("cargo:rustc-cfg={STAGED_CFG}");
}
for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Swift) {
let file = dir.join(format!("{}-{}.swift", crate_name, arm.platforms.join("-")));
write_if_changed(&file, &render_swift(bridge, arm, crate_name))?;
}
for arm in bridge
.arms
.iter()
.filter(|a| matches!(a.lang, Lang::C | Lang::Cpp))
{
let cpp = arm.lang == Lang::Cpp;
let file = dir.join(format!(
"{}-{}.{}",
crate_name,
arm.platforms.join("-"),
if cpp { "cpp" } else { "c" }
));
write_if_changed(&file, &render_c(bridge, arm, crate_name))?;
let selected = active
.as_deref()
.is_some_and(|p| arm.platforms.iter().any(|a| a == p));
if !selected {
continue;
}
let mut build = cc::Build::new();
build.file(&file).cpp(cpp).warnings(false);
if cpp {
build.std("c++17");
}
build.compile(&format!("day_bridge_{}", crate_name.replace('-', "_")));
for lib in arm
.options
.get("link")
.map(|v| v.trim_matches(['[', ']']).to_string())
.unwrap_or_default()
.split(',')
.map(|l| l.trim().trim_matches('"'))
.filter(|l| !l.is_empty())
{
println!("cargo:rustc-link-lib={lib}");
}
if let Some(pkg) = arm.options.get("pkg_config") {
println!("cargo:rustc-link-lib={pkg}");
}
}
Ok(())
}
fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn parse_into(text: &str, source: &str, bridge: &mut Bridge) -> Result<(), String> {
let mut at = 0;
while let Some(found) = text[at..].find("bridge!") {
let start = at + found;
let line_start = text[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
if text[line_start..start].trim_start().starts_with("//") {
at = start + "bridge!".len();
continue;
}
let after = start + "bridge!".len();
let Some(brace) = text[after..]
.find(|c: char| !c.is_whitespace())
.map(|i| after + i)
else {
break;
};
if text.as_bytes().get(brace) != Some(&b'{') {
at = after;
continue;
}
let end = match_delim(text, brace, b'{', b'}')
.ok_or_else(|| "unterminated `bridge! {` block".to_string())?;
let first = bridge.arms.len();
parse_body(&text[brace + 1..end], line_of(text, brace), bridge)?;
for arm in &mut bridge.arms[first..] {
arm.source = Some(source.to_string());
}
at = end + 1;
}
Ok(())
}
fn parse_body(body: &str, base_line: usize, bridge: &mut Bridge) -> Result<(), String> {
let mut at = 0;
while let Some(found) = body[at..].find("#[day_bridge::") {
let start = at + found;
let open = start + "#[".len() - 1; let close = match_delim(body, open, b'[', b']')
.ok_or_else(|| "unterminated bridge attribute".to_string())?;
let attr = &body[start + 2..close];
let line = base_line + line_of(body, start) - 1;
let rest = &body[close + 1..];
let kind = attr
.trim_start_matches("day_bridge::")
.split(['(', ' '])
.next()
.unwrap_or("")
.trim();
let consumed = match kind {
"declare" => parse_declare(rest, line, bridge)?,
"prelude" => {
return Err(format!(
"line {line}: a standalone `prelude` attribute no longer exists — write it as \
`lang!(prelude = r#\" … \"#, body = r#\" … \"#)` on the arm it belongs to \
(docs/bridge.md \"The file\")"
));
}
"impl" => parse_impl(attr, rest, line, bridge)?,
"data" => 0, other => return Err(format!("line {line}: unknown bridge attribute `{other}`")),
};
at = close + 1 + consumed;
}
Ok(())
}
fn parse_declare(rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
let open = rest
.find('{')
.ok_or_else(|| format!("line {line}: `declare` needs an `extern \"day\" {{ … }}` block"))?;
let close = match_delim(rest, open, b'{', b'}')
.ok_or_else(|| format!("line {line}: unterminated `extern \"day\"` block"))?;
let block = strip_comments(&rest[open + 1..close]);
for raw in block.split(';') {
let sig = raw.trim();
if sig.is_empty() {
continue;
}
let sig = sig
.strip_prefix("fn ")
.ok_or_else(|| format!("line {line}: `{sig}` is not a `fn` declaration"))?;
let name_end = sig
.find('(')
.ok_or_else(|| format!("line {line}: `{sig}` has no argument list"))?;
let name = sig[..name_end].trim().to_string();
let args_end = match_delim(sig, name_end, b'(', b')')
.ok_or_else(|| format!("line {line}: `{name}` has an unterminated argument list"))?;
let mut args = Vec::new();
for arg in split_top(&sig[name_end + 1..args_end], ',') {
let arg = arg.trim();
if arg.is_empty() {
continue;
}
let (n, t) = arg
.split_once(':')
.ok_or_else(|| format!("line {line}: argument `{arg}` needs a type"))?;
args.push((n.trim().to_string(), t.trim().to_string()));
}
let ret = sig[args_end + 1..]
.trim()
.strip_prefix("->")
.map(|r| r.trim().to_string())
.unwrap_or_default();
bridge.decls.push(Decl {
name,
args,
ret,
line,
});
}
Ok(close + 1)
}
fn parse_impl(attr: &str, rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
let lang = attr_lang(attr, line)?;
let inner = attr
.split_once('(')
.map(|(_, v)| v.trim_end().trim_end_matches(')'))
.unwrap_or("");
let mut platforms = Vec::new();
let mut options = BTreeMap::new();
for part in split_top(inner, ',') {
let part = part.trim();
if part.is_empty() || Lang::parse(part).is_some() {
continue;
}
let Some((key, value)) = part.split_once('=') else {
return Err(format!("line {line}: `{part}` is not `key = value`"));
};
let key = key.trim();
let value = value.trim().trim_matches('"');
if key == "platforms" {
for p in value.trim_matches(['[', ']']).split(',') {
let p = p.trim();
if p.is_empty() {
continue;
}
if !PLATFORMS.contains(&p) {
return Err(format!(
"line {line}: unknown platform `{p}` (expected one of {})",
PLATFORMS.join(", ")
));
}
platforms.push(p.to_string());
}
} else {
if !ARM_OPTIONS.contains(&key) {
return Err(format!(
"line {line}: unknown arm option `{key}` (expected one of {})",
ARM_OPTIONS.join(", ")
));
}
if key == "encoding" && value != "utf8" && value != "utf16" {
return Err(format!(
"line {line}: `encoding = \"{value}\"` — expected \"utf8\" or \"utf16\""
));
}
if key == "support" && value != "native" && value != "emulated" {
return Err(format!(
"line {line}: `support = \"{value}\"` — expected \"native\" or \"emulated\""
));
}
options.insert(key.to_string(), value.to_string());
}
}
if platforms.is_empty() {
return Err(format!("line {line}: an arm must name `platforms = [ … ]`"));
}
let (prelude, body, consumed, body_line) = if lang == Lang::Rust {
let (body, consumed) = rust_item_after(rest, line)?;
(None, Some(body), consumed, line)
} else if options.contains_key("src") {
(None, None, 0, line)
} else {
let call = macro_call_after(rest, line)?;
(
call.prelude,
Some(call.body),
call.consumed,
line + call.body_skipped + 1,
)
};
if let Some(text) = &prelude {
for bad in ["package ", "namespace ", "module "] {
if text.lines().any(|l| l.trim_start().starts_with(bad)) {
return Err(format!(
"line {line}: a `{}` line belongs to the generator, not a prelude — daybridge \
derives it from the crate name (docs/bridge.md \"Names\")",
bad.trim()
));
}
}
}
bridge.arms.push(Arm {
lang,
platforms,
body,
prelude,
src: options.get("src").cloned(),
options,
source: None,
line,
body_line,
});
Ok(consumed)
}
fn attr_lang(attr: &str, line: usize) -> Result<Lang, String> {
let inner = attr.split_once('(').map(|(_, v)| v).unwrap_or("");
let first = inner.split([',', ')']).next().unwrap_or("").trim();
Lang::parse(first).ok_or_else(|| format!("line {line}: unknown bridge language `{first}`"))
}
struct MacroCall {
prelude: Option<String>,
body: String,
consumed: usize,
body_skipped: usize,
}
fn macro_call_after(rest: &str, line: usize) -> Result<MacroCall, String> {
let open = rest.find('(').ok_or_else(|| {
format!("line {line}: expected a language macro, e.g. `java!(r#\" … \"#)`")
})?;
let close = match_delim(rest, open, b'(', b')')
.ok_or_else(|| format!("line {line}: unterminated language macro"))?;
let mut prelude: Option<String> = None;
let mut body: Option<(String, usize)> = None;
let mut at = open + 1;
while let Some(rel) = find_raw_open(&rest[at..close]) {
let r = at + rel;
let hashes = rest[r + 1..].bytes().take_while(|b| *b == b'#').count();
let start = r + 1 + hashes + 1; let terminator = format!("\"{}", "#".repeat(hashes));
let end = rest[start..close]
.find(&terminator)
.map(|i| start + i)
.ok_or_else(|| format!("line {line}: unterminated raw string"))?;
let key = rest[at..r]
.trim()
.trim_start_matches(',')
.trim()
.trim_end_matches('=')
.trim()
.to_string();
let text = dedent(&rest[start..end]);
match key.as_str() {
"prelude" => prelude = Some(text),
"body" | "" => body = Some((text, rest[..start].matches('\n').count())),
other => {
return Err(format!(
"line {line}: unknown argument `{other}` — a language macro takes `prelude` \
and `body` (docs/bridge.md \"The file\")"
));
}
}
at = end + terminator.len();
}
let (body, body_skipped) = body.ok_or_else(|| {
format!("line {line}: expected a raw-string body, e.g. `java!(r#\" … \"#)`")
})?;
Ok(MacroCall {
prelude,
body,
consumed: close + 1,
body_skipped,
})
}
fn find_raw_open(text: &str) -> Option<usize> {
let b = text.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'r' {
let mut j = i + 1;
while j < b.len() && b[j] == b'#' {
j += 1;
}
if b.get(j) == Some(&b'"') {
return Some(i);
}
}
i += 1;
}
None
}
fn rust_item_after(rest: &str, line: usize) -> Result<(String, usize), String> {
let open = rest
.find('{')
.ok_or_else(|| format!("line {line}: expected a Rust `fn` body"))?;
let close = match_delim(rest, open, b'{', b'}')
.ok_or_else(|| format!("line {line}: unterminated Rust body"))?;
Ok((dedent_item(rest[..=close].trim()), close + 1))
}
fn match_delim(text: &str, from: usize, open: u8, close: u8) -> Option<usize> {
let b = text.as_bytes();
let mut depth = 0usize;
let mut i = from;
while i < b.len() {
match b[i] {
b'r' if raw_open_hashes(b, i).is_some() => {
let hashes = raw_open_hashes(b, i).unwrap_or(0);
let terminator: Vec<u8> = std::iter::once(b'"')
.chain(std::iter::repeat_n(b'#', hashes))
.collect();
i += 1 + hashes + 1;
while i < b.len() && !b[i..].starts_with(&terminator) {
i += 1;
}
i += terminator.len();
continue;
}
b'"' => {
i += 1;
while i < b.len() && b[i] != b'"' {
i += if b[i] == b'\\' { 2 } else { 1 };
}
}
b'/' if b.get(i + 1) == Some(&b'/') => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
}
c if c == open => depth += 1,
c if c == close => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn raw_open_hashes(b: &[u8], i: usize) -> Option<usize> {
if b.get(i) != Some(&b'r') {
return None;
}
let mut j = i + 1;
while b.get(j) == Some(&b'#') {
j += 1;
}
(b.get(j) == Some(&b'"')).then_some(j - i - 1)
}
fn split_top(text: &str, sep: char) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0i32;
let mut cur = String::new();
for c in text.chars() {
match c {
'(' | '[' | '<' | '{' => depth += 1,
')' | ']' | '>' | '}' => depth -= 1,
_ => {}
}
if c == sep && depth == 0 {
out.push(std::mem::take(&mut cur));
} else {
cur.push(c);
}
}
out.push(cur);
out
}
fn strip_comments(text: &str) -> String {
text.lines()
.map(|l| l.split_once("//").map(|(a, _)| a).unwrap_or(l))
.collect::<Vec<_>>()
.join("\n")
}
fn line_of(text: &str, at: usize) -> usize {
text[..at].matches('\n').count() + 1
}
fn dedent(body: &str) -> String {
let indent = body
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
body.lines()
.map(|l| {
if l.len() >= indent && l.is_char_boundary(indent) {
&l[indent..]
} else {
l.trim_start()
}
})
.collect::<Vec<_>>()
.join("\n")
.trim_matches('\n')
.to_string()
}
fn dedent_item(item: &str) -> String {
let mut lines = item.lines();
let Some(first) = lines.next() else {
return String::new();
};
let rest: Vec<&str> = lines.collect();
let indent = rest
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
let mut out = String::from(first);
for line in rest {
out.push('\n');
out.push_str(if line.len() >= indent && line.is_char_boundary(indent) {
&line[indent..]
} else {
line.trim_start()
});
}
out
}
fn validate(bridge: &Bridge) -> Result<(), String> {
if bridge.decls.is_empty() && bridge.arms.is_empty() {
return Ok(());
}
for decl in &bridge.decls {
for (arg, ty) in &decl.args {
check_type(ty, true)
.map_err(|e| format!("line {}: `{}`'s `{arg}`: {e}", decl.line, decl.name))?;
}
if !decl.ret.is_empty() {
let inner = decl
.ret
.strip_prefix("Result<")
.and_then(|r| r.strip_suffix('>'))
.map(|r| split_top(r, ',').first().cloned().unwrap_or_default())
.unwrap_or_else(|| decl.ret.clone());
let inner = inner.trim();
if !inner.is_empty() && inner != "()" {
check_type(inner, false)
.map_err(|e| format!("line {}: `{}`'s return: {e}", decl.line, decl.name))?;
}
}
}
for arm in &bridge.arms {
for decl in &bridge.decls {
for (arg, ty) in &decl.args {
if !implemented(arm.lang, ty, true) {
return Err(format!(
"line {}: `{}`'s `{arg}: {ty}` is in the type table but the {} generator \
does not marshal it yet (docs/bridge.md \"Types\")",
arm.line,
decl.name,
arm.lang.key()
));
}
}
let Some(ty) = result_value(&decl.ret) else {
continue;
};
if !implemented(arm.lang, &ty, false) {
return Err(match arm.lang {
Lang::C | Lang::Cpp | Lang::Swift => format!(
"line {}: `{}` returns a value, which the {} arm cannot express yet — \
return `Result<(), day_bridge::Error>` there, or split the value into \
its own function",
arm.line,
decl.name,
arm.lang.key()
),
_ => format!(
"line {}: `{}` returns `{ty}`, which the {} generator does not marshal \
yet (docs/bridge.md \"Types\")",
arm.line,
decl.name,
arm.lang.key()
),
});
}
}
}
let mut claimed: BTreeMap<&str, (Lang, usize)> = BTreeMap::new();
for arm in &bridge.arms {
for p in &arm.platforms {
match claimed.get(p.as_str()) {
Some(&(lang, first)) if lang != arm.lang => {
return Err(format!(
"line {}: platform `{p}` is already claimed by the {} arm on line {first}",
arm.line,
lang.key()
));
}
_ => {
claimed.insert(p, (arm.lang, arm.line));
}
}
}
}
if !bridge.decls.is_empty() && !claimed.contains_key("other") {
return Err(
"no `other` arm: a bridged crate must compile under day-mock on any host \
(docs/bridge.md \"Platform selection\")"
.into(),
);
}
let rust: Vec<&str> = bridge
.arms
.iter()
.filter(|a| a.lang == Lang::Rust)
.filter_map(|a| a.body.as_deref())
.collect();
if !rust.is_empty() {
for decl in &bridge.decls {
let wanted = format!("fn {}", decl.name);
if !rust.iter().any(|body| body.contains(&wanted)) {
return Err(format!(
"line {}: no rust arm implements `{}`",
decl.line, decl.name
));
}
}
}
Ok(())
}
fn implemented(lang: Lang, ty: &str, argument: bool) -> bool {
let ty = ty.trim();
if lang == Lang::Rust {
return true;
}
if argument {
return SCALARS.contains(&ty) || ty == "&str";
}
match lang {
Lang::Kotlin | Lang::Java => SCALARS.contains(&ty) || ty == "String",
Lang::Js | Lang::ArkTs => SCALARS.contains(&ty),
_ => false,
}
}
fn check_type(ty: &str, argument: bool) -> Result<(), String> {
let ty = ty.trim();
if SCALARS.contains(&ty) {
return Ok(());
}
if argument && (ty == "&str" || ty == "&[u8]") {
return Ok(());
}
if !argument && (ty == "String" || ty == "Vec<u8>") {
return Ok(());
}
if ty.starts_with("Option<") {
return Err(format!(
"`{ty}` does not cross a bridge — model absence in the value, or return `Result` \
(docs/bridge.md \"Types\")"
));
}
if ty.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
return Ok(());
}
Err(format!(
"`{ty}` is outside the v1 type table (docs/bridge.md \"Types\")"
))
}
fn symbol(crate_name: &str, decl: &Decl) -> String {
format!("day_bridge_{}_{}", crate_name.replace('-', "_"), decl.name)
}
fn c_type(ty: &str, utf16: bool) -> &'static str {
match ty.trim() {
"bool" | "i32" => "int32_t",
"i64" => "int64_t",
"f32" => "float",
"f64" => "double",
"&str" if utf16 => "const char16_t*",
"&str" => "const char*",
_ => "const void*",
}
}
fn rust_c_type(ty: &str, utf16: bool) -> &'static str {
match ty.trim() {
"bool" | "i32" => "i32",
"i64" => "i64",
"f32" => "f32",
"f64" => "f64",
"&str" if utf16 => "*const u16",
"&str" => "*const std::ffi::c_char",
_ => "*const std::ffi::c_void",
}
}
fn render_c(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"/* @generated by day-build from {source}:{} — edit the arm, never this file. */",
arm.line
);
let _ = writeln!(out, "#include <stdint.h>");
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(out, "\n#line {} {}", arm.body_line, quote(source));
let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
let _ = writeln!(out, "#line 1 {}", quote("<day-bridge adapters>"));
if arm.lang == Lang::Cpp {
let _ = writeln!(out, "extern \"C\" {{");
}
for decl in &bridge.decls {
let params: Vec<String> = decl
.args
.iter()
.map(|(n, t)| format!("{} {n}", c_type(t, utf16)))
.collect();
let names: Vec<&str> = decl.args.iter().map(|(n, _)| n.as_str()).collect();
let params = if params.is_empty() {
"void".to_string()
} else {
params.join(", ")
};
if decl.ret.is_empty() {
let _ = writeln!(
out,
"void {}({params}) {{ {}({}); }}",
symbol(crate_name, decl),
decl.name,
names.join(", ")
);
} else {
let _ = writeln!(
out,
"int32_t {}({params}) {{ return {}({}); }}",
symbol(crate_name, decl),
decl.name,
names.join(", ")
);
}
}
if arm.lang == Lang::Cpp {
let _ = writeln!(out, "}}");
}
out
}
fn render_swift(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"// @generated by day-build from {source}:{} — edit the arm, never this file.",
arm.line
);
let _ = writeln!(out, "import Foundation");
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(
out,
"\n#sourceLocation(file: {}, line: {})",
quote(source),
arm.body_line
);
let _ = writeln!(out, "{}", arm.body.as_deref().unwrap_or(""));
let _ = writeln!(out, "#sourceLocation()\n");
for decl in &bridge.decls {
let params: Vec<String> = decl
.args
.iter()
.map(|(n, t)| format!("{n}: {}", swift_abi_type(t)))
.collect();
let ret = if decl.ret.is_empty() { "" } else { " -> Int32" };
let _ = writeln!(out, "@_cdecl({})", quote(&symbol(crate_name, decl)));
let _ = writeln!(
out,
"public func {}({}){ret} {{",
symbol(crate_name, decl),
params.join(", ")
);
let mut passed: Vec<String> = Vec::new();
for (n, t) in &decl.args {
match t.trim() {
"&str" => {
let _ = writeln!(out, " let {n}_s = String(cString: {n})");
passed.push(format!("{n}: {n}_s"));
}
"bool" => {
let _ = writeln!(out, " let {n}_b = {n} != 0");
passed.push(format!("{n}: {n}_b"));
}
_ => passed.push(format!("{n}: {n}")),
}
}
let call = format!("{}({})", decl.name, passed.join(", "));
if decl.ret.is_empty() {
let _ = writeln!(out, " {call}");
} else {
let _ = writeln!(out, " do {{");
let _ = writeln!(out, " try {call}");
let _ = writeln!(out, " return 0");
let _ = writeln!(out, " }} catch {{");
let _ = writeln!(
out,
" FileHandle.standardError.write(\"day-bridge: \\(error)\\n\".data(using: .utf8)!)"
);
let _ = writeln!(out, " return 1");
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, "}}\n");
}
out
}
fn render_js(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
//# sourceURL={source}",
arm.line
);
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
let _ = writeln!(
out,
"// The shim calls this once at boot and spreads the result into the wasm import object."
);
let _ = writeln!(out, "export function register(rt) {{");
let _ = writeln!(out, " return {{");
for decl in &bridge.decls {
let mut params: Vec<String> = Vec::new();
let mut passed: Vec<String> = Vec::new();
for (n, t) in &decl.args {
if t.trim() == "&str" {
params.push(format!("{n}_ptr"));
params.push(format!("{n}_len"));
passed.push(format!("rt.str({n}_ptr, {n}_len)"));
} else {
params.push(n.clone());
passed.push(n.clone());
}
}
let call = format!("{}({})", decl.name, passed.join(", "));
let _ = writeln!(
out,
" {}({}) {{",
symbol(crate_name, decl),
params.join(", ")
);
match (decl.ret.is_empty(), result_value(&decl.ret)) {
(true, _) => {
let _ = writeln!(out, " {call};");
}
(false, None) => {
let _ = writeln!(out, " try {{");
let _ = writeln!(out, " {call};");
let _ = writeln!(out, " return 0;");
let _ = writeln!(out, " }} catch (e) {{");
let _ = writeln!(
out,
" console.error('day-bridge: {}', e);",
decl.name
);
let _ = writeln!(out, " return 1;");
let _ = writeln!(out, " }}");
}
(false, Some(_)) => {
let _ = writeln!(out, " return {call};");
}
}
let _ = writeln!(out, " }},");
}
let _ = writeln!(out, " }};");
let _ = writeln!(out, "}}");
out
}
fn render_arkts(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"// @generated by day-build from {source}:{} — edit the arm, never this file.",
arm.line
);
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
let _ = writeln!(
out,
"// The host calls this once at startup; the returned record is registered with the napi\n\
// module so the Rust side can reach each arm by name."
);
let _ = writeln!(
out,
"export function register(): Record<string, Function> {{"
);
let _ = writeln!(out, " return {{");
for decl in &bridge.decls {
let _ = writeln!(out, " '{}': {},", symbol(crate_name, decl), decl.name);
}
let _ = writeln!(out, " }};");
let _ = writeln!(out, "}}");
out
}
fn render_js_rust(bridge: &Bridge, crate_name: &str) -> String {
let mut out = String::new();
let _ = writeln!(out, "#[link(wasm_import_module = \"env\")]");
let _ = writeln!(out, "unsafe extern \"C\" {{");
for decl in &bridge.decls {
let mut params: Vec<String> = Vec::new();
for (n, t) in &decl.args {
if t.trim() == "&str" {
params.push(format!("{n}_ptr: *const u8"));
params.push(format!("{n}_len: usize"));
} else {
params.push(format!("{n}: {}", rust_c_type(t, false)));
}
}
let ret = match (decl.ret.is_empty(), result_value(&decl.ret)) {
(true, _) => String::new(),
(false, None) => " -> i32".to_string(),
(false, Some(ty)) => format!(" -> {ty}"),
};
let _ = writeln!(
out,
" fn {}({}){ret};",
symbol(crate_name, decl),
params.join(", ")
);
}
let _ = writeln!(out, "}}\n");
for decl in &bridge.decls {
let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
let ret = if decl.ret.is_empty() {
String::new()
} else {
format!(" -> {}", decl.ret)
};
let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
let mut passed: Vec<String> = Vec::new();
for (n, t) in &decl.args {
if t.trim() == "&str" {
passed.push(format!("{n}.as_ptr()"));
passed.push(format!("{n}.len()"));
} else if t.trim() == "bool" {
passed.push(format!("{n} as i32"));
} else {
passed.push(n.clone());
}
}
let call = format!(
"unsafe {{ {}({}) }}",
symbol(crate_name, decl),
passed.join(", ")
);
match (decl.ret.is_empty(), result_value(&decl.ret)) {
(true, _) => {
let _ = writeln!(out, " {call};");
}
(false, None) => {
let _ = writeln!(out, " if {call} == 0 {{");
let _ = writeln!(out, " Ok(())");
let _ = writeln!(out, " }} else {{");
let _ = writeln!(
out,
" Err(day_bridge::Error::Foreign(\"{}\".into()))",
decl.name
);
let _ = writeln!(out, " }}");
}
(false, Some(_)) => {
let _ = writeln!(out, " Ok({call})");
}
}
let _ = writeln!(out, "}}\n");
}
out
}
fn render_kotlin(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let pkg = kotlin_package(crate_name);
let object = kotlin_object(crate_name);
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
// Kotlin carries no line directive: an error below is at {source}:{} plus the offset.",
arm.line, arm.body_line
);
let _ = writeln!(out, "package {pkg}\n");
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
let _ = writeln!(out, "object {object} {{");
for decl in &bridge.decls {
let params: Vec<String> = decl
.args
.iter()
.map(|(n, t)| format!("{n}: {}", kotlin_type(t)))
.collect();
let call = format!(
"{pkg}.{}({})",
decl.name,
decl.args
.iter()
.map(|(n, _)| format!("{n} = {n}"))
.collect::<Vec<_>>()
.join(", ")
);
let value = result_value(&decl.ret);
let ret = match value.as_deref() {
None => String::new(),
Some(ty) => format!(": {}", kotlin_type(ty)),
};
let _ = writeln!(out, " @JvmStatic");
let _ = writeln!(out, " fun {}({}){ret} {{", decl.name, params.join(", "));
if value.is_some() {
let _ = writeln!(out, " return {call}");
} else {
let _ = writeln!(out, " {call}");
}
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, "}}");
out
}
fn render_java(arm: &Arm, crate_name: &str) -> String {
let pkg = kotlin_package(crate_name);
let class = kotlin_object(crate_name);
let source = arm.source.as_deref().unwrap_or("src/lib.rs");
let mut out = String::new();
let _ = writeln!(
out,
"// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
// Java carries no line directive: an error below is at {source}:{} plus the offset.",
arm.line, arm.body_line
);
let _ = writeln!(out, "package {pkg};\n");
if let Some(prelude) = &arm.prelude {
let _ = writeln!(out, "{}", prelude);
}
let _ = writeln!(out, "\npublic final class {class} {{");
let _ = writeln!(out, " private {class}() {{}}\n");
for line in arm.body.as_deref().unwrap_or("").lines() {
if line.trim().is_empty() {
let _ = writeln!(out);
} else {
let _ = writeln!(out, " {line}");
}
}
let _ = writeln!(out, "}}");
out
}
fn render_jvm_rust(bridge: &Bridge, crate_name: &str) -> String {
let class = kotlin_package(crate_name).replace('.', "/") + "/" + &kotlin_object(crate_name);
let mut out = String::new();
for decl in &bridge.decls {
let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
let ret = if decl.ret.is_empty() {
String::new()
} else {
format!(" -> {}", decl.ret)
};
let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
let _ = writeln!(out, " use day_android::{{DayEnv, with_env}};");
let _ = writeln!(out, " if !day_android::vm_ready() {{");
let _ = writeln!(
out,
" return{};",
if decl.ret.is_empty() {
String::new()
} else {
" Err(day_bridge::Error::Runtime)".to_string()
}
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, " let called = with_env(|env| {{");
let mut jvalues: Vec<String> = Vec::new();
for (n, t) in &decl.args {
match t.trim() {
"&str" => {
let _ = writeln!(out, " let {n}_j = env.new_string({n}).ok()?;");
jvalues.push(format!("(&{n}_j).into()"));
}
"bool" => jvalues.push(format!(
"day_android::jni::objects::JValue::Bool({n} as u8)"
)),
"i32" => jvalues.push(format!("day_android::jni::objects::JValue::Int({n})")),
"i64" => jvalues.push(format!("day_android::jni::objects::JValue::Long({n})")),
"f32" => jvalues.push(format!("day_android::jni::objects::JValue::Float({n})")),
"f64" => jvalues.push(format!("day_android::jni::objects::JValue::Double({n})")),
_ => jvalues.push(n.clone()),
}
}
let _ = writeln!(
out,
" let outcome = env.dcall_static({}, {}, {}, &[{}]);",
quote(&class),
quote(&decl.name),
quote(&jni_signature(decl)),
jvalues.join(", ")
);
let _ = writeln!(out, " if env.exception_check() {{");
let _ = writeln!(out, " env.exception_describe(); // → logcat");
let _ = writeln!(out, " env.exception_clear();");
let _ = writeln!(out, " }}");
match (decl.ret.is_empty(), result_value(&decl.ret)) {
(true, _) => {
let _ = writeln!(out, " outcome.ok()?;");
let _ = writeln!(out, " Some(())");
let _ = writeln!(out, " }});");
let _ = writeln!(out, " let _ = called;");
}
(false, None) => {
let _ = writeln!(out, " outcome.ok()?;");
let _ = writeln!(out, " Some(())");
let _ = writeln!(out, " }});");
let _ = writeln!(
out,
" // A Java exception fails `dcall_static`, so a throwing"
);
let _ = writeln!(out, " // arm arrives here as `None`.");
let _ = writeln!(out, " match called {{");
let _ = writeln!(out, " Some(()) => Ok(()),");
let _ = writeln!(
out,
" None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
decl.name
);
let _ = writeln!(out, " }}");
}
(false, Some(ty)) => {
if ty == "String" {
let _ = writeln!(out, " let obj = outcome.ok()?.l().ok()?;");
let _ = writeln!(out, " if obj.is_null() {{");
let _ = writeln!(out, " return Some(String::new());");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " env.dstr(&day_android::as_jstring(obj)).ok()");
} else {
let _ = writeln!(out, " outcome.ok()?.{}().ok()", jvalue_accessor(&ty));
}
let _ = writeln!(out, " }});");
let _ = writeln!(
out,
" // A Java exception fails `dcall_static`, so a throwing"
);
let _ = writeln!(out, " // arm arrives here as `None`.");
let _ = writeln!(out, " match called {{");
let _ = writeln!(out, " Some(v) => Ok(v),");
let _ = writeln!(
out,
" None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
decl.name
);
let _ = writeln!(out, " }}");
}
}
let _ = writeln!(out, "}}\n");
}
out
}
fn result_value(ret: &str) -> Option<String> {
let inner = ret
.trim()
.strip_prefix("Result<")
.and_then(|r| r.strip_suffix('>'))?;
let value = split_top(inner, ',').first()?.trim().to_string();
(!value.is_empty() && value != "()").then_some(value)
}
fn jvalue_accessor(ty: &str) -> &'static str {
match ty.trim() {
"bool" => "z",
"i32" => "i",
"i64" => "j",
"f32" => "f",
"f64" => "d",
_ => "i",
}
}
fn jni_signature(decl: &Decl) -> String {
let args: String = decl
.args
.iter()
.map(|(_, t)| match t.trim() {
"bool" => "Z",
"i32" => "I",
"i64" => "J",
"f32" => "F",
"f64" => "D",
"&str" => "Ljava/lang/String;",
_ => "Ljava/lang/Object;",
})
.collect();
let ret = match result_value(&decl.ret).as_deref() {
None => "V",
Some("bool") => "Z",
Some("i32") => "I",
Some("i64") => "J",
Some("f32") => "F",
Some("f64") => "D",
Some("String") => "Ljava/lang/String;",
Some(_) => "Ljava/lang/Object;",
};
format!("({args}){ret}")
}
fn kotlin_type(ty: &str) -> &'static str {
match ty.trim() {
"bool" => "Boolean",
"i32" => "Int",
"i64" => "Long",
"f32" => "Float",
"f64" => "Double",
"&str" | "String" => "String",
_ => "Any",
}
}
fn kotlin_package(crate_name: &str) -> String {
format!("dev.daybrite.day.bridge.{}", crate_name.replace('-', "_"))
}
fn kotlin_object(crate_name: &str) -> String {
let mut out = String::new();
for part in crate_name.split('-') {
let mut chars = part.chars();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
out.push_str(chars.as_str());
}
}
format!("{out}Bridge")
}
fn swift_abi_type(ty: &str) -> &'static str {
match ty.trim() {
"bool" | "i32" => "Int32",
"i64" => "Int64",
"f32" => "Float",
"f64" => "Double",
"&str" => "UnsafePointer<CChar>",
_ => "UnsafeRawPointer",
}
}
fn render_c_rust(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
let mut out = String::new();
let _ = writeln!(out, "unsafe extern \"C\" {{");
for decl in &bridge.decls {
let args: Vec<String> = decl
.args
.iter()
.map(|(n, t)| format!("{n}: {}", rust_c_type(t, utf16)))
.collect();
let ret = if decl.ret.is_empty() { "" } else { " -> i32" };
let _ = writeln!(
out,
" fn {}({}){ret};",
symbol(crate_name, decl),
args.join(", ")
);
}
let _ = writeln!(out, "}}\n");
for decl in &bridge.decls {
let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
let ret = if decl.ret.is_empty() {
String::new()
} else {
format!(" -> {}", decl.ret)
};
let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
let mut passed: Vec<String> = Vec::new();
for (n, t) in &decl.args {
match t.trim() {
"&str" if utf16 => {
let _ = writeln!(
out,
" let mut {n}_w: Vec<u16> = {n}.encode_utf16().collect();"
);
let _ = writeln!(out, " {n}_w.push(0);");
passed.push(format!("{n}_w.as_ptr()"));
}
"&str" => {
let _ = writeln!(
out,
" let Ok({n}_c) = std::ffi::CString::new({n}) else {{"
);
let _ = writeln!(
out,
" return {};",
if decl.ret.is_empty() {
"".to_string()
} else {
"Err(day_bridge::Error::Encoding)".to_string()
}
);
let _ = writeln!(out, " }};");
passed.push(format!("{n}_c.as_ptr()"));
}
"bool" => passed.push(format!("{n} as i32")),
_ => passed.push(n.clone()),
}
}
let call = format!(
"unsafe {{ {}({}) }}",
symbol(crate_name, decl),
passed.join(", ")
);
if decl.ret.is_empty() {
let _ = writeln!(out, " {call};");
} else {
let _ = writeln!(out, " if {call} == 0 {{");
let _ = writeln!(out, " Ok(())");
let _ = writeln!(out, " }} else {{");
let _ = writeln!(
out,
" Err(day_bridge::Error::Foreign(\"{} failed\".into()))",
decl.name
);
let _ = writeln!(out, " }}");
}
let _ = writeln!(out, "}}\n");
}
out
}
fn staged_by_cli(lang: Lang) -> bool {
matches!(
lang,
Lang::Swift | Lang::Kotlin | Lang::Java | Lang::ArkTs | Lang::Js
)
}
const STAGED_CFG: &str = "day_bridge_staged";
fn arm_cfg(arm: &Arm, bridge: &Bridge) -> String {
if arm.platforms.iter().any(|p| p == "other") {
let mut claimed: Vec<&str> = bridge
.arms
.iter()
.flat_map(|a| a.platforms.iter())
.filter(|p| p.as_str() != "other")
.map(|p| cfg_for(p))
.collect();
claimed.sort_unstable();
claimed.dedup();
return format!("not(any({}))", claimed.join(", "));
}
let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
list.sort_unstable();
list.dedup();
let platform = if list.len() == 1 {
list[0].to_string()
} else {
format!("any({})", list.join(", "))
};
if staged_by_cli(arm.lang) {
format!("all({platform}, {STAGED_CFG})")
} else {
platform
}
}
fn unstaged_cfg(arm: &Arm) -> String {
let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
list.sort_unstable();
list.dedup();
let platform = if list.len() == 1 {
list[0].to_string()
} else {
format!("any({})", list.join(", "))
};
format!("all({platform}, not({STAGED_CFG}))")
}
fn cfg_attr(pred: &str) -> Option<String> {
(pred != "not(any())").then(|| format!("#[cfg({pred})]"))
}
fn render_rust(bridge: &Bridge, crate_name: &str) -> String {
let mut out = String::from(
"// @generated by day-build from this crate's `day_bridge::bridge!` block.\n\
// Edit the arms in the crate source, never this file (docs/bridge.md).\n\n",
);
if bridge.decls.is_empty() {
return out;
}
for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Rust) {
if let Some(cfg) = cfg_attr(&arm_cfg(arm, bridge)) {
let _ = writeln!(out, "{cfg}");
}
let _ = writeln!(out, "#[allow(dead_code)]");
let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
}
let fallback: Vec<&Arm> = bridge
.arms
.iter()
.filter(|a| a.lang == Lang::Rust && a.platforms.iter().any(|p| p == "other"))
.collect();
for arm in bridge.arms.iter().filter(|a| staged_by_cli(a.lang)) {
for fb in &fallback {
let _ = writeln!(out, "#[cfg({})]", unstaged_cfg(arm));
let _ = writeln!(out, "#[allow(dead_code)]");
let _ = writeln!(out, "{}\n", fb.body.as_deref().unwrap_or(""));
}
}
for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Js) {
let block = render_js_rust(bridge, crate_name);
let cfg = arm_cfg(arm, bridge);
for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
let _ = writeln!(
out,
"#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
item.trim_end()
);
}
}
for arm in bridge
.arms
.iter()
.filter(|a| matches!(a.lang, Lang::Kotlin | Lang::Java))
{
let block = render_jvm_rust(bridge, crate_name);
let cfg = arm_cfg(arm, bridge);
for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
let _ = writeln!(
out,
"#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
item.trim_end()
);
}
}
for arm in bridge
.arms
.iter()
.filter(|a| matches!(a.lang, Lang::C | Lang::Cpp | Lang::Swift))
{
let block = render_c_rust(bridge, arm, crate_name);
match cfg_attr(&arm_cfg(arm, bridge)) {
Some(cfg) => {
for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
let _ = writeln!(out, "{cfg}\n#[allow(dead_code)]\n{}\n", item.trim_end());
}
}
None => {
let _ = writeln!(out, "{block}");
}
}
}
let mut levels: Vec<(String, &'static str)> = Vec::new();
for arm in &bridge.arms {
let support = if arm.lang == Lang::Rust && arm.platforms.iter().any(|p| p == "other") {
"Unsupported"
} else if arm.options.get("support").map(String::as_str) == Some("emulated") {
"Emulated"
} else {
"Native"
};
let cfg = arm_cfg(arm, bridge);
if !levels.iter().any(|(seen, _)| seen == &cfg) {
levels.push((cfg, support));
}
if staged_by_cli(arm.lang) {
let cfg = unstaged_cfg(arm);
if !levels.iter().any(|(seen, _)| seen == &cfg) {
levels.push((cfg, "Unsupported"));
}
}
}
for decl in &bridge.decls {
for (cfg, support) in &levels {
if let Some(attr) = cfg_attr(cfg) {
let _ = writeln!(out, "{attr}");
}
let _ = writeln!(
out,
"#[allow(dead_code)]\npub(crate) fn {}_support() -> day_bridge::Support {{\n \
day_bridge::Support::{support}\n}}\n",
decl.name
);
}
}
out
}
fn generated_name(arm: &Arm, crate_name: &str) -> String {
if arm.lang == Lang::Java {
return format!("{}.java", kotlin_object(crate_name));
}
let ext = match arm.lang {
Lang::Swift => "swift",
Lang::Kotlin => "kt",
Lang::Java => "java",
Lang::ArkTs => "ets",
Lang::Js => "js",
Lang::Cpp => "cpp",
Lang::C => "c",
Lang::Rust => "rs",
};
format!("{crate_name}-{}.{ext}", arm.platforms.join("-"))
}
fn quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn write_if_changed(path: &Path, content: &str) -> Result<(), String> {
if std::fs::read(path).is_ok_and(|cur| cur == content.as_bytes()) {
return Ok(());
}
std::fs::write(path, content).map_err(|e| format!("{}: {e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
const SPEECH: &str = r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" {
fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
fn stop_native();
}
#[day_bridge::impl(kotlin, platforms = [android])]
kotlin!(
prelude = r#"
import android.speech.tts.TextToSpeech
"#,
body = r#"
fun speak_native(text: String) { engine?.speak(text) }
"#,
);
#[day_bridge::impl(rust, platforms = [other])]
fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
Err(day_bridge::Error::Unsupported)
}
#[day_bridge::impl(rust, platforms = [other])]
fn stop_native() {}
}
"###;
fn parse(src: &str) -> Bridge {
let mut b = Bridge::default();
parse_into(src, "src/lib.rs", &mut b).expect("parse");
b
}
fn parse_err(src: &str) -> String {
let mut b = Bridge::default();
parse_into(src, "src/lib.rs", &mut b).expect_err("should not parse")
}
#[test]
fn parses_declarations_and_arms() {
let b = parse(SPEECH);
assert_eq!(b.decls.len(), 2);
assert_eq!(b.decls[0].name, "speak_native");
assert_eq!(b.decls[0].args, vec![("text".into(), "&str".into())]);
assert_eq!(b.decls[0].ret, "Result<(), day_bridge::Error>");
assert_eq!(b.decls[1].name, "stop_native");
assert!(b.decls[1].args.is_empty());
assert_eq!(b.arms.len(), 3);
assert_eq!(
b.arms[0].prelude.as_deref(),
Some("import android.speech.tts.TextToSpeech"),
"the prelude belongs to the arm that declared it"
);
assert_eq!(b.arms[0].lang, Lang::Kotlin);
assert_eq!(b.arms[0].platforms, vec!["android".to_string()]);
assert!(
b.arms[0]
.body
.as_deref()
.unwrap()
.starts_with("fun speak_native")
);
}
#[test]
fn validates_and_renders_the_rust_arm() {
let b = parse(SPEECH);
validate(&b).expect("valid");
let rust = render_rust(&b, "day-part-speech");
assert!(
rust.contains("#[cfg(not(any(target_os = \"android\")))]"),
"{rust}"
);
assert!(rust.contains("fn speak_native(_text: &str)"));
assert!(rust.contains("pub(crate) fn speak_native_support()"));
assert!(
rust.contains("Support::Native"),
"the android arm reports Native"
);
assert!(
rust.contains("Support::Unsupported"),
"the fallback reports Unsupported"
);
}
#[test]
fn rejects_a_type_outside_the_table() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn f(x: Option<i32>); }
#[day_bridge::impl(rust, platforms = [other])]
fn f(_x: Option<i32>) {}
}
"###,
);
let err = validate(&b).unwrap_err();
assert!(err.contains("does not cross a bridge"), "{err}");
}
#[test]
fn rejects_a_missing_fallback() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn f(); }
#[day_bridge::impl(kotlin, platforms = [android])]
kotlin!(r#" fun f() {} "#);
}
"###,
);
assert!(validate(&b).unwrap_err().contains("no `other` arm"));
}
#[test]
fn rejects_two_arms_claiming_one_platform() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn f(); }
#[day_bridge::impl(kotlin, platforms = [android])]
kotlin!(r#" fun f() {} "#);
#[day_bridge::impl(js, platforms = [android])]
js!(r#" export function f() {} "#);
#[day_bridge::impl(rust, platforms = [other])]
fn f() {}
}
"###,
);
assert!(validate(&b).unwrap_err().contains("already claimed"));
}
#[test]
fn a_body_containing_a_quote_hash_needs_more_hashes() {
let b = parse(
r####"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn focus_native(); }
#[day_bridge::impl(js, platforms = [web])]
js!(r##"
export function focus_native() { document.querySelector("#speech").focus(); }
"##);
#[day_bridge::impl(rust, platforms = [other])]
fn focus_native() {}
}
"####,
);
let arm = b.arms.iter().find(|a| a.lang == Lang::Js).unwrap();
let body = arm.body.as_deref().unwrap();
assert!(
body.contains("querySelector(\"#speech\")") && body.ends_with("}"),
"the whole body survives:\n{body}"
);
assert!(b.arms.iter().any(|a| a.lang == Lang::Rust), "{:?}", b.arms);
}
#[test]
fn rejects_unknown_arm_options_and_values() {
let bad_key = parse_err(
r###"
day_bridge::bridge! {
#[day_bridge::impl(c, platforms = [linux], linkk = ["speechd"])]
c!(r#" void f(void) {} "#);
}
"###,
);
assert!(bad_key.contains("unknown arm option `linkk`"), "{bad_key}");
let bad_encoding = parse_err(
r###"
day_bridge::bridge! {
#[day_bridge::impl(cpp, platforms = [windows], encoding = "utf-16")]
cpp!(r#" void f(void) {} "#);
}
"###,
);
assert!(
bad_encoding.contains("expected \"utf8\" or \"utf16\""),
"{bad_encoding}"
);
let bad_support = parse_err(
r###"
day_bridge::bridge! {
#[day_bridge::impl(arkts, platforms = [ohos], support = "partial")]
arkts!(r#" export function f() {} "#);
}
"###,
);
assert!(
bad_support.contains("expected \"native\" or \"emulated\""),
"{bad_support}"
);
}
#[test]
fn rejects_a_package_line_in_a_prelude() {
let err = parse_err(
r###"
day_bridge::bridge! {
#[day_bridge::impl(kotlin, platforms = [android])]
kotlin!(
prelude = r#"
package dev.example.mine
"#,
body = r#"
fun f() {}
"#,
);
}
"###,
);
assert!(err.contains("belongs to the generator"), "{err}");
}
#[test]
fn a_prelude_reaches_only_its_own_arm() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn f(); }
#[day_bridge::impl(c, platforms = [linux])]
c!(
prelude = r#"
#include <linux_only.h>
"#,
body = r#" void f(void) {} "#,
);
#[day_bridge::impl(c, platforms = [windows])]
c!(
prelude = r#"
#include <windows.h>
"#,
body = r#" void f(void) {} "#,
);
#[day_bridge::impl(rust, platforms = [other])]
fn f() {}
}
"###,
);
let windows = b
.arms
.iter()
.find(|a| a.platforms.iter().any(|p| p == "windows"))
.unwrap();
let c = render_c(&b, windows, "day-part-demo");
assert!(c.contains("#include <windows.h>"), "{c}");
assert!(
!c.contains("linux_only.h"),
"no leak from the other arm:\n{c}"
);
}
#[test]
fn a_standalone_prelude_attribute_says_what_replaced_it() {
let err = parse_err(
r###"
day_bridge::bridge! {
#[day_bridge::prelude(swift)]
swift!(r#" import AVFoundation "#);
}
"###,
);
assert!(err.contains("no longer exists"), "{err}");
assert!(err.contains("prelude = r#"), "{err}");
}
#[test]
fn renders_the_kotlin_adapter_and_its_jni_side() {
let b = parse(SPEECH);
let arm = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
let kt = render_kotlin(&b, arm, "day-part-speech");
assert!(
kt.contains("package dev.daybrite.day.bridge.day_part_speech"),
"{kt}"
);
assert!(
kt.contains("import android.speech.tts.TextToSpeech"),
"prelude hoisted:\n{kt}"
);
assert!(kt.contains("object DayPartSpeechBridge {"), "{kt}");
assert!(
kt.contains("dev.daybrite.day.bridge.day_part_speech.speak_native(text = text)"),
"the declared name is used verbatim, not camel-cased:\n{kt}"
);
assert!(
!kt.contains("catch ("),
"the arm's exceptions cross as-is:\n{kt}"
);
let rust = render_jvm_rust(&b, "day-part-speech");
assert!(
rust.contains(
"env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
),
"{rust}"
);
assert!(
rust.contains("let text_j = env.new_string(text).ok()?;"),
"{rust}"
);
assert!(
rust.contains("Err(day_bridge::Error::Foreign(\"speak_native\".into()))"),
"a failed call becomes Foreign:\n{rust}"
);
}
#[test]
fn renders_the_java_adapter_the_jvm_side_shares() {
let b = parse(&SPEECH.replace("kotlin", "java"));
let arm = b.arms.iter().find(|a| a.lang == Lang::Java).unwrap();
let java = render_java(arm, "day-part-speech");
assert!(
java.contains("package dev.daybrite.day.bridge.day_part_speech;"),
"{java}"
);
assert!(
java.contains("import android.speech.tts.TextToSpeech"),
"prelude hoisted:\n{java}"
);
assert!(
java.contains("public final class DayPartSpeechBridge {"),
"{java}"
);
assert!(
java.contains("speak_native"),
"the declared name is used verbatim:\n{java}"
);
assert_eq!(
adapter_name(arm, "day-part-speech"),
"DayPartSpeechBridge.java"
);
let rust = render_jvm_rust(&b, "day-part-speech");
assert!(
rust.contains(
"env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
),
"{rust}"
);
}
#[test]
fn a_cpp_arm_exports_unmangled_utf16_adapters() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" {
fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
fn stop_native();
}
#[day_bridge::impl(cpp, platforms = [windows], encoding = "utf16", link = ["ole32", "sapi"])]
cpp!(r#" int32_t speak_native(const char16_t* t) { return 0; } "#);
#[day_bridge::impl(rust, platforms = [other])]
fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
Err(day_bridge::Error::Unsupported)
}
#[day_bridge::impl(rust, platforms = [other])]
fn stop_native() {}
}
"###,
);
validate(&b).expect("valid");
let arm = b.arms.iter().find(|a| a.lang == Lang::Cpp).unwrap();
let cpp = render_c(&b, arm, "day-part-speech");
assert!(cpp.contains("extern \"C\" {"), "{cpp}");
assert!(
cpp.contains(
"int32_t day_bridge_day_part_speech_speak_native(const char16_t* text) { return speak_native(text); }"
),
"{cpp}"
);
let rust = render_c_rust(&b, arm, "day-part-speech");
assert!(
rust.contains("fn day_bridge_day_part_speech_speak_native(text: *const u16) -> i32;"),
"{rust}"
);
assert!(
rust.contains("let mut text_w: Vec<u16> = text.encode_utf16().collect();")
&& rust.contains("text_w.push(0);"),
"the wide string is built and NUL-terminated in Rust:\n{rust}"
);
}
#[test]
fn a_c_arm_takes_no_extern_c_wrapper() {
let b = parse(
r###"
day_bridge::bridge! {
#[day_bridge::declare]
extern "day" { fn f(text: &str); }
#[day_bridge::impl(c, platforms = [linux])]
c!(r#" void f(const char* t) {} "#);
#[day_bridge::impl(rust, platforms = [other])]
fn f(_text: &str) {}
}
"###,
);
let arm = b.arms.iter().find(|a| a.lang == Lang::C).unwrap();
let c = render_c(&b, arm, "day-part-demo");
assert!(!c.contains("extern \"C\""), "{c}");
assert!(c.contains("(const char* text)"), "{c}");
}
#[test]
fn jni_descriptors_match_the_declaration() {
let unit = Decl {
name: "stop".into(),
args: vec![],
ret: String::new(),
line: 1,
};
assert_eq!(jni_signature(&unit), "()V");
let mixed = Decl {
name: "f".into(),
args: vec![
("a".into(), "&str".into()),
("b".into(), "i64".into()),
("c".into(), "bool".into()),
],
ret: "Result<(), day_bridge::Error>".into(),
line: 1,
};
assert_eq!(jni_signature(&mixed), "(Ljava/lang/String;JZ)V");
let valued = Decl {
name: "level".into(),
args: vec![],
ret: "Result<i32, day_bridge::Error>".into(),
line: 1,
};
assert_eq!(jni_signature(&valued), "()I");
assert_eq!(result_value(&valued.ret).as_deref(), Some("i32"));
}
#[test]
fn the_cli_can_parse_a_crate_without_cargo() {
let b = parse(SPEECH);
let kotlin = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
assert_eq!(
adapter_name(kotlin, "day-part-speech"),
"day-part-speech-android.kt"
);
}
}