From 795423de2a0f29c50c8193d89afefc571d183ad1 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Sep 2024 00:20:03 +0200 Subject: [PATCH] feat(java): callback API Co-authored-by: alemi --- dist/java/src/mp/code/Extensions.java | 17 ++++++ dist/java/src/mp/code/Utils.java | 10 ---- dist/java/src/mp/code/Workspace.java | 2 +- src/ffi/java/buffer.rs | 55 +++++++++++++++++--- src/ffi/java/client.rs | 18 +++---- src/ffi/java/cursor.rs | 51 ++++++++++++++++-- src/ffi/java/ext.rs | 34 ++++++++++++ src/ffi/java/mod.rs | 75 ++++++++++++--------------- src/ffi/java/utils.rs | 17 ------ src/ffi/java/workspace.rs | 20 +++---- 10 files changed, 197 insertions(+), 102 deletions(-) create mode 100644 dist/java/src/mp/code/Extensions.java delete mode 100644 dist/java/src/mp/code/Utils.java create mode 100644 src/ffi/java/ext.rs delete mode 100644 src/ffi/java/utils.rs diff --git a/dist/java/src/mp/code/Extensions.java b/dist/java/src/mp/code/Extensions.java new file mode 100644 index 0000000..0d82ffa --- /dev/null +++ b/dist/java/src/mp/code/Extensions.java @@ -0,0 +1,17 @@ +package mp.code; + +public class Extensions { + /** + * Hashes the given {@link String} using CodeMP's hashing algorithm (xxh3). + * @param input the string to hash + * @return the hash + */ + public static native long hash(String input); + + /** + * Drive the underlying library's asynchronous event loop. + * @param block true if it should use the current thread, false if it should + * spawn a separate one + */ + public static native void drive(boolean block); +} diff --git a/dist/java/src/mp/code/Utils.java b/dist/java/src/mp/code/Utils.java deleted file mode 100644 index 7174ba7..0000000 --- a/dist/java/src/mp/code/Utils.java +++ /dev/null @@ -1,10 +0,0 @@ -package mp.code; - -public class Utils { - /** - * Hashes the given {@link String} using CodeMP's hashing algorithm (xxh3). - * @param input the string to hash - * @return the hash - */ - public static native long hash(String input); -} diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 3b5da95..06c4e8c 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -95,7 +95,7 @@ public class Workspace { this.argument = argument; } - public Optional getUserJoined() { + public Optional getUserJoined() { if(this.type == Type.USER_JOIN) { return Optional.of(this.argument); } else return Optional.empty(); diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 8315feb..93225f5 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -1,8 +1,8 @@ use jni::{objects::{JClass, JObject, JValueGen}, sys::{jlong, jobject, jstring}, JNIEnv}; -use crate::{api::Controller, ffi::java::handle_callback}; +use crate::api::Controller; -use super::{JExceptable, RT}; +use super::JExceptable; /// Gets the name of the buffer. #[no_mangle] @@ -26,7 +26,7 @@ pub extern "system" fn Java_mp_code_BufferController_get_1content( self_ptr: jlong, ) -> jstring { let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) }; - let content = RT.block_on(controller.content()) + let content = super::tokio().block_on(controller.content()) .jexcept(&mut env); env.new_string(content) .jexcept(&mut env) @@ -41,7 +41,7 @@ pub extern "system" fn Java_mp_code_BufferController_try_1recv( self_ptr: jlong, ) -> jobject { let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) }; - let change = RT.block_on(controller.try_recv()).jexcept(&mut env); + let change = super::tokio().block_on(controller.try_recv()).jexcept(&mut env); recv_jni(&mut env, change) } @@ -53,7 +53,7 @@ pub extern "system" fn Java_mp_code_BufferController_recv( self_ptr: jlong, ) -> jobject { let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) }; - let change = RT.block_on(controller.recv()).map(Some).jexcept(&mut env); + let change = super::tokio().block_on(controller.recv()).map(Some).jexcept(&mut env); recv_jni(&mut env, change) } @@ -88,6 +88,18 @@ fn recv_jni(env: &mut JNIEnv, change: Option) -> jobject }.as_raw() } +/// Clears the callback for buffer changes. +#[no_mangle] +pub extern "system" fn Java_mp_code_BufferController_clear_1callback( + _env: JNIEnv, + _class: JClass, + self_ptr: jlong, +) { + unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) } + .clear_callback(); +} + +/// Registers a callback for buffer changes. #[no_mangle] pub extern "system" fn Java_mp_code_BufferController_callback<'local>( mut env: JNIEnv, @@ -95,7 +107,36 @@ pub extern "system" fn Java_mp_code_BufferController_callback<'local>( self_ptr: jlong, cb: JObject<'local>, ) { - handle_callback!("mp/code/BufferController", env, self_ptr, cb, crate::buffer::Controller); + let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) }; + + let Ok(cb_ref) = env.new_global_ref(cb) else { + env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") + .expect("Failed to throw exception!"); + return; + }; + + controller.callback(move |controller: crate::buffer::Controller| { + let jvm = super::jvm(); + let mut env = jvm.attach_current_thread_permanently() + .expect("failed attaching to main JVM thread"); + if let Err(e) = env.with_local_frame(5, |env| { + use crate::ffi::java::JObjectify; + let jcontroller = controller.jobjectify(env)?; + let sig = format!("(L{};)V", "java/lang/Object"); + if let Err(e) = env.call_method( + &cb_ref, + "invoke", + &sig, + &[jni::objects::JValueGen::Object(&jcontroller)] + ) { + tracing::error!("error invoking callback: {e:?}"); + }; + Ok::<(), jni::errors::Error>(()) + }) { + tracing::error!("error invoking callback: {e}"); + let _ = env.exception_describe(); + } + }); } /// Receive from Java, converts and sends a [crate::api::TextChange]. @@ -147,7 +188,7 @@ pub extern "system" fn Java_mp_code_BufferController_send<'local>( }).jexcept(&mut env); let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) }; - RT.block_on(controller.send(crate::api::TextChange { + super::tokio().block_on(controller.send(crate::api::TextChange { start, end, content, diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index c0dbd5b..d3afd50 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,7 +1,7 @@ use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jint, jlong, jobject, jobjectArray}, JNIEnv}; use crate::{api::Config, client::Client, Workspace}; -use super::{JExceptable, JObjectify, RT}; +use super::{JExceptable, JObjectify}; /// Connect using the given credentials to the default server, and return a [Client] to interact with it. #[no_mangle] @@ -63,7 +63,7 @@ pub extern "system" fn Java_mp_code_Client_connectToServer<'local>( } fn connect_internal(mut env: JNIEnv, config: Config) -> jobject { - RT.block_on(Client::connect(config)) + super::tokio().block_on(Client::connect(config)) .map(|client| Box::into_raw(Box::new(client)) as jlong) .map(|ptr| { env.find_class("mp/code/Client") @@ -98,7 +98,7 @@ pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>( let workspace_id = unsafe { env.get_string_unchecked(&input) } .map(|wid| wid.to_string_lossy().to_string()) .jexcept(&mut env); - RT.block_on(client.join_workspace(workspace_id)) + super::tokio().block_on(client.join_workspace(workspace_id)) .map(|workspace| spawn_updater(workspace.clone())) .map(|workspace| Box::into_raw(Box::new(workspace)) as jlong) .map(|ptr| { @@ -120,7 +120,7 @@ pub extern "system" fn Java_mp_code_Client_create_1workspace<'local>( let workspace_id = unsafe { env.get_string_unchecked(&input) } .map(|wid| wid.to_string_lossy().to_string()) .jexcept(&mut env); - RT + super::tokio() .block_on(client.create_workspace(workspace_id)) .jexcept(&mut env); } @@ -137,7 +137,7 @@ pub extern "system" fn Java_mp_code_Client_delete_1workspace<'local>( let workspace_id = unsafe { env.get_string_unchecked(&input) } .map(|wid| wid.to_string_lossy().to_string()) .jexcept(&mut env); - RT + super::tokio() .block_on(client.delete_workspace(workspace_id)) .jexcept(&mut env); } @@ -158,7 +158,7 @@ pub extern "system" fn Java_mp_code_Client_invite_1to_1workspace<'local>( let user_name = unsafe { env.get_string_unchecked(&usr) } .map(|wid| wid.to_string_lossy().to_string()) .jexcept(&mut env); - RT + super::tokio() .block_on(client.invite_to_workspace(workspace_id, user_name)) .jexcept(&mut env); } @@ -173,7 +173,7 @@ pub extern "system" fn Java_mp_code_Client_list_1workspaces<'local>( invited: jboolean ) -> jobjectArray { let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; - let list = RT + let list = super::tokio() .block_on(client.list_workspaces(owned != 0, invited != 0)) .jexcept(&mut env); env.find_class("java/lang/String") @@ -210,7 +210,7 @@ pub extern "system" fn Java_mp_code_Client_active_1workspaces<'local>( // TODO: this stays until we get rid of the arc then i'll have to find a better way fn spawn_updater(workspace: Workspace) -> Workspace { let w = workspace.clone(); - RT.spawn(async move { + super::tokio().spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(60)).await; w.fetch_buffers().await.unwrap(); @@ -264,7 +264,7 @@ pub extern "system" fn Java_mp_code_Client_refresh<'local>( self_ptr: jlong, ) { let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; - RT.block_on(client.refresh()) + super::tokio().block_on(client.refresh()) .jexcept(&mut env); } diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 31f43a5..3ce9056 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,7 +1,7 @@ use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv}; use crate::api::Controller; -use super::{handle_callback, JExceptable, RT}; +use super::JExceptable; /// Try to fetch a [crate::api::Cursor], or returns null if there's nothing. #[no_mangle] @@ -11,7 +11,7 @@ pub extern "system" fn Java_mp_code_CursorController_try_1recv( self_ptr: jlong, ) -> jobject { let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::cursor::Controller)) }; - let cursor = RT.block_on(controller.try_recv()).jexcept(&mut env); + let cursor = super::tokio().block_on(controller.try_recv()).jexcept(&mut env); jni_recv(&mut env, cursor) } @@ -23,7 +23,7 @@ pub extern "system" fn Java_mp_code_CursorController_recv( self_ptr: jlong, ) -> jobject { let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::cursor::Controller)) }; - let cursor = RT.block_on(controller.recv()).map(Some).jexcept(&mut env); + let cursor = super::tokio().block_on(controller.recv()).map(Some).jexcept(&mut env); jni_recv(&mut env, cursor) } @@ -56,6 +56,18 @@ fn jni_recv(env: &mut JNIEnv, cursor: Option) -> jobject { }.as_raw() } +/// Clears the callback for cursor changes. +#[no_mangle] +pub extern "system" fn Java_mp_code_CursorController_clear_1callback( + _env: JNIEnv, + _class: JClass, + self_ptr: jlong, +) { + unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::cursor::Controller)) } + .clear_callback(); +} + +/// Registers a callback for cursor changes. #[no_mangle] pub extern "system" fn Java_mp_code_CursorController_callback<'local>( mut env: JNIEnv, @@ -63,7 +75,36 @@ pub extern "system" fn Java_mp_code_CursorController_callback<'local>( self_ptr: jlong, cb: JObject<'local>, ) { - handle_callback!("mp/code/CursorController", env, self_ptr, cb, crate::cursor::Controller); + let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::cursor::Controller)) }; + + let Ok(cb_ref) = env.new_global_ref(cb) else { + env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") + .expect("Failed to throw exception!"); + return; + }; + + controller.callback(move |controller: crate::cursor::Controller| { + let jvm = super::jvm(); + let mut env = jvm.attach_current_thread_permanently() + .expect("failed attaching to main JVM thread"); + if let Err(e) = env.with_local_frame(5, |env| { + use crate::ffi::java::JObjectify; + let jcontroller = controller.jobjectify(env)?; + let sig = format!("(L{};)V", "java/lang/Object"); + if let Err(e) = env.call_method( + &cb_ref, + "invoke", + &sig, + &[jni::objects::JValueGen::Object(&jcontroller)] + ) { + tracing::error!("error invoking callback: {e:?}"); + }; + Ok::<(), jni::errors::Error>(()) + }) { + tracing::error!("error invoking callback: {e}"); + let _ = env.exception_describe(); + } + }); } /// Receive from Java, converts and sends a [crate::api::Cursor]. @@ -109,7 +150,7 @@ pub extern "system" fn Java_mp_code_CursorController_send<'local>( }; let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::cursor::Controller)) }; - RT.block_on(controller.send(crate::api::Cursor { + super::tokio().block_on(controller.send(crate::api::Cursor { start: (start_row, start_col), end: (end_row, end_col), buffer, diff --git a/src/ffi/java/ext.rs b/src/ffi/java/ext.rs new file mode 100644 index 0000000..696641d --- /dev/null +++ b/src/ffi/java/ext.rs @@ -0,0 +1,34 @@ +use jni::{objects::{JClass, JString}, sys::{jboolean, jlong}, JNIEnv}; + +use super::JExceptable; + +/// Calculate the XXH3 hash for a given String. +#[no_mangle] +pub extern "system" fn Java_mp_code_Extensions_hash<'local>( + mut env: JNIEnv, + _class: JClass<'local>, + content: JString<'local>, +) -> jlong { + let content: String = env.get_string(&content) + .map(|s| s.into()) + .jexcept(&mut env); + let hash = crate::ext::hash(content.as_bytes()); + i64::from_ne_bytes(hash.to_ne_bytes()) +} + +/// Tells the [tokio] runtime how to drive the event loop. +#[no_mangle] +pub extern "system" fn Java_mp_code_Extensions_drive( + _env: JNIEnv, + _class: JClass, + block: jboolean +) { + if block != 0 { + super::tokio().block_on(std::future::pending::<()>()); + } else { + std::thread::spawn(|| { + super::tokio().block_on(std::future::pending::<()>()); + }); + } +} + diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 3bcb617..50379bb 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -2,10 +2,37 @@ pub mod client; pub mod workspace; pub mod cursor; pub mod buffer; -pub mod utils; +pub mod ext; -lazy_static::lazy_static! { - pub(crate) static ref RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("could not create tokio runtime"); +/// Gets or creates the relevant [tokio::runtime::Runtime]. +fn tokio() -> &'static tokio::runtime::Runtime { + use std::sync::OnceLock; + static RT: OnceLock = OnceLock::new(); + RT.get_or_init(|| + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("could not create tokio runtime") + ) +} + +/// A static reference to [jni::JavaVM] that is set on JNI load. +static mut JVM: Option> = None; + +/// Safe accessor for the [jni::JavaVM] static. +pub(crate) fn jvm() -> std::sync::Arc { + unsafe { JVM.clone() }.unwrap() +} + +/// Called upon initialisation of the JVM. +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn JNI_OnLoad( + vm: jni::JavaVM, + _: *mut std::ffi::c_void +) -> jni::sys::jint { + unsafe { JVM = Some(std::sync::Arc::new(vm)) }; + jni::sys::JNI_VERSION_1_1 } /// Set up logging. Useful for debugging. @@ -149,7 +176,7 @@ impl<'local> JObjectify<'local> for crate::cursor::Controller { class, "(J)V", &[ - jni::objects::JValueGen::Long(Box::into_raw(Box::new(&self)) as jni::sys::jlong) + jni::objects::JValueGen::Long(Box::into_raw(Box::new(self)) as jni::sys::jlong) ] ) } @@ -164,46 +191,8 @@ impl<'local> JObjectify<'local> for crate::buffer::Controller { class, "(J)V", &[ - jni::objects::JValueGen::Long(Box::into_raw(Box::new(&self)) as jni::sys::jlong) + jni::objects::JValueGen::Long(Box::into_raw(Box::new(self)) as jni::sys::jlong) ] ) } } - -macro_rules! handle_callback { - ($jtype:literal, $env:ident, $self_ptr:ident, $cb:ident, $t:ty) => { - let controller = unsafe { Box::leak(Box::from_raw($self_ptr as *mut $t)) }; - - let Ok(jvm) = $env.get_java_vm() else { - $env.throw_new("mp/code/exceptions/JNIException", "Failed to get JVM reference!") - .expect("Failed to throw exception!"); - return; - }; - - let Ok(cb_ref) = $env.new_global_ref($cb) else { - $env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") - .expect("Failed to throw exception!"); - return; - }; - controller.callback(move |controller: $t| { - use std::ops::DerefMut; - use crate::ffi::java::JObjectify; - let mut guard = jvm.attach_current_thread().unwrap(); - let jcontroller = match controller.jobjectify(guard.deref_mut()) { - Err(e) => return tracing::error!("could not convert callback argument: {e:?}"), - Ok(x) => x, - }; - let sig = format!("(L{};)V", $jtype); - if let Err(e) = guard.call_method(&cb_ref, - "invoke", - &sig, - &[jni::objects::JValueGen::Object(&jcontroller)] - ) { - tracing::error!("error invoking callback: {e:?}"); - } - }); - }; -} - -pub(crate) use handle_callback; - diff --git a/src/ffi/java/utils.rs b/src/ffi/java/utils.rs deleted file mode 100644 index 18abd03..0000000 --- a/src/ffi/java/utils.rs +++ /dev/null @@ -1,17 +0,0 @@ -use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv}; - -use super::JExceptable; - -/// Calculate the XXH3 hash for a given String. -#[no_mangle] -pub extern "system" fn Java_mp_code_Utils_hash<'local>( - mut env: JNIEnv, - _class: JClass<'local>, - content: JString<'local>, -) -> jlong { - let content: String = env.get_string(&content) - .map(|s| s.into()) - .jexcept(&mut env); - let hash = crate::ext::hash(content.as_bytes()); - i64::from_ne_bytes(hash.to_ne_bytes()) -} diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 540580f..504a4fa 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,7 +1,7 @@ use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jlong, jobject, jobjectArray, jstring}, JNIEnv}; use crate::Workspace; -use super::{JExceptable, JObjectify, RT}; +use super::{JExceptable, JObjectify}; /// Get the workspace id. #[no_mangle] @@ -55,7 +55,7 @@ pub extern "system" fn Java_mp_code_Workspace_create_1buffer<'local>( let path = unsafe { env.get_string_unchecked(&input) } .map(|path| path.to_string_lossy().to_string()) .jexcept(&mut env); - RT.block_on(ws.create(&path)) + super::tokio().block_on(ws.create(&path)) .jexcept(&mut env); } @@ -103,7 +103,7 @@ pub extern "system" fn Java_mp_code_Workspace_attach_1to_1buffer<'local>( let path = unsafe { env.get_string_unchecked(&input) } .map(|path| path.to_string_lossy().to_string()) .jexcept(&mut env); - RT.block_on(workspace.attach(&path)) + super::tokio().block_on(workspace.attach(&path)) .map(|buffer| buffer.jobjectify(&mut env).jexcept(&mut env)) .jexcept(&mut env) .as_raw() @@ -142,7 +142,7 @@ pub extern "system" fn Java_mp_code_Workspace_fetch_1buffers( self_ptr: jlong, ) { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; - RT.block_on(workspace.fetch_buffers()).jexcept(&mut env); + super::tokio().block_on(workspace.fetch_buffers()).jexcept(&mut env); } /// Update the local user list. @@ -153,7 +153,7 @@ pub extern "system" fn Java_mp_code_Workspace_fetch_1users( self_ptr: jlong, ) { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; - RT.block_on(workspace.fetch_users()).jexcept(&mut env); + super::tokio().block_on(workspace.fetch_users()).jexcept(&mut env); } /// List users attached to a buffer. @@ -168,7 +168,7 @@ pub extern "system" fn Java_mp_code_Workspace_list_1buffer_1users<'local>( let buffer = unsafe { env.get_string_unchecked(&input) } .map(|buffer| buffer.to_string_lossy().to_string()) .jexcept(&mut env); - let users = RT.block_on(workspace.list_buffer_users(&buffer)) + let users = super::tokio().block_on(workspace.list_buffer_users(&buffer)) .jexcept(&mut env); env.find_class("java/util/UUID") @@ -194,7 +194,7 @@ pub extern "system" fn Java_mp_code_Workspace_delete_1buffer<'local>( let buffer = unsafe { env.get_string_unchecked(&input) } .map(|buffer| buffer.to_string_lossy().to_string()) .jexcept(&mut env); - RT.block_on(workspace.delete(&buffer)) + super::tokio().block_on(workspace.delete(&buffer)) .jexcept(&mut env); } @@ -206,7 +206,7 @@ pub extern "system" fn Java_mp_code_Workspace_event( self_ptr: jlong ) -> jobject { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; - RT.block_on(workspace.event()) + super::tokio().block_on(workspace.event()) .map(|event| { let (name, arg) = match event { crate::api::Event::FileTreeUpdated(arg) => ("FILE_TREE_UPDATED", env.new_string(arg).unwrap_or_default()), @@ -247,10 +247,10 @@ pub extern "system" fn Java_mp_code_Workspace_select_1buffer( } } - RT.block_on(crate::ext::select_buffer( + super::tokio().block_on(crate::ext::select_buffer( &controllers, Some(std::time::Duration::from_millis(timeout as u64)), - &RT, + super::tokio(), )).jexcept(&mut env) .map(|buf| { env.find_class("mp/code/BufferController").and_then(|class|