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()
}