feat(java): callback API

Co-authored-by: alemi <me@alemi.dev>
This commit is contained in:
zaaarf 2024-09-16 00:20:03 +02:00
parent b7680b15c1
commit 795423de2a
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
10 changed files with 197 additions and 102 deletions

17
dist/java/src/mp/code/Extensions.java vendored Normal file
View file

@ -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);
}

View file

@ -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);
}

View file

@ -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<crate::api::TextChange>) -> 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,

View file

@ -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);
}

View file

@ -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<crate::api::Cursor>) -> 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,

34
src/ffi/java/ext.rs Normal file
View file

@ -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::<()>());
});
}
}

View file

@ -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<tokio::runtime::Runtime> = 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<std::sync::Arc<jni::JavaVM>> = None;
/// Safe accessor for the [jni::JavaVM] static.
pub(crate) fn jvm() -> std::sync::Arc<jni::JavaVM> {
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;

View file

@ -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())
}

View file

@ -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|