Skip to main content

tao_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote, ToTokens};
3use syn::{
4  bracketed,
5  parse::{Parse, ParseStream},
6  parse_macro_input,
7  punctuated::Punctuated,
8  token::Comma,
9  Ident, LitStr, Token, Type,
10};
11
12struct AndroidFnInput {
13  domain: Ident,
14  package: Ident,
15  class: Ident,
16  function: Ident,
17  args: Punctuated<Type, Comma>,
18  non_jni_args: Punctuated<Type, Comma>,
19  ret: Option<Type>,
20  function_before: Option<Ident>,
21}
22
23struct IdentArgPair(syn::Ident, syn::Type);
24
25impl ToTokens for IdentArgPair {
26  fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
27    let ident = &self.0;
28    let type_ = &self.1;
29    let tok = quote! {
30      #ident: #type_
31    };
32    tokens.extend([tok]);
33  }
34}
35
36impl Parse for AndroidFnInput {
37  fn parse(input: ParseStream) -> syn::Result<Self> {
38    let domain: Ident = input.parse()?;
39    let _: Comma = input.parse()?;
40    let package: Ident = input.parse()?;
41    let _: Comma = input.parse()?;
42    let class: Ident = input.parse()?;
43    let _: Comma = input.parse()?;
44    let function: Ident = input.parse()?;
45    let _: Comma = input.parse()?;
46
47    let args;
48    let _: syn::token::Bracket = bracketed!(args in input);
49    let args = args.parse_terminated(Type::parse, Token![,])?;
50    let _: syn::Result<Comma> = input.parse();
51
52    let ret = if input.peek(Ident) {
53      let ret = input.parse::<Type>()?;
54      let _: syn::Result<Comma> = input.parse();
55      if ret.to_token_stream().to_string() == "__VOID__" {
56        None
57      } else {
58        Some(ret)
59      }
60    } else {
61      None
62    };
63
64    let non_jni_args = if input.peek(syn::token::Bracket) {
65      let non_jni_args;
66      let _: syn::token::Bracket = bracketed!(non_jni_args in input);
67      let non_jni_args = non_jni_args.parse_terminated(Type::parse, Token![,])?;
68      let _: syn::Result<Comma> = input.parse();
69      non_jni_args
70    } else {
71      Punctuated::new()
72    };
73
74    let function_before = if input.peek(Ident) {
75      let function: Ident = input.parse()?;
76      let _: syn::Result<Comma> = input.parse();
77      Some(function)
78    } else {
79      None
80    };
81    Ok(Self {
82      domain,
83      package,
84      class,
85      function,
86      ret,
87      args,
88      non_jni_args,
89      function_before,
90    })
91  }
92}
93
94/// Generates a JNI binding for a Rust function so you can use it as the extern for Java/Kotlin class methods in your android app.
95///
96/// This macro expects 5 mandatory parameters and 3 optional:
97/// 1. snake_case representation of the reversed domain of the app. for example: com_tao
98/// 2. snake_case representation of the package name. for example: tao_app
99/// 3. Java/Kotlin class name.
100/// 4. Rust function name (`ident`).
101/// 5. List of extra types your Rust function expects. Pass empty array if the function doesn't need any arguments.
102///   - If your function takes an arguments as reference with a lifetime tied to the [`JNIEnv`], it should use `'local` as the lifetime name as it is the
103///     lifetime name generated by the macro.
104///     Note that all rust functions should expect the first two parameters to be [`JNIEnv`] and [`JClass`] so make sure they are imported into scope).
105/// 6. (Optional) Return type of your rust function.
106///   - If your function returns a reference with a lifetime tied to the [`JNIEnv`], it should use `'local` as the lifetime name as it is the
107///     lifetime name generated by the macro.
108///   - if you want to use the next macro parameter you need to provide a type or just pass `__VOID__` if the function doesn't return anything.
109/// 7. (Optional) List of `ident`s to pass to the rust function when invoked (This mostly exists for internal usage by `tao` crate).
110/// 8. (Optional) Function to be invoked right before invoking the rust function (This mostly exists for internal usage by `tao` crate).
111///
112/// ## Example 1: Basic
113///
114/// ```
115/// # use tao_macros::android_fn;
116/// # struct JNIEnv<'a> {
117/// #  _marker: &'a std::marker::PhantomData<()>,
118/// # }
119/// # type JClass<'a> = JNIEnv<'a>;
120/// android_fn![com_example, tao, OperationsClass, add, [i32, i32], i32];
121/// unsafe fn add(_env: JNIEnv, _class: JClass, a: i32, b: i32) -> i32 {
122///   a + b
123/// }
124/// ```
125/// which will expand into:
126/// ```
127/// # struct JNIEnv<'a> {
128/// #  _marker: &'a std::marker::PhantomData<()>,
129/// # }
130/// # type JClass<'a> = JNIEnv<'a>;
131/// #[no_mangle]
132/// unsafe extern "C" fn Java_com_example_tao_OperationsClass_add<'local>(
133///   env: JNIEnv<'local>,
134///   class: JClass<'local>,
135///   a_1: i32,
136///   a_2: i32
137/// )  -> i32 {
138///   add(env, class, a_1, a_2)
139/// }
140///
141/// unsafe fn add<'local>(_env: JNIEnv<'local>, _class: JClass<'local>, a: i32, b: i32) -> i32 {
142///   a + b
143/// }
144/// ```
145/// and now you can extern the function in your Java/kotlin:
146///
147/// ```kotlin
148/// class OperationsClass {
149///   external fun add(a: Int, b: Int): Int;
150/// }
151/// ```
152///
153/// ## Example 2: Return a reference with a lifetime
154///
155/// ```
156/// # use tao_macros::android_fn;
157/// # struct JNIEnv<'a> {
158/// #  _marker: &'a std::marker::PhantomData<()>,
159/// # }
160/// # type JClass<'a> = JNIEnv<'a>;
161/// # type JObject<'a> = JNIEnv<'a>;
162/// android_fn![com_example, tao, OperationsClass, add, [JObject<'local>], JClass<'local>];
163/// unsafe fn add<'local>(mut _env: JNIEnv<'local>, class: JClass<'local>, obj: JObject<'local>) -> JClass<'local> {
164///   class
165/// }
166/// ```
167/// which will expand into:
168/// ```
169/// # struct JNIEnv<'a> {
170/// #  _marker: &'a std::marker::PhantomData<()>,
171/// # }
172/// # type JClass<'a> = JNIEnv<'a>;
173/// # type JObject<'a> = JNIEnv<'a>;
174/// #[no_mangle]
175/// unsafe extern "C" fn Java_com_example_tao_OperationsClass_add<'local>(
176///   env: JNIEnv<'local>,
177///   class: JClass<'local>,
178///   a_1: JObject<'local>,
179/// )  -> JClass<'local> {
180///   add(env, class, a_1)
181/// }
182///
183/// unsafe fn add<'local>(mut _env: JNIEnv<'local>, class: JClass<'local>, obj: JObject<'local>) -> JClass<'local> {
184///   class
185/// }
186/// ```
187///
188/// [`JNIEnv`]: https://docs.rs/jni/latest/jni/struct.JNIEnv.html
189/// [`JClass`]: https://docs.rs/jni/latest/jni/objects/struct.JClass.html
190#[proc_macro]
191pub fn android_fn(tokens: TokenStream) -> TokenStream {
192  let tokens = parse_macro_input!(tokens as AndroidFnInput);
193  let AndroidFnInput {
194    domain,
195    package,
196    class,
197    function,
198    ret,
199    args,
200    non_jni_args,
201    function_before,
202  } = tokens;
203
204  let domain = domain.to_string();
205  let package = package.to_string().replace('_', "_1");
206  let class = class.to_string();
207  let args = args
208    .into_iter()
209    .enumerate()
210    .map(|(i, t)| IdentArgPair(format_ident!("a_{}", i), t))
211    .collect::<Vec<_>>();
212  let non_jni_args = non_jni_args.into_iter().collect::<Vec<_>>();
213
214  let java_fn_name = format_ident!(
215    "Java_{domain}_{package}_{class}_{function}",
216    domain = domain,
217    package = package,
218    class = class,
219    function = function,
220  );
221
222  let args_ = args.iter().map(|a| &a.0);
223
224  let ret = if let Some(ret) = ret {
225    syn::ReturnType::Type(
226      syn::token::RArrow(proc_macro2::Span::call_site()),
227      Box::new(ret),
228    )
229  } else {
230    syn::ReturnType::Default
231  };
232
233  let function_before = function_before.map(|function_before| quote!(#function_before();));
234
235  let comma_before_args = if args.is_empty() {
236    None
237  } else {
238    Some(syn::token::Comma(proc_macro2::Span::call_site()))
239  };
240
241  let comma_before_non_jni_args = if non_jni_args.is_empty() {
242    None
243  } else {
244    Some(syn::token::Comma(proc_macro2::Span::call_site()))
245  };
246
247  quote! {
248    #[no_mangle]
249    unsafe extern "C" fn #java_fn_name<'local>(
250      env: JNIEnv<'local>,
251      class: JClass<'local>,
252      #(#args),*
253    )  #ret {
254      #function_before
255      #function(env, class #comma_before_args #(#args_),* #comma_before_non_jni_args #(#non_jni_args),*)
256    }
257
258  }
259  .into()
260}
261
262struct GeneratePackageNameInput {
263  domain: Ident,
264  package: Ident,
265}
266
267impl Parse for GeneratePackageNameInput {
268  fn parse(input: ParseStream) -> syn::Result<Self> {
269    let domain: Ident = input.parse()?;
270    let _: Comma = input.parse()?;
271    let package: Ident = input.parse()?;
272    let _: syn::Result<Comma> = input.parse();
273
274    Ok(Self { domain, package })
275  }
276}
277
278/// Generate the package name used for invoking Java/Kotlin methods at compile-time
279///
280/// This macro expects 2 parameters:
281/// 1. snake_case representation of the reversed domain of the app.
282/// 2. snake_case representation of the package name.
283///
284/// Note that `_` is the separator for the domain parts.
285/// For instance the `com.tauri.app` identifier should be represented as
286/// `generate_package_name!(com_tauri, app)`.
287/// To escape the `_` character you can use `_1` which aligns with the default NDK implementation.
288///
289/// ## Example
290///
291/// ```
292/// # use tao_macros::generate_package_name;
293///
294/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app);
295/// ```
296/// which can be used later on with JNI:
297/// ```
298/// # use tao_macros::generate_package_name;
299/// # fn find_my_class(env: i32, activity: i32, package: String) -> Result<(), ()> { Ok(()) }
300/// # let env = 0;
301/// # let activity = 0;
302///
303/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app); // constructs `com/example/tao_app`
304/// let ipc_class = find_my_class(env, activity, format!("{}/Ipc", PACKAGE_NAME)).unwrap();
305/// ```
306#[proc_macro]
307pub fn generate_package_name(tokens: TokenStream) -> TokenStream {
308  let tokens = parse_macro_input!(tokens as GeneratePackageNameInput);
309  let GeneratePackageNameInput { domain, package } = tokens;
310
311  // note that this character is invalid in an identifier so it's safe to use as replacement
312  const TEMP_ESCAPE_UNDERSCORE_REPLACEMENT: &str = "<";
313
314  let domain = domain
315    .to_string()
316    .replace("_1", TEMP_ESCAPE_UNDERSCORE_REPLACEMENT)
317    .replace('_', "/")
318    .replace(TEMP_ESCAPE_UNDERSCORE_REPLACEMENT, "_");
319  let package = package.to_string();
320
321  let path = format!("{}/{}", domain, package);
322  let litstr = LitStr::new(&path, proc_macro2::Span::call_site());
323
324  quote! {#litstr}.into()
325}