Skip to content

Port java-spaghetti-gen to cafebabe; cache method/field IDs; fix jclass memory leaks #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions java-spaghetti-gen/src/emit_rust/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl<'a> Field<'a> {
}

let env_param = if self.java.is_static() {
"env: ::java_spaghetti::Env<'env>"
"__jni_env: ::java_spaghetti::Env<'env>"
} else {
"self: &::java_spaghetti::Ref<'env, Self>"
};
Expand Down Expand Up @@ -157,28 +157,37 @@ impl<'a> Field<'a> {
out,
"{indent}{attributes}pub fn {get}<'env>({env_param}) -> {rust_get_type} {{",
)?;
writeln!(
out,
"{indent} static __FIELD: ::std::sync::OnceLock<usize> = ::std::sync::OnceLock::new();"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cast to usize and back?

The semantics of usize<->ptr casts are a bit tricky, .addr() doesn't expose the provenance, so it can't be used to reconstruct the pointer with as casts. For this to be sound you'd have to use as casts in both directions, or use .expose_provenance(), .with_exposed_provenance_mut() (which are equivalent to as)

If the reason for usize is because the pointer isn't Send/Sync it'd be cleaner to make a newtype and unsafe impl Send/Sync on it. This way the pointer can stay as a pointer all the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the reason for usize is because the pointer isn't Send/Sync it'd be cleaner to make a newtype and unsafe impl Send/Sync on it. This way the pointer can stay as a pointer all the time.

I have modified the code according to your suggestion. I tested the newly generated bindings and it works. Thank you.

)?;
writeln!(out, "{indent} unsafe {{")?;
if !self.java.is_static() {
writeln!(out, "{indent} let env = self.env();")?;
writeln!(out, "{indent} let __jni_env = self.env();")?;
}
writeln!(
out,
"{} let (__jni_class, __jni_field) = env.require_class_{}field({}, {}, {});",
indent,
"{indent} let __jni_class = Self::__class_global_ref(__jni_env);"
)?;
writeln!(
out,
"{indent} \
let __jni_field = *__FIELD.get_or_init(|| \
__jni_env.require_{}field(__jni_class, {}, {}).addr()\
) as ::java_spaghetti::sys::jfieldID;",
if self.java.is_static() { "static_" } else { "" },
StrEmitter(self.class.path().as_str()),
StrEmitter(self.java.name()),
StrEmitter(FieldSigWriter(self.java.descriptor()))
)?;
if self.java.is_static() {
writeln!(
out,
"{indent} env.get_static_{field_fragment}_field(__jni_class, __jni_field)",
"{indent} __jni_env.get_static_{field_fragment}_field(__jni_class, __jni_field)",
)?;
} else {
writeln!(
out,
"{indent} env.get_{field_fragment}_field(self.as_raw(), __jni_field)",
"{indent} __jni_env.get_{field_fragment}_field(self.as_raw(), __jni_field)",
)?;
}
writeln!(out, "{indent} }}")?;
Expand All @@ -202,28 +211,39 @@ impl<'a> Field<'a> {
out,
"{indent}{attributes}pub fn {set}<{lifetimes}>({env_param}, value: {rust_set_type}) {{",
)?;
writeln!(
out,
"{indent} static __FIELD: ::std::sync::OnceLock<usize> = ::std::sync::OnceLock::new();"
)?;
writeln!(out, "{indent} unsafe {{")?;
if !self.java.is_static() {
writeln!(out, "{indent} let env = self.env();")?;
writeln!(out, "{indent} let __jni_env = self.env();")?;
}
writeln!(
out,
"{} let (__jni_class, __jni_field) = env.require_class_{}field({}, {}, {});",
indent,
"{indent} let __jni_class = Self::__class_global_ref(__jni_env);"
)?;
writeln!(
out,
"{indent} \
let __jni_field = *__FIELD.get_or_init(|| \
__jni_env.require_{}field(__jni_class, {}, {}).addr()\
) as ::java_spaghetti::sys::jfieldID;",
if self.java.is_static() { "static_" } else { "" },
StrEmitter(self.class.path().as_str()),
StrEmitter(self.java.name()),
StrEmitter(FieldSigWriter(self.java.descriptor()))
)?;
if self.java.is_static() {
writeln!(
out,
"{indent} env.set_static_{field_fragment}_field(__jni_class, __jni_field, value)",
"{indent} \
__jni_env.set_static_{field_fragment}_field(__jni_class, __jni_field, value)",
)?;
} else {
writeln!(
out,
"{indent} env.set_{field_fragment}_field(self.as_raw(), __jni_field, value)",
"{indent} \
__jni_env.set_{field_fragment}_field(self.as_raw(), __jni_field, value)",
)?;
}
writeln!(out, "{indent} }}")?;
Expand Down
56 changes: 31 additions & 25 deletions java-spaghetti-gen/src/emit_rust/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,22 +184,17 @@ impl<'a> Method<'a> {

writeln!(out)?;
for reason in &emit_reject_reasons {
writeln!(out, "{}// Not emitting: {}", indent, reason)?;
writeln!(out, "{indent}// Not emitting: {reason}")?;
}
if let Some(url) = KnownDocsUrl::from_method(context, self) {
writeln!(out, "{}/// {}", indent, url)?;
writeln!(out, "{indent}/// {url}")?;
} else {
writeln!(out, "{}/// {}", indent, self.java.name())?;
writeln!(out, "{indent}/// {}", self.java.name())?;
}
writeln!(
out,
"{}{}{}fn {}<'env>({}) -> ::std::result::Result<{}, ::java_spaghetti::Local<'env, {}>> {{",
indent,
attributes,
access,
method_name,
params_decl,
ret_decl,
"{indent}{attributes}{access}fn {method_name}<'env>({params_decl}) -> \
::std::result::Result<{ret_decl}, ::java_spaghetti::Local<'env, {}>> {{",
context.throwable_rust_path(mod_)
)?;
writeln!(
Expand All @@ -211,43 +206,54 @@ impl<'a> Method<'a> {
self.java.name(),
MethodSigWriter(self.java.descriptor())
)?;
writeln!(out, "{} unsafe {{", indent)?;
writeln!(out, "{} let __jni_args = [{}];", indent, params_array)?;

writeln!(
out,
"{indent} static __METHOD: ::std::sync::OnceLock<usize> = ::std::sync::OnceLock::new();"
)?;
writeln!(out, "{indent} unsafe {{")?;
writeln!(out, "{indent} let __jni_args = [{params_array}];")?;
if !self.java.is_constructor() && !self.java.is_static() {
writeln!(out, "{} let __jni_env = self.env();", indent)?;
writeln!(out, "{indent} let __jni_env = self.env();")?;
}

writeln!(
out,
"{} let (__jni_class, __jni_method) = __jni_env.require_class_{}method({}, {}, {});",
indent,
"{indent} let __jni_class = Self::__class_global_ref(__jni_env);"
)?;
writeln!(
out,
"{indent} \
let __jni_method = *__METHOD.get_or_init(|| \
__jni_env.require_{}method(__jni_class, {}, {}).addr()\
) as ::java_spaghetti::sys::jmethodID;",
if self.java.is_static() { "static_" } else { "" },
StrEmitter(self.class.path().as_str()),
StrEmitter(self.java.name()),
StrEmitter(MethodSigWriter(self.java.descriptor()))
)?;

if self.java.is_constructor() {
writeln!(
out,
"{} __jni_env.new_object_a(__jni_class, __jni_method, __jni_args.as_ptr())",
indent
"{indent} \
__jni_env.new_object_a(__jni_class, __jni_method, __jni_args.as_ptr())",
)?;
} else if self.java.is_static() {
writeln!(
out,
"{} __jni_env.call_static_{}_method_a(__jni_class, __jni_method, __jni_args.as_ptr())",
indent, ret_method_fragment
"{indent} \
__jni_env.call_static_{}_method_a(__jni_class, __jni_method, __jni_args.as_ptr())",
ret_method_fragment
)?;
} else {
writeln!(
out,
"{} __jni_env.call_{}_method_a(self.as_raw(), __jni_method, __jni_args.as_ptr())",
indent, ret_method_fragment
"{indent} \
__jni_env.call_{}_method_a(self.as_raw(), __jni_method, __jni_args.as_ptr())",
ret_method_fragment
)?;
}
writeln!(out, "{} }}", indent)?;
writeln!(out, "{}}}", indent)?;
writeln!(out, "{indent} }}")?;
writeln!(out, "{indent}}}")?;
Ok(())
}
}
28 changes: 22 additions & 6 deletions java-spaghetti-gen/src/emit_rust/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::io;
use super::fields::Field;
use super::known_docs_url::KnownDocsUrl;
use super::methods::Method;
use super::StrEmitter;
use crate::emit_rust::Context;
use crate::identifiers::{FieldMangling, RustIdentifier};
use crate::parser_util::{Class, Id, IdPart};
Expand Down Expand Up @@ -122,12 +123,12 @@ impl Struct {
}
writeln!(
out,
"unsafe impl ::java_spaghetti::JniType for {rust_name} {{
fn static_with_jni_type<R>(callback: impl FnOnce(&str) -> R) -> R {{
callback({:?})
}}
}}",
self.java.path().as_str().to_string() + "\0",
"unsafe impl ::java_spaghetti::JniType for {rust_name} {{\
\n fn static_with_jni_type<R>(callback: impl FnOnce(&str) -> R) -> R {{\
\n callback({})\
\n }}\
\n}}",
StrEmitter(self.java.path().as_str()),
)?;

