mod abi;
mod metadata;
use anyhow::{bail, Context, Result};
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use std::{
collections::{BTreeMap, HashSet},
fs,
path::Path,
};
use syn::{FnArg, GenericArgument, ImplItem, Item, PathArguments, ReturnType, Type};
const MARKER: &str = "// @generated by JFFI from core/src; do not edit this file.";
fn is_attr(a: &syn::Attribute, name: &str) -> bool {
a.path().segments.last().is_some_and(|s| s.ident == name)
}
fn derives(attrs: &[syn::Attribute], wanted: &[&str]) -> bool {
attrs.iter().filter(|a| is_attr(a, "derive")).any(|a| {
let mut found = false;
let _ = a.parse_nested_meta(|meta| {
if meta
.path
.segments
.last()
.is_some_and(|s| wanted.iter().any(|n| s.ident == *n))
{
found = true;
}
Ok(())
});
found
})
}
fn helper_key(name: &str) -> String {
name.as_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn camel(name: &str) -> String {
let name = name.strip_prefix("r#").unwrap_or(name);
let mut parts = name.trim_start_matches("r#").split('_');
let mut result = parts.next().unwrap_or_default().to_string();
for part in parts {
let mut chars = part.chars();
if let Some(first) = chars.next() {
result.extend(first.to_uppercase());
}
result.extend(chars);
}
result
}
#[derive(Clone)]
enum Definition {
Record(syn::ItemStruct),
Enum(syn::ItemEnum),
Alias(syn::ItemType),
Object,
Callback(syn::ItemTrait),
Custom(Type),
}
#[derive(Default)]
struct Api {
abi: Option<abi::Contract>,
cfg: Option<BuildCfg>,
module_alias: Option<syn::Ident>,
native_paths: BTreeMap<String, syn::Path>,
native_members: BTreeMap<String, syn::Ident>,
js_members: BTreeMap<String, String>,
external_dependencies: BTreeMap<String, toml::Value>,
functions: BTreeMap<String, (Vec<syn::Ident>, syn::ImplItemFn)>,
methods: Vec<syn::ImplItemFn>,
object_methods: BTreeMap<String, Vec<syn::ImplItemFn>>,
root_reexports: HashSet<String>,
types: BTreeMap<String, (Vec<syn::Ident>, Definition)>,
}
fn insert_type(
api: &mut Api,
name: &syn::Ident,
modules: &[syn::Ident],
definition: Definition,
) -> Result<()> {
if let Some((_, existing)) = api.types.get(&name.to_string()) {
if matches!(
(&definition, existing),
(Definition::Alias(_), Definition::Custom(_))
) {
return Ok(());
}
if matches!(
(&definition, existing),
(Definition::Custom(_), Definition::Alias(_))
) {
api.types
.insert(name.to_string(), (modules.to_vec(), definition));
return Ok(());
}
}
if api
.types
.insert(name.to_string(), (modules.to_vec(), definition))
.is_some()
{
bail!("Ambiguous exported type name {name}; web API names must be unique");
}
Ok(())
}
fn use_names(tree: &syn::UseTree, names: &mut HashSet<String>) {
match tree {
syn::UseTree::Path(p) => use_names(&p.tree, names),
syn::UseTree::Name(n) => {
names.insert(n.ident.to_string());
}
syn::UseTree::Group(g) => {
for item in &g.items {
use_names(item, names);
}
}
_ => {}
}
}
struct CustomArgs {
name: syn::Ident,
builtin: Type,
}
impl syn::parse::Parse for CustomArgs {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let name = input.parse()?;
input.parse::<syn::Token![,]>()?;
let builtin = input.parse()?;
let _options: TokenStream = input.parse()?;
Ok(Self { name, builtin })
}
}
fn callback_name(ty: &Type) -> Option<String> {
let Type::TraitObject(object) = ty else {
return None;
};
object.bounds.iter().find_map(|b| {
if let syn::TypeParamBound::Trait(t) = b {
let name = t.path.segments.last()?.ident.to_string();
(!matches!(name.as_str(), "Send" | "Sync")).then_some(name)
} else {
None
}
})
}
#[derive(Default)]
struct BuildCfg {
facts: HashSet<String>,
}
impl BuildCfg {
fn matches(&self, meta: &syn::Meta) -> Result<bool> {
use syn::parse::Parser;
Ok(match meta {
syn::Meta::Path(_) | syn::Meta::NameValue(_) => {
self.facts.contains(&meta.to_token_stream().to_string())
}
syn::Meta::List(list) => {
let args =
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
.parse2(list.tokens.clone())?;
let values = args
.iter()
.map(|m| self.matches(m))
.collect::<Result<Vec<_>>>()?;
if list.path.is_ident("all") {
values.iter().all(|v| *v)
} else if list.path.is_ident("any") {
values.iter().any(|v| *v)
} else if list.path.is_ident("not") && values.len() == 1 {
!values[0]
} else {
bail!("Unsupported cfg predicate {}", list.to_token_stream());
}
}
})
}
fn attrs(&self, attrs: &mut Vec<syn::Attribute>) -> Result<bool> {
use syn::parse::Parser;
let mut output = Vec::new();
let mut pending = std::collections::VecDeque::from(std::mem::take(attrs));
while let Some(attr) = pending.pop_front() {
if is_attr(&attr, "cfg") {
if !self.matches(&attr.parse_args::<syn::Meta>()?)? {
return Ok(false);
}
} else if is_attr(&attr, "cfg_attr") {
let args = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
)?;
let mut args = args.into_iter();
let predicate = args.next().context("Empty cfg_attr")?;
if self.matches(&predicate)? {
for meta in args.rev() {
pending.push_front(
syn::Attribute::parse_outer
.parse2(quote!(#[#meta]))?
.remove(0),
);
}
}
} else {
output.push(attr);
}
}
*attrs = output;
Ok(true)
}
fn load(root: &Path, release: bool) -> Result<Self> {
let mut cfg = Self::default();
let output = std::process::Command::new("cargo")
.current_dir(root)
.args([
"metadata",
"--format-version",
"1",
"--filter-platform",
"wasm32-unknown-unknown",
])
.output()
.context("Resolving Core features for WASM")?;
if !output.status.success() {
bail!(
"Resolving Core cfg features failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)?;
let core_manifest = root.join("core/Cargo.toml").canonicalize()?;
let package = metadata["packages"]
.as_array()
.context("Cargo packages missing")?
.iter()
.find(|p| {
p["manifest_path"]
.as_str()
.is_some_and(|p| Path::new(p) == core_manifest)
})
.context("Core package missing")?;
let adapter: toml::Value =
toml::from_str(&fs::read_to_string(root.join("ffi-web/Cargo.toml"))?)?;
let adapter_name = adapter["package"]["name"]
.as_str()
.context("Web adapter package name missing")?;
let tree = std::process::Command::new("cargo")
.current_dir(root)
.args([
"tree",
"--package",
adapter_name,
"--target",
"wasm32-unknown-unknown",
"--edges",
"normal,build",
"--prefix",
"none",
"--format",
"{p}|{f}",
])
.output()
.context("Resolving web adapter features")?;
if !tree.status.success() {
bail!(
"Resolving web features failed: {}",
String::from_utf8_lossy(&tree.stderr)
);
}
let prefix = format!(
"{} v{} ",
package["name"].as_str().unwrap(),
package["version"].as_str().unwrap()
);
let tree = String::from_utf8(tree.stdout)?;
let features = tree
.lines()
.find(|line| line.starts_with(&prefix))
.and_then(|line| line.split_once('|'))
.map(|(_, features)| features.trim())
.context("Core is not a dependency of the web adapter")?;
let mut command = std::process::Command::new("cargo");
command.current_dir(root).args([
"rustc",
"--package",
package["name"].as_str().context("Core name missing")?,
"--lib",
"--target",
"wasm32-unknown-unknown",
"--no-default-features",
]);
if !features.is_empty() {
command.arg("--features").arg(features);
}
if release {
command.arg("--release");
}
crate::commands::build::configure_wasm_compiler(&mut command);
let output = command
.args(["--", "--print", "cfg"])
.output()
.context("Reading effective Core compiler configuration")?;
if !output.status.success() {
bail!(
"Reading Core cfg failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
for line in String::from_utf8(output.stdout)?.lines() {
let meta: syn::Meta = syn::parse_str(line)?;
cfg.facts.insert(meta.to_token_stream().to_string());
}
Ok(cfg)
}
fn filter(&self, items: &mut Vec<Item>) -> Result<()> {
fn attrs(item: &mut Item) -> Option<&mut Vec<syn::Attribute>> {
Some(match item {
Item::Struct(v) => &mut v.attrs,
Item::Enum(v) => &mut v.attrs,
Item::Impl(v) => &mut v.attrs,
Item::Trait(v) => &mut v.attrs,
Item::Fn(v) => &mut v.attrs,
Item::Mod(v) => &mut v.attrs,
Item::Type(v) => &mut v.attrs,
Item::Use(v) => &mut v.attrs,
Item::Macro(v) => &mut v.attrs,
_ => return None,
})
}
let mut filtered = Vec::new();
for mut item in std::mem::take(items) {
if let Some(attrs) = attrs(&mut item) {
if !self.attrs(attrs)? {
continue;
}
}
match &mut item {
Item::Impl(v) => {
let mut keep = Vec::new();
for mut i in std::mem::take(&mut v.items) {
if let ImplItem::Fn(f) = &mut i {
if !self.attrs(&mut f.attrs)? {
continue;
}
}
keep.push(i);
}
v.items = keep;
}
Item::Trait(v) => {
let mut keep = Vec::new();
for mut i in std::mem::take(&mut v.items) {
if let syn::TraitItem::Fn(f) = &mut i {
if !self.attrs(&mut f.attrs)? {
continue;
}
}
keep.push(i);
}
v.items = keep;
}
Item::Struct(v) => self.fields(&mut v.fields)?,
Item::Enum(v) => {
let mut variants = syn::punctuated::Punctuated::new();
for mut variant in std::mem::take(&mut v.variants) {
if self.attrs(&mut variant.attrs)? {
self.fields(&mut variant.fields)?;
variants.push(variant);
}
}
v.variants = variants;
}
_ => {}
}
filtered.push(item);
}
*items = filtered;
Ok(())
}
fn fields(&self, fields: &mut syn::Fields) -> Result<()> {
let fields = match fields {
syn::Fields::Named(v) => &mut v.named,
syn::Fields::Unnamed(v) => &mut v.unnamed,
_ => return Ok(()),
};
let mut keep = syn::punctuated::Punctuated::new();
for mut field in std::mem::take(fields) {
if self.attrs(&mut field.attrs)? {
keep.push(field);
}
}
*fields = keep;
Ok(())
}
}
fn collect(items: &[Item], dir: &Path, modules: &[syn::Ident], api: &mut Api) -> Result<()> {
let mut items = items.to_vec();
if let Some(cfg) = &api.cfg {
cfg.filter(&mut items)?;
}
for item in &items {
match item {
Item::Use(u) if modules.is_empty() && matches!(u.vis, syn::Visibility::Public(_)) => {
use_names(&u.tree, &mut api.root_reexports)
}
Item::Struct(s) if derives(&s.attrs, &["Object"]) => {
insert_type(api, &s.ident, modules, Definition::Object)?
}
Item::Struct(s) if derives(&s.attrs, &["Record"]) => {
insert_type(api, &s.ident, modules, Definition::Record(s.clone()))?
}
Item::Enum(e) if derives(&e.attrs, &["Enum", "Error"]) => {
insert_type(api, &e.ident, modules, Definition::Enum(e.clone()))?
}
Item::Type(t) if matches!(t.vis, syn::Visibility::Public(_)) => {
insert_type(api, &t.ident, modules, Definition::Alias(t.clone()))?
}
Item::Impl(block)
if matches!(&*block.self_ty, Type::Path(_))
&& block.trait_.is_none()
&& block.attrs.iter().any(|a| is_attr(a, "export")) =>
{
if !block.generics.params.is_empty()
|| !block
.attrs
.iter()
.all(|a| is_attr(a, "export") || is_attr(a, "doc"))
{
bail!("Conditional or generic Core exports require an explicit web adapter");
}
for item in &block.items {
if let ImplItem::Fn(method) = item {
if matches!(method.vis, syn::Visibility::Public(_)) {
let Type::Path(owner) = &*block.self_ty else {
unreachable!()
};
let owner = owner.path.segments.last().unwrap().ident.to_string();
if owner == "Core" {
api.methods.push(method.clone());
} else {
api.object_methods
.entry(owner)
.or_default()
.push(method.clone());
}
}
}
}
}
Item::Trait(t) if t.attrs.iter().any(|a| is_attr(a, "export")) => {
insert_type(api, &t.ident, modules, Definition::Callback(t.clone()))?;
}
Item::Macro(m)
if m.mac.path.segments.last().is_some_and(|s| {
matches!(
s.ident.to_string().as_str(),
"custom_type" | "custom_newtype"
)
}) =>
{
let args = syn::parse2::<CustomArgs>(m.mac.tokens.clone())?;
insert_type(api, &args.name, modules, Definition::Custom(args.builtin))?;
}
Item::Fn(f) if f.attrs.iter().any(|a| is_attr(a, "export")) => {
if !matches!(f.vis, syn::Visibility::Public(_)) {
bail!("Exported function {} must be public", f.sig.ident);
}
let method = syn::ImplItemFn {
attrs: f
.attrs
.iter()
.filter(|a| !is_attr(a, "export"))
.cloned()
.collect(),
vis: f.vis.clone(),
defaultness: None,
sig: f.sig.clone(),
block: *f.block.clone(),
};
if api
.functions
.insert(f.sig.ident.to_string(), (modules.to_vec(), method))
.is_some()
{
bail!("Ambiguous exported function {}", f.sig.ident);
}
}
Item::Mod(module) => {
if module.attrs.iter().any(|a| is_attr(a, "cfg")) {
continue;
}
let name = module.ident.to_string();
let mut nested = modules.to_vec();
nested.push(module.ident.clone());
if let Some((_, items)) = &module.content {
collect(items, &dir.join(&name), &nested, api)?;
} else {
let flat = dir.join(format!("{name}.rs"));
let path = if let Some(attr) = module.attrs.iter().find(|a| is_attr(a, "path"))
{
let syn::Meta::NameValue(value) = &attr.meta else {
bail!("Invalid module path");
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(value),
..
}) = &value.value
else {
bail!("Invalid module path");
};
dir.join(value.value())
} else if flat.is_file() {
flat
} else {
dir.join(&name).join("mod.rs")
};
let parsed = syn::parse_file(
&fs::read_to_string(&path)
.with_context(|| format!("Reading {}", path.display()))?,
)?;
collect(&parsed.items, &dir.join(name), &nested, api)?;
}
}
_ => {}
}
}
Ok(())
}
fn scalar(ty: &Type) -> bool {
let Type::Path(p) = ty else {
return matches!(ty, Type::Tuple(t) if t.elems.is_empty());
};
p.path.segments.len() == 1
&& matches!(p.path.segments[0].arguments, PathArguments::None)
&& matches!(
p.path.segments[0].ident.to_string().as_str(),
"String"
| "bool"
| "u8"
| "u16"
| "u32"
| "u64"
| "i8"
| "i16"
| "i32"
| "i64"
| "f32"
| "f64"
)
}
fn generic<'a>(ty: &'a Type, name: &str) -> Option<Vec<&'a Type>> {
let Type::Path(p) = ty else { return None };
let last = p.path.segments.last()?;
if last.ident != name {
return None;
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
Some(
args.args
.iter()
.filter_map(|a| {
if let GenericArgument::Type(t) = a {
Some(t)
} else {
None
}
})
.collect(),
)
}
fn arc_receiver(receiver: &syn::Receiver) -> bool {
receiver.colon_token.is_some()
&& generic(&receiver.ty, "Arc").is_some_and(|args| {
args.len() == 1 && matches!(args[0],Type::Path(p) if p.path.is_ident("Self"))
})
}
struct Generator<'a> {
api: &'a Api,
module: syn::Ident,
}
impl Generator<'_> {
fn native(&self, name: &str) -> Result<TokenStream> {
if let Some(path) = self.api.native_paths.get(name) {
return Ok(quote!(#path));
}
let (modules, _) = self.api.types.get(name).with_context(|| {
format!(
"Unknown exported type {name}; derive uniffi::Record or uniffi::Enum on data types"
)
})?;
let module = &self.module;
let ident = format_ident!("{}", name);
if self.api.root_reexports.contains(name) {
Ok(quote!(#module::#ident))
} else {
Ok(quote!(#module::#(#modules::)*#ident))
}
}
fn mirror(&self, ty: &Type, stack: &mut HashSet<String>) -> Result<(TokenStream, String)> {
if let Type::Tuple(t) = ty {
let entries: Vec<_> = t
.elems
.iter()
.map(|t| self.mirror(t, stack))
.collect::<Result<_>>()?;
let rust: Vec<_> = entries.iter().map(|p| &p.0).collect();
let ts = if entries.is_empty() {
"null".to_string()
} else {
format!(
"[{}]",
entries
.iter()
.map(|p| p.1.as_str())
.collect::<Vec<_>>()
.join(", ")
)
};
return Ok((quote!((#(#rust,)*)), ts));
}
if let Type::Array(a) = ty {
let (inner, ts) = self.mirror(&a.elem, stack)?;
let len = &a.len;
return Ok((quote!([#inner; #len]), format!("Array<{ts}>")));
}
let Type::Path(path) = ty else {
bail!("Unsupported exported Rust type: {}", ty.to_token_stream());
};
let segment = path.path.segments.last().context("Empty type path")?;
let name = segment.ident.to_string();
if name == "JffiBytes" {
return Ok((quote!(jffi_bytes::Bytes), "JffiBytes".into()));
}
if matches!(name.as_str(), "SystemTime" | "Duration") {
let (rust, ts) = if name == "SystemTime" {
(quote!(jffi_time::Timestamp), "JffiTimestamp")
} else {
(quote!(jffi_time::Duration), "JffiDuration")
};
return Ok((rust, ts.into()));
}
if scalar(ty) {
return Ok((
quote!(#ty),
match name.as_str() {
"String" => "string",
"bool" => "boolean",
"i64" | "u64" => "bigint",
_ => "number",
}
.into(),
));
}
for container in ["Arc", "Box"] {
if let Some(args) = generic(ty, container) {
if args.len() == 1 {
if let Some(name) = callback_name(args[0]) {
if !matches!(
self.api.types.get(&name),
Some((_, Definition::Callback(_)))
) {
bail!("Unknown exported callback trait {name}");
}
let ident = format_ident!("JffiCallback{}Ref", name);
return Ok((quote!(jffi_types::#ident), name));
}
}
}
}
if let Some(args) = generic(ty, "Arc") {
if args.len() != 1 {
bail!("Invalid Arc type");
}
let Type::Path(path) = args[0] else {
bail!("Arc must reference an exported object");
};
let name = path.path.segments.last().unwrap().ident.to_string();
if !matches!(self.api.types.get(&name), Some((_, Definition::Object))) {
bail!("Arc<{name}> is not an exported UniFFI object");
}
let ident = format_ident!("JffiObject{}Ref", name);
return Ok((quote!(jffi_types::#ident), name));
}
for container in ["Option", "Vec", "HashMap", "BTreeMap", "Box"] {
if let Some(args) = generic(ty, container) {
let expected = if container.ends_with("Map") { 2 } else { 1 };
if args.len() != expected {
bail!("Invalid {container} type arguments");
}
let (first, ts) = self.mirror(args[0], stack)?;
return Ok(match container {
"Option" => (quote!(Option<#first>), format!("{ts} | null")),
"Vec" => (quote!(Vec<#first>), format!("Array<{ts}>")),
"Box" => (quote!(Box<#first>), ts),
_ => {
let (second, value_ts) = self.mirror(args[1], stack)?;
let container = format_ident!("{}", container);
(
quote!(std::collections::#container<#first, #second>),
format!("Map<{ts}, {value_ts}>"),
)
}
});
}
}
if !matches!(segment.arguments, PathArguments::None) {
bail!(
"Unsupported generic exported type: {}",
ty.to_token_stream()
);
}
let (_, definition) = self.api.types.get(&name).with_context(|| format!("No exported data definition for {name}; opaque objects/callbacks need an explicit web adapter"))?;
if let Definition::Custom(_) = definition {
let ident = format_ident!("JffiCustom{}", name);
return Ok((quote!(jffi_types::#ident), name));
}
if let Definition::Alias(alias) = definition {
if !stack.insert(name.clone()) {
bail!("Recursive type alias: {name}");
}
let (rust, _) = self.mirror(&alias.ty, stack)?;
stack.remove(&name);
return Ok((rust, name));
}
Ok((quote!(jffi_types::#segment), name))
}
fn convert(&self, ty: &Type, value: TokenStream, into_native: bool) -> Result<TokenStream> {
if scalar(ty) {
return Ok(value);
}
if let Type::Tuple(tuple) = ty {
let values = tuple
.elems
.iter()
.enumerate()
.map(|(i, ty)| {
let i = syn::Index::from(i);
self.convert(ty, quote!(#value.#i), into_native)
})
.collect::<Result<Vec<_>>>()?;
return Ok(quote!((#(#values,)*)));
}
if let Type::Array(array) = ty {
let inner = self.convert(&array.elem, quote!(item), into_native)?;
return Ok(quote!((#value).map(|item| #inner)));
}
for container in ["Arc", "Box"] {
if let Some(args) = generic(ty, container) {
if args.len() == 1 {
if let Some(name) = callback_name(args[0]) {
let ident = format_ident!("JffiCallback{}Ref", name);
let shared = format_ident!("JffiShared{}", name);
let native = self.native(&name)?;
return Ok(match (container, into_native) {
("Arc", true) => quote!((#value).0),
("Arc", false) => quote!(jffi_types::#ident(#value)),
("Box", true) => {
quote!(Box::new(jffi_types::#shared((#value).0)) as Box<dyn #native>)
}
_ => quote!(jffi_types::#ident(std::sync::Arc::from(#value))),
});
}
}
}
}
if let Some(args) = generic(ty, "Arc") {
let Type::Path(path) = args[0] else {
bail!("Arc must reference an exported object");
};
let name = path.path.segments.last().unwrap().ident.to_string();
let ident = format_ident!("JffiObject{}Ref", name);
return Ok(if into_native {
quote!((#value).0)
} else {
quote!(jffi_types::#ident(#value))
});
}
for name in ["Option", "Vec", "HashMap", "BTreeMap", "Box"] {
if let Some(args) = generic(ty, name) {
let inner = self.convert(args[0], quote!(item), into_native)?;
return Ok(match name {
"Option" => quote!((#value).map(|item| #inner)),
"Vec" => quote!((#value).into_iter().map(|item| #inner).collect()),
"Box" => {
let inner = self.convert(args[0], quote!(*#value), into_native)?;
quote!(Box::new(#inner))
}
_ => {
let k = self.convert(args[0], quote!(key), into_native)?;
let v = self.convert(args[1], quote!(val), into_native)?;
quote!((#value).into_iter().map(|(key, val)| (#k, #v)).collect())
}
});
}
}
let Type::Path(path) = ty else {
bail!("Unsupported conversion");
};
let name = path.path.segments.last().unwrap().ident.to_string();
if name == "JffiBytes" {
return Ok(if into_native {
quote!((#value).0)
} else {
quote!(jffi_bytes::Bytes(#value))
});
}
if matches!(name.as_str(), "SystemTime" | "Duration") {
let wrapper = if name == "SystemTime" {
quote!(jffi_time::Timestamp)
} else {
quote!(jffi_time::Duration)
};
return Ok(if into_native {
quote!((#value).0)
} else {
quote!(#wrapper(#value))
});
}
if let Some((_, Definition::Alias(alias))) = self.api.types.get(&name) {
return self.convert(&alias.ty, value, into_native);
}
if matches!(self.api.types.get(&name), Some((_, Definition::Custom(_)))) {
let ident = format_ident!("JffiCustom{}", name);
return Ok(if into_native {
quote!((#value).0.into_inner().expect("validated custom value"))
} else {
quote!(jffi_types::#ident(std::cell::RefCell::new(Some(#value))))
});
}
if into_native {
let native = self.native(&name)?;
Ok(quote!(#native::from(#value)))
} else {
let ident = format_ident!("{}", name);
Ok(quote!(jffi_types::#ident::from(#value)))
}
}
fn fields(
&self,
fields: &syn::Fields,
) -> Result<(TokenStream, String, Vec<TokenStream>, Vec<TokenStream>)> {
let mut definitions = Vec::new();
let mut ts = Vec::new();
let mut to_native = Vec::new();
let mut from_native = Vec::new();
let mut names = HashSet::new();
for (i, field) in fields.iter().enumerate() {
let binding = field
.ident
.clone()
.unwrap_or_else(|| format_ident!("field_{i}"));
let (ty, ts_type) = self.mirror(&field.ty, &mut HashSet::new())?;
let a = self.convert(&field.ty, quote!(#binding), true)?;
let b = self.convert(&field.ty, quote!(#binding), false)?;
if let Some(ident) = &field.ident {
let js = camel(&ident.to_string());
if !names.insert(js.clone()) {
bail!("Conflicting field name {js}");
}
definitions.push(quote!(#ident: #ty));
ts.push(format!("{js}: {ts_type}"));
to_native.push(quote!(#ident: #a));
from_native.push(quote!(#ident: #b));
} else {
definitions.push(quote!(#ty));
ts.push(ts_type);
to_native.push(a);
from_native.push(b);
}
}
Ok(match fields {
syn::Fields::Named(_) => (
quote!({#(#definitions,)*}),
format!("{{ {} }}", ts.join("; ")),
to_native,
from_native,
),
syn::Fields::Unnamed(_) => (
quote!((#(#definitions,)*)),
format!("[{}]", ts.join(", ")),
to_native,
from_native,
),
syn::Fields::Unit => (quote!(), "null".into(), to_native, from_native),
})
}
fn native_type(&self, ty: &Type) -> Result<TokenStream> {
match ty {
Type::Tuple(t) => {
let items = t
.elems
.iter()
.map(|t| self.native_type(t))
.collect::<Result<Vec<_>>>()?;
Ok(quote!((#(#items,)*)))
}
Type::Reference(r) => {
let elem = self.native_type(&r.elem)?;
let lifetime = &r.lifetime;
let mutability = &r.mutability;
Ok(quote!(&#lifetime #mutability #elem))
}
Type::TraitObject(_) => {
let name = callback_name(ty).context("Callback trait is missing")?;
let native = self.native(&name)?;
Ok(quote!(dyn #native))
}
Type::Path(path) => {
let last = path.path.segments.last().context("Empty type")?;
let name = last.ident.to_string();
if name == "JffiBytes" {
return Ok(quote!(Vec<u8>));
}
if name == "SystemTime" {
return Ok(quote!(std::time::SystemTime));
}
if name == "Duration" {
return Ok(quote!(std::time::Duration));
}
if self.api.types.contains_key(&name)
&& matches!(last.arguments, PathArguments::None)
{
return self.native(&name);
}
for container in [
"Option", "Result", "Vec", "HashMap", "BTreeMap", "Arc", "Box",
] {
if let Some(args) = generic(ty, container) {
let args = args
.iter()
.map(|t| self.native_type(t))
.collect::<Result<Vec<_>>>()?;
let container = match container {
"HashMap" => quote!(std::collections::HashMap),
"BTreeMap" => quote!(std::collections::BTreeMap),
"Arc" => quote!(std::sync::Arc),
"Box" => quote!(Box),
"Option" => quote!(Option),
"Result" => quote!(Result),
_ => quote!(Vec),
};
return Ok(quote!(#container<#(#args),*>));
}
}
Ok(quote!(#ty))
}
_ => Ok(quote!(#ty)),
}
}
fn custom_type(&self, name: &str, builtin: &Type) -> Result<(TokenStream, String)> {
let native = self.native(name)?;
let module = &self.module;
let native_builtin = self.native_type(builtin)?;
let (mirror, ts) = self.mirror(builtin, &mut HashSet::from([name.to_string()]))?;
let to_native = self.convert(builtin, quote!(value), true)?;
let from_native = self.convert(builtin, quote!(value), false)?;
let ident = format_ident!("JffiCustom{}", name);
Ok((
quote! {
pub struct #ident(pub std::cell::RefCell<Option<#native>>);
impl serde::Serialize for #ident {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok,S::Error> {
use serde::ser::Error;
let native=self.0.borrow_mut().take().ok_or_else(||S::Error::custom("Custom wire value already consumed"))?;
let mut bytes=Vec::new();
<#native as uniffi::FfiConverter<#module::UniFfiTag>>::write(native,&mut bytes);
let value=<#native_builtin as uniffi::FfiConverter<#module::UniFfiTag>>::try_read(&mut bytes.as_slice()).map_err(|error|S::Error::custom(format!("{error:#}")))?;
let value: #mirror=#from_native;
serde::Serialize::serialize(&value,serializer)
}
}
impl<'de> serde::Deserialize<'de> for #ident {
fn deserialize<D: serde::Deserializer<'de>>(deserializer:D)->Result<Self,D::Error> {
use serde::de::Error;
let value=<#mirror as serde::Deserialize>::deserialize(deserializer)?;
let value: #native_builtin=#to_native;
let mut bytes=Vec::new();
<#native_builtin as uniffi::FfiConverter<#module::UniFfiTag>>::write(value,&mut bytes);
let value=<#native as uniffi::FfiConverter<#module::UniFfiTag>>::try_read(&mut bytes.as_slice()).map_err(|error|D::Error::custom(format!("{error:#}")))?;
Ok(Self(std::cell::RefCell::new(Some(value))))
}
}
},
format!("export type {name} = {ts};\n"),
))
}
fn callback_type(&self, name: &str, trait_: &syn::ItemTrait) -> Result<(TokenStream, String)> {
if !trait_.generics.params.is_empty() {
bail!("Generic callback {name} is not an exported concrete interface");
}
let native = self.native(name)?;
let module = &self.module;
let adapter = format_ident!("JffiJs{}", name);
let shared = format_ident!("JffiShared{}", name);
let reference = format_ident!("JffiCallback{}Ref", name);
let registry = format_ident!("JFFI_CALLBACKS_{}", helper_key(name).to_uppercase());
let counter = format_ident!("JFFI_CALLBACK_COUNTER_{}", helper_key(name).to_uppercase());
let mut js_impl = Vec::new();
let mut delegates = Vec::new();
let mut exports = Vec::new();
let mut checks = Vec::new();
let mut declarations = Vec::new();
let mut names = HashSet::new();
let mut has_async = false;
for item in &trait_.items {
let syn::TraitItem::Fn(method) = item else {
bail!("Unsupported callback item in {name}");
};
let sig = &method.sig;
let method_name = &sig.ident;
if sig.unsafety.is_some() || !sig.generics.params.is_empty() {
bail!("Unsupported generic/unsafe callback {name}::{method_name}");
}
let mut inputs = sig.inputs.iter();
let Some(FnArg::Receiver(receiver)) = inputs.next() else {
bail!("Callback {name}::{method_name} needs a receiver");
};
let by_arc = arc_receiver(receiver);
if !by_arc && !(receiver.reference.is_some() && receiver.mutability.is_none()) {
bail!("Callback {name}::{method_name} requires &self or self: Arc<Self>");
}
let receiver = if by_arc {
quote!(self:std::sync::Arc<Self>)
} else {
quote!(&self)
};
let js_name = camel(
self.api
.js_members
.get(&format!("{name}::{method_name}"))
.map(String::as_str)
.unwrap_or(&method_name.to_string()),
);
if !names.insert(js_name.clone()) {
bail!("Conflicting callback method {name}::{js_name}");
}
let mut native_args = Vec::new();
let mut arg_names = Vec::new();
let mut js_args = Vec::new();
let mut native_conversions = Vec::new();
let mut closure_args = Vec::new();
let mut ts_args = Vec::new();
for input in inputs {
let FnArg::Typed(arg) = input else {
bail!("Unsupported callback receiver");
};
let syn::Pat::Ident(pat) = &*arg.pat else {
bail!("Callback arguments need names");
};
let arg_name = &pat.ident;
let arg_ty = &arg.ty;
let native_ty = self.native_type(arg_ty)?;
let (mirror, ts) = self.mirror(arg_ty, &mut HashSet::new())?;
native_args.push(quote!(#arg_name:#native_ty));
arg_names.push(quote!(#arg_name));
ts_args.push(format!("{}: {ts}", camel(&arg_name.to_string())));
let into_js = self.convert(arg_ty, quote!(#arg_name), false)?;
js_args.push(
quote! { let value: #mirror=#into_js; args.push(&jffi_serialize(&value)?); },
);
closure_args.push(quote!(#arg_name:JsValue));
let into_native = self.convert(arg_ty, quote!(#arg_name), true)?;
native_conversions.push(quote! {
let #arg_name: #mirror=serde_wasm_bindgen::from_value(#arg_name).map_err(|e|js_sys::TypeError::new(&e.to_string()))?;
let #arg_name=#into_native;
});
}
let unit: Type = syn::parse_quote!(());
let output = match &sig.output {
ReturnType::Default => &unit,
ReturnType::Type(_, t) => t,
};
let native_output = self.native_type(output)?;
let fallible = generic(output, "Result");
let actual = if let Some(ref args) = fallible {
args[0]
} else {
output
};
let (mirror_output, ts_output) = self.mirror(actual, &mut HashSet::new())?;
let to_native = self.convert(actual, quote!(value), true)?;
let to_js = self.convert(actual, quote!(value), false)?;
let asyncness = &sig.asyncness;
let await_token = asyncness.map(|_| quote!(.await));
has_async |= asyncness.is_some();
let ts_output = if matches!(actual,Type::Tuple(t)if t.elems.is_empty()) {
"void".into()
} else {
ts_output
};
let ts_output = if asyncness.is_some() {
format!("Promise<{ts_output}>")
} else {
ts_output
};
declarations.push(format!(" {js_name}({}): {ts_output};", ts_args.join(", ")));
checks.push(quote! {
let method=js_sys::Reflect::get(&value,&JsValue::from_str(#js_name)).map_err(|_|D::Error::custom(concat!("Cannot read callback method ",#js_name)))?;
if !method.is_function() {return Err(D::Error::custom(concat!("Missing callback method ",#js_name)));}
});
let call_js = quote! {
let callback=#registry.with(|r|r.borrow().get(&self.id).cloned()).ok_or_else(||js_sys::Error::new("Callback has been released"))?;
let method=js_sys::Reflect::get(&callback,&JsValue::from_str(#js_name))?.dyn_into::<js_sys::Function>().map_err(|_|js_sys::TypeError::new("Callback member is not a function"))?;
let args=js_sys::Array::new(); #(#js_args)*
method.apply(&callback,&args)
};
let read_return = if matches!(actual,Type::Tuple(t)if t.elems.is_empty()) {
quote!(let _=value; Ok(()))
} else {
quote! {
let value: #mirror_output=serde_wasm_bindgen::from_value(value).map_err(|e|js_sys::TypeError::new(&e.to_string()))?;
Ok(#to_native)
}
};
let invoke = if asyncness.is_some() {
quote!((async {
let pending={let value=(|| -> Result<JsValue,JsValue> {#call_js})()?; jffi_async::begin(value)};
let value=pending.await?;
#read_return
}).await)
} else {
quote!((|| {let value=(|| -> Result<JsValue,JsValue> {#call_js})()?; #read_return})())
};
let expected_error = if let Some(ref args) = fallible {
let (mirror_error, _) = self.mirror(args[1], &mut HashSet::new())?;
let error = self.convert(args[1], quote!(details), true)?;
quote! {
if let Ok(details)=js_sys::Reflect::get(&error,&JsValue::from_str("details")) {
if let Ok(details)=serde_wasm_bindgen::from_value::<#mirror_error>(details) {return Err(#error);}
}
}
} else {
quote!()
};
let success = if fallible.is_some() {
quote!(Ok(value))
} else {
quote!(value)
};
js_impl.push(quote! {
#asyncness fn #method_name(#receiver,#(#native_args),*) -> #native_output {
let outcome: Result<_,JsValue>=#invoke;
match outcome {Ok(value)=>#success,Err(error)=>{
#expected_error
<#native_output as uniffi::LiftReturn<#module::UniFfiTag>>::handle_callback_unexpected_error(uniffi::UnexpectedUniFFICallbackError::new(format!("{:?}",error)))
}}
}
});
delegates.push(quote!(#asyncness fn #method_name(#receiver,#(#native_args),*) -> #native_output { self.0.clone().#method_name(#(#arg_names),*) #await_token }));
let native_call = quote!(callback.clone().#method_name(#(#arg_names),*) #await_token);
let native_call = if let Some(ref types) = fallible {
let (mirror, _) = self.mirror(types[1], &mut HashSet::new())?;
let err = self.convert(types[1], quote!(error), false)?;
quote! {match #native_call {Ok(value)=>value,Err(error)=>{
let details:#mirror=#err;
let error=js_sys::Error::new("Rust callback returned an error");
js_sys::Reflect::set(error.as_ref(),&JsValue::from_str("details"),&jffi_serialize(&details)?)?;
return Err(error.into());
}}}
} else {
native_call
};
let body = quote! {#(#native_conversions)* let value=#native_call; let value:#mirror_output=#to_js; jffi_serialize(&value)};
let count = arg_names.len();
let js_types = (0..count).map(|_| quote!(JsValue)).collect::<Vec<_>>();
let variadic = count > 8;
let extracts = arg_names
.iter()
.enumerate()
.map(|(i, arg)| {
let i = i as u32;
quote!(let #arg=arguments.get(#i);)
})
.collect::<Vec<_>>();
let closure = if variadic && asyncness.is_some() {
quote! {
let function=wasm_bindgen::closure::Closure::wrap(Box::new(move |arguments:js_sys::Array| -> js_sys::Promise {
let callback=callback.clone();wasm_bindgen_futures::future_to_promise(async move {#(#extracts)* #body})
}) as Box<dyn Fn(js_sys::Array)->js_sys::Promise>);
}
} else if variadic {
quote! {
let function=wasm_bindgen::closure::Closure::wrap(Box::new(move |arguments:js_sys::Array| -> Result<JsValue,JsValue> {#(#extracts)* #body}) as Box<dyn Fn(js_sys::Array)->Result<JsValue,JsValue>>);
}
} else if asyncness.is_some() {
quote! {
let function=wasm_bindgen::closure::Closure::wrap(Box::new(move |#(#closure_args),*| -> js_sys::Promise {
let callback=callback.clone(); wasm_bindgen_futures::future_to_promise(async move {#body})
}) as Box<dyn Fn(#(#js_types),*) -> js_sys::Promise>);
}
} else {
quote! {
let function=wasm_bindgen::closure::Closure::wrap(Box::new(move |#(#closure_args),*| -> Result<JsValue,JsValue> {#body}) as Box<dyn Fn(#(#js_types),*) -> Result<JsValue,JsValue>>);
}
};
let adapt = variadic.then(|| quote!(let function=jffi_callback_variadic(&function);));
exports.push(quote! {
let callback=self.0.clone(); #closure
let function=function.into_js_value(); #adapt
js_sys::Reflect::set(object.as_ref(),&JsValue::from_str(#js_name),&function).map_err(|_|S::Error::custom("Unable to create callback object"))?;
});
}
let non_send = trait_.attrs.iter().any(|a| {
let text = a.meta.to_token_stream().to_string();
(is_attr(a, "async_trait")
|| (is_attr(a, "cfg_attr")
&& text.contains("target_arch")
&& text.contains("wasm32")))
&& text.contains("? Send")
});
let async_attribute = has_async.then(|| {
if non_send {
quote!(#[async_trait::async_trait(?Send)])
} else {
quote!(#[async_trait::async_trait])
}
});
Ok((
quote! {
thread_local! {
static #registry:std::cell::RefCell<std::collections::BTreeMap<u32,JsValue>>=std::cell::RefCell::new(std::collections::BTreeMap::new());
static #counter:std::cell::Cell<u32>=const{std::cell::Cell::new(0)};
}
#[derive(Debug)] struct #adapter {id:u32}
impl Drop for #adapter {fn drop(&mut self){#registry.with(|r|{r.borrow_mut().remove(&self.id);});}}
#async_attribute impl #native for #adapter {#(#js_impl)*}
pub struct #shared(pub std::sync::Arc<dyn #native>);
#async_attribute impl #native for #shared {#(#delegates)*}
pub struct #reference(pub std::sync::Arc<dyn #native>);
impl serde::Serialize for #reference {
fn serialize<S:serde::Serializer>(&self,serializer:S)->Result<S::Ok,S::Error>{
use serde::ser::Error;
let object=js_sys::Object::new(); #(#exports)*
serde_wasm_bindgen::preserve::serialize(&JsValue::from(object),serializer)
}
}
impl<'de> serde::Deserialize<'de> for #reference {
fn deserialize<D:serde::Deserializer<'de>>(deserializer:D)->Result<Self,D::Error>{
use serde::de::Error;
let value:JsValue=serde_wasm_bindgen::preserve::deserialize(deserializer)?;
if !value.is_object(){return Err(D::Error::custom("Expected a callback object"));}
#(#checks)*
let id=#counter.with(|c|{let id=c.get().checked_add(1).expect("Callback handle limit reached");c.set(id);id});
#registry.with(|r|{r.borrow_mut().insert(id,value);});
Ok(Self(std::sync::Arc::new(#adapter{id})))
}
}
},
format!(
"export interface {name} {{\n{}\n}}\n",
declarations.join("\n")
),
))
}
fn data_types(&self) -> Result<(TokenStream, String)> {
let mut rust = Vec::new();
let mut ts = "export interface JffiRustError<T = unknown> extends Error { details: T }\n"
.to_string();
for (name, (_, definition)) in &self.api.types {
let ident = format_ident!("{}", name);
let native = self.native(name)?;
match definition {
Definition::Custom(builtin) => {
let (code, declaration) = self.custom_type(name, builtin)?;
rust.push(code);
ts += &declaration;
}
Definition::Callback(trait_) => {
let (code, declaration) = self.callback_type(name, trait_)?;
rust.push(code);
ts += &declaration;
}
Definition::Object => {
let reference = format_ident!("JffiObject{}Ref", name);
let wrap = format_ident!("jffi_wrap_{}", helper_key(name));
let registry =
format_ident!("JFFI_REGISTRY_{}", helper_key(name).to_uppercase());
rust.push(quote! {
pub struct #reference(pub std::sync::Arc<#native>);
impl serde::Serialize for #reference {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let value = JsValue::from(#wrap(self.0.clone()));
serde_wasm_bindgen::preserve::serialize(&value, serializer)
}
}
impl<'de> serde::Deserialize<'de> for #reference {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let value: JsValue = serde_wasm_bindgen::preserve::deserialize(deserializer)?;
let kind = js_sys::Reflect::get(&value, &JsValue::from_str("__jffiType")).map_err(|_|D::Error::custom("Expected an exported object"))?;
if kind.as_string().as_deref() != Some(#name) { return Err(D::Error::custom(concat!("Expected ", #name))); }
let id = js_sys::Reflect::get(&value, &JsValue::from_str("__jffiHandle")).map_err(|_|D::Error::custom("Invalid object handle"))?
.as_f64().filter(|n| *n >= 1.0 && *n <= u32::MAX as f64 && n.fract() == 0.0).ok_or_else(||D::Error::custom("Invalid object handle"))? as u32;
#registry.with(|registry| registry.borrow().get(&id).cloned()).map(Self).ok_or_else(||D::Error::custom("Object has been freed"))
}
}
});
}
Definition::Alias(a) => {
let (_, ty) = self.mirror(&a.ty, &mut HashSet::new())?;
ts += &format!("export type {name} = {ty};\n");
}
Definition::Record(record) => {
if record.attrs.iter().any(|a| is_attr(a, "cfg"))
|| record
.fields
.iter()
.any(|f| f.attrs.iter().any(|a| is_attr(a, "cfg")))
{
bail!("Conditional record {name} requires an explicit web adapter");
}
if !record.generics.params.is_empty() {
bail!("Generic record {name} requires a concrete exported type");
}
let (fields, ts_fields, to_native, from_native) =
self.fields(&record.fields)?;
let bindings: Vec<_> = record
.fields
.iter()
.enumerate()
.map(|(i, f)| {
f.ident
.clone()
.unwrap_or_else(|| format_ident!("field_{i}"))
})
.collect();
let (pattern, native_ctor, dto_ctor, suffix) = match &record.fields {
syn::Fields::Named(_) => (
quote!({#(#bindings,)*}),
quote!(#native {#(#to_native,)*}),
quote!(Self {#(#from_native,)*}),
quote!(),
),
syn::Fields::Unnamed(_) => (
quote!((#(#bindings,)*)),
quote!(#native (#(#to_native,)*)),
quote!(Self (#(#from_native,)*)),
quote!(;),
),
syn::Fields::Unit => (quote!(), quote!(#native), quote!(Self), quote!(;)),
};
rust.push(quote! {
#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")]
pub struct #ident #fields #suffix
impl From<#ident> for #native { fn from(value: #ident) -> Self { let #ident #pattern = value; #native_ctor } }
impl From<#native> for #ident { fn from(value: #native) -> Self { let #native #pattern = value; #dto_ctor } }
});
ts += &format!("export type {name} = {ts_fields};\n");
}
Definition::Enum(en) => {
if !en.generics.params.is_empty() {
bail!("Generic enum {name} requires concrete types");
}
let mut variants = Vec::new();
let mut to_native_arms = Vec::new();
let mut from_native_arms = Vec::new();
let mut ts_variants = Vec::new();
for variant in &en.variants {
let v = &variant.ident;
let native_v = self
.api
.native_members
.get(&format!("{name}::{v}"))
.unwrap_or(v);
let (fields, ts_fields, to_native, from_native) =
self.fields(&variant.fields)?;
variants.push(quote!(#v #fields));
let bindings: Vec<_> = variant
.fields
.iter()
.enumerate()
.map(|(i, f)| {
f.ident
.clone()
.unwrap_or_else(|| format_ident!("field_{i}"))
})
.collect();
let (pattern, native_ctor, dto_ctor) = match &variant.fields {
syn::Fields::Named(_) => (
quote!({#(#bindings,)*}),
quote!(#native::#native_v {#(#to_native,)*}),
quote!(Self::#v {#(#from_native,)*}),
),
syn::Fields::Unnamed(_) => (
quote!((#(#bindings,)*)),
quote!(#native::#native_v (#(#to_native,)*)),
quote!(Self::#v (#(#from_native,)*)),
),
syn::Fields::Unit => {
(quote!(), quote!(#native::#native_v), quote!(Self::#v))
}
};
to_native_arms.push(quote!(#ident::#v #pattern => #native_ctor));
from_native_arms.push(quote!(#native::#native_v #pattern => #dto_ctor));
let value = if matches!(variant.fields, syn::Fields::Unit) {
String::new()
} else if variant.fields.len() == 1
&& matches!(variant.fields, syn::Fields::Unnamed(_))
{
let (_, ty) = self.mirror(
&variant.fields.iter().next().unwrap().ty,
&mut HashSet::new(),
)?;
format!("; value: {ty}")
} else {
format!("; value: {ts_fields}")
};
ts_variants.push(format!("{{ tag: \"{v}\"{value} }}"));
}
rust.push(quote! {
#[derive(serde::Serialize, serde::Deserialize)] #[serde(tag = "tag", content = "value", rename_all_fields = "camelCase")]
pub enum #ident { #(#variants,)* }
impl From<#ident> for #native { fn from(value: #ident) -> Self { match value { #(#to_native_arms,)* } } }
impl From<#native> for #ident { fn from(value: #native) -> Self { match value { #(#from_native_arms,)* } } }
});
ts += &format!("export type {name} = {};\n", ts_variants.join(" | "));
}
}
}
Ok((
quote!(#[allow(dead_code)] mod jffi_types { use super::*; #(#rust)* }),
ts,
))
}
}
fn render(module: &str, api: &Api) -> Result<String> {
if let Some(contract) = &api.abi {
return abi::render(
&api.module_alias
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| module.to_string()),
contract,
);
}
let module = api
.module_alias
.clone()
.unwrap_or(syn::parse_str::<syn::Ident>(module)?);
let generator = Generator {
api,
module: module.clone(),
};
let (data_types, ts) = generator.data_types()?;
let mut externs = Vec::new();
let mut classes = Vec::new();
let empty_methods = Vec::new();
let functions: Vec<_> = api.functions.values().map(|(_, f)| f.clone()).collect();
let core_methods = std::iter::once(("Core".to_string(), &api.methods)).filter(|_| {
!api.methods.is_empty() || matches!(api.types.get("Core"), Some((_, Definition::Object)))
});
let others = api
.types
.iter()
.filter(|(name, (_, def))| name.as_str() != "Core" && matches!(def, Definition::Object))
.map(|(name, _)| {
(
name.clone(),
api.object_methods.get(name).unwrap_or(&empty_methods),
)
});
for (class_name, methods) in core_methods
.chain(others)
.chain(std::iter::once(("JffiFunctions".to_string(), &functions)))
{
let standalone = class_name == "JffiFunctions";
let class = format_ident!("{}", class_name);
let wrapper = format_ident!("Ffi{}", class_name);
let native = if standalone {
quote!(#module)
} else if class_name == "Core" && !api.native_paths.contains_key("Core") {
quote!(#module::Core)
} else {
generator.native(&class_name)?
};
let wrap = format_ident!("jffi_wrap_{}", helper_key(&class_name));
let registry = format_ident!("JFFI_REGISTRY_{}", helper_key(&class_name).to_uppercase());
let counter = format_ident!("JFFI_COUNTER_{}", helper_key(&class_name).to_uppercase());
let mut generated = Vec::new();
let mut names = HashSet::new();
let mut constructor = false;
for (index, method) in methods.iter().enumerate() {
let sig = &method.sig;
let name = &sig.ident;
let native_name = api
.native_members
.get(&format!("{class_name}::{name}"))
.unwrap_or(name);
let unsupported = || {
anyhow::anyhow!("Cannot generate web binding for {class_name}::{name}: generic/unsafe/conditional methods or unsupported receivers require an explicit adapter")
};
if sig.unsafety.is_some()
|| sig.abi.is_some()
|| !sig.generics.params.is_empty()
|| sig.generics.where_clause.is_some()
|| sig.variadic.is_some()
|| !method
.attrs
.iter()
.all(|a| is_attr(a, "doc") || is_attr(a, "constructor"))
{
return Err(unsupported());
}
let is_constructor = method.attrs.iter().any(|a| is_attr(a, "constructor"));
let mut inputs = sig.inputs.iter();
if !is_constructor && !standalone {
let Some(FnArg::Receiver(receiver)) = inputs.next() else {
return Err(unsupported());
};
let by_arc = arc_receiver(receiver);
if !by_arc && !(receiver.reference.is_some() && receiver.mutability.is_none()) {
return Err(unsupported());
}
}
let mut args = Vec::new();
let mut declarations = Vec::new();
let mut conversions = Vec::new();
for (arg_index, input) in inputs.enumerate() {
let FnArg::Typed(arg) = input else {
return Err(unsupported());
};
let syn::Pat::Ident(pattern) = &*arg.pat else {
return Err(unsupported());
};
if pattern.by_ref.is_some() || pattern.subpat.is_some() {
return Err(unsupported());
}
let ident = &pattern.ident;
let borrowed = matches!(&*arg.ty, Type::Reference(_));
let ty = if let Type::Reference(reference) = &*arg.ty {
&reference.elem
} else {
&arg.ty
};
let argument = if borrowed {
quote!(&#ident)
} else {
quote!(#ident)
};
let (mirror, ts_type) = generator
.mirror(ty, &mut HashSet::new())
.with_context(|| format!("{class_name}::{name} argument {ident}"))?;
if scalar(ty) {
declarations.push(quote!(#ident: #ty));
args.push(argument.clone());
} else {
let js_type = format_ident!("JffiArg{class_name}{index}_{arg_index}");
externs.push(
quote!(#[wasm_bindgen(typescript_type = #ts_type)] pub type #js_type;),
);
declarations.push(quote!(#ident: #js_type));
let native = generator.convert(ty, quote!(#ident), true)?;
conversions.push(quote! {
let #ident: #mirror = serde_wasm_bindgen::from_value(#ident.into()).map_err(|e| js_sys::TypeError::new(&format!("{}: {}", stringify!(#ident), e)))?;
let #ident = #native;
});
args.push(argument.clone());
}
}
if is_constructor {
let output = match &sig.output {
ReturnType::Type(_, ty) => &**ty,
_ => return Err(unsupported()),
};
let fallible = generic(output, "Result");
let actual = if let Some(ref args) = fallible {
if args.len() != 2 {
return Err(unsupported());
}
args[0]
} else {
output
};
let arc = generic(actual, "Arc");
let actual_class = if let Some(ref args) = arc {
if args.len() != 1 {
return Err(unsupported());
}
args[0]
} else {
actual
};
if !matches!(actual_class, Type::Path(p) if p.path.is_ident("Self") || p.path.is_ident(&class_name))
{
return Err(unsupported());
}
let await_token = sig.asyncness.map(|_| quote!(.await));
let call = quote!(#native::#native_name(#(#args),*) #await_token);
let call = if let Some(ref types) = fallible {
let (mirror, _) = generator.mirror(types[1], &mut HashSet::new())?;
let error = generator.convert(types[1], quote!(error), false)?;
quote! { match #call { Ok(value) => value, Err(error) => {
let details: #mirror = #error;
let error = js_sys::Error::new("Rust constructor failed");
js_sys::Reflect::set(error.as_ref(), &JsValue::from_str("details"), &jffi_serialize(&details)?)?;
return Err(error.into());
} } }
} else {
call
};
let create = quote!({ let core: std::sync::Arc<#native> = (#call).into(); core });
let asyncness = &sig.asyncness;
if name == "new" && sig.asyncness.is_none() {
if constructor {
return Err(unsupported());
}
constructor = true;
generated.push(quote! {
#[wasm_bindgen(constructor)] pub fn new(#(#declarations),*) -> Result<Self, JsValue> {
#(#conversions)* Ok(#wrap(#create))
}
});
} else {
let js_name = syn::parse_str::<syn::Ident>(&camel(&name.to_string()))?;
generated.push(quote! {
#[wasm_bindgen(js_name = #js_name)] pub #asyncness fn #name(#(#declarations),*) -> Result<#wrapper, JsValue> {
#(#conversions)* Ok(#wrap(#create))
}
});
}
continue;
}
let js_name = camel(&name.to_string());
if (!standalone && matches!(js_name.as_str(), "free" | "constructor"))
|| !names.insert(js_name.clone())
{
bail!("Conflicting JavaScript method name: {js_name}");
}
let js_name = syn::parse_str::<syn::Ident>(&js_name)?;
let unit: Type = syn::parse_quote!(());
let output = match &sig.output {
ReturnType::Default => &unit,
ReturnType::Type(_, t) => t,
};
let fallible = generic(output, "Result");
let result_ty = if let Some(ref args) = fallible {
if args.len() != 2 {
bail!("Invalid Result for {name}");
}
args[0]
} else {
output
};
let (output_mirror, ts_type) = generator
.mirror(result_ty, &mut HashSet::new())
.with_context(|| format!("{class_name}::{name} result"))?;
let await_token = sig.asyncness.map(|_| quote!(.await));
let call = if standalone {
let (modules, _) = &api.functions[&name.to_string()];
if let Some(path) = api.native_paths.get(&format!("fn:{}", name)) {
quote!(#path(#(#args),*) #await_token)
} else if api.root_reexports.contains(&name.to_string()) {
quote!(#module::#name(#(#args),*) #await_token)
} else {
quote!(#module::#(#modules::)*#name(#(#args),*) #await_token)
}
} else {
quote!(self.core.clone().#native_name(#(#args),*) #await_token)
};
let call = if let Some(ref args) = fallible {
let error = generator.convert(args[1], quote!(error), false)?;
let (error_mirror, _) = generator.mirror(args[1], &mut HashSet::new())?;
quote! {
match #call {
Ok(value) => value,
Err(error) => {
let details: #error_mirror = #error;
let details = jffi_serialize(&details)?;
let error = js_sys::Error::new(concat!(#class_name, "::", stringify!(#name), " failed"));
js_sys::Reflect::set(error.as_ref(), &JsValue::from_str("details"), &details)?;
return Err(error.into());
}
}
}
} else {
call
};
let (out_ty, body) = if scalar(result_ty) {
(quote!(#result_ty), quote!(Ok(#call)))
} else {
let js_type = format_ident!("JffiReturn{class_name}{index}");
externs
.push(quote!(#[wasm_bindgen(typescript_type = #ts_type)] pub type #js_type;));
let value = generator.convert(result_ty, quote!(value), false)?;
(
quote!(#js_type),
quote! { let value = #call; let value: #output_mirror = #value; Ok(jffi_serialize(&value)?.unchecked_into()) },
)
};
let asyncness = &sig.asyncness;
let receiver = (!standalone).then(|| quote!(&self,));
generated.push(quote! {
#[wasm_bindgen(js_name = #js_name)]
pub #asyncness fn #name(#receiver #(#declarations),*) -> Result<#out_ty, JsValue> {
#(#conversions)* #body
}
});
}
if standalone {
classes.extend(generated);
continue;
}
classes.push(quote! {
thread_local! {
static #registry: std::cell::RefCell<std::collections::BTreeMap<u32, std::sync::Arc<#native>>> = std::cell::RefCell::new(std::collections::BTreeMap::new());
static #counter: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
fn #wrap(core: std::sync::Arc<#native>) -> #wrapper {
let id = #counter.with(|counter| { let id = counter.get().checked_add(1).expect("Object handle limit reached"); counter.set(id); id });
#registry.with(|registry| { registry.borrow_mut().insert(id, core.clone()); });
#wrapper { core, id }
}
#[wasm_bindgen(js_name = #class)] pub struct #wrapper { core: std::sync::Arc<#native>, id: u32 }
impl Drop for #wrapper { fn drop(&mut self) { #registry.with(|registry| { registry.borrow_mut().remove(&self.id); }); } }
#[wasm_bindgen(js_class = #class)] impl #wrapper {
#[wasm_bindgen(getter, js_name = __jffiHandle, skip_typescript)] pub fn jffi_handle(&self) -> u32 { self.id }
#[wasm_bindgen(getter, js_name = __jffiType, skip_typescript)] pub fn jffi_type(&self) -> String { #class_name.into() }
#(#generated)*
}
});
}
let tokens = quote! {
#![cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[allow(unused_imports)] use wasm_bindgen::JsCast;
#[wasm_bindgen(inline_js = "export function jffi_callback_variadic(f) { return (...args) => f(args); }")]
extern "C" { fn jffi_callback_variadic(function:&JsValue)->JsValue; }
#[allow(dead_code)]
mod jffi_async {
use super::*;
use std::{future::Future,pin::Pin,task::{Context,Poll}};
type Pending=Pin<Box<dyn Future<Output=Result<JsValue,JsValue>>>>;
thread_local! {
static PENDING:std::cell::RefCell<std::collections::BTreeMap<u32,Pending>>=std::cell::RefCell::new(std::collections::BTreeMap::new());
static NEXT:std::cell::Cell<u32>=const{std::cell::Cell::new(0)};
}
pub struct AwaitPromise {id:u32}
pub fn begin(value:JsValue)->AwaitPromise {
let id=NEXT.with(|c|{let id=c.get().checked_add(1).expect("Promise handle limit reached");c.set(id);id});
let future=wasm_bindgen_futures::JsFuture::from(js_sys::Promise::resolve(&value));
PENDING.with(|p|{p.borrow_mut().insert(id,Box::pin(future));});
AwaitPromise{id}
}
impl Future for AwaitPromise {
type Output=Result<JsValue,JsValue>;
fn poll(self:Pin<&mut Self>,cx:&mut Context<'_>)->Poll<Self::Output> {
let result=PENDING.with(|p|p.borrow_mut().get_mut(&self.id).expect("Missing promise").as_mut().poll(cx));
if result.is_ready() {PENDING.with(|p|{p.borrow_mut().remove(&self.id);});}
result
}
}
impl Drop for AwaitPromise {fn drop(&mut self){PENDING.with(|p|{p.borrow_mut().remove(&self.id);});}}
}
#[allow(dead_code)] mod jffi_bytes {
pub struct Bytes(pub Vec<u8>);
impl serde::Serialize for Bytes { fn serialize<S:serde::Serializer>(&self,serializer:S)->Result<S::Ok,S::Error>{serializer.serialize_bytes(&self.0)} }
impl<'de> serde::Deserialize<'de> for Bytes { fn deserialize<D:serde::Deserializer<'de>>(deserializer:D)->Result<Self,D::Error>{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value=Bytes;
fn expecting(&self,f:&mut std::fmt::Formatter)->std::fmt::Result{f.write_str("a Uint8Array or byte buffer")}
fn visit_bytes<E:serde::de::Error>(self,value:&[u8])->Result<Bytes,E>{Ok(Bytes(value.to_vec()))}
fn visit_byte_buf<E:serde::de::Error>(self,value:Vec<u8>)->Result<Bytes,E>{Ok(Bytes(value))}
fn visit_seq<A:serde::de::SeqAccess<'de>>(self,mut values:A)->Result<Bytes,A::Error>{let mut out=Vec::new();while let Some(value)=values.next_element::<u8>()?{out.push(value);}Ok(Bytes(out))}
}
deserializer.deserialize_byte_buf(Visitor)
} }
}
#[wasm_bindgen(typescript_custom_section)] const JFFI_BYTES_TYPE:&str="export type JffiBytes = Uint8Array;";
#[allow(dead_code)] mod jffi_time {
use serde::{Serialize, Deserialize};
pub struct Timestamp(pub std::time::SystemTime);
pub struct Duration(pub std::time::Duration);
impl Serialize for Duration { fn serialize<S: serde::Serializer>(&self, serializer:S)->Result<S::Ok,S::Error> { self.0.as_nanos().serialize(serializer) } }
impl<'de> Deserialize<'de> for Duration { fn deserialize<D:serde::Deserializer<'de>>(deserializer:D)->Result<Self,D::Error> {
let nanos=u128::deserialize(deserializer)?;
let seconds=u64::try_from(nanos / 1_000_000_000).map_err(serde::de::Error::custom)?;
Ok(Self(std::time::Duration::new(seconds,(nanos % 1_000_000_000) as u32)))
} }
impl Serialize for Timestamp { fn serialize<S:serde::Serializer>(&self,serializer:S)->Result<S::Ok,S::Error> {
let nanos=match self.0.duration_since(std::time::UNIX_EPOCH) { Ok(d)=>d.as_nanos() as i128, Err(e)=>-(e.duration().as_nanos() as i128) }; nanos.serialize(serializer)
} }
impl<'de> Deserialize<'de> for Timestamp { fn deserialize<D:serde::Deserializer<'de>>(deserializer:D)->Result<Self,D::Error> {
let nanos=i128::deserialize(deserializer)?; let magnitude=nanos.unsigned_abs();
let seconds=u64::try_from(magnitude / 1_000_000_000).map_err(serde::de::Error::custom)?;
let delta=std::time::Duration::new(seconds,(magnitude % 1_000_000_000) as u32);
let time=if nanos<0 {std::time::UNIX_EPOCH.checked_sub(delta)} else {std::time::UNIX_EPOCH.checked_add(delta)}.ok_or_else(||serde::de::Error::custom("Timestamp outside Rust SystemTime range"))?;
Ok(Self(time))
} }
}
#[wasm_bindgen(typescript_custom_section)] const JFFI_TIME_TYPES: &str = "/** Nanoseconds since the Unix epoch. */ export type JffiTimestamp = bigint;\n/** Nonnegative nanoseconds. */ export type JffiDuration = bigint;";
#data_types
#[wasm_bindgen(typescript_custom_section)] const JFFI_TYPES: &str = #ts;
#[wasm_bindgen] extern "C" { #(#externs)* }
#[allow(dead_code)] fn jffi_serialize<T: serde::Serialize>(value: &T) -> Result<JsValue, JsValue> {
value.serialize(&serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true).serialize_missing_as_null(true))
.map_err(|e| js_sys::TypeError::new(&e.to_string()).into())
}
#(#classes)*
};
Ok(format!(
"{MARKER}\n{}",
prettyplease::unparse(&syn::parse2(tokens)?)
))
}
fn update_dependencies(root: &Path, api: &Api) -> Result<()> {
let path = root.join("ffi-web/Cargo.toml");
let mut doc: toml_edit::DocumentMut = fs::read_to_string(&path)?.parse()?;
for (name, version) in [
("serde-wasm-bindgen", "0.6"),
("js-sys", "0.3"),
("wasm-bindgen-futures", "0.4"),
] {
if doc["dependencies"].get(name).is_none() {
doc["dependencies"][name] = toml_edit::value(version);
}
}
if doc["dependencies"].get("serde").is_none() {
let mut table = toml_edit::InlineTable::new();
table.insert("version", "1".into());
let mut features = toml_edit::Array::new();
features.push("derive");
table.insert("features", features.into());
doc["dependencies"]["serde"] = toml_edit::value(table);
} else {
let entry = &mut doc["dependencies"]["serde"];
if let Some(version) = entry.as_str() {
let mut table = toml_edit::InlineTable::new();
table.insert("version", version.into());
let mut features = toml_edit::Array::new();
features.push("derive");
table.insert("features", features.into());
*entry = toml_edit::value(table);
} else if entry
.get("features")
.and_then(|v| v.as_array())
.is_none_or(|a| !a.iter().any(|v| v.as_str() == Some("derive")))
{
if entry.get("features").is_none() {
entry["features"] = toml_edit::value(toml_edit::Array::new());
}
entry["features"]
.as_array_mut()
.context("serde features must be an array")?
.push("derive");
}
}
if doc["dependencies"].get("async-trait").is_none() {
doc["dependencies"]["async-trait"] = toml_edit::value("0.1");
}
let core_manifest: toml::Value =
toml::from_str(&fs::read_to_string(root.join("core/Cargo.toml"))?)?;
let mut uniffi = core_manifest
.get("dependencies")
.and_then(|d| d.get("uniffi"))
.cloned();
if uniffi
.as_ref()
.and_then(|v| v.get("workspace"))
.and_then(|v| v.as_bool())
== Some(true)
{
let workspace: toml::Value = toml::from_str(&fs::read_to_string(root.join("Cargo.toml"))?)?;
uniffi = workspace
.get("workspace")
.and_then(|v| v.get("dependencies"))
.and_then(|v| v.get("uniffi"))
.cloned();
}
if let Some(dependency) = uniffi {
let dependency =
toml::to_string(&std::collections::BTreeMap::from([("uniffi", dependency)]))?;
let parsed: toml_edit::DocumentMut = dependency.parse()?;
doc["dependencies"]["uniffi"] = parsed["uniffi"].clone();
}
if api.abi.is_some() {
doc["dependencies"]["serde_json"] = toml_edit::value("1");
}
for (name, value) in &api.external_dependencies {
let rendered = toml::to_string(&BTreeMap::from([(name, value)]))?;
let parsed: toml_edit::DocumentMut = rendered.parse()?;
doc["dependencies"][name] = parsed[name].clone();
}
let stale: Vec<_> = doc["dependencies"]
.as_table_like()
.into_iter()
.flat_map(|t| t.iter())
.map(|(name, _)| name.to_string())
.filter(|name| {
name.starts_with("jffi_external_") && !api.external_dependencies.contains_key(name)
})
.collect();
if let Some(table) = doc["dependencies"].as_table_like_mut() {
for name in stale {
table.remove(&name);
}
}
let updated = doc.to_string();
if fs::read_to_string(&path)? != updated {
fs::write(path, updated)?;
}
Ok(())
}
pub(crate) fn sync(root: &Path) -> Result<()> {
sync_impl(root, None)
}
pub(crate) fn sync_for_build(root: &Path, release: bool) -> Result<()> {
sync_impl(root, Some(release))
}
fn sync_impl(root: &Path, release: Option<bool>) -> Result<()> {
let manifest: toml::Value = toml::from_str(&fs::read_to_string(root.join("core/Cargo.toml"))?)?;
let library = manifest
.get("lib")
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str())
.or_else(|| {
manifest
.get("package")
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str())
})
.context("Core library name is missing")?
.replace('-', "_");
let source = fs::read_to_string(root.join("core/src/lib.rs"))?;
let path = root.join("ffi-web/src/lib.rs");
if let Ok(existing) = fs::read_to_string(&path) {
if !existing.starts_with(MARKER) {
let module = syn::parse_str::<syn::Ident>(&library)?;
let legacy = quote! {
use #module::Core; use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_name = Core)] pub struct FfiCore { core: Core, }
#[wasm_bindgen(js_class = Core)] impl FfiCore {
#[wasm_bindgen(constructor)] pub fn new() -> Self { Self { core: Core::new() } }
pub fn greeting(&self) -> String { self.core.greeting() }
}
};
if syn::parse_file(&existing)?.into_token_stream().to_string() != legacy.to_string() {
bail!("{} contains custom web bindings. Back it up and remove it to enable automatic generation; custom code was preserved",path.display());
}
}
}
fs::create_dir_all(path.parent().unwrap())?;
update_dependencies(root, &Api::default())?;
let api = if let Some(release) = release {
metadata::load(root, release, &library)?
} else {
let mut api = Api::default();
collect(
&syn::parse_file(&source)?.items,
&root.join("core/src"),
&[],
&mut api,
)?;
api
};
let generated = render(&library, &api)?;
update_dependencies(root, &api)?;
if fs::read_to_string(&path).ok().as_deref() != Some(&generated) {
fs::write(path, generated)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn generate(source: &str) -> Result<String> {
let parsed = syn::parse_file(source)?;
let mut api = Api::default();
collect(&parsed.items, Path::new("."), &[], &mut api)?;
render("sample_core", &api)
}
#[test]
fn exports_camel_case_parameters_and_only_exported_methods() {
let output = generate("#[uniffi::export] impl Core { #[uniffi::constructor] pub fn new() -> Self {} pub fn get_count(&self) -> i32 {} pub fn add_count(&self, amount: i32) -> i32 {} pub fn reset(&self) {} } impl Core { pub fn internal(&self) {} }").unwrap();
assert!(output.contains("js_name = getCount"));
assert!(output.contains("self.core.clone().add_count(amount)"));
assert!(!output.contains("internal"));
syn::parse_file(&output).unwrap();
}
#[test]
fn generates_rich_type_schema_and_live_object_wrappers() {
let source = format!("#[derive(uniffi::Object)] pub struct Core; #[uniffi::export] impl Core {{ #[uniffi::constructor] pub fn new() -> Self {{}} }} {}", include_str!("../tests/fixtures/web_types.rs"));
let output = generate(&source).unwrap();
for expected in [
"export type WebProfile",
"Map<string, boolean>",
"bigCount: bigint",
"Array<WebChoice | null>",
"pub enum WebChoice",
"pub struct JffiObjectWebCounterRef",
"pub async fn async_profile",
"__jffiHandle",
"details",
"initial_count: i32",
] {
assert!(output.contains(expected), "missing {expected}");
}
assert!(!output.contains("any"));
syn::parse_file(&output).unwrap();
}
#[test]
fn generates_callbacks_and_custom_types_from_native_contracts() {
let source=format!("#[derive(uniffi::Object)] pub struct Core; #[uniffi::export] impl Core {{ #[uniffi::constructor] pub fn new() -> Self {{}} }} {} {}",include_str!("../tests/fixtures/web_types.rs"),include_str!("../tests/fixtures/web_callbacks.rs"));
let output = generate(&source).unwrap();
for expected in [
"export interface WebFormatter",
"export interface WebService",
"export type WebEmail = string",
"export type WebUserId = bigint",
"export type WebLabel = string",
"Promise<WebCustomRecord>",
"FfiConverter",
"handle_callback_unexpected_error",
"async_trait::async_trait(?Send)",
"struct AwaitPromise",
"impl Drop for JffiJsWebFormatter",
] {
assert!(output.contains(expected), "Missing {expected}");
}
assert!(!output.contains("unsafe impl"));
syn::parse_file(&output).unwrap();
}
#[test]
fn standalone_functions_without_core_and_conditional_schema() {
let mut api = Api::default();
let mut cfg = BuildCfg::default();
for fact in [
"target_arch = \"wasm32\"",
"debug_assertions",
"feature = \"extra\"",
] {
cfg.facts.insert(
syn::parse_str::<syn::Meta>(fact)
.unwrap()
.to_token_stream()
.to_string(),
);
}
api.cfg = Some(cfg);
let source = r#"
#[cfg(not(target_arch="wasm32"))] mod absent;
#[cfg(target_arch="wasm32")] pub mod browser {
#[cfg_attr(feature="extra", uniffi::export)] pub async fn get_value(value: i32) -> i32 { value }
#[cfg(any(test, not(feature="extra")))] #[uniffi::export] pub fn disabled() {}
}
#[derive(uniffi::Record)] pub struct Choice {
#[cfg(debug_assertions)] pub enabled: bool,
#[cfg(not(debug_assertions))] pub disabled: bool,
}
"#;
collect(
&syn::parse_file(source).unwrap().items,
Path::new("."),
&[],
&mut api,
)
.unwrap();
let output = render("sample_core", &api).unwrap();
assert!(output.contains("js_name = getValue"));
assert!(output.contains("sample_core::browser::get_value"));
assert!(output.contains("enabled: boolean"));
assert!(!output.contains("disabled"));
assert!(!output.contains("FfiCore"));
}
#[test]
fn rejects_unsupported_types_and_name_collisions() {
for method in [
"pub fn read(&self) -> Result<String, Error> {}",
"pub fn get_count(&self) {} pub fn getCount(&self) {}",
] {
assert!(generate(&format!("#[uniffi::export] impl Core {{ #[uniffi::constructor] pub fn new() -> Self {{}} {method} }}")).is_err());
}
}
#[test]
fn updates_generated_methods_and_preserves_custom_files() {
let root = std::env::temp_dir().join(format!("jffi-web-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(root.join("core/src")).unwrap();
fs::write(
root.join("core/Cargo.toml"),
"[package]\nname='sample-core'\n",
)
.unwrap();
let source = "#[uniffi::export] impl Core { #[uniffi::constructor] pub fn new() -> Self {} pub fn greeting(&self) -> String {} }";
fs::write(root.join("core/src/lib.rs"), source).unwrap();
fs::create_dir_all(root.join("ffi-web")).unwrap();
if !root.join("ffi-web/Cargo.toml").exists() {
fs::write(
root.join("ffi-web/Cargo.toml"),
"[package]\nname=\"sample-web\"\n[dependencies]\n",
)
.unwrap();
}
sync(&root).unwrap();
fs::write(
root.join("core/src/lib.rs"),
source.replace(
"pub fn greeting(&self) -> String",
"pub fn get_count(&self) -> i32",
),
)
.unwrap();
sync(&root).unwrap();
let path = root.join("ffi-web/src/lib.rs");
let output = fs::read_to_string(&path).unwrap();
assert!(output.contains("getCount"));
assert!(!output.contains("greeting"));
fs::write(&path, "// custom\npub fn custom() {}\n").unwrap();
assert!(sync(&root).is_err());
assert!(fs::read_to_string(path).unwrap().contains("pub fn custom"));
fs::remove_dir_all(root).unwrap();
}
}