#![forbid(unsafe_code)]
#![warn(missing_docs)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
FnArg, ItemFn, Pat, ReturnType, Type,
};
#[proc_macro_attribute]
pub fn ffi_export(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as FfiArgs);
let func = parse_macro_input!(item as ItemFn);
match expand_ffi_export(args, func) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
struct FfiArgs {
name: Option<String>,
prefix: Option<String>,
}
impl Parse for FfiArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut name = None;
let mut prefix = None;
if input.is_empty() {
return Ok(FfiArgs { name, prefix });
}
let metas: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
input.parse_terminated(syn::MetaNameValue::parse, syn::Token![,])?;
for m in metas {
let key = m
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
let val = match m.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => s.value(),
other => {
return Err(syn::Error::new(other.span(), "expected string literal"));
}
};
match key.as_str() {
"name" => name = Some(val),
"prefix" => prefix = Some(val),
_ => return Err(syn::Error::new(m.path.span(), "unknown ffi_export option")),
}
}
Ok(FfiArgs { name, prefix })
}
}
fn expand_ffi_export(args: FfiArgs, mut func: ItemFn) -> syn::Result<TokenStream2> {
let original = func.clone();
let rust_ident = func.sig.ident.clone();
let prefix = args.prefix.unwrap_or_else(|| "weby_".to_string());
let c_ident = match args.name {
Some(n) => format_ident!("{}", n),
None => format_ident!("{}{}", prefix, rust_ident),
};
let is_async = func.sig.asyncness.is_some();
let mut c_params = Vec::<TokenStream2>::new();
let mut conversions = Vec::<TokenStream2>::new();
let mut call_args = Vec::<TokenStream2>::new();
for input in &func.sig.inputs {
let FnArg::Typed(pat_ty) = input else {
return Err(syn::Error::new(
input.span(),
"ffi_export does not support `self` receivers; use a free function with an opaque handle parameter",
));
};
let Pat::Ident(pat_ident) = &*pat_ty.pat else {
return Err(syn::Error::new(
pat_ty.pat.span(),
"expected simple identifier",
));
};
let name = pat_ident.ident.clone();
let ty = &*pat_ty.ty;
let kind = classify_input(ty)?;
match kind {
InputKind::CStrRef => {
let raw = format_ident!("__{}_raw", name);
c_params.push(quote! { #raw: *const ::std::os::raw::c_char });
conversions.push(quote! {
let #name: &str = match unsafe {
if #raw.is_null() { return -1; }
::std::ffi::CStr::from_ptr(#raw).to_str()
} {
Ok(s) => s,
Err(_) => return -2,
};
});
call_args.push(quote! { #name });
}
InputKind::Scalar(scalar) => {
c_params.push(quote! { #name: #scalar });
call_args.push(quote! { #name });
}
InputKind::Opaque(path) => {
c_params.push(quote! { #name: #path });
call_args.push(quote! { #name });
}
}
}
let (c_return, post_call): (TokenStream2, TokenStream2) = match &func.sig.output {
ReturnType::Default => (quote! { () }, quote! { drop(__result) }),
ReturnType::Type(_, ty) => match classify_output(ty)? {
OutputKind::Scalar(s) => (quote! { #s }, quote! { __result }),
OutputKind::Opaque(path) => (quote! { #path }, quote! { __result }),
OutputKind::ResultUnit => (
quote! { i32 },
quote! {
match __result {
Ok(()) => 0,
Err(_) => -100,
}
},
),
OutputKind::ResultScalar(s) => {
c_params.push(quote! { __out: *mut #s });
(
quote! { i32 },
quote! {
match __result {
Ok(v) => unsafe {
if !__out.is_null() { *__out = v; }
0
},
Err(_) => -100,
}
},
)
}
OutputKind::ResultOpaque(path) => {
c_params.push(quote! { __out: *mut #path });
(
quote! { i32 },
quote! {
match __result {
Ok(v) => unsafe {
if !__out.is_null() { *__out = v; }
0
},
Err(_) => -100,
}
},
)
}
},
};
func.sig.asyncness = None;
let invocation = if is_async {
quote! {
let __result = {
static __RT: ::std::sync::OnceLock<::tokio::runtime::Runtime> =
::std::sync::OnceLock::new();
let rt = __RT.get_or_init(|| {
::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("ffi_export tokio runtime")
});
rt.block_on(async {
#rust_ident(#(#call_args),*).await
})
};
}
} else {
quote! {
let __result = #rust_ident(#(#call_args),*);
}
};
let extern_fn = quote! {
#[no_mangle]
pub unsafe extern "C" fn #c_ident(#(#c_params),*) -> #c_return {
#(#conversions)*
#invocation
#post_call
}
};
Ok(quote! {
#original
#extern_fn
})
}
enum InputKind {
CStrRef,
Scalar(TokenStream2),
Opaque(TokenStream2),
}
fn classify_input(ty: &Type) -> syn::Result<InputKind> {
if let Type::Reference(r) = ty {
if let Type::Path(p) = &*r.elem {
if p.path.is_ident("str") {
return Ok(InputKind::CStrRef);
}
}
}
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
let s = seg.ident.to_string();
match s.as_str() {
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" | "isize"
| "bool" | "f32" | "f64" => {
return Ok(InputKind::Scalar(quote! { #ty }));
}
_ => {
return Ok(InputKind::Opaque(quote! { #ty }));
}
}
}
}
if let Type::Ptr(_) = ty {
return Ok(InputKind::Opaque(quote! { #ty }));
}
Err(syn::Error::new(
ty.span(),
"ffi_export: unsupported parameter type",
))
}
enum OutputKind {
Scalar(TokenStream2),
Opaque(TokenStream2),
ResultUnit,
ResultScalar(TokenStream2),
ResultOpaque(TokenStream2),
}
fn classify_output(ty: &Type) -> syn::Result<OutputKind> {
if let Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Result" {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
let inner = args.args.first();
match inner {
Some(syn::GenericArgument::Type(Type::Tuple(t))) if t.elems.is_empty() => {
return Ok(OutputKind::ResultUnit);
}
Some(syn::GenericArgument::Type(inner_ty)) => {
return Ok(match classify_input(inner_ty)? {
InputKind::Scalar(s) => OutputKind::ResultScalar(s),
InputKind::Opaque(p) => OutputKind::ResultOpaque(p),
InputKind::CStrRef => {
return Err(syn::Error::new(
inner_ty.span(),
"ffi_export: returning &str isn't supported; use String + caller-allocated buffer instead",
));
}
});
}
_ => {}
}
}
return Err(syn::Error::new(
seg.ident.span(),
"ffi_export: malformed Result<...>",
));
}
}
let s = p
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
match s.as_str() {
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" | "isize"
| "bool" | "f32" | "f64" => {
return Ok(OutputKind::Scalar(quote! { #ty }));
}
_ => {
return Ok(OutputKind::Opaque(quote! { #ty }));
}
}
}
Err(syn::Error::new(
ty.span(),
"ffi_export: unsupported return type",
))
}
#[proc_macro_attribute]
pub fn wallet_op(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
pub fn asset_storage(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}