use std::collections::BTreeMap;
pub(crate) fn is_populated(header: &str) -> bool {
header.contains("__swift_bridge__$")
}
pub(crate) fn assemble(binding_crate_name: &str, core_h: &str, crate_h: &str) -> String {
let marker = crate::core::hash::SWIFT_C_UMBRELLA_HEADER_MARKER;
format!(
"#ifndef RUST_BRIDGE_C_H\n\
#define RUST_BRIDGE_C_H\n\
\n\
{marker}\n\
// Concatenates SwiftBridgeCore.h and {binding_crate_name}.h produced by\n\
// `cargo build -p {binding_crate_name}` via swift_bridge_build.\n\
\n\
{core_h}\n\
{crate_h}\n\
#endif /* RUST_BRIDGE_C_H */\n"
)
}
pub(crate) fn resolve_fresh(
binding_crate_name: &str,
core_h: &str,
crate_h: &str,
existing: Option<&str>,
) -> anyhow::Result<String> {
let assembled = assemble(binding_crate_name, core_h, crate_h);
let committed = existing.filter(|header| is_populated(header));
let undefined = undefined_core_types(&assembled);
if !undefined.is_empty() {
let missing = undefined.join(", ");
if let Some(committed) = committed {
tracing::warn!(
"swift-bridge build output for `{binding_crate_name}` assembles an incomplete C umbrella \
header (declares __swift_bridge__$ functions using {missing}, but never defines those \
types) -- keeping the committed RustBridgeC.h instead. Re-run `cargo build -p \
{binding_crate_name}` to refresh it."
);
return Ok(committed.to_string());
}
anyhow::bail!(
"refusing to write an incomplete RustBridgeC.h: the swift-bridge build output for \
`{binding_crate_name}` declares __swift_bridge__$ functions using {missing}, but never \
defines those types, and no populated header is committed to fall back on. The \
SwiftBridgeCore.h in that crate's build directory is empty or partial -- re-run `cargo \
build -p {binding_crate_name}`"
);
}
if let Some(committed) = committed
&& declares_the_same_c(committed, &assembled)
{
return Ok(committed.to_string());
}
Ok(assembled)
}
pub(crate) fn undefined_core_types(header: &str) -> Vec<String> {
let tokens = c_tokens(header);
let defined = defined_type_names(&tokens);
let mut undefined: Vec<String> = tokens
.iter()
.filter(|token| is_core_type_name(token) && !defined.contains(token.as_str()))
.cloned()
.collect();
undefined.sort();
undefined.dedup();
undefined
}
fn is_core_type_name(token: &str) -> bool {
token == "RustStr" || token.starts_with("__private__")
}
fn defined_type_names(tokens: &[String]) -> std::collections::HashSet<&str> {
let mut defined = std::collections::HashSet::new();
for window in tokens.windows(3) {
if window[0] == "}" && window[2] == ";" {
defined.insert(window[1].as_str());
}
if window[0] == "struct" && window[2] == "{" {
defined.insert(window[1].as_str());
}
}
defined
}
pub(crate) fn declares_the_same_c(left: &str, right: &str) -> bool {
declaration_counts(left) == declaration_counts(right)
}
fn declaration_counts(header: &str) -> BTreeMap<String, usize> {
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
let mut body = String::new();
for line in strip_comments(header).lines() {
if !line.trim_start().starts_with('#') {
body.push_str(line);
body.push('\n');
continue;
}
let directive = c_tokens(line).join(" ");
if !directive.is_empty() {
*counts.entry(directive).or_insert(0) += 1;
}
}
for declaration in split_statements(&c_tokens(&body)) {
*counts.entry(declaration).or_insert(0) += 1;
}
counts
}
fn split_statements(tokens: &[String]) -> Vec<String> {
let mut statements = Vec::new();
let mut current: Vec<&str> = Vec::new();
for token in tokens {
current.push(token);
if token == ";" {
statements.push(current.join(" "));
current.clear();
}
}
if !current.is_empty() {
statements.push(current.join(" "));
}
statements
}
fn c_tokens(header: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut identifier = String::new();
for character in strip_comments(header).chars() {
if character.is_alphanumeric() || character == '_' || character == '$' {
identifier.push(character);
continue;
}
if !identifier.is_empty() {
tokens.push(std::mem::take(&mut identifier));
}
if !character.is_whitespace() {
tokens.push(character.to_string());
}
}
if !identifier.is_empty() {
tokens.push(identifier);
}
tokens
}
fn strip_comments(header: &str) -> String {
let mut out = String::with_capacity(header.len());
let mut characters = header.chars().peekable();
while let Some(character) = characters.next() {
if character != '/' {
out.push(character);
continue;
}
match characters.peek() {
Some('/') => {
for inner in characters.by_ref() {
if inner == '\n' {
break;
}
}
out.push('\n');
}
Some('*') => {
characters.next();
let mut previous = '\0';
for inner in characters.by_ref() {
if previous == '*' && inner == '/' {
break;
}
previous = inner;
}
out.push(' ');
}
_ => out.push(character),
}
}
out
}
#[cfg(test)]
mod tests;