1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
  bracketed,
  parse::{Parse, ParseStream},
  parse_macro_input,
  punctuated::Punctuated,
  token::Comma,
  Ident, LitStr, Token, Type,
};

struct AndroidFnInput {
  domain: Ident,
  package: Ident,
  class: Ident,
  function: Ident,
  args: Punctuated<Type, Comma>,
  non_jni_args: Punctuated<Type, Comma>,
  ret: Option<Type>,
  function_before: Option<Ident>,
}

struct IdentArgPair(syn::Ident, syn::Type);

impl ToTokens for IdentArgPair {
  fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
    let ident = &self.0;
    let type_ = &self.1;
    let tok = quote! {
      #ident: #type_
    };
    tokens.extend([tok]);
  }
}

impl Parse for AndroidFnInput {
  fn parse(input: ParseStream) -> syn::Result<Self> {
    let domain: Ident = input.parse()?;
    let _: Comma = input.parse()?;
    let package: Ident = input.parse()?;
    let _: Comma = input.parse()?;
    let class: Ident = input.parse()?;
    let _: Comma = input.parse()?;
    let function: Ident = input.parse()?;
    let _: Comma = input.parse()?;

    let args;
    let _: syn::token::Bracket = bracketed!(args in input);
    let args = args.parse_terminated(Type::parse, Token![,])?;
    let _: syn::Result<Comma> = input.parse();

    let ret = if input.peek(Ident) {
      let ret = input.parse::<Type>()?;
      let _: syn::Result<Comma> = input.parse();
      if ret.to_token_stream().to_string() == "__VOID__" {
        None
      } else {
        Some(ret)
      }
    } else {
      None
    };

    let non_jni_args = if input.peek(syn::token::Bracket) {
      let non_jni_args;
      let _: syn::token::Bracket = bracketed!(non_jni_args in input);
      let non_jni_args = non_jni_args.parse_terminated(Type::parse, Token![,])?;
      let _: syn::Result<Comma> = input.parse();
      non_jni_args
    } else {
      Punctuated::new()
    };

    let function_before = if input.peek(Ident) {
      let function: Ident = input.parse()?;
      let _: syn::Result<Comma> = input.parse();
      Some(function)
    } else {
      None
    };
    Ok(Self {
      domain,
      package,
      class,
      function,
      ret,
      args,
      non_jni_args,
      function_before,
    })
  }
}

/// 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.
///
/// This macro expects 5 mandatory parameters and 3 optional:
/// 1. snake_case representation of the reversed domain of the app. for example: com_tao
/// 2. snake_case representation of the package name. for example: tao_app
/// 3. Java/Kotlin class name.
/// 4. Rust function name (`ident`).
/// 5. List of extra types your Rust function expects. Pass empty array if the function doesn't need any arugments.
///   - 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
///     lifetime name generated by the macro.
///     Note that all rust functions should expect the first two parameters to be [`JNIEnv`] and [`JClass`] so make sure they are imported into scope).
/// 6. (Optional) Return type of your rust function.
///   - 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
///     lifetime name generated by the macro.
///   - 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.
/// 7. (Optional) List of `ident`s to pass to the rust function when invoked (This mostly exists for internal usage by `tao` crate).
/// 8. (Optional) Function to be invoked right before invoking the rust function (This mostly exists for internal usage by `tao` crate).
///
/// ## Example 1: Basic
///
/// ```
/// # use tao_macros::android_fn;
/// # struct JNIEnv<'a> {
/// #  _marker: &'a std::marker::PhantomData<()>,
/// # }
/// # type JClass<'a> = JNIEnv<'a>;
/// android_fn![com_example, tao, OperationsClass, add, [i32, i32], i32];
/// unsafe fn add(_env: JNIEnv, _class: JClass, a: i32, b: i32) -> i32 {
///   a + b
/// }
/// ```
/// which will expand into:
/// ```
/// # struct JNIEnv<'a> {
/// #  _marker: &'a std::marker::PhantomData<()>,
/// # }
/// # type JClass<'a> = JNIEnv<'a>;
/// #[no_mangle]
/// unsafe extern "C" fn Java_com_example_tao_OperationsClass_add<'local>(
///   env: JNIEnv<'local>,
///   class: JClass<'local>,
///   a_1: i32,
///   a_2: i32
/// )  -> i32 {
///   add(env, class, a_1, a_2)
/// }
///
/// unsafe fn add<'local>(_env: JNIEnv<'local>, _class: JClass<'local>, a: i32, b: i32) -> i32 {
///   a + b
/// }
/// ```
/// and now you can extern the function in your Java/kotlin:
///
/// ```kotlin
/// class OperationsClass {
///   external fun add(a: Int, b: Int): Int;
/// }
/// ```
///
/// ## Example 2: Return a reference with a lifetime
///
/// ```
/// # use tao_macros::android_fn;
/// # struct JNIEnv<'a> {
/// #  _marker: &'a std::marker::PhantomData<()>,
/// # }
/// # type JClass<'a> = JNIEnv<'a>;
/// # type JObject<'a> = JNIEnv<'a>;
/// android_fn![com_example, tao, OperationsClass, add, [JObject<'local>], JClass<'local>];
/// unsafe fn add<'local>(mut _env: JNIEnv<'local>, class: JClass<'local>, obj: JObject<'local>) -> JClass<'local> {
///   class
/// }
/// ```
/// which will expand into:
/// ```
/// # struct JNIEnv<'a> {
/// #  _marker: &'a std::marker::PhantomData<()>,
/// # }
/// # type JClass<'a> = JNIEnv<'a>;
/// # type JObject<'a> = JNIEnv<'a>;
/// #[no_mangle]
/// unsafe extern "C" fn Java_com_example_tao_OperationsClass_add<'local>(
///   env: JNIEnv<'local>,
///   class: JClass<'local>,
///   a_1: JObject<'local>,
/// )  -> JClass<'local> {
///   add(env, class, a_1)
/// }
///
/// unsafe fn add<'local>(mut _env: JNIEnv<'local>, class: JClass<'local>, obj: JObject<'local>) -> JClass<'local> {
///   class
/// }
/// ```
///
/// - [`JNIEnv`]: https://docs.rs/jni/latest/jni/struct.JNIEnv.html
/// - [`JClass`]: https://docs.rs/jni/latest/jni/objects/struct.JClass.html
#[proc_macro]
pub fn android_fn(tokens: TokenStream) -> TokenStream {
  let tokens = parse_macro_input!(tokens as AndroidFnInput);
  let AndroidFnInput {
    domain,
    package,
    class,
    function,
    ret,
    args,
    non_jni_args,
    function_before,
  } = tokens;

  let domain = domain.to_string();
  let package = package.to_string().replace('_', "_1");
  let class = class.to_string();
  let args = args
    .into_iter()
    .enumerate()
    .map(|(i, t)| IdentArgPair(format_ident!("a_{}", i), t))
    .collect::<Vec<_>>();
  let non_jni_args = non_jni_args.into_iter().collect::<Vec<_>>();

  let java_fn_name = format_ident!(
    "Java_{domain}_{package}_{class}_{function}",
    domain = domain,
    package = package,
    class = class,
    function = function,
  );

  let args_ = args.iter().map(|a| &a.0);

  let ret = if let Some(ret) = ret {
    syn::ReturnType::Type(
      syn::token::RArrow(proc_macro2::Span::call_site()),
      Box::new(ret),
    )
  } else {
    syn::ReturnType::Default
  };

  let comma_before_non_jni_args = if non_jni_args.is_empty() {
    None
  } else {
    Some(syn::token::Comma(proc_macro2::Span::call_site()))
  };

  quote! {
    #[no_mangle]
    unsafe extern "C" fn #java_fn_name<'local>(
      env: JNIEnv<'local>,
      class: JClass<'local>,
      #(#args),*
    )  #ret {
      #function_before();
      #function(env, class, #(#args_),*  #comma_before_non_jni_args #(#non_jni_args),*)
    }

  }
  .into()
}

