js_sandbox_macros_ios/
lib.rs

1// Copyright (c) 2020-2023 js-sandbox contributors. Zlib license.
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::{quote, ToTokens};
6use syn::spanned::Spanned as _;
7
8#[proc_macro_attribute]
9pub fn js_api(_attr: TokenStream, input: TokenStream) -> TokenStream {
10	let item = syn::parse_macro_input!(input as syn::ItemTrait);
11
12	let stream2 = match generate_api(item) {
13		Ok(stream) => stream,
14		Err(err) => err.to_compile_error(),
15	};
16
17	TokenStream::from(stream2)
18}
19
20fn generate_api(item: syn::ItemTrait) -> syn::Result<TokenStream2> {
21	let name = &item.ident;
22	let struct_ = generate_struct(&item)?;
23	let methods = generate_impl_methods(&item)?;
24	let marker_impl = generate_marker_trait_impl(&item)?;
25
26	Ok(quote! {
27		#struct_
28		impl<'a> #name<'a> {
29			#methods
30		}
31		#marker_impl
32	})
33}
34
35fn generate_struct(item: &syn::ItemTrait) -> syn::Result<TokenStream2> {
36	let name = &item.ident;
37	let visibility = &item.vis;
38
39	Ok(quote! {
40		#visibility struct #name<'a> {
41			script: &'a mut js_sandbox_ios::Script,
42		}
43	})
44}
45
46fn generate_marker_trait_impl(item: &syn::ItemTrait) -> syn::Result<TokenStream2> {
47	let name = &item.ident;
48	let visibility = &item.vis;
49
50	Ok(quote! {
51		impl<'a> js_sandbox_ios::JsApi<'a> for #name<'a> {
52			#visibility fn from_script(script: &'a mut js_sandbox_ios::Script) -> Self {
53				Self { script }
54			}
55		}
56	})
57}
58
59macro_rules! syntax_error {
60	($err:expr, $($fmt:tt)*) => (
61		{ return Err(syn::Error::new($err.span(), format!($($fmt)*))); }
62	)
63}
64
65enum ReturnType {
66	Unit,
67	Direct(syn::Type),
68	ResultWrap(syn::Type),
69}
70
71fn generate_impl_methods(item: &syn::ItemTrait) -> syn::Result<TokenStream2> {
72	let mut result = TokenStream2::new();
73	for item in item.items.iter() {
74		let method = match item {
75			syn::TraitItem::Fn(f) => f,
76			other => syntax_error!(other, "only methods are allowed"),
77		};
78		if let Some(tok) = &method.sig.constness {
79			syntax_error!(tok, "const functions are not supported");
80		}
81		if let Some(tok) = &method.sig.asyncness {
82			syntax_error!(tok, "async functions are not supported");
83		}
84		if let Some(tok) = &method.default {
85			syntax_error!(tok, "cannot specify an implementation of methods");
86		}
87		if let Some(rcv) = method.sig.receiver() {
88			if rcv.mutability.is_none() {
89				syntax_error!(
90					rcv,
91					"receiver must be `&mut self`; values and shared references are not supported"
92				);
93			}
94		} else {
95			syntax_error!(
96				method.sig.ident,
97				"receiver must be `&mut self`; associated methods are not supported"
98			);
99		}
100
101		let mut args = Vec::new();
102		for arg in method.sig.inputs.iter() {
103			let arg = match arg {
104				syn::FnArg::Receiver(_) => continue,
105				syn::FnArg::Typed(arg) => arg,
106			};
107			let ident = match &*arg.pat {
108				syn::Pat::Ident(i) => i,
109				other => syntax_error!(other, "parameter must be a bare identifier"),
110			};
111			if let Some(tok) = &ident.by_ref {
112				syntax_error!(tok, "parameter must be a value; by-reference unsupported");
113			}
114			if let Some(tok) = &ident.mutability {
115				syntax_error!(tok, "parameter must not be mutable");
116			}
117			if let Some((_, tok)) = &ident.subpat {
118				syntax_error!(tok, "parameter cannot have destructured bindings");
119			}
120
121			let ident = &ident.ident;
122			args.push(ident);
123		}
124
125		let sig = &method.sig;
126		let attrs = &method.attrs;
127		let fn_name = quote_token(&method.sig.ident);
128
129		let return_type: TokenStream2;
130		let transform: TokenStream2;
131		match parse_return_type(&method.sig.output)? {
132			ReturnType::Direct(ty) => {
133				return_type = ty.to_token_stream();
134				let ty_str = quote_token(&ty);
135				transform = quote! {
136					result.expect(concat!("cannot convert to type `", #ty_str, "`"))
137				}
138			}
139			ReturnType::ResultWrap(ty) => {
140				return_type = ty.to_token_stream();
141				transform = quote! { result };
142			}
143			ReturnType::Unit => {
144				return_type = quote! { () };
145				transform = quote! {
146					result.expect("JS function call failed");
147				}
148			}
149		};
150
151		result.extend(quote! {
152			#(#attrs)*
153			#sig {
154				let args = (
155					#(#args,),*
156				);
157
158				let result: js_sandbox_ios::JsResult<#return_type> = self.script.call(#fn_name, args);
159				#transform
160			}
161		});
162	}
163
164	Ok(result)
165}
166
167fn parse_return_type(tok: &syn::ReturnType) -> syn::Result<ReturnType> {
168	match tok {
169		syn::ReturnType::Default => {
170			// no explicit return type
171			Ok(ReturnType::Unit)
172		}
173		syn::ReturnType::Type(_, ty) => {
174			if let syn::Type::Path(path) = ty.as_ref() {
175				let seg = path.path.segments.first();
176
177				if seg.is_none() {
178					syntax_error!(tok, "unsupported return type; expected T or JsResult<T>, where T: Deserializable")
179				}
180
181				let seg = seg.unwrap();
182
183				if seg.ident == "JsResult" || seg.ident == "js_sandbox_ios::JsResult" {
184					// -> JsResult<T>
185					match &seg.arguments {
186						syn::PathArguments::None => {}
187						syn::PathArguments::AngleBracketed(ret) => {
188							if let Some(syn::GenericArgument::Type(ty)) = ret.args.first() {
189								return Ok(ReturnType::ResultWrap((*ty).clone()));
190							}
191						}
192						syn::PathArguments::Parenthesized(_) => {}
193					}
194				} else {
195					// -> T
196					return Ok(ReturnType::Direct((**ty).clone()));
197				}
198			}
199
200			syntax_error!(
201				tok,
202				"unsupported return type; expected T or JsResult<T>, where T: Deserializable"
203			)
204		}
205	}
206}
207
208fn quote_token(token: &dyn quote::ToTokens) -> syn::Lit {
209	syn::Lit::Str(syn::LitStr::new(
210		&token.to_token_stream().to_string(),
211		token.span(),
212	))
213}