mod ty;
pub use ty::{Family, Prim, Ty, WireType};
use crate::ir::{
CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructDef,
StructField, TypeRef,
};
use heck::ToUpperCamelCase;
use crate::abi::{
async_callback_params, async_input_params, callback_method_signature, context_param,
error_out_param, lower_param, lower_return, sync_signature, AbiParam, CType, ConstPos,
};
use crate::resolved::ResolvedApi;
pub const RUNTIME_SYMBOLS: &[&str] = &[
"str",
"bytes",
"reader",
"writer",
"abi_version",
"error",
"error_set",
"error_clear",
"error_free",
"free_bytes",
"cancel_token",
"cancel_token_create",
"cancel_token_cancel",
"cancel_token_is_cancelled",
"cancel_token_destroy",
"alloc",
"dealloc",
"debug_live",
];
pub const RESERVED_SYMBOL_FAMILIES: &[&str] = &[
"str_", "bytes_", "reader_", "writer_", "list_", "map_", "opt_",
];
pub const ABI_VERSION: u32 = 3;
#[must_use]
pub fn checksum_symbol(prefix: &str, module: &str) -> String {
format!("{prefix}_{module}_checksum")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbiFn {
pub symbol: String,
pub params: Vec<AbiParam>,
pub ret: CType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallShape {
Sync(AbiFn),
Async(AsyncBinding),
Iterator(IteratorBinding),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AsyncBinding {
pub launch: AbiFn,
pub callback_type: String,
pub callback_params: Vec<AbiParam>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IteratorBinding {
pub elem: Ty,
pub iter_tag: String,
pub launch: AbiFn,
pub next: AbiFn,
pub destroy_symbol: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamBinding {
pub name: String,
pub ty: Ty,
pub doc: Option<String>,
pub abi: Vec<AbiParam>,
}
impl ParamBinding {
pub fn new(name: impl Into<String>, ty: Ty, doc: Option<String>, module: &str) -> Self {
let name = name.into();
let abi = lower_param(&name, &ty, module);
Self { name, ty, doc, abi }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FnBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub cancellable: bool,
pub is_async: bool,
pub throws: bool,
pub has_self: bool,
pub params: Vec<ParamBinding>,
pub ret: Option<Ty>,
pub c_base: String,
pub shape: CallShape,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldBinding {
pub name: String,
pub doc: Option<String>,
pub ty: Ty,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub fields: Vec<FieldBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub variants: Vec<EnumVariantBinding>,
pub rich: bool,
}
impl EnumBinding {
pub fn is_rich(&self) -> bool {
self.rich
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantBinding {
pub name: String,
pub value: i32,
pub doc: Option<String>,
pub c_const: String,
pub fields: Vec<FieldBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub constructors: Vec<FnBinding>,
pub methods: Vec<FnBinding>,
pub statics: Vec<FnBinding>,
pub clone_symbol: String,
pub destroy_symbol: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallbackMethodBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub params: Vec<ParamBinding>,
pub ret: Option<Ty>,
pub abi_params: Vec<AbiParam>,
pub abi_ret: CType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallbackInterfaceBinding {
pub name: String,
pub doc: Option<String>,
pub deprecated: Option<String>,
pub c_tag: String,
pub vtable_tag: String,
pub methods: Vec<CallbackMethodBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorCodeBinding {
pub name: String,
pub value: i32,
pub message: String,
pub doc: Option<String>,
pub c_const: String,
pub fields: Vec<FieldBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorBinding {
pub name: String,
pub type_name: String,
pub owner_path: String,
pub declared_here: bool,
pub c_tag: String,
pub codes: Vec<ErrorCodeBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleBinding {
pub name: String,
pub checksum: Option<u64>,
pub segments: Vec<String>,
pub path: String,
pub dot_path: String,
pub doc: Option<String>,
pub error: Option<ErrorBinding>,
pub enums: Vec<EnumBinding>,
pub structs: Vec<StructBinding>,
pub interfaces: Vec<InterfaceBinding>,
pub callback_interfaces: Vec<CallbackInterfaceBinding>,
pub functions: Vec<FnBinding>,
}
impl ModuleBinding {
pub fn callback_interface(&self, name: &str) -> Option<&CallbackInterfaceBinding> {
self.callback_interfaces.iter().find(|c| c.name == name)
}
pub fn is_empty(&self) -> bool {
self.enums.is_empty()
&& self.structs.is_empty()
&& self.interfaces.is_empty()
&& self.callback_interfaces.is_empty()
&& self.functions.is_empty()
&& !self.declares_error()
}
pub fn declares_error(&self) -> bool {
self.error.as_ref().is_some_and(|e| e.declared_here)
}
pub fn callables(&self) -> impl Iterator<Item = &FnBinding> {
self.functions
.iter()
.chain(self.interfaces.iter().flat_map(|i| {
i.constructors
.iter()
.chain(i.methods.iter())
.chain(i.statics.iter())
}))
}
pub fn has_async(&self) -> bool {
self.callables().any(|f| f.is_async)
}
pub fn has_iterators(&self) -> bool {
self.callables()
.any(|f| matches!(f.shape, CallShape::Iterator(_)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingModel {
pub prefix: String,
pub version: String,
pub modules: Vec<ModuleBinding>,
}
impl BindingModel {
pub fn build(api: &ResolvedApi) -> Self {
let prefix = api.identity().prefix.as_str();
let mut modules = Vec::new();
let lowerer = Lowerer { api, prefix };
for m in &api.modules {
lowerer.module(m, &[], None, &mut modules);
}
Self {
prefix: prefix.to_string(),
version: api.version.clone(),
modules,
}
}
pub fn roots(&self) -> impl Iterator<Item = &ModuleBinding> {
self.modules.iter().filter(|m| m.segments.len() == 1)
}
pub fn children<'a>(
&'a self,
parent: &'a ModuleBinding,
) -> impl Iterator<Item = &'a ModuleBinding> + 'a {
self.modules.iter().filter(move |m| {
m.segments.len() == parent.segments.len() + 1
&& m.segments[..parent.segments.len()] == parent.segments[..]
})
}
pub fn functions(&self) -> impl Iterator<Item = (&ModuleBinding, &FnBinding)> {
self.modules
.iter()
.flat_map(|m| m.functions.iter().map(move |f| (m, f)))
}
pub fn callables(&self) -> impl Iterator<Item = (&ModuleBinding, &FnBinding)> {
self.modules
.iter()
.flat_map(|m| m.callables().map(move |f| (m, f)))
}
pub fn callback_interfaces(
&self,
) -> impl Iterator<Item = (&ModuleBinding, &CallbackInterfaceBinding)> {
self.modules
.iter()
.flat_map(|m| m.callback_interfaces.iter().map(move |c| (m, c)))
}
pub fn interface(&self, name: &str, current: &str) -> Option<&InterfaceBinding> {
let (dot_path, bare) = match name.rsplit_once('.') {
Some((p, n)) => (p, n),
None => (current, name),
};
self.modules
.iter()
.find(|m| m.dot_path == dot_path)
.and_then(|m| m.interfaces.iter().find(|i| i.name == bare))
}
pub fn c_symbols(&self) -> Vec<(String, String)> {
let p = &self.prefix;
let mut out: Vec<(String, String)> = RUNTIME_SYMBOLS
.iter()
.map(|s| (format!("{p}_{s}"), format!("the runtime symbol '{p}_{s}'")))
.collect();
for m in self.roots() {
out.push((
checksum_symbol(p, &m.name),
format!("the checksum of module '{}'", m.name),
));
}
let callable = |out: &mut Vec<(String, String)>, f: &FnBinding, owner: &str| {
let what = format!("'{owner}.{}'", f.name);
match &f.shape {
CallShape::Sync(abi) => out.push((abi.symbol.clone(), format!("function {what}"))),
CallShape::Async(a) => {
out.push((a.launch.symbol.clone(), format!("async function {what}")));
out.push((
a.callback_type.clone(),
format!("the completion type of {what}"),
));
}
CallShape::Iterator(it) => {
out.push((it.launch.symbol.clone(), format!("function {what}")));
out.push((it.iter_tag.clone(), format!("the iterator type of {what}")));
out.push((
it.next.symbol.clone(),
format!("the iterator step of {what}"),
));
out.push((
it.destroy_symbol.clone(),
format!("the iterator destructor of {what}"),
));
}
}
};
for m in &self.modules {
let dot = &m.dot_path;
if let Some(e) = m.error.as_ref().filter(|e| e.declared_here) {
out.push((e.c_tag.clone(), format!("error domain '{dot}.{}'", e.name)));
for c in &e.codes {
out.push((
c.c_const.clone(),
format!("error code '{dot}.{}.{}'", e.name, c.name),
));
}
}
for e in &m.enums {
out.push((e.c_tag.clone(), format!("enum '{dot}.{}'", e.name)));
if e.rich {
out.push((
format!("{}_Tag", e.c_tag),
format!("the tag type of enum '{dot}.{}'", e.name),
));
}
for v in &e.variants {
out.push((
v.c_const.clone(),
format!("enum variant '{dot}.{}.{}'", e.name, v.name),
));
}
}
for s in &m.structs {
out.push((
format!("{p}_{}_{}", m.path, s.name),
format!("record '{dot}.{}'", s.name),
));
}
for c in &m.callback_interfaces {
out.push((
c.vtable_tag.clone(),
format!("the vtable of callback interface '{dot}.{}'", c.name),
));
}
for i in &m.interfaces {
let owner = format!("{dot}.{}", i.name);
out.push((i.c_tag.clone(), format!("interface '{owner}'")));
out.push((i.clone_symbol.clone(), format!("the clone of '{owner}'")));
out.push((
i.destroy_symbol.clone(),
format!("the destructor of '{owner}'"),
));
for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) {
callable(&mut out, f, &owner);
}
}
for f in &m.functions {
callable(&mut out, f, dot);
}
}
out
}
pub fn has_async(&self) -> bool {
self.modules.iter().any(ModuleBinding::has_async)
}
pub fn has_iterators(&self) -> bool {
self.modules.iter().any(ModuleBinding::has_iterators)
}
pub fn has_callback_interfaces(&self) -> bool {
self.modules
.iter()
.any(|m| !m.callback_interfaces.is_empty())
}
pub fn has_interfaces(&self) -> bool {
self.modules.iter().any(|m| !m.interfaces.is_empty())
}
pub fn has_buffers(&self) -> bool {
self.modules.iter().any(|m| {
!m.structs.is_empty()
|| m.enums.iter().any(|e| e.rich)
|| m.error
.as_ref()
.is_some_and(|e| e.codes.iter().any(|c| !c.fields.is_empty()))
}) || self.any_type(&|t| t.is_buffered())
}
pub fn has_buffered_objects(&self) -> bool {
self.any_top_type(&|ty, position| match position {
TypePosition::Field => ty.contains_object(),
TypePosition::Boundary => ty.is_buffered() && ty.contains_object(),
})
}
pub fn any_type(&self, pred: &dyn Fn(&Ty) -> bool) -> bool {
self.any_top_type(&|ty, _| ty.any(pred))
}
fn any_top_type(&self, pred: &dyn Fn(&Ty, TypePosition) -> bool) -> bool {
let boundary = |ty: &Ty| pred(ty, TypePosition::Boundary);
let field = |ty: &Ty| pred(ty, TypePosition::Field);
self.modules.iter().any(|m| {
m.callables().any(|f| {
f.params.iter().any(|p| boundary(&p.ty)) || f.ret.as_ref().is_some_and(boundary)
}) || m.callback_interfaces.iter().any(|c| {
c.methods.iter().any(|f| {
f.params.iter().any(|p| boundary(&p.ty)) || f.ret.as_ref().is_some_and(boundary)
})
}) || m
.structs
.iter()
.any(|s| s.fields.iter().any(|f| field(&f.ty)))
|| m.enums.iter().any(|e| {
e.variants
.iter()
.any(|v| v.fields.iter().any(|f| field(&f.ty)))
})
|| m.error.as_ref().is_some_and(|e| {
e.declared_here
&& e.codes
.iter()
.any(|c| c.fields.iter().any(|f| field(&f.ty)))
})
})
}
}
#[derive(Clone, Copy)]
enum TypePosition {
Boundary,
Field,
}
struct Lowerer<'a> {
api: &'a ResolvedApi,
prefix: &'a str,
}
struct Scope<'a> {
path: &'a str,
}
impl Lowerer<'_> {
fn ty(&self, ty: &TypeRef) -> Ty {
self.api.resolve(ty)
}
fn module(
&self,
module: &Module,
parent: &[String],
inherited_error: Option<&ErrorBinding>,
out: &mut Vec<ModuleBinding>,
) {
let mut segments = parent.to_vec();
segments.push(module.name.clone());
let path = segments.join("_");
let dot_path = segments.join(".");
let scope = Scope { path: &path };
let prefix = self.prefix;
let error = match &module.errors {
Some(domain) => Some(self.error_domain(domain, &scope)),
None => inherited_error.cloned().map(|mut e| {
e.declared_here = false;
e
}),
};
let enums = module
.enums
.iter()
.map(|e| self.enum_def(e, &scope))
.collect();
let structs = module.structs.iter().map(|s| self.struct_def(s)).collect();
let interfaces = module
.interfaces
.iter()
.map(|i| self.interface(i, &scope))
.collect();
let callback_interfaces = module
.callback_interfaces
.iter()
.map(|c| self.callback_interface(c, &scope))
.collect();
let functions = module
.functions
.iter()
.map(|f| {
let c_base = format!("{prefix}_{path}_{}", f.name);
self.callable(f, &scope, &c_base, None)
})
.collect();
let doc = module
.doc
.clone()
.or_else(|| module.functions.iter().find_map(|f| f.doc.clone()));
out.push(ModuleBinding {
name: module.name.clone(),
checksum: parent
.is_empty()
.then(|| crate::checksum::module_checksum(module)),
segments: segments.clone(),
path: path.clone(),
dot_path: dot_path.clone(),
doc,
error: error.clone(),
enums,
structs,
interfaces,
callback_interfaces,
functions,
});
for child in &module.modules {
self.module(child, &segments, error.as_ref(), out);
}
}
fn error_domain(&self, domain: &ErrorDomain, scope: &Scope<'_>) -> ErrorBinding {
let c_tag = format!("{}_{}_{}", self.prefix, scope.path, domain.name);
ErrorBinding {
name: domain.name.clone(),
type_name: crate::errors::type_name(&domain.name, "Error"),
owner_path: scope.path.to_string(),
declared_here: true,
c_tag: c_tag.clone(),
codes: domain
.codes
.iter()
.map(|c| ErrorCodeBinding {
name: c.name.clone(),
value: c.code,
message: c.message.clone(),
doc: c.doc.clone(),
c_const: format!("{c_tag}_{}", c.name),
fields: self.fields(&c.fields),
})
.collect(),
}
}
fn fields(&self, fields: &[StructField]) -> Vec<FieldBinding> {
fields
.iter()
.map(|f| FieldBinding {
name: f.name.clone(),
doc: f.doc.clone(),
ty: self.ty(&f.ty),
})
.collect()
}
fn struct_def(&self, s: &StructDef) -> StructBinding {
StructBinding {
name: s.name.clone(),
doc: s.doc.clone(),
deprecated: s.deprecated.clone(),
fields: self.fields(&s.fields),
}
}
fn enum_def(&self, e: &EnumDef, scope: &Scope<'_>) -> EnumBinding {
let c_tag = format!("{}_{}_{}", self.prefix, scope.path, e.name);
let variants = e
.variants
.iter()
.map(|v| EnumVariantBinding {
name: v.name.clone(),
value: v.value,
doc: v.doc.clone(),
c_const: format!("{c_tag}_{}", v.name),
fields: self.fields(&v.fields),
})
.collect();
EnumBinding {
name: e.name.clone(),
doc: e.doc.clone(),
deprecated: e.deprecated.clone(),
c_tag,
variants,
rich: e.is_rich(),
}
}
fn params(&self, params: &[crate::ir::Param], scope: &Scope<'_>) -> Vec<ParamBinding> {
params
.iter()
.map(|p| ParamBinding::new(&p.name, self.ty(&p.ty), p.doc.clone(), scope.path))
.collect()
}
fn callback_interface(
&self,
c: &CallbackInterfaceDef,
scope: &Scope<'_>,
) -> CallbackInterfaceBinding {
let c_tag = format!("{}_{}_{}", self.prefix, scope.path, c.name);
let methods = c
.methods
.iter()
.map(|m| {
let params = self.params(&m.params, scope);
let ret = m.returns.as_ref().map(|r| self.ty(r));
let sig = callback_method_signature(¶ms, ret.as_ref(), scope.path);
CallbackMethodBinding {
name: m.name.clone(),
doc: m.doc.clone(),
deprecated: m.deprecated.clone(),
params,
ret,
abi_params: sig.params,
abi_ret: sig.ret,
}
})
.collect();
CallbackInterfaceBinding {
name: c.name.clone(),
doc: c.doc.clone(),
deprecated: c.deprecated.clone(),
vtable_tag: format!("{c_tag}_vtable"),
c_tag,
methods,
}
}
fn interface(&self, iface: &InterfaceDef, scope: &Scope<'_>) -> InterfaceBinding {
let c_tag = format!("{}_{}_{}", self.prefix, scope.path, iface.name);
let self_slot = AbiParam::new(
"self",
CType::Ptr {
konst: ConstPos::West,
pointee: Box::new(CType::StructTag {
module: scope.path.to_string(),
name: iface.name.clone(),
}),
},
);
let member = |name: &str| format!("{c_tag}_{name}");
let constructors = iface
.constructors
.iter()
.map(|c| {
let mut f = c.clone();
f.returns = Some(TypeRef::Named(iface.name.clone()));
self.callable(&f, scope, &member(&c.name), None)
})
.collect();
let methods = iface
.methods
.iter()
.map(|m| self.callable(m, scope, &member(&m.name), Some(self_slot.clone())))
.collect();
let statics = iface
.statics
.iter()
.map(|s| self.callable(s, scope, &member(&s.name), None))
.collect();
InterfaceBinding {
name: iface.name.clone(),
doc: iface.doc.clone(),
deprecated: iface.deprecated.clone(),
c_tag: c_tag.clone(),
constructors,
methods,
statics,
clone_symbol: format!("{c_tag}_clone"),
destroy_symbol: format!("{c_tag}_destroy"),
}
}
fn callable(
&self,
f: &Function,
scope: &Scope<'_>,
c_base: &str,
self_slot: Option<AbiParam>,
) -> FnBinding {
let prefix = self.prefix;
let path = scope.path;
let params = self.params(&f.params, scope);
let ret = f.returns.as_ref().map(|r| self.ty(r));
let core_base = c_base
.strip_prefix(&format!("{prefix}_"))
.expect("c_base always starts with the symbol prefix")
.to_string();
let with_self = |mut params: Vec<AbiParam>| {
if let Some(s) = &self_slot {
params.insert(0, s.clone());
}
params
};
let shape = if let Some(elem) = ret.as_ref().and_then(Ty::iterator_elem) {
let pascal = f.name.to_upper_camel_case();
let owner = &core_base[..core_base.len() - f.name.len() - 1];
let iter_core = format!("{owner}_{pascal}Iterator");
let iter_tag = format!("{prefix}_{iter_core}");
let mut launch_params: Vec<AbiParam> =
params.iter().flat_map(|p| p.abi.iter().cloned()).collect();
launch_params.push(error_out_param());
let launch = AbiFn {
symbol: c_base.to_string(),
params: with_self(launch_params),
ret: CType::ptr(CType::Named(iter_core.clone())),
};
let item = lower_return(elem, path);
let mut next_params = vec![
AbiParam::new("iter", CType::ptr(CType::Named(iter_core.clone()))),
AbiParam::new("out_item", CType::ptr(item.ret)),
];
next_params.extend(item.out_params);
next_params.push(error_out_param());
let next = AbiFn {
symbol: format!("{iter_tag}_next"),
params: next_params,
ret: CType::Int32,
};
CallShape::Iterator(IteratorBinding {
elem: elem.clone(),
iter_tag: iter_tag.clone(),
launch,
next,
destroy_symbol: format!("{iter_tag}_destroy"),
})
} else if f.r#async {
let callback_type = format!("{c_base}_callback");
let mut launch_params = async_input_params(¶ms, f.cancellable);
launch_params.push(AbiParam::new(
"callback",
CType::Named(format!("{core_base}_callback")),
));
launch_params.push(context_param());
let launch = AbiFn {
symbol: c_base.to_string(),
params: with_self(launch_params),
ret: CType::Void,
};
CallShape::Async(AsyncBinding {
launch,
callback_type,
callback_params: async_callback_params(ret.as_ref(), path),
})
} else {
let sig = sync_signature(¶ms, ret.as_ref(), path);
CallShape::Sync(AbiFn {
symbol: c_base.to_string(),
params: with_self(sig.params),
ret: sig.ret,
})
};
FnBinding {
name: f.name.clone(),
doc: f.doc.clone(),
deprecated: f.deprecated.clone(),
cancellable: f.cancellable,
is_async: f.r#async,
throws: f.throws,
has_self: self_slot.is_some(),
params,
ret,
c_base: c_base.to_string(),
shape,
}
}
}
pub fn iterator_item_ctype(elem: &Ty, module: &str) -> CType {
lower_return(elem, module).ret
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{
Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param,
StructDef, StructField,
};
fn param(name: &str, ty: TypeRef) -> Param {
Param {
name: name.into(),
ty,
doc: None,
}
}
fn func(name: &str, params: Vec<Param>, returns: Option<TypeRef>) -> Function {
Function {
name: name.into(),
params,
returns,
doc: None,
throws: false,
r#async: false,
cancellable: false,
deprecated: None,
}
}
fn module(name: &str) -> Module {
Module {
name: name.into(),
doc: None,
functions: vec![],
interfaces: vec![],
callback_interfaces: vec![],
structs: vec![],
enums: vec![],
errors: None,
modules: vec![],
}
}
fn api(modules: Vec<Module>) -> ResolvedApi {
ResolvedApi::assume_valid(Api {
version: crate::ir::CURRENT_SCHEMA_VERSION.into(),
modules,
})
}
fn rendered(abi: &AbiFn) -> Vec<String> {
abi.params
.iter()
.map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name))
.collect()
}
#[test]
fn sync_function_symbol_and_sig() {
let m = Module {
functions: vec![func(
"add",
vec![param("a", TypeRef::I32), param("b", TypeRef::I32)],
Some(TypeRef::I32),
)],
..module("math")
};
let model = BindingModel::build(
&api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let f = &model.modules[0].functions[0];
assert_eq!(f.c_base, "weaveffi_math_add");
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(abi.symbol, "weaveffi_math_add");
assert_eq!(abi.ret, CType::Int32);
assert_eq!(
rendered(abi),
["int32_t a", "int32_t b", "weaveffi_error* out_err"]
);
let model = BindingModel::build(
&api(vec![module("net")]).with_identity(crate::pkg::Identity::named("acme")),
);
assert_eq!(model.prefix, "acme");
}
#[test]
fn async_function_has_launch_and_callback() {
let m = Module {
functions: vec![Function {
cancellable: true,
r#async: true,
..func(
"fetch",
vec![param("id", TypeRef::I64)],
Some(TypeRef::StringUtf8),
)
}],
..module("net")
};
let model = BindingModel::build(
&api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let CallShape::Async(a) = &model.modules[0].functions[0].shape else {
panic!("expected async")
};
assert_eq!(a.launch.symbol, "weaveffi_net_fetch");
assert_eq!(a.callback_type, "weaveffi_net_fetch_callback");
let names: Vec<&str> = a.launch.params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["id", "cancel_token", "callback", "context"]);
assert_eq!(a.callback_params[0].name, "context");
assert_eq!(a.callback_params[1].name, "err");
assert_eq!(a.callback_params[2].name, "result_ptr");
assert!(model.has_async());
}
#[test]
fn iterator_function_has_next_and_destroy() {
let m = Module {
functions: vec![func(
"get_messages",
vec![],
Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))),
)],
..module("events")
};
let model = BindingModel::build(
&api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let CallShape::Iterator(it) = &model.modules[0].functions[0].shape else {
panic!("expected iterator")
};
assert_eq!(it.iter_tag, "weaveffi_events_GetMessagesIterator");
assert_eq!(it.launch.symbol, "weaveffi_events_get_messages");
assert_eq!(it.next.symbol, "weaveffi_events_GetMessagesIterator_next");
assert_eq!(
it.destroy_symbol,
"weaveffi_events_GetMessagesIterator_destroy"
);
assert_eq!(it.elem, Ty::StringUtf8);
assert_eq!(it.next.ret, CType::Int32);
assert_eq!(it.next.params[1].ty.render_c("weaveffi"), "const uint8_t**");
assert!(model.has_iterators());
}
#[test]
fn user_types_resolve_to_kinds_and_buffers() {
let shared = Module {
structs: vec![StructDef {
name: "Contact".into(),
doc: None,
deprecated: Some("use Person".into()),
fields: vec![
StructField {
name: "name".into(),
ty: TypeRef::StringUtf8,
doc: None,
},
StructField {
name: "status".into(),
ty: TypeRef::Named("Status".into()),
doc: None,
},
StructField {
name: "store".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("Store".into()))),
doc: None,
},
],
}],
enums: vec![EnumDef {
name: "Status".into(),
doc: None,
deprecated: None,
variants: vec![EnumVariant {
name: "Ok".into(),
value: 0,
doc: None,
fields: vec![],
}],
}],
interfaces: vec![InterfaceDef {
name: "Store".into(),
doc: None,
deprecated: None,
constructors: vec![func("open", vec![], None)],
methods: vec![func(
"save",
vec![param("contact", TypeRef::Named("Contact".into()))],
Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))),
)],
statics: vec![],
}],
..module("contacts")
};
let other = Module {
functions: vec![func(
"status_of",
vec![param("store", TypeRef::Named("Store".into()))],
Some(TypeRef::Named("Status".into())),
)],
..module("ops")
};
let model = BindingModel::build(
&api(vec![shared, other]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let contacts = &model.modules[0];
let s = &contacts.structs[0];
assert_eq!(s.deprecated.as_deref(), Some("use Person"));
assert_eq!(s.fields[1].ty, Ty::Enum("contacts.Status".into()));
assert_eq!(
s.fields[2].ty,
Ty::Optional(Box::new(Ty::Interface("contacts.Store".into())))
);
assert_eq!(contacts.enums[0].c_tag, "weaveffi_contacts_Status");
assert_eq!(
contacts.enums[0].variants[0].c_const,
"weaveffi_contacts_Status_Ok"
);
let iface = &contacts.interfaces[0];
assert_eq!(iface.c_tag, "weaveffi_contacts_Store");
assert_eq!(iface.clone_symbol, "weaveffi_contacts_Store_clone");
assert_eq!(iface.destroy_symbol, "weaveffi_contacts_Store_destroy");
assert_eq!(
iface.constructors[0].ret,
Some(Ty::Interface("contacts.Store".into()))
);
let save = &iface.methods[0];
assert!(save.has_self);
assert_eq!(save.params[0].ty, Ty::Record("contacts.Contact".into()));
let CallShape::Sync(abi) = &save.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"const weaveffi_contacts_Store* self",
"const uint8_t* contact_ptr",
"size_t contact_len",
"size_t* out_len",
"weaveffi_error* out_err"
]
);
assert_eq!(abi.ret.render_c("weaveffi"), "const uint8_t*");
let ops = &model.modules[1];
let f = &ops.functions[0];
assert_eq!(f.params[0].ty, Ty::Interface("contacts.Store".into()));
assert_eq!(f.ret, Some(Ty::Enum("contacts.Status".into())));
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"const weaveffi_contacts_Store* store",
"weaveffi_error* out_err"
]
);
assert_eq!(abi.ret.render_c("weaveffi"), "weaveffi_contacts_Status");
assert!(model.has_buffers());
assert!(model.has_buffered_objects());
assert!(model.has_interfaces());
assert_eq!(
model.interface("contacts.Store", "ops").map(|i| &i.c_tag),
Some(&"weaveffi_contacts_Store".to_string())
);
assert_eq!(
model.interface("Store", "contacts").map(|i| &i.c_tag),
Some(&"weaveffi_contacts_Store".to_string())
);
}
#[test]
fn callback_interfaces_lower_to_vtables() {
let m = Module {
callback_interfaces: vec![CallbackInterfaceDef {
name: "Listener".into(),
doc: Some("Receives messages.".into()),
deprecated: None,
methods: vec![
func("on_message", vec![param("text", TypeRef::StringUtf8)], None),
func("should_stop", vec![], Some(TypeRef::Bool)),
],
}],
functions: vec![func(
"subscribe",
vec![param("listener", TypeRef::Named("Listener".into()))],
None,
)],
..module("events")
};
let model = BindingModel::build(
&api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let mb = &model.modules[0];
let cb = &mb.callback_interfaces[0];
assert_eq!(cb.c_tag, "weaveffi_events_Listener");
assert_eq!(cb.vtable_tag, "weaveffi_events_Listener_vtable");
let on_message = &cb.methods[0];
let slots: Vec<String> = on_message
.abi_params
.iter()
.map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name))
.collect();
assert_eq!(
slots,
[
"void* ctx",
"const uint8_t* text_ptr",
"size_t text_len",
"weaveffi_error* out_err"
]
);
assert_eq!(on_message.abi_ret, CType::Void);
assert_eq!(cb.methods[1].abi_ret, CType::Bool);
assert!(mb.callback_interface("Listener").is_some());
assert!(model.has_callback_interfaces());
let f = &mb.functions[0];
assert_eq!(
f.params[0].ty,
Ty::CallbackInterface("events.Listener".into())
);
let CallShape::Sync(abi) = &f.shape else {
panic!("expected sync")
};
assert_eq!(
rendered(abi),
[
"void* listener_ctx",
"const weaveffi_events_Listener_vtable* listener_vtable",
"weaveffi_error* out_err"
]
);
assert!(!model.has_buffers());
assert!(!model.has_buffered_objects());
}
#[test]
fn nested_modules_flatten_pre_order_with_paths_and_docs() {
let inner = Module {
functions: vec![Function {
doc: Some("Leaf.".into()),
..func("leaf_fn", vec![], None)
}],
..module("inner")
};
let outer = Module {
doc: Some("Outer module.".into()),
functions: vec![func("outer_fn", vec![], None)],
modules: vec![inner],
..module("outer")
};
let model = BindingModel::build(
&api(vec![outer]).with_identity(crate::pkg::Identity::named("weaveffi")),
);
let paths: Vec<&str> = model.modules.iter().map(|m| m.path.as_str()).collect();
assert_eq!(paths, ["outer", "outer_inner"]);
assert_eq!(model.modules[1].dot_path, "outer.inner");
assert_eq!(
model.modules[1].functions[0].c_base,
"weaveffi_outer_inner_leaf_fn"
);
assert_eq!(model.modules[0].doc.as_deref(), Some("Outer module."));
assert_eq!(model.modules[1].doc.as_deref(), Some("Leaf."));
}
}