42d808732b
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
279 lines
8.7 KiB
Rust
279 lines
8.7 KiB
Rust
//! The checked-in C header matches the exported functions, their parameter and return
|
|
//! types, and the constants. Evidence: `context/test-evidence/zset-ffi.md`.
|
|
|
|
use std::collections::BTreeMap;
|
|
use zset_ffi::ZS_ABI_VERSION;
|
|
use zset_ffi::engine::Code;
|
|
|
|
const SRC: &str = include_str!("../src/lib.rs");
|
|
const HEADER: &str = include_str!("../include/zset_ffi.h");
|
|
|
|
fn squash(s: &str) -> String {
|
|
s.split_whitespace().collect()
|
|
}
|
|
|
|
/// Splits at the commas that are not inside brackets.
|
|
fn split_params(params: &str) -> Vec<String> {
|
|
let (mut depth, mut cur, mut out) = (0i32, String::new(), Vec::new());
|
|
for c in params.chars() {
|
|
match c {
|
|
'(' | '<' => depth += 1,
|
|
')' | '>' => depth -= 1,
|
|
',' if depth == 0 => {
|
|
out.push(std::mem::take(&mut cur));
|
|
continue;
|
|
}
|
|
_ => {}
|
|
}
|
|
cur.push(c);
|
|
}
|
|
if !cur.trim().is_empty() {
|
|
out.push(cur);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The C type a Rust ABI type must be declared as. Anything unlisted fails the test, so a
|
|
/// new type needs a line here and a conscious choice.
|
|
fn c_type(rust: &str) -> String {
|
|
let t = squash(rust);
|
|
let c = match t.as_str() {
|
|
"*mut*mutZsZSet" => "ZsZSet **",
|
|
"*mutZsZSet" => "ZsZSet *",
|
|
"*constZsZSet" => "const ZsZSet *",
|
|
"*mutZsCircuit" => "ZsCircuit *",
|
|
"*mut*mutZsCircuit" => "ZsCircuit **",
|
|
"*mutZsRebuild" => "ZsRebuild *",
|
|
"*mut*mutZsRebuild" => "ZsRebuild **",
|
|
"*constu8" => "const uint8_t *",
|
|
"*mutu8" => "uint8_t *",
|
|
"*mutc_void" => "void *",
|
|
"*mutu32" => "uint32_t *",
|
|
"*muti64" => "int64_t *",
|
|
"*mutusize" => "size_t *",
|
|
"*muti32" => "int32_t *",
|
|
"*constc_char" => "const char *",
|
|
"usize" => "size_t",
|
|
"i64" => "int64_t",
|
|
"u32" => "uint32_t",
|
|
"i32" => "int32_t",
|
|
"Option<KeepFn>" => "ZsKeepFn",
|
|
"Option<FlatMapFn>" => "ZsFlatMapFn",
|
|
"Option<ExpiredFn>" => "ZsExpiredFn",
|
|
"Emit" => "ZsEmit",
|
|
other => panic!("no C type is declared for the Rust type `{other}`"),
|
|
};
|
|
squash(c)
|
|
}
|
|
|
|
/// `name -> (return type, parameter types)` from the Rust source.
|
|
fn exported() -> BTreeMap<String, (String, Vec<String>)> {
|
|
let mut out = BTreeMap::new();
|
|
let fns = SRC
|
|
.split("extern \"C\" fn ")
|
|
.skip(1)
|
|
.filter(|p| !p.starts_with('$'));
|
|
for part in fns {
|
|
let name = part.split('(').next().unwrap().to_string();
|
|
let rest = &part[name.len() + 1..];
|
|
let mut depth = 1;
|
|
let end = rest
|
|
.char_indices()
|
|
.find(|(_, c)| {
|
|
depth += i32::from(*c == '(') - i32::from(*c == ')');
|
|
depth == 0
|
|
})
|
|
.unwrap()
|
|
.0;
|
|
let params: Vec<String> = split_params(&rest[..end])
|
|
.iter()
|
|
.map(|p| c_type(p.split_once(':').unwrap().1))
|
|
.collect();
|
|
let ret = rest[end + 1..].split('{').next().unwrap().trim();
|
|
let ret = match ret.strip_prefix("->") {
|
|
Some(r) => c_type(r),
|
|
None => "void".to_string(),
|
|
};
|
|
out.insert(name, (ret, params));
|
|
}
|
|
// The `unary_op!` expansions share the macro body's signature.
|
|
let body = &SRC[SRC.find("macro_rules! unary_op").unwrap()..];
|
|
let sig = body.split("extern \"C\" fn $name(").nth(1).unwrap();
|
|
let (params, tail) = sig.split_once(')').unwrap();
|
|
let unary_params: Vec<String> = split_params(params)
|
|
.iter()
|
|
.map(|p| c_type(p.split_once(':').unwrap().1))
|
|
.collect();
|
|
let unary_ret = c_type(
|
|
tail.split('{')
|
|
.next()
|
|
.unwrap()
|
|
.trim()
|
|
.trim_start_matches("->"),
|
|
);
|
|
for line in SRC.lines().filter(|l| l.starts_with("unary_op!(")) {
|
|
let name = line["unary_op!(".len()..].split(',').next().unwrap();
|
|
out.insert(name.to_string(), (unary_ret.clone(), unary_params.clone()));
|
|
}
|
|
out
|
|
}
|
|
|
|
fn strip_comments(text: &str) -> String {
|
|
let mut flat = text.replace('\n', " ");
|
|
while let Some(open) = flat.find("/*") {
|
|
let close = flat[open..].find("*/").expect("an unterminated comment") + open + 2;
|
|
flat.replace_range(open..close, " ");
|
|
}
|
|
flat
|
|
}
|
|
|
|
/// A header parameter without its name: `const ZsZSet *z` is `const ZsZSet *`.
|
|
fn param_type(p: &str) -> String {
|
|
let p = p.trim();
|
|
let name_start = p
|
|
.rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
|
|
.map_or(0, |i| i + 1);
|
|
if name_start == 0 {
|
|
return squash(p);
|
|
}
|
|
squash(&p[..name_start])
|
|
}
|
|
|
|
fn declared() -> BTreeMap<String, (String, Vec<String>)> {
|
|
let mut out = BTreeMap::new();
|
|
for decl in strip_comments(HEADER).split(';') {
|
|
let Some(open) = decl.find('(') else { continue };
|
|
let before = &decl[..open];
|
|
let name_at = before
|
|
.rfind(|c: char| !(c.is_alphanumeric() || c == '_'))
|
|
.map_or(0, |i| i + 1);
|
|
let name = &before[name_at..];
|
|
if !name.starts_with("zs_") || before.contains("typedef") || before.contains("(*") {
|
|
continue;
|
|
}
|
|
let ret = squash(&before[..name_at]);
|
|
let params = decl[open + 1..].rsplit_once(')').unwrap().0;
|
|
let params = if params.trim() == "void" {
|
|
Vec::new()
|
|
} else {
|
|
split_params(params).iter().map(|p| param_type(p)).collect()
|
|
};
|
|
out.insert(name.to_string(), (ret, params));
|
|
}
|
|
out
|
|
}
|
|
|
|
fn defines() -> BTreeMap<String, i64> {
|
|
HEADER
|
|
.lines()
|
|
.filter_map(|l| l.strip_prefix("#define "))
|
|
.filter_map(|l| {
|
|
let (k, v) = l.split_once(' ')?;
|
|
Some((k.to_string(), v.trim().parse().ok()?))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn the_header_declares_exactly_the_exported_functions_with_their_types() {
|
|
let (src, hdr) = (exported(), declared());
|
|
assert!(
|
|
src.len() > 30,
|
|
"the scanner saw {} functions, it is not reading the source",
|
|
src.len()
|
|
);
|
|
assert_eq!(src, hdr);
|
|
}
|
|
|
|
#[test]
|
|
fn the_header_constants_are_the_rust_constants() {
|
|
let d = defines();
|
|
for code in Code::ALL {
|
|
assert_eq!(
|
|
d[code.header_name()],
|
|
*code as i64,
|
|
"{}",
|
|
code.header_name()
|
|
);
|
|
}
|
|
let err_lines = d.keys().filter(|k| k.starts_with("ZS_ERR_")).count();
|
|
assert_eq!(
|
|
err_lines,
|
|
Code::ALL.len(),
|
|
"a header code without a Rust variant"
|
|
);
|
|
assert_eq!(d["ZS_ABI_VERSION"], i64::from(ZS_ABI_VERSION));
|
|
assert_eq!(d["ZS_OK"], 0);
|
|
assert_eq!(
|
|
[
|
|
d["ZS_AGG_COUNT"],
|
|
d["ZS_AGG_SUM"],
|
|
d["ZS_AGG_MIN"],
|
|
d["ZS_AGG_MAX"]
|
|
],
|
|
[0, 1, 2, 3]
|
|
);
|
|
}
|
|
|
|
/// `typedef R (*Name)(params)` from the header, and `pub type X = extern "C" fn(..) -> R` from
|
|
/// `engine.rs`, as `name -> (return, params)`.
|
|
type Sigs = BTreeMap<String, (String, Vec<String>)>;
|
|
|
|
fn callback_types() -> (Sigs, Sigs) {
|
|
let mut hdr = BTreeMap::new();
|
|
for decl in strip_comments(HEADER).split(';') {
|
|
let Some(at) = decl.find("(*") else { continue };
|
|
if !decl.contains("typedef") {
|
|
continue;
|
|
}
|
|
let ret = squash(
|
|
decl.split("typedef")
|
|
.nth(1)
|
|
.unwrap()
|
|
.split('(')
|
|
.next()
|
|
.unwrap(),
|
|
);
|
|
let rest = &decl[at + 2..];
|
|
let (name, after) = rest.split_once(')').unwrap();
|
|
let params = after
|
|
.trim()
|
|
.trim_start_matches('(')
|
|
.rsplit_once(')')
|
|
.unwrap()
|
|
.0;
|
|
let params = split_params(params).iter().map(|p| param_type(p)).collect();
|
|
hdr.insert(name.trim().to_string(), (ret, params));
|
|
}
|
|
let engine = include_str!("../src/engine.rs");
|
|
let mut rust = BTreeMap::new();
|
|
for line in engine.lines().filter(|l| l.contains("= extern \"C\" fn(")) {
|
|
let name = line
|
|
.trim_start_matches("pub type ")
|
|
.split(' ')
|
|
.next()
|
|
.unwrap();
|
|
let sig = line.split("fn(").nth(1).unwrap();
|
|
let (params, tail) = sig.rsplit_once(')').unwrap();
|
|
let ret = match tail.trim().trim_end_matches(';').trim().strip_prefix("->") {
|
|
Some(r) => c_type(r),
|
|
None => "void".to_string(),
|
|
};
|
|
let params = split_params(params).iter().map(|p| c_type(p)).collect();
|
|
rust.insert(format!("Zs{name}"), (ret, params));
|
|
}
|
|
(hdr, rust)
|
|
}
|
|
|
|
#[test]
|
|
fn the_header_callback_typedefs_match_the_rust_callback_types() {
|
|
let (hdr, rust) = callback_types();
|
|
assert_eq!(
|
|
rust.len(),
|
|
4,
|
|
"the scanner saw {} callback types",
|
|
rust.len()
|
|
);
|
|
assert_eq!(rust, hdr);
|
|
}
|