From 6212718e992eb18e88e98e8a7eb976c3dcf726b1 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Wed, 7 Aug 2024 10:22:01 +0200 Subject: [PATCH] feat: recv, buffer_list, tweaks, gradle --- .gitignore | 11 ++++ java/build.gradle | 49 +++++++++++++++ java/settings.gradle | 1 + java/src/mp/code/BufferController.java | 12 +++- java/src/mp/code/Client.java | 40 +++++++------ java/src/mp/code/CursorController.java | 11 +++- java/src/mp/code/Workspace.java | 22 +++---- src/buffer/tools.rs | 10 ++-- src/ffi/java/buffer_controller.rs | 21 ++++++- src/ffi/java/client.rs | 83 +++++++++++++++----------- src/ffi/java/cursor_controller.rs | 20 ++++++- src/ffi/java/workspace.rs | 65 +++++++++++++++++--- src/workspace.rs | 5 ++ 13 files changed, 266 insertions(+), 84 deletions(-) create mode 100644 java/build.gradle create mode 100644 java/settings.gradle diff --git a/.gitignore b/.gitignore index a4b82d1..bb25495 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,14 @@ java/*.iml java/.idea/ java/*.h java/**/*.class +java/build/ +java/.classpath +java/.gradle/ +java/.project +java/.settings/ +java/bin/ + +# intellij insists on creating the wrapper every time even if it's not strictly necessary +java/gradle/ +java/gradlew +java/gradlew.bat diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 0000000..31d157c --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'maven-publish' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.palantir.git-version' version '3.1.0' +} + +group = 'mp.code' +version = versionDetails().lastTag + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +sourceSets { + main.java.srcDirs = ['src/'] +} + +dependencies { + implementation 'com.github.adamheinrich:native-utils:master-SNAPSHOT' +} + +shadowJar { + archiveClassifier.set('') + dependencies { + include(dependency('com.github.adamheinrich:native-utils:master-SNAPSHOT')) + } +} + +def rustDir = projectDir.toPath() + .parent + .resolve('target') + .resolve('release') + .toFile() +processResources { + from(rustDir) { + include('*.dll') + include('*.so') + into('natives/') + } +} + +tasks.register('cargoBuild', Exec) { + workingDir '.' + commandLine 'cargo', 'build', '--release', '--features=java' +} + +build.dependsOn cargoBuild diff --git a/java/settings.gradle b/java/settings.gradle new file mode 100644 index 0000000..c1b754b --- /dev/null +++ b/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'codemp' diff --git a/java/src/mp/code/BufferController.java b/java/src/mp/code/BufferController.java index 4b7fdf2..0c14522 100644 --- a/java/src/mp/code/BufferController.java +++ b/java/src/mp/code/BufferController.java @@ -1,8 +1,11 @@ package mp.code; +import mp.code.data.Cursor; import mp.code.data.TextChange; import mp.code.exceptions.CodeMPException; +import java.util.Optional; + public class BufferController { private final long ptr; @@ -21,8 +24,13 @@ public class BufferController { } private static native TextChange try_recv(long self) throws CodeMPException; - public TextChange tryRecv() throws CodeMPException { - return try_recv(this.ptr); + public Optional tryRecv() throws CodeMPException { + return Optional.ofNullable(try_recv(this.ptr)); + } + + private static native Cursor recv(long self) throws CodeMPException; + public Cursor recv() throws CodeMPException { + return recv(this.ptr); } private static native void send(long self, TextChange change) throws CodeMPException; diff --git a/java/src/mp/code/Client.java b/java/src/mp/code/Client.java index 8a30807..934900b 100644 --- a/java/src/mp/code/Client.java +++ b/java/src/mp/code/Client.java @@ -1,23 +1,22 @@ package mp.code; +import cz.adamh.utils.NativeUtils; import mp.code.exceptions.CodeMPException; +import java.io.IOException; import java.util.Optional; public class Client { private final long ptr; - private final String url; - public static native long setup_tracing(String path); - - private static native long connect(String url) throws CodeMPException; - public Client(String url) throws CodeMPException { - this.ptr = connect(url); - this.url = url; + public static native Client connect(String url) throws CodeMPException; + Client(long ptr) { + this.ptr = ptr; } + private static native String get_url(long self); public String getUrl() { - return this.url; + return get_url(this.ptr); } private static native void login(long self, String username, String password, String workspace) throws CodeMPException; @@ -25,19 +24,14 @@ public class Client { login(this.ptr, username, password, workspace); } - private static native long join_workspace(long self, String id) throws CodeMPException; + private static native Workspace join_workspace(long self, String id) throws CodeMPException; public Workspace joinWorkspace(String id) throws CodeMPException { - return new Workspace(join_workspace(this.ptr, id)); + return join_workspace(this.ptr, id); } - private static native long get_workspace(long self); + private static native Workspace get_workspace(long self); public Optional getWorkspace() { - long ptr = get_workspace(this.ptr); - if(ptr == 0) { // TODO it would be better to init in rust directly - return Optional.empty(); - } else { - return Optional.of(new Workspace(ptr)); - } + return Optional.ofNullable(get_workspace(this.ptr)); } private static native void free(long self); @@ -46,5 +40,17 @@ public class Client { protected void finalize() { free(this.ptr); } + + private static native void setup_tracing(String path); + static { + try { + if(System.getProperty("os.name").startsWith("Windows")) + NativeUtils.loadLibraryFromJar("/natives/codemp_intellij.dll"); + else NativeUtils.loadLibraryFromJar("/natives/libcodemp_intellij.so"); + setup_tracing(System.getenv().get("CODEMP_TRACING_LOG")); + } catch(IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/java/src/mp/code/CursorController.java b/java/src/mp/code/CursorController.java index c18e9d3..f4d903a 100644 --- a/java/src/mp/code/CursorController.java +++ b/java/src/mp/code/CursorController.java @@ -3,6 +3,8 @@ package mp.code; import mp.code.data.Cursor; import mp.code.exceptions.CodeMPException; +import java.util.Optional; + public class CursorController { private final long ptr; @@ -11,8 +13,13 @@ public class CursorController { } private static native Cursor try_recv(long self) throws CodeMPException; - public Cursor tryRecv() throws CodeMPException { - return try_recv(this.ptr); + public Optional tryRecv() throws CodeMPException { + return Optional.ofNullable(try_recv(this.ptr)); + } + + private static native Cursor recv(long self) throws CodeMPException; + public Cursor recv() throws CodeMPException { + return recv(this.ptr); } private static native void send(long self, Cursor cursor) throws CodeMPException; diff --git a/java/src/mp/code/Workspace.java b/java/src/mp/code/Workspace.java index 4af0226..1d796c4 100644 --- a/java/src/mp/code/Workspace.java +++ b/java/src/mp/code/Workspace.java @@ -1,5 +1,7 @@ package mp.code; +import java.util.Optional; + import mp.code.exceptions.CodeMPException; public class Workspace { @@ -14,14 +16,14 @@ public class Workspace { return get_workspace_id(this.ptr); } - private static native long get_cursor(long self); + private static native CursorController get_cursor(long self); public CursorController getCursor() { - return new CursorController(get_cursor(this.ptr)); + return get_cursor(this.ptr); } - private static native long get_buffer(long self, String path); - public BufferController getBuffer(String path) { - return new BufferController(get_buffer(this.ptr, path)); + private static native BufferController get_buffer(long self, String path); + public Optional getBuffer(String path) { + return Optional.ofNullable(get_buffer(this.ptr, path)); } private static native String[] get_file_tree(long self); @@ -34,9 +36,9 @@ public class Workspace { return new BufferController(create_buffer(path)); } - private static native long attach_to_buffer(long self) throws CodeMPException; - public BufferController attachToBuffer() throws CodeMPException { - return new BufferController(attach_to_buffer(ptr)); + private static native BufferController attach_to_buffer(long self, String path) throws CodeMPException; + public BufferController attachToBuffer(String path) throws CodeMPException { + return attach_to_buffer(ptr, path); } private static native void fetch_buffers(long self) throws CodeMPException; @@ -60,8 +62,8 @@ public class Workspace { } private static native BufferController select_buffer(long self, long timeout) throws CodeMPException; - public BufferController selectBuffer(long timeout) throws CodeMPException { - return select_buffer(this.ptr, timeout); + public Optional selectBuffer(long timeout) throws CodeMPException { + return Optional.ofNullable(select_buffer(this.ptr, timeout)); } private static native void free(long self); diff --git a/src/buffer/tools.rs b/src/buffer/tools.rs index 5e1870d..c4f8765 100644 --- a/src/buffer/tools.rs +++ b/src/buffer/tools.rs @@ -1,5 +1,4 @@ use crate::{Error, api::Controller}; -use std::sync::Arc; use tokio::sync::mpsc; /// invoke .poll() on all given buffer controllers and wait, returning the first one ready @@ -13,15 +12,16 @@ use tokio::sync::mpsc; /// /// returns an error if all buffers returned errors while polling. pub async fn select_buffer( - buffers: &[Arc], + buffers: &[crate::buffer::Controller], timeout: Option, -) -> crate::Result>> { + runtime: &tokio::runtime::Runtime +) -> crate::Result> { let (tx, mut rx) = mpsc::unbounded_channel(); let mut tasks = Vec::new(); for buffer in buffers { let _tx = tx.clone(); let _buffer = buffer.clone(); - tasks.push(tokio::spawn(async move { + tasks.push(runtime.spawn(async move { match _buffer.poll().await { Ok(()) => _tx.send(Ok(Some(_buffer))), Err(_) => _tx.send(Err(Error::Channel { send: true })), @@ -30,7 +30,7 @@ pub async fn select_buffer( } if let Some(d) = timeout { let _tx = tx.clone(); - tasks.push(tokio::spawn(async move { + tasks.push(runtime.spawn(async move { tokio::time::sleep(d).await; _tx.send(Ok(None)) })); diff --git a/src/ffi/java/buffer_controller.rs b/src/ffi/java/buffer_controller.rs index 09d87d1..e094d97 100644 --- a/src/ffi/java/buffer_controller.rs +++ b/src/ffi/java/buffer_controller.rs @@ -2,7 +2,7 @@ use jni::{objects::{JClass, JObject, JValueGen}, sys::{jlong, jobject, jstring}, use crate::api::Controller; -use super::util::JExceptable; +use super::{util::JExceptable, RT}; #[no_mangle] pub extern "system" fn Java_mp_code_BufferController_get_1name( @@ -37,7 +37,23 @@ 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)) }; - match controller.try_recv().jexcept(&mut env) { + let change = controller.try_recv().jexcept(&mut env); + recv_jni(&mut env, change) +} + +#[no_mangle] +pub extern "system" fn Java_mp_code_BufferController_recv( + mut env: JNIEnv, + _class: JClass, + 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); + recv_jni(&mut env, change) +} + +fn recv_jni(env: &mut JNIEnv, change: Option) -> jobject { + match change { None => JObject::null().as_raw(), Some(event) => { let class = env.find_class("mp/code/data/TextChange").expect("Couldn't find class!"); @@ -52,6 +68,7 @@ pub extern "system" fn Java_mp_code_BufferController_try_1recv( ).expect("failed creating object").into_raw() } } + } #[no_mangle] diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 4f944a0..4dba552 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,4 +1,4 @@ -use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv}; +use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv}; use crate::{client::Client, Workspace}; use super::{util::JExceptable, RT}; @@ -9,48 +9,21 @@ pub extern "system" fn Java_mp_code_Client_free(_env: JNIEnv, _class: JClass, in let _ = unsafe { Box::from_raw(input as *mut Client) }; } -/// Sets up tracing subscriber -#[no_mangle] -pub extern "system" fn Java_mp_code_Client_setup_1tracing<'local>( - mut env: JNIEnv, - _class: JClass<'local>, - path: JString<'local> -) { - let path: Option = if path.is_null() { - None - } else { - Some(env.get_string(&path).expect("Couldn't get java string!").into()) - }; - - super::setup_logger(true, path); -} - /// Connects to a given URL and returns a [Client] to interact with that server. #[no_mangle] pub extern "system" fn Java_mp_code_Client_connect<'local>( mut env: JNIEnv, _class: JClass<'local>, input: JString<'local> -) -> jlong { +) -> jobject { let url: String = env.get_string(&input).expect("Couldn't get java string!").into(); RT.block_on(crate::Client::new(&url)) .map(|client| Box::into_raw(Box::new(client)) as jlong) - .jexcept(&mut env) -} - -/// Gets a [Workspace] by name and returns a pointer to it. -#[no_mangle] -pub extern "system" fn Java_mp_code_Client_get_1workspace<'local>( - env: JNIEnv<'local>, - _class: JClass<'local>, - self_ptr: jlong, - input: JString<'local> -) -> jlong { - let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; - let workspace_id = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") }; - client.get_workspace(workspace_id.to_str().expect("Not UTF-8")) - .map(|workspace| Box::into_raw(Box::new(workspace)) as jlong) - .unwrap_or_default() + .map(|ptr| { + let class = env.find_class("mp/code/Client").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]) + .expect("Failed to initialise object") + }).jexcept(&mut env).as_raw() } /// Logs in to a specific [Workspace]. @@ -78,13 +51,17 @@ pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>( _class: JClass<'local>, self_ptr: jlong, input: JString<'local> -) -> jlong { +) -> jobject { let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; let workspace_id = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") }; RT.block_on(client.join_workspace(workspace_id.to_str().expect("Not UTF-8"))) .map(|workspace| spawn_updater(workspace.clone())) .map(|workspace| Box::into_raw(Box::new(workspace)) as jlong) - .jexcept(&mut env) + .map(|ptr| { + let class = env.find_class("mp/code/Workspace").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]) + .expect("Failed to initialise object") + }).jexcept(&mut env).as_raw() } // TODO: this stays until we get rid of the arc then i'll have to find a better way @@ -99,3 +76,37 @@ fn spawn_updater(workspace: Workspace) -> Workspace { }); workspace } + +/// Gets a [Workspace] by name and returns a pointer to it. +#[no_mangle] +pub extern "system" fn Java_mp_code_Client_get_1workspace<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + self_ptr: jlong, + input: JString<'local> +) -> jobject { + let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; + let workspace_id = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") }; + client.get_workspace(workspace_id.to_str().expect("Not UTF-8")) + .map(|workspace| Box::into_raw(Box::new(workspace)) as jlong) + .map(|ptr| { + let class = env.find_class("mp/code/Workspace").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]) + .expect("Failed to initialise object") + }).unwrap_or(JObject::null()).as_raw() +} + +/// Sets up the tracing subscriber. +#[no_mangle] +pub extern "system" fn Java_mp_code_Client_setup_1tracing<'local>( + mut env: JNIEnv, + _class: JClass<'local>, + path: JString<'local> +) { + super::setup_logger( + true, + Some(path) + .filter(|p| p.is_null()) + .map(|p| env.get_string(&p).expect("couldn't get java string").into()) + ); +} diff --git a/src/ffi/java/cursor_controller.rs b/src/ffi/java/cursor_controller.rs index aa4aab1..a2be4b8 100644 --- a/src/ffi/java/cursor_controller.rs +++ b/src/ffi/java/cursor_controller.rs @@ -1,6 +1,8 @@ use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv}; use crate::{api::Controller, ffi::java::util::JExceptable}; +use super::RT; + #[no_mangle] pub extern "system" fn Java_mp_code_CursorController_try_1recv( mut env: JNIEnv, @@ -8,7 +10,23 @@ 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)) }; - match controller.try_recv().jexcept(&mut env) { + let cursor = controller.try_recv().jexcept(&mut env); + jni_recv(&mut env, cursor) +} + +#[no_mangle] +pub extern "system" fn Java_mp_code_CursorController_recv( + mut env: JNIEnv, + _class: JClass, + 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); + jni_recv(&mut env, cursor) +} + +fn jni_recv(env: &mut JNIEnv, cursor: Option) -> jobject { + match cursor { None => JObject::null().as_raw(), Some(event) => { let class = env.find_class("mp/code/data/Cursor").expect("Couldn't find class!"); diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 6012726..9fa958e 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,4 +1,4 @@ -use jni::{objects::{JClass, JObject, JString}, sys::{jlong, jobjectArray, jstring}, JNIEnv}; +use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject, jobjectArray, jstring}, JNIEnv}; use crate::Workspace; use super::{util::JExceptable, RT}; @@ -25,25 +25,35 @@ pub extern "system" fn Java_mp_code_Workspace_get_1workspace_1id<'local>( /// Gets a cursor controller by name and returns a pointer to it. #[no_mangle] pub extern "system" fn Java_mp_code_Workspace_get_1cursor<'local>( - _env: JNIEnv<'local>, + mut env: JNIEnv<'local>, _class: JClass<'local>, self_ptr: jlong -) -> jlong { +) -> jobject { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; - Box::into_raw(Box::new(workspace.cursor())) as jlong + let class = env.find_class("mp/code/CursorController").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(workspace.cursor())) as jlong)]) + .expect("Failed to initialise object") + .as_raw() } /// Gets a buffer controller by name and returns a pointer to it. #[no_mangle] pub extern "system" fn Java_mp_code_Workspace_get_1buffer<'local>( - env: JNIEnv<'local>, + mut env: JNIEnv<'local>, _class: JClass<'local>, self_ptr: jlong, input: JString<'local> -) -> jlong { +) -> jobject { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; let path = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") }; - Box::into_raw(Box::new(workspace.buffer_by_name(path.to_str().expect("Not UTF-8")))) as jlong + if let Some(buf) = workspace.buffer_by_name(path.to_str().expect("Not UTF-8!")) { + let class = env.find_class("mp/code/BufferController").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(buf)) as jlong)]) + .expect("Failed to initialise object") + .as_raw() + } else { + JObject::null().as_raw() + } } /// Creates a new buffer. @@ -87,12 +97,16 @@ pub extern "system" fn Java_mp_code_Workspace_attach_1to_1buffer<'local>( _class: JClass<'local>, self_ptr: jlong, input: JString<'local> -) -> jlong { +) -> jobject { let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; let path = unsafe { env.get_string_unchecked(&input).expect("Couldn't get java string!") }; RT.block_on(workspace.attach(path.to_str().expect("Not UTF-8!"))) .map(|buffer| Box::into_raw(Box::new(buffer)) as jlong) - .jexcept(&mut env) + .map(|ptr| { + let class = env.find_class("mp/code/BufferController").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]) + .expect("Failed to initialise object") + }).jexcept(&mut env).as_raw() } /// Updates the local buffer list. @@ -155,3 +169,36 @@ pub extern "system" fn Java_mp_code_Workspace_delete_1buffer<'local>( let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; RT.block_on(workspace.delete(buffer.to_str().expect("Not UTF-8!"))).jexcept(&mut env); } + +/// Polls a list of buffers, returning the first ready one. +#[no_mangle] +pub extern "system" fn Java_mp_code_Workspace_select_1buffer( + mut env: JNIEnv, + _class: JClass, + self_ptr: jlong, + timeout: jlong +) -> jobject { + let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; + let buffers = workspace.buffer_list(); + let mut controllers = Vec::default(); + for buffer in buffers { + if let Some(controller) = workspace.buffer_by_name(&buffer) { + controllers.push(controller); + } + } + + let active = RT.block_on(crate::buffer::tools::select_buffer( + &controllers, + Some(std::time::Duration::from_millis(timeout as u64)), + &RT, + )).jexcept(&mut env); + + if let Some(buf) = active { + let class = env.find_class("mp/code/BufferController").expect("Failed to find class"); + env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(buf)) as jlong)]) + .expect("Failed to initialise object") + .as_raw() + } else { + JObject::null().as_raw() + } +} diff --git a/src/workspace.rs b/src/workspace.rs index e0bd5d4..9eb65ae 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -241,6 +241,11 @@ impl Workspace { self.0.buffers.get(path).map(|x| x.clone()) } + /// get a list of all the currently attached to buffers + pub fn buffer_list(&self) -> Vec { + self.0.buffers.iter().map(|elem| elem.key().clone()).collect() + } + /// get the currently cached "filetree" pub fn filetree(&self) -> Vec { self.0.filetree.iter().map(|f| f.clone()).collect()