use super::signature::{parse_signature, JavaType};
use jni_sys::{
jarray, jclass, jfloat, jint, jmethodID, jobject, jsize, jvalue, JNIEnv, JNINativeMethod, JavaVM, JNI_OK,
JNI_VERSION_1_6,
};
use std::cell::Cell;
use std::collections::HashMap;
use std::ffi::{c_void, CStr, CString};
use std::marker::PhantomData;
use std::ptr;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError};
static VM: AtomicPtr<JavaVM> = AtomicPtr::new(ptr::null_mut());
static CLASSES: OnceLock<Mutex<HashMap<String, JavaObject>>> = OnceLock::new();
macro_rules! jni {
($env:expr, $name:ident $(, $arg:expr)* $(,)?) => {
((**$env).v1_6.$name)($env $(, $arg)*)
};
}
pub(crate) unsafe fn initialize(vm: *mut c_void) {
VM.store(vm.cast(), Ordering::Release);
}
pub(crate) fn is_initialized() -> bool {
!VM.load(Ordering::Acquire).is_null()
}
struct Attachment {
attached_here: Cell<bool>,
}
impl Drop for Attachment {
fn drop(&mut self) {
if self.attached_here.get() {
let vm = VM.load(Ordering::Acquire);
if !vm.is_null() {
unsafe {
((**vm).v1_4.DetachCurrentThread)(vm);
}
}
}
}
}
ferroui_base::ferro_thread_local! {
static ATTACHMENT: Attachment = const { Attachment { attached_here: Cell::new(false) } };
}
fn env() -> *mut JNIEnv {
let vm = VM.load(Ordering::Acquire);
if vm.is_null() {
panic!("The Android backend was not loaded by a Java virtual machine (JNI_OnLoad did not run).");
}
let mut env: *mut c_void = ptr::null_mut();
let status = unsafe { ((**vm).v1_4.GetEnv)(vm, &mut env, JNI_VERSION_1_6) };
if status == JNI_OK && !env.is_null() {
return env.cast();
}
let status = unsafe { ((**vm).v1_4.AttachCurrentThreadAsDaemon)(vm, &mut env, ptr::null_mut()) };
if status != JNI_OK || env.is_null() {
panic!("The thread could not be attached to the Java virtual machine (status {status}).");
}
let _ = ATTACHMENT.try_with(|attachment| attachment.attached_here.set(true));
env.cast()
}
fn check_exception(env: *mut JNIEnv, context: &dyn Fn() -> String) {
let pending = unsafe { jni!(env, ExceptionCheck) };
if pending {
unsafe {
jni!(env, ExceptionDescribe);
jni!(env, ExceptionClear);
}
panic!("A Java exception was thrown by {} (it is described in the log of the system).", context());
}
}
pub trait JavaRef {
#[doc(hidden)]
fn raw(&self) -> jobject;
}
pub struct JavaObject {
raw: jobject,
}
unsafe impl Send for JavaObject {}
unsafe impl Sync for JavaObject {}
impl JavaObject {
pub(crate) unsafe fn from_raw(raw: jobject) -> Option<JavaObject> {
if raw.is_null() {
return None;
}
let env = env();
let global = unsafe { jni!(env, NewGlobalRef, raw) };
if global.is_null() {
panic!("The Java virtual machine is out of memory (NewGlobalRef).");
}
Some(JavaObject { raw: global })
}
pub fn handle(&self) -> isize {
self.raw as isize
}
pub fn is_same_object(&self, other: &dyn JavaRef) -> bool {
let env = env();
unsafe { jni!(env, IsSameObject, self.raw, other.raw()) }
}
}
impl JavaRef for JavaObject {
fn raw(&self) -> jobject {
self.raw
}
}
impl Clone for JavaObject {
fn clone(&self) -> Self {
unsafe { JavaObject::from_raw(self.raw) }.expect("a Java object is never null")
}
}
impl Drop for JavaObject {
fn drop(&mut self) {
if !is_initialized() {
return;
}
let env = env();
unsafe { jni!(env, DeleteGlobalRef, self.raw) };
}
}
pub struct JavaLocal {
raw: jobject,
_thread: PhantomData<*mut ()>,
}
impl JavaLocal {
unsafe fn from_raw(raw: jobject) -> Option<JavaLocal> {
(!raw.is_null()).then_some(JavaLocal { raw, _thread: PhantomData })
}
pub fn to_global(&self) -> JavaObject {
unsafe { JavaObject::from_raw(self.raw) }.expect("a Java object is never null")
}
}
impl JavaRef for JavaLocal {
fn raw(&self) -> jobject {
self.raw
}
}
impl Drop for JavaLocal {
fn drop(&mut self) {
let env = env();
unsafe { jni!(env, DeleteLocalRef, self.raw) };
}
}
impl JavaLocal {
pub(crate) fn into_raw(self) -> jobject {
let raw = self.raw;
std::mem::forget(self);
raw
}
}
pub fn new_string(text: &str) -> JavaLocal {
new_string_local(env(), text)
}
pub fn new_string_array(elements: &[String]) -> JavaLocal {
let env = env();
let class = JavaClass::find("java/lang/String");
let array = unsafe {
JavaLocal::from_raw(jni!(env, NewObjectArray, elements.len() as jsize, class.class.raw, ptr::null_mut()))
};
check_exception(env, &|| "the creation of an array of strings".to_string());
let array = array.unwrap_or_else(|| panic!("The Java virtual machine is out of memory (NewObjectArray)."));
for (index, element) in elements.iter().enumerate() {
let string = new_string_local(env, element);
unsafe { jni!(env, SetObjectArrayElement, array.raw, index as jsize, string.raw) };
}
array
}
#[derive(Clone)]
pub struct JavaClass {
class: JavaObject,
name: String,
}
impl JavaClass {
pub fn find(name: &str) -> JavaClass {
let classes = CLASSES.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(class) = classes.lock().unwrap_or_else(PoisonError::into_inner).get(name) {
return JavaClass { class: class.clone(), name: name.to_string() };
}
let env = env();
let name_c = CString::new(name).expect("a class name has no NUL");
let local = unsafe { JavaLocal::from_raw(jni!(env, FindClass, name_c.as_ptr())) };
check_exception(env, &|| format!("the lookup of the class {name}"));
let Some(local) = local else {
panic!("The Java class {name} was not found.");
};
let class = local.to_global();
classes.lock().unwrap_or_else(PoisonError::into_inner).insert(name.to_string(), class.clone());
JavaClass { class, name: name.to_string() }
}
pub(crate) fn preload(names: &[&str]) {
for name in names {
JavaClass::find(name);
}
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Clone, Copy)]
pub enum JavaValue<'a> {
Boolean(bool),
Int(i32),
Long(i64),
Float(f32),
Double(f64),
Object(Option<&'a dyn JavaRef>),
String(&'a str),
}
impl JavaValue<'_> {
fn java_type(&self) -> JavaType {
match self {
JavaValue::Boolean(_) => JavaType::Boolean,
JavaValue::Int(_) => JavaType::Int,
JavaValue::Long(_) => JavaType::Long,
JavaValue::Float(_) => JavaType::Float,
JavaValue::Double(_) => JavaType::Double,
JavaValue::Object(_) | JavaValue::String(_) => JavaType::Object,
}
}
}
enum Target<'a> {
Instance(&'a dyn JavaRef),
Static(&'a JavaClass),
Constructor(&'a JavaClass),
}
enum Returned {
Void,
Boolean(bool),
Int(i32),
Long(i64),
Float(f32),
Object(Option<JavaLocal>),
}
fn new_string_local(env: *mut JNIEnv, text: &str) -> JavaLocal {
let utf16: Vec<u16> = text.encode_utf16().collect();
let local = unsafe { JavaLocal::from_raw(jni!(env, NewString, utf16.as_ptr(), utf16.len() as jsize)) };
check_exception(env, &|| "the creation of a string".to_string());
local.unwrap_or_else(|| panic!("The Java virtual machine is out of memory (NewString)."))
}
fn invoke(target: Target<'_>, name: &str, signature: &str, args: &[JavaValue<'_>], returns: JavaType) -> Returned {
let describe = || match &target {
Target::Instance(_) => format!("the method {name}{signature}"),
Target::Static(class) => format!("the static method {}.{name}{signature}", class.name),
Target::Constructor(class) => format!("the constructor {}{signature}", class.name),
};
let Some((parameters, return_type)) = parse_signature(signature) else {
panic!("'{signature}' is not a method signature ({}).", describe());
};
if return_type != returns {
panic!("{} does not return {returns:?}.", describe());
}
if parameters.len() != args.len()
|| parameters.iter().zip(args).any(|(parameter, arg)| *parameter != arg.java_type())
{
panic!("The arguments of a call do not match the signature of {}.", describe());
}
let env = env();
let mut strings = Vec::new();
let values: Vec<jvalue> = args
.iter()
.map(|arg| match arg {
JavaValue::Boolean(value) => jvalue { z: *value },
JavaValue::Int(value) => jvalue { i: *value },
JavaValue::Long(value) => jvalue { j: *value },
JavaValue::Float(value) => jvalue { f: *value },
JavaValue::Double(value) => jvalue { d: *value },
JavaValue::Object(value) => jvalue { l: value.map_or(ptr::null_mut(), JavaRef::raw) },
JavaValue::String(value) => {
let local = new_string_local(env, value);
let value = jvalue { l: local.raw };
strings.push(local);
value
}
})
.collect();
let name_c = CString::new(name).expect("a method name has no NUL");
let signature_c = CString::new(signature).expect("a signature has no NUL");
let (class_local, class, object): (Option<JavaLocal>, jclass, jobject) = match &target {
Target::Instance(object) => {
let local =
unsafe { JavaLocal::from_raw(jni!(env, GetObjectClass, object.raw())) }.expect("an object has a class");
let class = local.raw;
(Some(local), class, object.raw())
}
Target::Static(class) | Target::Constructor(class) => (None, class.class.raw, ptr::null_mut()),
};
let method: jmethodID = unsafe {
match &target {
Target::Static(_) => jni!(env, GetStaticMethodID, class, name_c.as_ptr(), signature_c.as_ptr()),
Target::Instance(_) => jni!(env, GetMethodID, class, name_c.as_ptr(), signature_c.as_ptr()),
Target::Constructor(_) => jni!(env, GetMethodID, class, c"<init>".as_ptr(), signature_c.as_ptr()),
}
};
if method.is_null() {
unsafe { jni!(env, ExceptionClear) };
panic!("The Java layer has no {}.", describe());
}
let args_ptr = values.as_ptr();
let returned = unsafe {
match (&target, returns) {
(Target::Constructor(_), _) => {
Returned::Object(JavaLocal::from_raw(jni!(env, NewObjectA, class, method, args_ptr)))
}
(Target::Instance(_), JavaType::Void) => {
jni!(env, CallVoidMethodA, object, method, args_ptr);
Returned::Void
}
(Target::Instance(_), JavaType::Boolean) => {
Returned::Boolean(jni!(env, CallBooleanMethodA, object, method, args_ptr))
}
(Target::Instance(_), JavaType::Int) => Returned::Int(jni!(env, CallIntMethodA, object, method, args_ptr)),
(Target::Instance(_), JavaType::Long) => {
Returned::Long(jni!(env, CallLongMethodA, object, method, args_ptr))
}
(Target::Instance(_), JavaType::Float) => {
Returned::Float(jni!(env, CallFloatMethodA, object, method, args_ptr))
}
(Target::Instance(_), JavaType::Object) => {
Returned::Object(JavaLocal::from_raw(jni!(env, CallObjectMethodA, object, method, args_ptr)))
}
(Target::Static(_), JavaType::Void) => {
jni!(env, CallStaticVoidMethodA, class, method, args_ptr);
Returned::Void
}
(Target::Static(_), JavaType::Boolean) => {
Returned::Boolean(jni!(env, CallStaticBooleanMethodA, class, method, args_ptr))
}
(Target::Static(_), JavaType::Int) => {
Returned::Int(jni!(env, CallStaticIntMethodA, class, method, args_ptr))
}
(Target::Static(_), JavaType::Long) => {
Returned::Long(jni!(env, CallStaticLongMethodA, class, method, args_ptr))
}
(Target::Static(_), JavaType::Float) => {
Returned::Float(jni!(env, CallStaticFloatMethodA, class, method, args_ptr))
}
(Target::Static(_), JavaType::Object) => {
Returned::Object(JavaLocal::from_raw(jni!(env, CallStaticObjectMethodA, class, method, args_ptr)))
}
(_, other) => panic!("Calls that return {other:?} are not used by the backend ({}).", describe()),
}
};
drop(class_local);
drop(strings);
check_exception(env, &describe);
returned
}
macro_rules! call_functions {
($( $(#[$doc:meta])* $instance:ident, $static_:ident -> $ty:ty, $java:ident, $returned:ident; )*) => {
$(
$(#[$doc])*
pub fn $instance(object: &dyn JavaRef, name: &str, signature: &str, args: &[JavaValue<'_>]) -> $ty {
match invoke(Target::Instance(object), name, signature, args, JavaType::$java) {
Returned::$returned(value) => value,
_ => unreachable!("the call returns what its signature states"),
}
}
$(#[$doc])*
pub fn $static_(class: &JavaClass, name: &str, signature: &str, args: &[JavaValue<'_>]) -> $ty {
match invoke(Target::Static(class), name, signature, args, JavaType::$java) {
Returned::$returned(value) => value,
_ => unreachable!("the call returns what its signature states"),
}
}
)*
};
}
call_functions! {
call_boolean, call_static_boolean -> bool, Boolean, Boolean;
call_int, call_static_int -> i32, Int, Int;
call_long, call_static_long -> i64, Long, Long;
call_float, call_static_float -> f32, Float, Float;
call_object, call_static_object -> Option<JavaLocal>, Object, Object;
}
pub fn call_void(object: &dyn JavaRef, name: &str, signature: &str, args: &[JavaValue<'_>]) {
invoke(Target::Instance(object), name, signature, args, JavaType::Void);
}
pub fn call_static_void(class: &JavaClass, name: &str, signature: &str, args: &[JavaValue<'_>]) {
invoke(Target::Static(class), name, signature, args, JavaType::Void);
}
pub fn new_object(class: &JavaClass, signature: &str, args: &[JavaValue<'_>]) -> JavaLocal {
match invoke(Target::Constructor(class), "<init>", signature, args, JavaType::Void) {
Returned::Object(Some(object)) => object,
_ => panic!("The constructor {}{signature} returned no object.", class.name),
}
}
fn instance_of(env: *mut JNIEnv, object: jobject, class_name: &str) -> bool {
let class = JavaClass::find(class_name);
unsafe { jni!(env, IsInstanceOf, object, class.class.raw) }
}
pub fn is_instance_of(object: &dyn JavaRef, class_name: &str) -> bool {
instance_of(env(), object.raw(), class_name)
}
pub fn string_of(object: &dyn JavaRef) -> String {
let env = env();
if !instance_of(env, object.raw(), "java/lang/String") {
panic!("The Java object is not a string.");
}
unsafe {
let length = jni!(env, GetStringLength, object.raw());
let mut buffer = vec![0u16; length.max(0) as usize];
jni!(env, GetStringRegion, object.raw(), 0, length, buffer.as_mut_ptr());
String::from_utf16_lossy(&buffer)
}
}
pub fn int_array_of(object: &dyn JavaRef) -> Vec<i32> {
let env = env();
if !instance_of(env, object.raw(), "[I") {
panic!("The Java object is not an array of int.");
}
unsafe { read_int_array(object.raw()) }
}
pub fn float_array_of(object: &dyn JavaRef) -> Vec<f32> {
let env = env();
if !instance_of(env, object.raw(), "[F") {
panic!("The Java object is not an array of float.");
}
unsafe { read_float_array(object.raw()) }
}
pub fn string_array_of(object: &dyn JavaRef) -> Vec<String> {
let env = env();
if !instance_of(env, object.raw(), "[Ljava/lang/String;") {
panic!("The Java object is not an array of strings.");
}
unsafe {
let length = jni!(env, GetArrayLength, object.raw());
(0..length)
.map(|index| {
JavaLocal::from_raw(jni!(env, GetObjectArrayElement, object.raw(), index))
.map(|element| string_of(&element))
.unwrap_or_default()
})
.collect()
}
}
pub(crate) unsafe fn read_string(string: jobject) -> Option<String> {
if string.is_null() {
return None;
}
let env = env();
unsafe {
let length = jni!(env, GetStringLength, string);
let mut buffer = vec![0u16; length.max(0) as usize];
jni!(env, GetStringRegion, string, 0, length, buffer.as_mut_ptr());
Some(String::from_utf16_lossy(&buffer))
}
}
pub(crate) unsafe fn read_int_array(array: jarray) -> Vec<i32> {
if array.is_null() {
return Vec::new();
}
let env = env();
unsafe {
let length = jni!(env, GetArrayLength, array);
let mut buffer: Vec<jint> = vec![0; length.max(0) as usize];
jni!(env, GetIntArrayRegion, array, 0, length, buffer.as_mut_ptr());
buffer
}
}
pub(crate) unsafe fn read_float_array(array: jarray) -> Vec<f32> {
if array.is_null() {
return Vec::new();
}
let env = env();
unsafe {
let length = jni!(env, GetArrayLength, array);
let mut buffer: Vec<jfloat> = vec![0.0; length.max(0) as usize];
jni!(env, GetFloatArrayRegion, array, 0, length, buffer.as_mut_ptr());
buffer
}
}
pub(crate) fn raw_env() -> *mut c_void {
env().cast()
}
pub(crate) struct NativeMethod {
pub name: &'static CStr,
pub signature: &'static CStr,
pub function: *mut c_void,
}
pub(crate) unsafe fn register_natives(class: &JavaClass, methods: &[NativeMethod]) {
let env = env();
let table: Vec<JNINativeMethod> = methods
.iter()
.map(|method| JNINativeMethod {
name: method.name.as_ptr().cast_mut(),
signature: method.signature.as_ptr().cast_mut(),
fnPtr: method.function,
})
.collect();
let status = unsafe { jni!(env, RegisterNatives, class.class.raw, table.as_ptr(), table.len() as jint) };
check_exception(env, &|| format!("the registration of the native methods of {}", class.name));
if status != JNI_OK {
panic!("The native methods of {} could not be registered (status {status}).", class.name);
}
}
pub(crate) fn throw_runtime_exception(message: &str) {
let env = env();
let message: String = message.chars().map(|c| if c == '\0' || c as u32 > 0xffff { '?' } else { c }).collect();
let message = CString::new(message).expect("the message has no NUL");
unsafe {
if jni!(env, ExceptionCheck) {
return;
}
let class = jni!(env, FindClass, c"java/lang/RuntimeException".as_ptr());
if !class.is_null() {
jni!(env, ThrowNew, class, message.as_ptr());
jni!(env, DeleteLocalRef, class);
}
}
}