1#![forbid(unsafe_code)]
14#![warn(missing_docs)]
15
16use proc_macro::TokenStream;
17use proc_macro2::TokenStream as TokenStream2;
18use quote::{format_ident, quote};
19use syn::{
20 parse::{Parse, ParseStream},
21 parse_macro_input,
22 spanned::Spanned,
23 FnArg, ItemFn, Pat, ReturnType, Type,
24};
25
26#[proc_macro_attribute]
57pub fn ffi_export(attr: TokenStream, item: TokenStream) -> TokenStream {
58 let args = parse_macro_input!(attr as FfiArgs);
59 let func = parse_macro_input!(item as ItemFn);
60
61 match expand_ffi_export(args, func) {
62 Ok(ts) => ts.into(),
63 Err(e) => e.to_compile_error().into(),
64 }
65}
66
67struct FfiArgs {
68 name: Option<String>,
70 prefix: Option<String>,
72}
73
74impl Parse for FfiArgs {
75 fn parse(input: ParseStream) -> syn::Result<Self> {
76 let mut name = None;
77 let mut prefix = None;
78 if input.is_empty() {
79 return Ok(FfiArgs { name, prefix });
80 }
81 let metas: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
82 input.parse_terminated(syn::MetaNameValue::parse, syn::Token![,])?;
83 for m in metas {
84 let key = m
85 .path
86 .get_ident()
87 .map(|i| i.to_string())
88 .unwrap_or_default();
89 let val = match m.value {
90 syn::Expr::Lit(syn::ExprLit {
91 lit: syn::Lit::Str(s),
92 ..
93 }) => s.value(),
94 other => {
95 return Err(syn::Error::new(other.span(), "expected string literal"));
96 }
97 };
98 match key.as_str() {
99 "name" => name = Some(val),
100 "prefix" => prefix = Some(val),
101 _ => return Err(syn::Error::new(m.path.span(), "unknown ffi_export option")),
102 }
103 }
104 Ok(FfiArgs { name, prefix })
105 }
106}
107
108fn expand_ffi_export(args: FfiArgs, mut func: ItemFn) -> syn::Result<TokenStream2> {
109 let original = func.clone();
110 let rust_ident = func.sig.ident.clone();
111 let prefix = args.prefix.unwrap_or_else(|| "weby_".to_string());
112 let c_ident = match args.name {
113 Some(n) => format_ident!("{}", n),
114 None => format_ident!("{}{}", prefix, rust_ident),
115 };
116
117 let is_async = func.sig.asyncness.is_some();
118
119 let mut c_params = Vec::<TokenStream2>::new();
124 let mut conversions = Vec::<TokenStream2>::new();
125 let mut call_args = Vec::<TokenStream2>::new();
126
127 for input in &func.sig.inputs {
128 let FnArg::Typed(pat_ty) = input else {
129 return Err(syn::Error::new(
130 input.span(),
131 "ffi_export does not support `self` receivers; use a free function with an opaque handle parameter",
132 ));
133 };
134 let Pat::Ident(pat_ident) = &*pat_ty.pat else {
135 return Err(syn::Error::new(
136 pat_ty.pat.span(),
137 "expected simple identifier",
138 ));
139 };
140 let name = pat_ident.ident.clone();
141 let ty = &*pat_ty.ty;
142 let kind = classify_input(ty)?;
143 match kind {
144 InputKind::CStrRef => {
145 let raw = format_ident!("__{}_raw", name);
146 c_params.push(quote! { #raw: *const ::std::os::raw::c_char });
147 conversions.push(quote! {
148 let #name: &str = match unsafe {
149 if #raw.is_null() { return -1; }
150 ::std::ffi::CStr::from_ptr(#raw).to_str()
151 } {
152 Ok(s) => s,
153 Err(_) => return -2,
154 };
155 });
156 call_args.push(quote! { #name });
157 }
158 InputKind::Scalar(scalar) => {
159 c_params.push(quote! { #name: #scalar });
160 call_args.push(quote! { #name });
161 }
162 InputKind::Opaque(path) => {
163 c_params.push(quote! { #name: #path });
164 call_args.push(quote! { #name });
165 }
166 }
167 }
168
169 let (c_return, post_call): (TokenStream2, TokenStream2) = match &func.sig.output {
172 ReturnType::Default => (quote! { () }, quote! { drop(__result) }),
173 ReturnType::Type(_, ty) => match classify_output(ty)? {
174 OutputKind::Scalar(s) => (quote! { #s }, quote! { __result }),
176 OutputKind::Opaque(path) => (quote! { #path }, quote! { __result }),
177 OutputKind::ResultUnit => (
178 quote! { i32 },
179 quote! {
180 match __result {
181 Ok(()) => 0,
182 Err(_) => -100,
183 }
184 },
185 ),
186 OutputKind::ResultScalar(s) => {
187 c_params.push(quote! { __out: *mut #s });
188 (
189 quote! { i32 },
190 quote! {
191 match __result {
192 Ok(v) => unsafe {
193 if !__out.is_null() { *__out = v; }
194 0
195 },
196 Err(_) => -100,
197 }
198 },
199 )
200 }
201 OutputKind::ResultOpaque(path) => {
202 c_params.push(quote! { __out: *mut #path });
203 (
204 quote! { i32 },
205 quote! {
206 match __result {
207 Ok(v) => unsafe {
208 if !__out.is_null() { *__out = v; }
209 0
210 },
211 Err(_) => -100,
212 }
213 },
214 )
215 }
216 },
217 };
218
219 func.sig.asyncness = None;
221
222 let invocation = if is_async {
223 quote! {
227 let __result = {
228 static __RT: ::std::sync::OnceLock<::tokio::runtime::Runtime> =
229 ::std::sync::OnceLock::new();
230 let rt = __RT.get_or_init(|| {
231 ::tokio::runtime::Builder::new_multi_thread()
232 .enable_all()
233 .build()
234 .expect("ffi_export tokio runtime")
235 });
236 rt.block_on(async {
237 #rust_ident(#(#call_args),*).await
238 })
239 };
240 }
241 } else {
242 quote! {
243 let __result = #rust_ident(#(#call_args),*);
244 }
245 };
246
247 let extern_fn = quote! {
248 #[no_mangle]
251 pub unsafe extern "C" fn #c_ident(#(#c_params),*) -> #c_return {
252 #(#conversions)*
255 #invocation
256 #post_call
257 }
258 };
259
260 Ok(quote! {
261 #original
262 #extern_fn
263 })
264}
265
266enum InputKind {
267 CStrRef,
268 Scalar(TokenStream2),
269 Opaque(TokenStream2),
270}
271
272fn classify_input(ty: &Type) -> syn::Result<InputKind> {
273 if let Type::Reference(r) = ty {
274 if let Type::Path(p) = &*r.elem {
275 if p.path.is_ident("str") {
276 return Ok(InputKind::CStrRef);
277 }
278 }
279 }
280 if let Type::Path(p) = ty {
281 if let Some(seg) = p.path.segments.last() {
282 let s = seg.ident.to_string();
283 match s.as_str() {
284 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" | "isize"
285 | "bool" | "f32" | "f64" => {
286 return Ok(InputKind::Scalar(quote! { #ty }));
287 }
288 _ => {
289 return Ok(InputKind::Opaque(quote! { #ty }));
292 }
293 }
294 }
295 }
296 if let Type::Ptr(_) = ty {
297 return Ok(InputKind::Opaque(quote! { #ty }));
298 }
299 Err(syn::Error::new(
300 ty.span(),
301 "ffi_export: unsupported parameter type",
302 ))
303}
304
305enum OutputKind {
306 Scalar(TokenStream2),
307 Opaque(TokenStream2),
308 ResultUnit,
309 ResultScalar(TokenStream2),
310 ResultOpaque(TokenStream2),
311}
312
313fn classify_output(ty: &Type) -> syn::Result<OutputKind> {
314 if let Type::Path(p) = ty {
315 if let Some(seg) = p.path.segments.last() {
316 if seg.ident == "Result" {
317 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
318 let inner = args.args.first();
319 match inner {
320 Some(syn::GenericArgument::Type(Type::Tuple(t))) if t.elems.is_empty() => {
321 return Ok(OutputKind::ResultUnit);
322 }
323 Some(syn::GenericArgument::Type(inner_ty)) => {
324 return Ok(match classify_input(inner_ty)? {
325 InputKind::Scalar(s) => OutputKind::ResultScalar(s),
326 InputKind::Opaque(p) => OutputKind::ResultOpaque(p),
327 InputKind::CStrRef => {
328 return Err(syn::Error::new(
329 inner_ty.span(),
330 "ffi_export: returning &str isn't supported; use String + caller-allocated buffer instead",
331 ));
332 }
333 });
334 }
335 _ => {}
336 }
337 }
338 return Err(syn::Error::new(
339 seg.ident.span(),
340 "ffi_export: malformed Result<...>",
341 ));
342 }
343 }
344 let s = p
345 .path
346 .segments
347 .last()
348 .map(|s| s.ident.to_string())
349 .unwrap_or_default();
350 match s.as_str() {
351 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "usize" | "isize"
352 | "bool" | "f32" | "f64" => {
353 return Ok(OutputKind::Scalar(quote! { #ty }));
354 }
355 _ => {
356 return Ok(OutputKind::Opaque(quote! { #ty }));
357 }
358 }
359 }
360 Err(syn::Error::new(
361 ty.span(),
362 "ffi_export: unsupported return type",
363 ))
364}
365
366#[proc_macro_attribute]
375pub fn wallet_op(_attr: TokenStream, item: TokenStream) -> TokenStream {
376 item
377}
378
379#[proc_macro_attribute]
382pub fn asset_storage(_attr: TokenStream, item: TokenStream) -> TokenStream {
383 item
384}