js_sandbox_macros_ios/
lib.rs1use 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 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 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 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}