Obtain jni_fn locally
This commit is contained in:
1 parent
40e4d40683
commit
71f91bf054
5 files changed
+444
-1
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "jni_fn"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Anton Lazarev <https://antonok.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
description = "JNI-compatible method signature generator"
|
||||||
|
repository = "https://gitlab.com/antonok/jni_fn"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["jni", "java", "jvm", "android", "ndk"]
|
||||||
|
categories = ["api-bindings", "development-tools::procedural-macro-helpers"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = "1.0.109", features = ["full"] }
|
||||||
|
quote = "*"
|
||||||
|
proc-macro2 = "*"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
jni = "0.19.0"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Anton Lazarev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
//! JNI-compatible method signature generator for Rust libraries.
|
||||||
|
//!
|
||||||
|
//! This crate was designed for use with the [`jni`](https://crates.io/crates/jni) crate, which
|
||||||
|
//! exposes JNI-compatible type bindings. Although it's possible to use `jni` without `jni_fn`, the
|
||||||
|
//! procedural macro defined here will make it easier to write the method signatures correctly.
|
||||||
|
//!
|
||||||
|
//! See the `jni_fn` attribute macro documentation below for more info and usage examples.
|
||||||
|
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
#![deny(unsafe_code)]
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::ToTokens;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
/// Annotate a function with this procedural macro attribute to expose it over the JNI.
|
||||||
|
///
|
||||||
|
/// This attribute takes a single string literal as an argument, specifying the package namespace
|
||||||
|
/// this function should be placed under.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use jni::{ JNIEnv, objects::{ JClass, JString }, sys::jstring };
|
||||||
|
/// use jni_fn::jni_fn;
|
||||||
|
///
|
||||||
|
/// #[jni_fn("com.example.RustBindings")]
|
||||||
|
/// pub fn sayHello(env: JNIEnv, _: JClass, name: JString) -> jstring {
|
||||||
|
/// let name_javastr = env.get_string(name).unwrap();
|
||||||
|
/// let name = name_javastr.to_str().unwrap();
|
||||||
|
///
|
||||||
|
/// env.new_string(format!("Hello, {}!", name))
|
||||||
|
/// .expect("Couldn't create java string!")
|
||||||
|
/// .into_inner()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `sayHello` function will automatically be expanded to have the correct ABI specification
|
||||||
|
/// and the appropriate JNI-compatible name, i.e. in this case -
|
||||||
|
/// `Java_com_example_RustBindings_sayHello`.
|
||||||
|
///
|
||||||
|
/// Then it can be accessed by, for example, Kotlin code as follows:
|
||||||
|
/// ```kotlin
|
||||||
|
/// package com.example.RustBindings
|
||||||
|
///
|
||||||
|
/// class RustBindings {
|
||||||
|
/// private external fun sayHello(name: String): String
|
||||||
|
///
|
||||||
|
/// fun greetWorld() {
|
||||||
|
/// println(sayHello("world"))
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn jni_fn(
|
||||||
|
attr: proc_macro::TokenStream,
|
||||||
|
item: proc_macro::TokenStream,
|
||||||
|
) -> proc_macro::TokenStream {
|
||||||
|
jni_fn2(attr.into(), item.into()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deals exclusively with `proc_macro2::TokenStream` instead of `proc_macro::TokenStream`,
|
||||||
|
/// allowing it and all interior functionality to be unit tested.
|
||||||
|
fn jni_fn2(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let attr_span = attr.span();
|
||||||
|
let item_span = item.span();
|
||||||
|
|
||||||
|
let mut function: syn::ItemFn = match syn::parse2(item) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_e) => {
|
||||||
|
return syn::Error::new(
|
||||||
|
item_span,
|
||||||
|
"The `jni_fn` attribute can only be applied to `fn` items",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let namespace = match syn::parse2::<syn::LitStr>(attr) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_e) => return syn::Error::new(attr_span, "The `jni_fn` attribute must have a single string literal supplied to specify the namespace").to_compile_error(),
|
||||||
|
}.value();
|
||||||
|
|
||||||
|
if !valid_namespace(&namespace) {
|
||||||
|
return syn::Error::new(
|
||||||
|
attr_span,
|
||||||
|
"Invalid package namespace supplied to `jni_fn` attribute",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let orig_fn_name = function.sig.ident.to_string();
|
||||||
|
|
||||||
|
function.sig.ident = syn::Ident::new(
|
||||||
|
&create_jni_fn_name(&namespace, &orig_fn_name),
|
||||||
|
function.sig.ident.span(),
|
||||||
|
);
|
||||||
|
|
||||||
|
function.attrs.push(syn::Attribute {
|
||||||
|
pound_token: Default::default(),
|
||||||
|
style: syn::AttrStyle::Outer,
|
||||||
|
bracket_token: Default::default(),
|
||||||
|
path: syn::parse_str("no_mangle").unwrap(),
|
||||||
|
tokens: TokenStream::new(),
|
||||||
|
});
|
||||||
|
function.attrs.push(syn::Attribute {
|
||||||
|
pound_token: Default::default(),
|
||||||
|
style: syn::AttrStyle::Outer,
|
||||||
|
bracket_token: Default::default(),
|
||||||
|
path: syn::parse_str("allow").unwrap(),
|
||||||
|
tokens: quote::quote! { (non_snake_case) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if function.sig.abi.is_some() {
|
||||||
|
return syn::Error::new(function.sig.abi.span(), "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically").to_compile_error();
|
||||||
|
}
|
||||||
|
function.sig.abi = Some(syn::Abi {
|
||||||
|
extern_token: Default::default(),
|
||||||
|
name: Some(syn::LitStr::new("system", function.sig.ident.span())),
|
||||||
|
});
|
||||||
|
|
||||||
|
if !matches!(function.vis, syn::Visibility::Public(_)) {
|
||||||
|
return syn::Error::new(
|
||||||
|
function.vis.span(),
|
||||||
|
"`jni_fn` attributed functions must have public visibility (`pub`)",
|
||||||
|
)
|
||||||
|
.to_compile_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
function.into_token_stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that `namespace` appears roughly like a valid package name.
|
||||||
|
///
|
||||||
|
/// A package name is a '.'-separated identifier list.
|
||||||
|
///
|
||||||
|
/// Identifiers are described in section 3.8 of the Java language specification, although some
|
||||||
|
/// JVM-compatible languages have slightly different restrictions on what is considered a valid
|
||||||
|
/// identifier. This function attempts to catch obviously incorrect strings.
|
||||||
|
///
|
||||||
|
/// Please submit an issue report or patch to make this more permissive if it's required for
|
||||||
|
/// valid JVM code! Otherwise, making it more restrictive is appreciated as long as it's confirmed
|
||||||
|
/// to work with multiple JVM-compatible languages.
|
||||||
|
fn valid_namespace(namespace: &str) -> bool {
|
||||||
|
/// These shouldn't occur _anywhere_ in the package name.
|
||||||
|
const FORBIDDEN_CHARS: &[char] = &[
|
||||||
|
' ', ',', ':', ';', '|', '\\', '/', '!', '@', '#', '%', '^', '&', '*', '(', ')', '{', '}',
|
||||||
|
'[', ']', '-', '`', '~', '\t', '\n', '\r',
|
||||||
|
];
|
||||||
|
|
||||||
|
for c in FORBIDDEN_CHARS {
|
||||||
|
if namespace.contains(*c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_ident(ident: &str) -> bool {
|
||||||
|
/// These shouldn't occur as the first character of an identifier.
|
||||||
|
const FORBIDDEN_START_CHARS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
|
||||||
|
if ident.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in FORBIDDEN_START_CHARS {
|
||||||
|
if ident.starts_with(*c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
for ident in namespace.split('.') {
|
||||||
|
if !is_valid_ident(ident) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a JNI-compatible function name from the given namespace and function name.
|
||||||
|
/// This does _not_ transform the provided function name into `snakeCase` if it's not already; but
|
||||||
|
/// `#[allow(non_snake_case)]` should be added to prevent errors.
|
||||||
|
///
|
||||||
|
/// Importantly, any underscores in the original namespace or function name need to be replaced by
|
||||||
|
/// "_1", and then dot separators need to be turned into underscores.
|
||||||
|
fn create_jni_fn_name(namespace: &str, fn_name: &str) -> String {
|
||||||
|
let namespace_underscored = namespace.replace('_', "_1").replace('.', "_");
|
||||||
|
let fn_name_underscored = fn_name.replace('_', "_1");
|
||||||
|
format!("Java_{}_{}", namespace_underscored, fn_name_underscored)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_jni_fn_name() {
|
||||||
|
assert_eq!(
|
||||||
|
create_jni_fn_name("com.example.Foo", "init"),
|
||||||
|
"Java_com_example_Foo_init"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
create_jni_fn_name("com.example.Bar", "closeIt"),
|
||||||
|
"Java_com_example_Bar_closeIt"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
create_jni_fn_name("com.example.Bar", "close_it"),
|
||||||
|
"Java_com_example_Bar_close_1it"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
create_jni_fn_name(
|
||||||
|
"org.signal.client.internal.Native",
|
||||||
|
"IdentityKeyPair_Deserialize"
|
||||||
|
),
|
||||||
|
"Java_org_signal_client_internal_Native_IdentityKeyPair_1Deserialize"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_namespace() {
|
||||||
|
assert!(valid_namespace("com.example.Foo"));
|
||||||
|
assert!(valid_namespace("com.antonok.kb"));
|
||||||
|
assert!(valid_namespace("org.signal.client.internal.Native"));
|
||||||
|
assert!(valid_namespace("net.under_score"));
|
||||||
|
assert!(!valid_namespace("com example Foo"));
|
||||||
|
assert!(!valid_namespace(" com.example.Foo"));
|
||||||
|
assert!(!valid_namespace("com.example.Foo "));
|
||||||
|
assert!(!valid_namespace("com.example.1Foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_code_generation() {
|
||||||
|
let attr = quote::quote! {
|
||||||
|
"com.example.Bar"
|
||||||
|
};
|
||||||
|
let source = quote::quote! {
|
||||||
|
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unsafe_fn() {
|
||||||
|
let attr = quote::quote! {
|
||||||
|
"com.example.Bar"
|
||||||
|
};
|
||||||
|
let source = quote::quote! {
|
||||||
|
pub unsafe fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_non_function() {
|
||||||
|
let attr = quote::quote! { "com.example.Foo" };
|
||||||
|
let source = quote::quote! {
|
||||||
|
enum NotAFunction {
|
||||||
|
Variant1,
|
||||||
|
Variant2(u8),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
compile_error! { "The `jni_fn` attribute can only be applied to `fn` items" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_attribute() {
|
||||||
|
let attr = quote::quote! {};
|
||||||
|
let source = quote::quote! {
|
||||||
|
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
compile_error! { "The `jni_fn` attribute must have a single string literal supplied to specify the namespace" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_namespace() {
|
||||||
|
let attr = quote::quote! { "." };
|
||||||
|
let source = quote::quote! {
|
||||||
|
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
compile_error! { "Invalid package namespace supplied to `jni_fn` attribute" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_specified_abi() {
|
||||||
|
let attr = quote::quote! { "com.example.Foo" };
|
||||||
|
let source = quote::quote! {
|
||||||
|
pub extern "C" fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
compile_error! { "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wrong_visibility() {
|
||||||
|
let attr = quote::quote! { "com.example.Foo" };
|
||||||
|
let source = quote::quote! {
|
||||||
|
fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded = jni_fn2(attr.into(), source.into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", expanded),
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
quote::quote! {
|
||||||
|
compile_error! { "`jni_fn` attributed functions must have public visibility (`pub`)" }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ jnix = "*"
|
|||||||
jnix-macros = "*"
|
jnix-macros = "*"
|
||||||
catch_panic = { path = "../catch_panic" }
|
catch_panic = { path = "../catch_panic" }
|
||||||
catch_panic_macros = { path = "../catch_panic_macros" }
|
catch_panic_macros = { path = "../catch_panic_macros" }
|
||||||
jni_fn = "*"
|
jni_fn = { path = "../jni_fn" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
Reference in new issue
Block a user