struct GeneratePackageNameInput {
  domain: Ident,
  package: Ident,
}

impl Parse for GeneratePackageNameInput {
  fn parse(input: ParseStream) -> syn::Result<Self> {
    let domain: Ident = input.parse()?;
    let _: Comma = input.parse()?;
    let package: Ident = input.parse()?;
    let _: syn::Result<Comma> = input.parse();

    Ok(Self { domain, package })
  }
}

/// Generate the package name used for invoking Java/Kotlin methods at compile-time
///
/// This macro expects 2 parameters:
/// 1. snake_case representation of the reversed domain of the app.
/// 2. snake_case representation of the package name.
///
/// Note that `_` is the separator for the domain parts.
/// For instance the `com.tauri.app` identifier should be represented as
/// `generate_package_name!(com_tauri, app)`.
/// To escape the `_` character you can use `_1` which aligns with the default NDK implementation.
///
/// ## Example
///
/// ```
/// # use tao_macros::generate_package_name;
///
/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app);
/// ```
/// which can be used later on with JNI:
/// ```
/// # use tao_macros::generate_package_name;
/// # fn find_my_class(env: i32, activity: i32, package: String) -> Result<(), ()> { Ok(()) }
/// # let env = 0;
/// # let activity = 0;
///
/// const PACKAGE_NAME: &str = generate_package_name!(com_example, tao_app); // constructs `com/example/tao_app`
/// let ipc_class = find_my_class(env, activity, format!("{}/Ipc", PACKAGE_NAME)).unwrap();
/// ```
#[proc_macro]
pub fn generate_package_name(tokens: TokenStream) -> TokenStream {
  let tokens = parse_macro_input!(tokens as GeneratePackageNameInput);
  let GeneratePackageNameInput { domain, package } = tokens;

  // note that this character is invalid in an identifier so it's safe to use as replacement
  const TEMP_ESCAPE_UNDERSCORE_REPLACEMENT: &str = "<";

  let domain = domain
    .to_string()
    .replace("_1", TEMP_ESCAPE_UNDERSCORE_REPLACEMENT)
    .replace('_', "/")
    .replace(TEMP_ESCAPE_UNDERSCORE_REPLACEMENT, "_");
  let package = package.to_string();

  let path = format!("{}/{}", domain, package);
  let litstr = LitStr::new(&path, proc_macro2::Span::call_site());

  quote! {#litstr}.into()
}