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 arugments.
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 comma_before_non_jni_args = if non_jni_args.is_empty() {
234 None
235 } else {
236 Some(syn::token::Comma(proc_macro2::Span::call_site()))
237 };
238
239 quote! {
240 #[no_mangle]
241 unsafe extern "C" fn #java_fn_name<'local>(
242 env: JNIEnv<'local>,
243 class: JClass<'local>,
244 #(#args),*
245 ) #ret {
246 #function_before();
247 #function(env, class, #(#args_),* #comma_before_non_jni_args #(#non_jni_args),*)
248 }
249
250 }
251 .into()
252}
253
254struct GeneratePackageNameInput {
255 domain: Ident,
256 package: Ident,
257}
258
259impl Parse for GeneratePackageNameInput {
260 fn parse(input: ParseStream) -> syn::Result<Self> {
261 let domain: Ident = input.parse()?;
262 let _: Comma = input.parse()?;
263 let package: Ident = input.parse()?;
264 let _: syn::Result<Comma> = input.parse();
265
266 Ok(Self { domain, package })
267 }
268}
269
270/// Generate the package name used for invoking Java/Kotlin methods at compile-time
271///
272/// This macro expects 2 parameters:
273/// 1. snake_case representation of the reversed domain of the app.
274/// 2. snake_case representation of the package name.
275///
276/// Note that `_` is the separator for the domain parts.
277/// For instance the `com.tauri.app` identifier should be represented as
278/// `generate_package_name!(com_tauri, app)`.
279/// To escape the `_` character you can use `_1` which aligns with the default NDK implementation.
280///
281/// ## Example
282///
283/// ```
284/// # use tao_macros::generate_package_name;
285///
286/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app);
287/// ```
288/// which can be used later on with JNI:
289/// ```
290/// # use tao_macros::generate_package_name;
291/// # fn find_my_class(env: i32, activity: i32, package: String) -> Result<(), ()> { Ok(()) }
292/// # let env = 0;
293/// # let activity = 0;
294///
295/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app); // constructs `com/example/tao_app`
296/// let ipc_class = find_my_class(env, activity, format!("{}/Ipc", PACKAGE_NAME)).unwrap();
297/// ```
298#[proc_macro]
299pub fn generate_package_name(tokens: TokenStream) -> TokenStream {
300 let tokens = parse_macro_input!(tokens as GeneratePackageNameInput);
301 let GeneratePackageNameInput { domain, package } = tokens;
302
303 // note that this character is invalid in an identifier so it's safe to use as replacement
304 const TEMP_ESCAPE_UNDERSCORE_REPLACEMENT: &str = "<";
305
306 let domain = domain
307 .to_string()
308 .replace("_1", TEMP_ESCAPE_UNDERSCORE_REPLACEMENT)
309 .replace('_', "/")
310 .replace(TEMP_ESCAPE_UNDERSCORE_REPLACEMENT, "_");
311 let package = package.to_string();
312
313 let path = format!("{}/{}", domain, package);
314 let litstr = LitStr::new(&path, proc_macro2::Span::call_site());
315
316 quote! {#litstr}.into()
317}