// recursively visit all superclasses and superinterfaces.
Expand All @@ -152,6 +153,21 @@ impl Struct {

writeln!(out, "impl {rust_name} {{")?;

writeln!(
out,
"\
\nfn __class_global_ref(__jni_env: ::java_spaghetti::Env) -> ::java_spaghetti::sys::jobject {{\
\n static __CLASS: ::std::sync::OnceLock<::java_spaghetti::Global<{}>> = ::std::sync::OnceLock::new();\
\n __CLASS.get_or_init(|| unsafe {{\
\n ::java_spaghetti::Local::from_raw(__jni_env, __jni_env.require_class({})).as_global()\
\n }}).as_raw()\
\n}}",
context
.java_to_rust_path(Id("java/lang/Object"), &self.rust.mod_)
.unwrap(),
StrEmitter(self.java.path().as_str()),
)?;

let mut id_repeats = HashMap::new();

let mut methods: Vec<Method> = self
Expand Down
28 changes: 21 additions & 7 deletions java-spaghetti/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::marker::PhantomData;
use std::os::raw::c_char;
use std::ptr::{self, null_mut};
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::OnceLock;

use jni_sys::*;

Expand Down Expand Up @@ -147,9 +148,15 @@ impl<'env> Env<'env> {
}

unsafe fn exception_to_string(self, exception: jobject) -> String {
// use JNI FindClass to avoid infinte recursion.
let throwable_class = self.require_class_jni("java/lang/Throwable\0");
let throwable_get_message = self.require_method(throwable_class, "getMessage\0", "()Ljava/lang/String;\0");
static METHOD_GET_MESSAGE: OnceLock<usize> = OnceLock::new();
let throwable_get_message = *METHOD_GET_MESSAGE.get_or_init(|| {
// use JNI FindClass to avoid infinte recursion.
let throwable_class = self.require_class_jni("java/lang/Throwable\0");
let method = self.require_method(throwable_class, "getMessage\0", "()Ljava/lang/String;\0");
((**self.env).v1_2.DeleteLocalRef)(self.env, throwable_class);
method.addr()
}) as jmethodID; // it is a global ID

let message =
((**self.env).v1_2.CallObjectMethodA)(self.env, exception, throwable_get_message, ptr::null_mut());
let e2: *mut _jobject = ((**self.env).v1_2.ExceptionOccurred)(self.env);
Expand All @@ -161,6 +168,7 @@ impl<'env> Env<'env> {
StringChars::from_env_jstring(self, message).to_string_lossy()
}

/// Note: the returned `jclass` is actually a new local reference of the class object.
pub unsafe fn require_class(self, class: &str) -> jclass {
// First try with JNI FindClass.
debug_assert!(class.ends_with('\0'));
Expand All @@ -183,10 +191,15 @@ impl<'env> Env<'env> {
.collect::<Vec<_>>();
let string = unsafe { self.new_string(chars.as_ptr(), chars.len() as jsize) };

// We still use JNI FindClass for this, to avoid a chicken-and-egg situation.
// If the system class loader cannot find java.lang.ClassLoader, things are pretty broken!
let cl_class = self.require_class_jni("java/lang/ClassLoader\0");
let cl_method = self.require_method(cl_class, "loadClass\0", "(Ljava/lang/String;)Ljava/lang/Class;\0");
static CL_METHOD: OnceLock<usize> = OnceLock::new();
let cl_method = *CL_METHOD.get_or_init(|| {
// We still use JNI FindClass for this, to avoid a chicken-and-egg situation.
// If the system class loader cannot find java.lang.ClassLoader, things are pretty broken!
let cl_class = self.require_class_jni("java/lang/ClassLoader\0");
let cl_method = self.require_method(cl_class, "loadClass\0", "(Ljava/lang/String;)Ljava/lang/Class;\0");
((**self.env).v1_2.DeleteLocalRef)(self.env, cl_class);
cl_method.addr()
}) as jmethodID; // it is a global ID

let args = [jvalue { l: string }];
let result: *mut _jobject =
Expand Down Expand Up @@ -275,6 +288,7 @@ impl<'env> Env<'env> {
}

// Multi-Query Methods
// XXX: Remove these unused functions.

pub unsafe fn require_class_method(self, class: &str, method: &str, descriptor: &str) -> (jclass, jmethodID) {
let class = self.require_class(class);
Expand Down
6 changes: 6 additions & 0 deletions java-spaghetti/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ impl VM {
self.0
}

/// Constructs `VM` with a valid `jni_sys::JavaVM` raw pointer.
///
/// # Safety
///
/// - Make sure the corresponding JVM will keep alive within the lifetime of current native library or application.
/// - Do not use any class redefinition feature, which may break the validity of method/field IDs to be cached.
pub unsafe fn from_raw(vm: *mut JavaVM) -> Self {
Self(vm)
}
Expand Down
Loading