1use crate::ir::{
45 Api, CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function,
46 InterfaceDef, Module, Param, StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION,
47};
48use syn::spanned::Spanned;
49
50fn is_marker(attr: &syn::Attribute, name: &str) -> bool {
56 attr.path().segments.last().is_some_and(|s| s.ident == name)
57}
58
59pub fn has_marker(attrs: &[syn::Attribute], name: &str) -> bool {
61 attrs.iter().any(|a| is_marker(a, name))
62}
63
64fn has_repr_i32(attrs: &[syn::Attribute]) -> bool {
65 attrs.iter().any(|a| {
66 a.path().is_ident("repr") && a.parse_args::<syn::Ident>().is_ok_and(|id| id == "i32")
67 })
68}
69
70fn parse_deprecated(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
72 let Some(attr) = attrs.iter().find(|a| a.path().is_ident("deprecated")) else {
73 return (None, None);
74 };
75 let mut since = None;
76 let mut note = None;
77 if matches!(attr.meta, syn::Meta::Path(_)) {
78 return (None, Some("deprecated".to_string()));
79 }
80 let _ = attr.parse_nested_meta(|meta| {
81 let Some(ident) = meta.path.get_ident() else {
82 return Ok(());
83 };
84 let value = meta.value()?;
85 let lit: syn::LitStr = value.parse()?;
86 match ident.to_string().as_str() {
87 "since" => since = Some(lit.value()),
88 "note" => note = Some(lit.value()),
89 _ => {}
90 }
91 Ok(())
92 });
93 if note.is_none() && since.is_none() {
94 note = Some("deprecated".to_string());
95 }
96 (since, note)
97}
98
99fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
100 let lines: Vec<String> = attrs
101 .iter()
102 .filter_map(|attr| {
103 let syn::Meta::NameValue(nv) = &attr.meta else {
104 return None;
105 };
106 if !nv.path.is_ident("doc") {
107 return None;
108 }
109 let syn::Expr::Lit(syn::ExprLit {
110 lit: syn::Lit::Str(s),
111 ..
112 }) = &nv.value
113 else {
114 return None;
115 };
116 let val = s.value();
117 Some(match val.strip_prefix(' ') {
118 Some(stripped) => stripped.to_string(),
119 None => val,
120 })
121 })
122 .collect();
123 (!lines.is_empty()).then(|| lines.join("\n"))
124}
125
126fn is_ident(ty: &syn::Type, name: &str) -> bool {
127 matches!(ty, syn::Type::Path(p) if p.path.is_ident(name))
128}
129
130fn single_generic_arg(seg: &syn::PathSegment) -> syn::Result<&syn::Type> {
131 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
132 return Err(syn::Error::new(seg.span(), "expected generic arguments"));
133 };
134 if args.args.len() != 1 {
135 return Err(syn::Error::new(
136 seg.span(),
137 "expected exactly 1 generic argument",
138 ));
139 }
140 let syn::GenericArgument::Type(ty) = &args.args[0] else {
141 return Err(syn::Error::new(seg.span(), "expected a type argument"));
142 };
143 Ok(ty)
144}
145
146fn two_generic_args(seg: &syn::PathSegment) -> syn::Result<(&syn::Type, &syn::Type)> {
147 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
148 return Err(syn::Error::new(seg.span(), "expected generic arguments"));
149 };
150 if args.args.len() != 2 {
151 return Err(syn::Error::new(
152 seg.span(),
153 "expected exactly 2 generic arguments",
154 ));
155 }
156 let syn::GenericArgument::Type(k) = &args.args[0] else {
157 return Err(syn::Error::new(seg.span(), "expected a key type argument"));
158 };
159 let syn::GenericArgument::Type(v) = &args.args[1] else {
160 return Err(syn::Error::new(
161 seg.span(),
162 "expected a value type argument",
163 ));
164 };
165 Ok((k, v))
166}
167
168fn type_path_ident(ty: &syn::Type) -> Option<String> {
169 let syn::Type::Path(p) = ty else { return None };
170 p.path.segments.last().map(|s| s.ident.to_string())
171}
172
173fn trait_object_name(ty: &syn::Type) -> Option<String> {
176 let syn::Type::TraitObject(obj) = ty else {
177 return None;
178 };
179 let mut names = obj.bounds.iter().filter_map(|b| match b {
180 syn::TypeParamBound::Trait(t) => {
181 let name = t.path.segments.last()?.ident.to_string();
182 (!matches!(name.as_str(), "Send" | "Sync" | "Unpin")).then_some(name)
183 }
184 _ => None,
185 });
186 let first = names.next()?;
187 names.next().is_none().then_some(first)
188}
189
190pub fn peel_arc(ty: &syn::Type) -> &syn::Type {
196 if let syn::Type::Path(p) = ty {
197 if let Some(seg) = p.path.segments.last() {
198 if seg.ident == "Arc" {
199 if let Ok(inner) = single_generic_arg(seg) {
200 return inner;
201 }
202 }
203 }
204 }
205 ty
206}
207
208fn is_cancel_token(ty: &syn::Type) -> bool {
216 type_path_ident(ty).as_deref() == Some("CancelToken")
217}
218
219pub fn type_ref_from_syn(ty: &syn::Type) -> syn::Result<TypeRef> {
240 match ty {
241 syn::Type::Reference(r) => {
242 if let syn::Type::Path(p) = r.elem.as_ref() {
243 if p.path.is_ident("str") {
244 return Ok(TypeRef::StringUtf8);
245 }
246 }
247 if let syn::Type::Slice(slice) = r.elem.as_ref() {
248 if is_ident(&slice.elem, "u8") {
249 return Ok(TypeRef::Bytes);
250 }
251 return Ok(TypeRef::List(Box::new(type_ref_from_syn(&slice.elem)?)));
252 }
253 type_ref_from_syn(&r.elem)
254 }
255 syn::Type::Ptr(_) => Err(syn::Error::new(
256 ty.span(),
257 "weaveffi: raw pointers cannot cross the FFI boundary; declare the pointee as a \
258 #[weaveffi::interface] and pass it as `&T` or `Arc<T>`",
259 )),
260 syn::Type::TraitObject(_) => {
261 let name = trait_object_name(ty).ok_or_else(|| {
262 syn::Error::new(
263 ty.span(),
264 "weaveffi: a trait object must name exactly one #[weaveffi::callback_interface] \
265 trait (plus optional `Send`/`Sync` bounds)",
266 )
267 })?;
268 Ok(TypeRef::Named(name))
269 }
270 syn::Type::Path(type_path) => {
271 let seg = type_path
272 .path
273 .segments
274 .last()
275 .ok_or_else(|| syn::Error::new(ty.span(), "empty type path"))?;
276 let ident = seg.ident.to_string();
277 match ident.as_str() {
278 "i8" => Ok(TypeRef::I8),
279 "i16" => Ok(TypeRef::I16),
280 "i32" => Ok(TypeRef::I32),
281 "i64" => Ok(TypeRef::I64),
282 "u8" => Ok(TypeRef::U8),
283 "u16" => Ok(TypeRef::U16),
284 "u32" => Ok(TypeRef::U32),
285 "f32" => Ok(TypeRef::F32),
286 "f64" => Ok(TypeRef::F64),
287 "bool" => Ok(TypeRef::Bool),
288 "String" => Ok(TypeRef::StringUtf8),
289 "u64" => Ok(TypeRef::U64),
290 "Arc" => type_ref_from_syn(single_generic_arg(seg)?),
294 "Box" | "Rc" => Err(syn::Error::new(
295 ty.span(),
296 format!(
297 "weaveffi: `{ident}` cannot cross the FFI boundary; objects and callback \
298 interfaces are shared, so spell them as `Arc<T>` / `Arc<dyn Trait>`"
299 ),
300 )),
301 "Vec" => {
302 let inner = single_generic_arg(seg)?;
303 if is_ident(inner, "u8") {
304 return Ok(TypeRef::Bytes);
305 }
306 Ok(TypeRef::List(Box::new(type_ref_from_syn(inner)?)))
307 }
308 "Option" => {
309 let inner = single_generic_arg(seg)?;
310 Ok(TypeRef::Optional(Box::new(type_ref_from_syn(inner)?)))
311 }
312 "Iter" => {
315 let inner = single_generic_arg(seg)?;
316 Ok(TypeRef::Iterator(Box::new(type_ref_from_syn(inner)?)))
317 }
318 "HashMap" | "BTreeMap" => {
319 let (k, v) = two_generic_args(seg)?;
320 Ok(TypeRef::Map(
321 Box::new(type_ref_from_syn(k)?),
322 Box::new(type_ref_from_syn(v)?),
323 ))
324 }
325 other => Ok(TypeRef::Named(other.to_string())),
326 }
327 }
328 _ => Err(syn::Error::new(ty.span(), "unsupported type syntax")),
329 }
330}
331
332pub fn peel_result(ty: &syn::Type) -> &syn::Type {
336 if let syn::Type::Path(p) = ty {
337 if let Some(seg) = p.path.segments.last() {
338 if seg.ident == "Result" {
339 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
340 if let Some(syn::GenericArgument::Type(ok)) = args.args.first() {
341 return ok;
342 }
343 }
344 }
345 }
346 }
347 ty
348}
349
350pub fn output_is_result(output: &syn::ReturnType) -> bool {
353 let syn::ReturnType::Type(_, ty) = output else {
354 return false;
355 };
356 matches!(ty.as_ref(), syn::Type::Path(p)
357 if p.path.segments.last().is_some_and(|s| s.ident == "Result"))
358}
359
360fn is_unit(ty: &syn::Type) -> bool {
361 matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty())
362}
363
364pub fn return_type_from_syn(output: &syn::ReturnType) -> syn::Result<Option<TypeRef>> {
371 match output {
372 syn::ReturnType::Default => Ok(None),
373 syn::ReturnType::Type(_, ty) => {
374 let inner = peel_result(ty);
375 if is_unit(inner) {
376 Ok(None)
377 } else {
378 Ok(Some(type_ref_from_syn(inner)?))
379 }
380 }
381 }
382}
383
384fn parse_discriminant(expr: &syn::Expr) -> syn::Result<i32> {
385 match expr {
386 syn::Expr::Lit(lit) => {
387 let syn::Lit::Int(int_lit) = &lit.lit else {
388 return Err(syn::Error::new(
389 expr.span(),
390 "expected an integer literal discriminant",
391 ));
392 };
393 int_lit.base10_parse::<i32>()
394 }
395 syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
396 Ok(-parse_discriminant(&unary.expr)?)
397 }
398 _ => Err(syn::Error::new(
399 expr.span(),
400 "unsupported discriminant expression",
401 )),
402 }
403}
404
405fn extract_params(
406 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
407) -> syn::Result<Vec<Param>> {
408 inputs
409 .iter()
410 .filter_map(|arg| match arg {
411 syn::FnArg::Typed(pt) => Some(pt),
412 syn::FnArg::Receiver(_) => None,
413 })
414 .filter(|pt| !is_cancel_token(&pt.ty))
417 .map(|pt| {
418 let param_name = match pt.pat.as_ref() {
419 syn::Pat::Ident(id) => id.ident.to_string(),
420 _ => return Err(syn::Error::new(pt.span(), "unsupported parameter pattern")),
421 };
422 if matches!(pt.ty.as_ref(), syn::Type::Reference(r) if r.mutability.is_some()) {
423 return Err(syn::Error::new(
424 pt.ty.span(),
425 "weaveffi: `&mut` parameters cannot cross the FFI boundary; take the value \
426 by `&T` or by value and return the updated result",
427 ));
428 }
429 let ty = type_ref_from_syn(&pt.ty)?;
430 let mut has_iterator = false;
434 ty.walk(&mut |t| has_iterator |= matches!(t, TypeRef::Iterator(_)));
435 if has_iterator {
436 return Err(syn::Error::new(
437 pt.ty.span(),
438 "weaveffi: `Iter<T>` is a pull handle the consumer drives and is only \
439 valid as a function's outermost return type, not as a parameter",
440 ));
441 }
442 Ok(Param {
443 name: param_name,
444 ty,
445 doc: extract_doc(&pt.attrs),
446 })
447 })
448 .collect()
449}
450
451fn function_from_sig(
455 sig: &syn::Signature,
456 attrs: &[syn::Attribute],
457 returns: Option<TypeRef>,
458) -> syn::Result<Function> {
459 if let Some(ret) = &returns {
460 let nested_iterator = |t: &TypeRef| {
461 let mut found = false;
462 t.walk(&mut |inner| found |= matches!(inner, TypeRef::Iterator(_)));
463 found
464 };
465 let invalid = match ret {
466 TypeRef::Iterator(elem) => nested_iterator(elem),
467 other => nested_iterator(other),
468 };
469 if invalid {
470 let span = match &sig.output {
471 syn::ReturnType::Type(_, ty) => ty.span(),
472 other => other.span(),
473 };
474 return Err(syn::Error::new(
475 span,
476 "weaveffi: `Iter<T>` is only valid as the outermost return type; it can't be \
477 nested in an `Option`, `Vec`, map, or another `Iter`",
478 ));
479 }
480 }
481 Ok(Function {
482 name: sig.ident.to_string(),
483 params: extract_params(&sig.inputs)?,
484 returns,
485 doc: extract_doc(attrs),
486 throws: output_is_result(&sig.output),
487 r#async: sig.asyncness.is_some(),
488 cancellable: has_marker(attrs, "cancellable"),
489 deprecated: parse_deprecated(attrs).1,
490 })
491}
492
493fn extract_function(item: &syn::ItemFn) -> syn::Result<Function> {
494 let returns = return_type_from_syn(&item.sig.output)?;
495 function_from_sig(&item.sig, &item.attrs, returns)
496}
497
498fn returns_self(output: &syn::ReturnType, iface: &str) -> bool {
502 let syn::ReturnType::Type(_, ty) = output else {
503 return false;
504 };
505 match type_path_ident(peel_arc(peel_result(ty))) {
506 Some(name) => name == "Self" || name == iface,
507 None => false,
508 }
509}
510
511fn extract_member(item: &syn::ImplItemFn, iface: &str, is_ctor: bool) -> syn::Result<Function> {
518 let returns = if is_ctor {
519 None
520 } else {
521 match return_type_from_syn(&item.sig.output)? {
522 Some(TypeRef::Named(name)) if name == "Self" => Some(TypeRef::Named(iface.to_string())),
524 other => other,
525 }
526 };
527 function_from_sig(&item.sig, &item.attrs, returns)
528}
529
530pub fn receiver_is_supported(recv: &syn::Receiver) -> bool {
537 if recv.mutability.is_some() {
538 return false;
539 }
540 if recv.reference.is_some() {
541 return true;
542 }
543 recv.colon_token.is_some() && type_path_ident(&recv.ty).as_deref() == Some("Arc")
544}
545
546fn members_from_impl(item_impl: &syn::ItemImpl, iface: &mut InterfaceDef) -> syn::Result<()> {
556 for impl_item in &item_impl.items {
557 let syn::ImplItem::Fn(f) = impl_item else {
558 continue;
559 };
560 if !matches!(f.vis, syn::Visibility::Public(_)) {
561 continue;
562 }
563 match f.sig.receiver() {
564 Some(recv) => {
565 if !receiver_is_supported(recv) {
566 return Err(syn::Error::new(
567 recv.span(),
568 "weaveffi: interface methods must take `&self` or `self: Arc<Self>`; \
569 use interior mutability (Mutex, RwLock, atomics) for mutable state, \
570 because the object is shared across the FFI boundary",
571 ));
572 }
573 iface.methods.push(extract_member(f, &iface.name, false)?);
574 }
575 None if returns_self(&f.sig.output, &iface.name) => {
576 iface
577 .constructors
578 .push(extract_member(f, &iface.name, true)?);
579 }
580 None => {
581 iface.statics.push(extract_member(f, &iface.name, false)?);
582 }
583 }
584 }
585 Ok(())
586}
587
588pub fn returns_foreign_result(output: &syn::ReturnType) -> bool {
593 let syn::ReturnType::Type(_, ty) = output else {
594 return false;
595 };
596 let syn::Type::Path(p) = ty.as_ref() else {
597 return false;
598 };
599 let Some(seg) = p.path.segments.last() else {
600 return false;
601 };
602 if seg.ident != "Result" {
603 return false;
604 }
605 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
606 return false;
607 };
608 match args.args.iter().nth(1) {
609 Some(syn::GenericArgument::Type(err)) => {
610 type_path_ident(err).as_deref() == Some("ForeignError")
611 }
612 _ => false,
613 }
614}
615
616fn extract_callback_interface(item: &syn::ItemTrait) -> syn::Result<CallbackInterfaceDef> {
626 let mut methods = Vec::new();
627 for trait_item in &item.items {
628 let syn::TraitItem::Fn(f) = trait_item else {
629 continue;
630 };
631 match f.sig.receiver() {
632 Some(recv) if recv.reference.is_some() && recv.mutability.is_none() => {}
633 Some(recv) => {
634 return Err(syn::Error::new(
635 recv.span(),
636 "weaveffi: callback interface methods must take `&self`",
637 ));
638 }
639 None => {
640 return Err(syn::Error::new(
641 f.sig.span(),
642 "weaveffi: callback interface methods must take `&self` (associated \
643 functions can't be implemented by the consumer)",
644 ));
645 }
646 }
647 let returns = return_type_from_syn(&f.sig.output)?;
648 let mut method = function_from_sig(&f.sig, &f.attrs, returns)?;
649 if returns_foreign_result(&f.sig.output) {
650 method.throws = false;
651 }
652 methods.push(method);
653 }
654 Ok(CallbackInterfaceDef {
655 name: item.ident.to_string(),
656 doc: extract_doc(&item.attrs),
657 deprecated: parse_deprecated(&item.attrs).1,
658 methods,
659 })
660}
661
662fn extract_struct(item: &syn::ItemStruct) -> syn::Result<StructDef> {
663 let name = item.ident.to_string();
664 let fields = match &item.fields {
665 syn::Fields::Named(named) => named
666 .named
667 .iter()
668 .map(|f| {
669 let field_name = f
670 .ident
671 .as_ref()
672 .ok_or_else(|| syn::Error::new(f.span(), "unnamed field in record"))?
673 .to_string();
674 Ok(StructField {
675 name: field_name,
676 ty: type_ref_from_syn(&f.ty)?,
677 doc: extract_doc(&f.attrs),
678 })
679 })
680 .collect::<syn::Result<_>>()?,
681 _ => {
682 return Err(syn::Error::new(
683 item.span(),
684 "only named fields are supported for #[weaveffi::record]",
685 ))
686 }
687 };
688 Ok(StructDef {
689 name,
690 doc: extract_doc(&item.attrs),
691 deprecated: parse_deprecated(&item.attrs).1,
692 fields,
693 })
694}
695
696fn extract_variant_fields(fields: &syn::FieldsNamed) -> syn::Result<Vec<StructField>> {
699 fields
700 .named
701 .iter()
702 .map(|f| {
703 let field_name = f
704 .ident
705 .as_ref()
706 .ok_or_else(|| syn::Error::new(f.span(), "unnamed field in enum variant"))?
707 .to_string();
708 Ok(StructField {
709 name: field_name,
710 ty: type_ref_from_syn(&f.ty)?,
711 doc: extract_doc(&f.attrs),
712 })
713 })
714 .collect()
715}
716
717fn extract_enum(item: &syn::ItemEnum) -> syn::Result<EnumDef> {
718 let name = item.ident.to_string();
719
720 let is_rich = item
726 .variants
727 .iter()
728 .any(|v| !matches!(v.fields, syn::Fields::Unit));
729
730 if !is_rich && !has_repr_i32(&item.attrs) {
731 return Err(syn::Error::new(
732 item.span(),
733 format!(
734 "enum `{}` must have #[repr(i32)] to be a #[weaveffi::enumeration]",
735 item.ident
736 ),
737 ));
738 }
739
740 let mut next_value: i32 = 0;
741 let variants = item
742 .variants
743 .iter()
744 .map(|v| {
745 let value = match v.discriminant.as_ref() {
746 Some((_, expr)) => parse_discriminant(expr)?,
747 None if is_rich => next_value,
748 None => {
749 return Err(syn::Error::new(
750 v.span(),
751 format!(
752 "enum `{name}` variant `{}` must have an explicit discriminant",
753 v.ident
754 ),
755 ))
756 }
757 };
758 next_value = value.wrapping_add(1);
759
760 let fields = match &v.fields {
761 syn::Fields::Unit => vec![],
762 syn::Fields::Named(named) => extract_variant_fields(named)?,
763 syn::Fields::Unnamed(_) => {
764 return Err(syn::Error::new(
765 v.span(),
766 format!(
767 "enum `{name}` variant `{}`: tuple-style variants are not supported; \
768 use named fields",
769 v.ident
770 ),
771 ))
772 }
773 };
774
775 Ok(EnumVariant {
776 name: v.ident.to_string(),
777 value,
778 doc: extract_doc(&v.attrs),
779 fields,
780 })
781 })
782 .collect::<syn::Result<_>>()?;
783 Ok(EnumDef {
784 name,
785 doc: extract_doc(&item.attrs),
786 deprecated: parse_deprecated(&item.attrs).1,
787 variants,
788 })
789}
790
791fn extract_error_domain(item: &syn::ItemEnum) -> syn::Result<ErrorDomain> {
804 let codes = item
805 .variants
806 .iter()
807 .map(|v| {
808 let fields = match &v.fields {
809 syn::Fields::Unit => vec![],
810 syn::Fields::Named(named) => extract_variant_fields(named)?,
811 syn::Fields::Unnamed(_) => {
812 return Err(syn::Error::new(
813 v.span(),
814 "weaveffi: #[weaveffi::error] payload variants must use named \
815 fields (tuple-style variants are not supported)",
816 ));
817 }
818 };
819 let Some((_, expr)) = v.discriminant.as_ref() else {
820 return Err(syn::Error::new(
821 v.span(),
822 format!(
823 "weaveffi: error variant `{}` must have an explicit discriminant \
824 (its stable ABI error code)",
825 v.ident
826 ),
827 ));
828 };
829 let doc = extract_doc(&v.attrs);
830 let message = doc
831 .as_deref()
832 .and_then(|d| d.lines().next())
833 .unwrap_or(&v.ident.to_string())
834 .to_string();
835 Ok(ErrorCode {
836 name: v.ident.to_string(),
837 code: parse_discriminant(expr)?,
838 message,
839 doc,
840 fields,
841 })
842 })
843 .collect::<syn::Result<Vec<_>>>()?;
844 Ok(ErrorDomain {
845 name: item.ident.to_string(),
846 codes,
847 })
848}
849
850pub fn module_from_item_mod(item_mod: &syn::ItemMod) -> syn::Result<Module> {
862 let name = item_mod.ident.to_string();
863 let mut functions = Vec::new();
864 let mut interfaces: Vec<InterfaceDef> = Vec::new();
865 let mut structs = Vec::new();
866 let mut enums = Vec::new();
867 let mut callback_interfaces = Vec::new();
868 let mut modules = Vec::new();
869 let mut errors: Option<ErrorDomain> = None;
870
871 if let Some((_, items)) = &item_mod.content {
872 for item in items {
873 match item {
874 syn::Item::Trait(t) if has_marker(&t.attrs, "callback_interface") => {
875 callback_interfaces.push(extract_callback_interface(t)?);
876 }
877 syn::Item::Fn(f) if has_marker(&f.attrs, "export") => {
878 functions.push(extract_function(f)?);
879 }
880 syn::Item::Struct(s) if has_marker(&s.attrs, "interface") => {
881 interfaces.push(InterfaceDef {
882 name: s.ident.to_string(),
883 doc: extract_doc(&s.attrs),
884 deprecated: parse_deprecated(&s.attrs).1,
885 constructors: vec![],
886 methods: vec![],
887 statics: vec![],
888 });
889 }
890 syn::Item::Struct(s) if has_marker(&s.attrs, "record") => {
891 structs.push(extract_struct(s)?);
892 }
893 syn::Item::Enum(e) if has_marker(&e.attrs, "error") => {
894 if errors.is_some() {
895 return Err(syn::Error::new(
896 e.span(),
897 "weaveffi: a module may declare at most one #[weaveffi::error] \
898 domain",
899 ));
900 }
901 errors = Some(extract_error_domain(e)?);
902 }
903 syn::Item::Enum(e) if has_marker(&e.attrs, "enumeration") => {
904 enums.push(extract_enum(e)?);
905 }
906 syn::Item::Mod(m) if has_marker(&m.attrs, "module") && m.content.is_some() => {
907 modules.push(module_from_item_mod(m)?);
908 }
909 _ => {}
910 }
911 }
912
913 for item in items {
917 let syn::Item::Impl(item_impl) = item else {
918 continue;
919 };
920 if item_impl.trait_.is_some() {
921 continue;
922 }
923 let Some(self_name) = type_path_ident(&item_impl.self_ty) else {
924 continue;
925 };
926 if let Some(iface) = interfaces.iter_mut().find(|i| i.name == self_name) {
927 members_from_impl(item_impl, iface)?;
928 }
929 }
930 }
931
932 Ok(Module {
933 name,
934 doc: extract_doc(&item_mod.attrs),
935 functions,
936 interfaces,
937 structs,
938 enums,
939 callback_interfaces,
940 errors,
941 modules,
942 })
943}
944
945pub fn api_from_file(file: &syn::File) -> syn::Result<Api> {
952 let mut modules = Vec::new();
953 for item in &file.items {
954 if let syn::Item::Mod(item_mod) = item {
955 if has_marker(&item_mod.attrs, "module") && item_mod.content.is_some() {
956 modules.push(module_from_item_mod(item_mod)?);
957 }
958 }
959 }
960 Ok(Api {
961 version: CURRENT_SCHEMA_VERSION.to_string(),
962 modules,
963 })
964}
965
966pub fn api_from_src(src: &str) -> syn::Result<Api> {
973 let file = syn::parse_file(src)?;
974 api_from_file(&file)
975}
976
977pub fn api_from_src_stringly(src: &str) -> Result<Api, String> {
984 api_from_src(src).map_err(|e| e.to_string())
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 fn one_module(src: &str) -> Module {
992 let api = api_from_src(src).unwrap();
993 assert_eq!(api.modules.len(), 1, "expected exactly one module");
994 api.modules.into_iter().next().unwrap()
995 }
996
997 #[test]
998 fn extracts_exported_function() {
999 let m = one_module(
1000 r#"
1001 #[weaveffi::module]
1002 mod math {
1003 #[weaveffi::export]
1004 pub fn add(a: i32, b: i32) -> i32 { a + b }
1005 }
1006 "#,
1007 );
1008 assert_eq!(m.name, "math");
1009 assert_eq!(m.functions.len(), 1);
1010 let f = &m.functions[0];
1011 assert_eq!(f.name, "add");
1012 assert_eq!(f.params[0].ty, TypeRef::I32);
1013 assert_eq!(f.returns, Some(TypeRef::I32));
1014 assert!(!f.r#async);
1015 }
1016
1017 #[test]
1018 fn unmarked_module_is_ignored() {
1019 let api = api_from_src(
1020 r#"
1021 mod plain {
1022 #[weaveffi::export]
1023 pub fn add(a: i32) -> i32 { a }
1024 }
1025 "#,
1026 )
1027 .unwrap();
1028 assert!(api.modules.is_empty());
1029 }
1030
1031 #[test]
1032 fn result_return_peels_to_ok_type() {
1033 let m = one_module(
1034 r#"
1035 #[weaveffi::module]
1036 mod m {
1037 #[weaveffi::export]
1038 pub fn get(id: u64) -> Result<Contact, MyError> { todo!() }
1039 }
1040 "#,
1041 );
1042 assert_eq!(
1043 m.functions[0].returns,
1044 Some(TypeRef::Named("Contact".into()))
1045 );
1046 assert_eq!(m.functions[0].params[0].ty, TypeRef::U64);
1047 }
1048
1049 #[test]
1050 fn result_unit_return_is_none() {
1051 let m = one_module(
1052 r#"
1053 #[weaveffi::module]
1054 mod m {
1055 #[weaveffi::export]
1056 pub fn run() -> Result<(), MyError> { Ok(()) }
1057 }
1058 "#,
1059 );
1060 assert_eq!(m.functions[0].returns, None);
1061 }
1062
1063 #[test]
1064 fn async_fn_is_async() {
1065 let m = one_module(
1066 r#"
1067 #[weaveffi::module]
1068 mod m {
1069 #[weaveffi::export]
1070 pub async fn fetch(url: String) -> String { url }
1071 }
1072 "#,
1073 );
1074 assert!(m.functions[0].r#async);
1075 }
1076
1077 #[test]
1078 fn record_and_enumeration() {
1079 let m = one_module(
1080 r#"
1081 #[weaveffi::module]
1082 mod contacts {
1083 #[weaveffi::enumeration]
1084 #[repr(i32)]
1085 pub enum ContactType { Personal = 0, Work = 1 }
1086
1087 #[weaveffi::record]
1088 pub struct Contact {
1089 pub id: i64,
1090 pub email: Option<String>,
1091 pub kind: ContactType,
1092 }
1093 }
1094 "#,
1095 );
1096 assert_eq!(m.enums.len(), 1);
1097 assert_eq!(m.enums[0].variants.len(), 2);
1098 assert_eq!(m.structs.len(), 1);
1099 let s = &m.structs[0];
1100 assert_eq!(
1101 s.fields[1].ty,
1102 TypeRef::Optional(Box::new(TypeRef::StringUtf8))
1103 );
1104 assert_eq!(s.fields[2].ty, TypeRef::Named("ContactType".into()));
1105 }
1106
1107 #[test]
1108 fn enumeration_requires_repr_i32() {
1109 let err = api_from_src(
1110 r#"
1111 #[weaveffi::module]
1112 mod m {
1113 #[weaveffi::enumeration]
1114 pub enum Bad { A = 0 }
1115 }
1116 "#,
1117 )
1118 .unwrap_err();
1119 assert!(err.to_string().contains("repr(i32)"));
1120 }
1121
1122 #[test]
1123 fn references_and_arcs_name_the_pointee() {
1124 let m = one_module(
1125 r#"
1126 #[weaveffi::module]
1127 mod m {
1128 #[weaveffi::export]
1129 pub fn open(name: &str, data: &[u8], tags: &[String]) -> Arc<Store> { todo!() }
1130 #[weaveffi::export]
1131 pub fn peek(store: &Store, other: Option<Arc<Store>>) -> Option<Arc<Store>> { None }
1132 }
1133 "#,
1134 );
1135 let open = &m.functions[0];
1136 assert_eq!(open.params[0].ty, TypeRef::StringUtf8);
1137 assert_eq!(open.params[1].ty, TypeRef::Bytes);
1138 assert_eq!(
1139 open.params[2].ty,
1140 TypeRef::List(Box::new(TypeRef::StringUtf8))
1141 );
1142 assert_eq!(open.returns, Some(TypeRef::Named("Store".into())));
1143 let peek = &m.functions[1];
1144 assert_eq!(peek.params[0].ty, TypeRef::Named("Store".into()));
1145 let opt_store = TypeRef::Optional(Box::new(TypeRef::Named("Store".into())));
1146 assert_eq!(peek.params[1].ty, opt_store);
1147 assert_eq!(peek.returns, Some(opt_store));
1148 }
1149
1150 #[test]
1151 fn raw_pointers_and_boxes_are_rejected() {
1152 let err = api_from_src(
1153 r#"
1154 #[weaveffi::module]
1155 mod m {
1156 #[weaveffi::export]
1157 pub fn close(token: *mut Token) {}
1158 }
1159 "#,
1160 )
1161 .unwrap_err();
1162 assert!(err.to_string().contains("raw pointers"));
1163 let err = api_from_src(
1164 r#"
1165 #[weaveffi::module]
1166 mod m {
1167 #[weaveffi::export]
1168 pub fn listen(l: Box<dyn Listener>) {}
1169 }
1170 "#,
1171 )
1172 .unwrap_err();
1173 assert!(err.to_string().contains("Arc<dyn Trait>"));
1174 let err = api_from_src(
1175 r#"
1176 #[weaveffi::module]
1177 mod m {
1178 #[weaveffi::export]
1179 pub fn bump(counter: &mut i32) {}
1180 }
1181 "#,
1182 )
1183 .unwrap_err();
1184 assert!(err.to_string().contains("&mut"));
1185 }
1186
1187 #[test]
1188 fn cancel_token_param_is_skipped() {
1189 let m = one_module(
1190 r#"
1191 #[weaveffi::module]
1192 mod m {
1193 #[weaveffi::export]
1194 #[weaveffi::cancellable]
1195 pub async fn compact(store: Arc<Store>, cancel: weaveffi::CancelToken) -> i64 { 0 }
1196 }
1197 "#,
1198 );
1199 let f = &m.functions[0];
1200 assert!(f.r#async);
1201 assert!(f.cancellable);
1202 assert_eq!(f.params.len(), 1);
1204 assert_eq!(f.params[0].name, "store");
1205 assert_eq!(f.params[0].ty, TypeRef::Named("Store".into()));
1206 }
1207
1208 #[test]
1209 fn interface_members_and_receivers() {
1210 let m = one_module(
1211 r#"
1212 #[weaveffi::module]
1213 mod m {
1214 #[weaveffi::interface]
1215 pub struct Store;
1216 impl Store {
1217 pub fn open(path: String) -> Result<Arc<Self>, StoreError> { todo!() }
1218 pub fn new() -> Self { Store }
1219 pub fn len(&self) -> u64 { 0 }
1220 pub fn share(self: Arc<Self>) -> Arc<Self> { self }
1221 pub fn default_path() -> String { String::new() }
1222 fn private(&self) {}
1223 }
1224 }
1225 "#,
1226 );
1227 let s = &m.interfaces[0];
1228 assert_eq!(s.constructors.len(), 2);
1229 assert!(s.constructors[0].throws);
1230 assert_eq!(s.constructors[0].returns, None);
1231 assert_eq!(s.methods.len(), 2);
1232 assert_eq!(s.methods[1].returns, Some(TypeRef::Named("Store".into())));
1233 assert_eq!(s.statics.len(), 1);
1234 }
1235
1236 #[test]
1237 fn mut_self_receiver_is_rejected() {
1238 let err = api_from_src(
1239 r#"
1240 #[weaveffi::module]
1241 mod m {
1242 #[weaveffi::interface]
1243 pub struct Store;
1244 impl Store {
1245 pub fn bump(&mut self) {}
1246 }
1247 }
1248 "#,
1249 )
1250 .unwrap_err();
1251 assert!(err.to_string().contains("&self"));
1252 }
1253
1254 #[test]
1255 fn callback_interface_trait() {
1256 let m = one_module(
1257 r#"
1258 #[weaveffi::module]
1259 mod m {
1260 /// Receives messages.
1261 #[weaveffi::callback_interface]
1262 pub trait Listener: Send + Sync {
1263 fn on_message(&self, text: String, meta: Option<Meta>);
1264 fn level(&self) -> i32;
1265 }
1266 #[weaveffi::export]
1267 pub fn subscribe(listener: Arc<dyn Listener>) {}
1268 #[weaveffi::export]
1269 pub fn subscribe_bounded(listener: Arc<dyn Listener + Send + Sync>) {}
1270 }
1271 "#,
1272 );
1273 assert_eq!(m.callback_interfaces.len(), 1);
1274 let cb = &m.callback_interfaces[0];
1275 assert_eq!(cb.name, "Listener");
1276 assert_eq!(cb.doc.as_deref(), Some("Receives messages."));
1277 assert_eq!(cb.methods.len(), 2);
1278 assert_eq!(cb.methods[0].params.len(), 2);
1279 assert_eq!(cb.methods[1].returns, Some(TypeRef::I32));
1280 assert_eq!(
1281 m.functions[0].params[0].ty,
1282 TypeRef::Named("Listener".into())
1283 );
1284 assert_eq!(
1285 m.functions[1].params[0].ty,
1286 TypeRef::Named("Listener".into())
1287 );
1288 }
1289
1290 #[test]
1291 fn callback_methods_may_return_foreign_results() {
1292 let m = one_module(
1293 r#"
1294 #[weaveffi::module]
1295 mod m {
1296 #[weaveffi::callback_interface]
1297 pub trait Listener: Send + Sync {
1298 fn level(&self) -> Result<i32, weaveffi::ForeignError>;
1299 fn ping(&self) -> Result<(), ForeignError>;
1300 fn strict(&self) -> Result<i32, String>;
1301 }
1302 }
1303 "#,
1304 );
1305 let methods = &m.callback_interfaces[0].methods;
1306 assert_eq!(methods[0].returns, Some(TypeRef::I32));
1307 assert!(!methods[0].throws);
1308 assert_eq!(methods[1].returns, None);
1309 assert!(!methods[1].throws);
1310 assert!(methods[2].throws, "other error types still throw");
1311 }
1312
1313 #[test]
1314 fn callback_interface_methods_need_self() {
1315 let err = api_from_src(
1316 r#"
1317 #[weaveffi::module]
1318 mod m {
1319 #[weaveffi::callback_interface]
1320 pub trait Listener {
1321 fn make() -> i32;
1322 }
1323 }
1324 "#,
1325 )
1326 .unwrap_err();
1327 assert!(err.to_string().contains("&self"));
1328 }
1329
1330 #[test]
1331 fn nested_modules() {
1332 let m = one_module(
1333 r#"
1334 #[weaveffi::module]
1335 mod outer {
1336 #[weaveffi::export]
1337 pub fn top() -> i32 { 0 }
1338 #[weaveffi::module]
1339 mod inner {
1340 #[weaveffi::export]
1341 pub fn deep(x: bool) -> bool { x }
1342 }
1343 }
1344 "#,
1345 );
1346 assert_eq!(m.modules.len(), 1);
1347 assert_eq!(m.modules[0].name, "inner");
1348 assert_eq!(m.modules[0].functions[0].name, "deep");
1349 }
1350}