Skip to content

Commit

Permalink
Turn JNINativeInterface + JNIInvokeInterface_ into unions
Browse files Browse the repository at this point in the history
This implements a `#[jni_to_union]` procmacro that lets us declare what
version of the JNI spec each function was added and then declare a union
that only exposes functions the specific version being referenced.

So instead of a struct like:

```rust
struct JNIInvokeInterface_ {
    pub reserved0: *mut c_void,
    ..
    pub GetVersion: unsafe extern "system" fn(env: *mut JNIEnv) -> jint,
    ..
    pub NewLocalRef: unsafe extern "system" fn(env: *mut JNIEnv, ref_: jobject) -> jobject,
}
```

we have a union like:
```
union JNIInvokeInterface_ {
    v1_1: JNIInvokeInterface__1_1,
    v1_2: JNIInvokeInterface__1_2,
    reserved: JNIInvokeInterface__reserved,
}
```

And would access `GetVersion` like: `env.v1_1.GetVersion` and access
`NewLocalRef` like: `env.v1_2.NewLocalRef`.

Each version struct includes all functions for that version and lower,
so it's also possible to access GetVersion like `env.v1_2.GetVersion`.

This way it's more explicit when you're accessing functions that
aren't part of JNI 1.1 which require you to have checked the version
of JNI the JVM supports.
  • Loading branch information
rib committed Jul 23, 2023
1 parent 0ab77e2 commit ede36e1
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 21 deletions.
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ keywords = ["java", "jni"]
edition = "2021"

[dependencies]
jni-to-union-macro = { path = "jni-to-union-macro" }

[dev-dependencies]
trybuild = "1"

[workspace]
members = ["systest"]
members = [
"systest",
"jni-to-union-macro"
]
11 changes: 11 additions & 0 deletions jni-to-union-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "jni-to-union-macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = { version = "1", features = ["full"] }
quote = "1"
195 changes: 195 additions & 0 deletions jni-to-union-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
extern crate proc_macro;

use std::{cmp::Ordering, collections::HashSet};

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, Ident, LitStr};

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct JniVersion {
major: u16,
minor: u16,
}
impl Default for JniVersion {
fn default() -> Self {
Self { major: 1, minor: 1 }
}
}
impl Ord for JniVersion {
fn cmp(&self, other: &Self) -> Ordering {
match self.major.cmp(&other.major) {
Ordering::Equal => self.minor.cmp(&other.minor),
major_order => major_order,
}
}
}
impl PartialOrd for JniVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl syn::parse::Parse for JniVersion {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let version: LitStr = input.parse()?;
let version = version.value();
if version == "reserved" {
// We special case version 999 later instead of making JniVersion an enum
return Ok(JniVersion {
major: 999,
minor: 0,
});
}
let mut split = version.splitn(2, '.');
const EXPECTED_MSG: &str = "Expected \"major.minor\" version number or \"reserved\"";
let major = split
.next()
.ok_or(syn::Error::new(input.span(), EXPECTED_MSG))?;
let major = major
.parse::<u16>()
.map_err(|_| syn::Error::new(input.span(), EXPECTED_MSG))?;
let minor = split
.next()
.unwrap_or("0")
.parse::<u16>()
.map_err(|_| syn::Error::new(input.span(), EXPECTED_MSG))?;
Ok(JniVersion { major, minor })
}
}

fn jni_to_union_impl(input: DeriveInput) -> syn::Result<TokenStream> {
let original_name = &input.ident;
let original_visibility = &input.vis;

let mut versions = HashSet::new();
let mut versioned_fields = vec![];

if let Data::Struct(data) = &input.data {
if let Fields::Named(fields) = &data.fields {
for field in &fields.named {
// Default to version 1.1
let mut min_version = JniVersion::default();

let mut field = field.clone();

let mut jni_added_attr = None;
field.attrs.retain(|attr| {
if attr.path.is_ident("jni_added") {
jni_added_attr = Some(attr.clone());
false
} else {
true
}
});
if let Some(attr) = jni_added_attr {
let version = attr.parse_args::<JniVersion>()?;
min_version = version;
}

versions.insert(min_version);
versioned_fields.push((min_version, field.clone()));
}

// Quote structs and union
let mut expanded = quote! {};

let mut union_members = quote!();

let mut versions: Vec<_> = versions.into_iter().collect();
versions.sort();

for version in versions {
let (struct_ident, version_ident, version_suffix) = if version.major == 999 {
(
Ident::new(&format!("{}_reserved", original_name), original_name.span()),
Ident::new("reserved", original_name.span()),
"reserved".to_string(),
)
} else if version.minor == 0 {
(
Ident::new(
&format!("{}_{}", original_name, version.major),
original_name.span(),
),
Ident::new(&format!("v{}", version.major), original_name.span()),
format!("{}", version.major),
)
} else {
let struct_ident = Ident::new(
&format!("{}_{}_{}", original_name, version.major, version.minor),
original_name.span(),
);
let version_ident = Ident::new(
&format!("v{}_{}", version.major, version.minor),
original_name.span(),
);
(
struct_ident,
version_ident,
format!("{}_{}", version.major, version.minor),
)
};

let last = versioned_fields
.iter()
.rposition(|(v, _f)| v <= &version)
.unwrap_or(versioned_fields.len());
let mut padding_idx = 0u32;

let mut version_field_tokens = quote!();
for (i, (field_min_version, field)) in versioned_fields.iter().enumerate() {
if i > last {
break;
}
if field_min_version > &version {
let reserved_ident = format_ident!("_padding_{}", padding_idx);
padding_idx += 1;
version_field_tokens.extend(quote! { #reserved_ident: *mut c_void, });
} else {
version_field_tokens.extend(quote! { #field, });
}
}
expanded.extend(quote! {
#[allow(non_snake_case, non_camel_case_types)]
#[repr(C)]
#[derive(Copy, Clone, Debug)]
#original_visibility struct #struct_ident {
#version_field_tokens
}
});

let api_comment =
format!("API when JNI version >= `JNI_VERSION_{}`", version_suffix);
union_members.extend(quote! {
#[doc = #api_comment]
#original_visibility #version_ident: #struct_ident,
});
}

expanded.extend(quote! {
#[repr(C)]
#original_visibility union #original_name {
#union_members
}
});

return Ok(TokenStream::from(expanded));
}
}

Err(syn::Error::new(
input.span(),
"Expected a struct with fields",
))
}

#[proc_macro_attribute]
pub fn jni_to_union(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);

match jni_to_union_impl(input) {
Ok(tokens) => tokens,
Err(err) => err.into_compile_error().into(),
}
}
Loading

0 comments on commit ede36e1

Please sign in to comment.