From e2ae53b35fc2872e7ded4393d48228e5e1a644cd Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 6 Aug 2024 23:30:00 +0200 Subject: [PATCH] feat: initial work on jni-rs java glue --- .gitignore | 6 + build.rs | 95 ----- java/src/mp/code/BufferController.java | 39 ++ java/src/mp/code/Client.java | 64 ++++ java/src/mp/code/CursorController.java | 29 ++ java/src/mp/code/Workspace.java | 70 ++++ java/src/mp/code/data/Cursor.java | 16 + java/src/mp/code/data/TextChange.java | 13 + .../mp/code/exceptions/ChannelException.java | 7 + .../code/exceptions/CodeMPLibException.java | 10 + .../code/exceptions/DeadlockedException.java | 7 + .../exceptions/InvalidStateException.java | 7 + .../code/exceptions/TransportException.java | 7 + src/ffi/java/client.rs | 86 +++++ src/ffi/java/mod.rs | 348 ++---------------- src/ffi/java/typemap.in | 39 -- src/ffi/java/util.rs | 29 ++ src/ffi/java/workspace.rs | 23 ++ 18 files changed, 437 insertions(+), 458 deletions(-) create mode 100644 java/src/mp/code/BufferController.java create mode 100644 java/src/mp/code/Client.java create mode 100644 java/src/mp/code/CursorController.java create mode 100644 java/src/mp/code/Workspace.java create mode 100644 java/src/mp/code/data/Cursor.java create mode 100644 java/src/mp/code/data/TextChange.java create mode 100644 java/src/mp/code/exceptions/ChannelException.java create mode 100644 java/src/mp/code/exceptions/CodeMPLibException.java create mode 100644 java/src/mp/code/exceptions/DeadlockedException.java create mode 100644 java/src/mp/code/exceptions/InvalidStateException.java create mode 100644 java/src/mp/code/exceptions/TransportException.java create mode 100644 src/ffi/java/client.rs delete mode 100644 src/ffi/java/typemap.in create mode 100644 src/ffi/java/util.rs create mode 100644 src/ffi/java/workspace.rs diff --git a/.gitignore b/.gitignore index 8a94fdd..a4b82d1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ node_modules/ package-lock.json index.d.ts index.node + +# java +java/*.iml +java/.idea/ +java/*.h +java/**/*.class diff --git a/build.rs b/build.rs index 4ba772a..9522fcb 100644 --- a/build.rs +++ b/build.rs @@ -6,93 +6,6 @@ extern crate pyo3_build_config; /// The main method of the buildscript, required by some glue modules. fn main() { - #[cfg(feature = "java")] - { - let pkg = "com.codemp.jni".to_string(); - let pkg_folder = pkg.replace('.', "/"); // java moment - - let out_dir = std::env::var("OUT_DIR").expect("cargo did not provide OUT_DIR"); - let out_dir = std::path::Path::new(&out_dir); - let generated_glue_file = out_dir.join("generated_glue.in"); - let src_dir = std::path::Path::new("src").join("ffi").join("java"); - let typemap_file = src_dir.join("typemap.in"); - rifgen::Generator::new( - rifgen::TypeCases::CamelCase, - rifgen::Language::Java, - vec![src_dir], - ) - .generate_interface(&generated_glue_file); - - // build java source path - let target = out_dir - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf(); // target/debug - - let mut java_target = target.clone(); // target/debug/java - java_target.push("java"); - - let mut pkg_path = java_target.clone(); // target/debug/java/src/com/codemp/intellij - pkg_path.push("src"); - pkg_path.push(pkg_folder); - - // delete folder if it exists, then create it - recreate_path(&pkg_path); - - // generate java code - let java_cfg = flapigen::JavaConfig::new(pkg_path.clone(), pkg); - let java_gen = flapigen::Generator::new(flapigen::LanguageConfig::JavaConfig(java_cfg)) - .rustfmt_bindings(true); - java_gen.expand_many( - "codemp-intellij", - &[&generated_glue_file, &typemap_file], - out_dir.join("glue.rs"), - ); - - #[cfg(feature = "java-artifact")] - { - // panic if no jdk - std::process::Command::new("javac") - .arg("--version") - .status() - .expect("java not found"); - - // compile java code - let mut java_compiled = java_target.clone(); // target/debug/java/classes - java_compiled.push("classes"); - recreate_path(&java_compiled); - - let mut javac_cmd = std::process::Command::new("javac"); - javac_cmd.arg("-d").arg(java_compiled.as_os_str()); - for java_file in pkg_path.read_dir().unwrap().filter_map(|e| e.ok()) { - javac_cmd.arg(java_file.path().as_os_str()); - } - javac_cmd.status().expect("failed to run javac"); - - // jar it! - let mut jar_file = target.clone(); // target/debug/codemp-java.jar - jar_file.push("codemp-java.jar"); - - let mut jar_cmd = std::process::Command::new("jar"); - jar_cmd - .current_dir(&java_compiled) - .arg("cf") - .arg(jar_file.as_os_str()); - for java_file in java_compiled.read_dir().unwrap().filter_map(|e| e.ok()) { - let relative_path = java_file.path().clone(); - let relative_path = relative_path.strip_prefix(&java_compiled).unwrap(); - jar_cmd.arg(relative_path.as_os_str()); - } - jar_cmd.status().expect("failed to run jar!"); - - println!("cargo:rerun-if-changed={}", generated_glue_file.display()); - } - } - #[cfg(feature = "js")] { napi_build::setup(); @@ -103,11 +16,3 @@ fn main() { pyo3_build_config::add_extension_module_link_args(); } } - -#[cfg(feature = "java")] -fn recreate_path(path: &std::path::PathBuf) { - if path.exists() { - std::fs::remove_dir_all(path).expect("failed to delete old dir!"); - } - std::fs::create_dir_all(path).expect("error while creating folder!"); -} diff --git a/java/src/mp/code/BufferController.java b/java/src/mp/code/BufferController.java new file mode 100644 index 0000000..edec72c --- /dev/null +++ b/java/src/mp/code/BufferController.java @@ -0,0 +1,39 @@ +package mp.code; + +import mp.code.data.TextChange; +import mp.code.exceptions.CodeMPLibException; + +public class BufferController { + private final long ptr; + + BufferController(long ptr) { + this.ptr = ptr; + } + + public static native String get_name(long self); + public String getName() { + return get_name(this.ptr); + } + + public static native String get_content(long self); + public String getContent() { + return get_content(this.ptr); + } + + private static native TextChange try_recv(long self) throws CodeMPLibException; + public TextChange tryRecv() throws CodeMPLibException { + return try_recv(this.ptr); + } + + private static native void send(long self, TextChange change) throws CodeMPLibException; + public void send(TextChange change) throws CodeMPLibException { + send(this.ptr, change); + } + + private static native void free(long self); + @Override + @SuppressWarnings("removal") + protected void finalize() throws Throwable { + free(this.ptr); + } +} diff --git a/java/src/mp/code/Client.java b/java/src/mp/code/Client.java new file mode 100644 index 0000000..aa3ea97 --- /dev/null +++ b/java/src/mp/code/Client.java @@ -0,0 +1,64 @@ +package mp.code; + +import mp.code.exceptions.CodeMPLibException; + +import java.util.Optional; +import java.util.UUID; + +public class Client { + private final long ptr; + private final String url; + + private static native long setup_tracing(String path); + + private static native long connect(String url) throws CodeMPLibException; + public Client(String url) throws CodeMPLibException { + this.ptr = connect(url); + this.url = url; + } + + public String getUrl() { + return this.url; + } + + private static native void login(long self, String username, String password, String workspace) throws CodeMPLibException; + public void login(String username, String password, String workspace) throws CodeMPLibException { + login(this.ptr, username, password, workspace); + } + + private static native long join_workspace(long self, String id) throws CodeMPLibException; + public Workspace joinWorkspace(String id) throws CodeMPLibException { + return new Workspace(join_workspace(this.ptr, id)); + } + + private static native long 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)); + } + } + + private static native void free(long self); + @Override + @SuppressWarnings("removal") // muh java 8 + protected void finalize() { + free(this.ptr); + } + + // TODO - remove everything past this line + public static void main(String[] args) throws CodeMPLibException { + Client c = new Client("http://alemi.dev:50053"); + c.login(UUID.randomUUID().toString(), "lmaodefaultpassword", "glue"); + c.joinWorkspace("glue"); + System.out.println("Done!"); + } + + static { + System.loadLibrary("codemp"); + setup_tracing(null); + } +} + diff --git a/java/src/mp/code/CursorController.java b/java/src/mp/code/CursorController.java new file mode 100644 index 0000000..352bc53 --- /dev/null +++ b/java/src/mp/code/CursorController.java @@ -0,0 +1,29 @@ +package mp.code; + +import mp.code.data.Cursor; +import mp.code.data.TextChange; + +public class CursorController { + private final long ptr; + + CursorController(long ptr) { + this.ptr = ptr; + } + + private static native Cursor recv(long self); + public Cursor recv() { + return recv(this.ptr); + } + + private static native void send(long self, Cursor cursor); + public void send(TextChange change, Cursor cursor) { + send(this.ptr, cursor); + } + + private static native void free(long self); + @Override + @SuppressWarnings("removal") + protected void finalize() throws Throwable { + free(this.ptr); + } +} diff --git a/java/src/mp/code/Workspace.java b/java/src/mp/code/Workspace.java new file mode 100644 index 0000000..fcfae78 --- /dev/null +++ b/java/src/mp/code/Workspace.java @@ -0,0 +1,70 @@ +package mp.code; + +import mp.code.exceptions.CodeMPLibException; + +public class Workspace { + private final long ptr; + + Workspace(long ptr) { + this.ptr = ptr; + } + + private static native String get_workspace_id(long self); + public String getWorkspaceId() { + return get_workspace_id(this.ptr); + } + + private static native long get_cursor(long self); + public CursorController getCursor() { + return new CursorController(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 void get_file_tree(long self); + public void getFileTree() { + // TODO vector? + } + + private static native long create_buffer(String path) throws CodeMPLibException; + public BufferController createBuffer(String path) throws CodeMPLibException { + return new BufferController(create_buffer(path)); + } + + private static native long attach_to_buffer(long self) throws CodeMPLibException; + public BufferController attachToBuffer() throws CodeMPLibException { + return new BufferController(attach_to_buffer(ptr)); + } + + private static native void fetch_buffers(long self) throws CodeMPLibException; + public void fetchBuffers() throws CodeMPLibException { + fetch_buffers(this.ptr); + } + + private static native void fetch_users(long self) throws CodeMPLibException; + public void fetchUsers() throws CodeMPLibException { + fetch_buffers(this.ptr); + } + + private static native void list_buffer_users(long self, String path) throws CodeMPLibException; + public void listBufferUsers(String path) throws CodeMPLibException { + // TODO pass vector + } + + private static native void delete_buffer(long self, String path) throws CodeMPLibException; + public void deleteBuffer(String path) throws CodeMPLibException { + delete_buffer(this.ptr, path); + } + + // TODO select_buffer + + private static native void free(long self); + @Override + @SuppressWarnings("removal") + protected void finalize() throws Throwable { + free(this.ptr); + } +} diff --git a/java/src/mp/code/data/Cursor.java b/java/src/mp/code/data/Cursor.java new file mode 100644 index 0000000..8f7a25e --- /dev/null +++ b/java/src/mp/code/data/Cursor.java @@ -0,0 +1,16 @@ +package mp.code.data; + +public class Cursor { + public final int startRow, startCol, endRow, endCol; + public final String buffer; + public final String user; + + public Cursor(int startRow, int startCol, int endRow, int endCol, String buffer, String user) { + this.startRow = startRow; + this.startCol = startCol; + this.endRow = endRow; + this.endCol = endCol; + this.buffer = buffer; + this.user = user; + } +} diff --git a/java/src/mp/code/data/TextChange.java b/java/src/mp/code/data/TextChange.java new file mode 100644 index 0000000..edba093 --- /dev/null +++ b/java/src/mp/code/data/TextChange.java @@ -0,0 +1,13 @@ +package mp.code.data; + +public class TextChange { + public final long start; + public final long end; + public final String content; + + public TextChange(long start, long end, String content) { + this.start = start; + this.end = end; + this.content = content; + } +} diff --git a/java/src/mp/code/exceptions/ChannelException.java b/java/src/mp/code/exceptions/ChannelException.java new file mode 100644 index 0000000..b070a32 --- /dev/null +++ b/java/src/mp/code/exceptions/ChannelException.java @@ -0,0 +1,7 @@ +package mp.code.exceptions; + +public class ChannelException extends CodeMPLibException { + public ChannelException(String input) { + super(input); + } +} diff --git a/java/src/mp/code/exceptions/CodeMPLibException.java b/java/src/mp/code/exceptions/CodeMPLibException.java new file mode 100644 index 0000000..c370deb --- /dev/null +++ b/java/src/mp/code/exceptions/CodeMPLibException.java @@ -0,0 +1,10 @@ +package mp.code.exceptions; + +/** + * A generic class for all our exceptions coming through the JNI from the library. + */ +public abstract class CodeMPLibException extends Exception { + protected CodeMPLibException(String msg) { + super(msg); + } +} diff --git a/java/src/mp/code/exceptions/DeadlockedException.java b/java/src/mp/code/exceptions/DeadlockedException.java new file mode 100644 index 0000000..b45f98e --- /dev/null +++ b/java/src/mp/code/exceptions/DeadlockedException.java @@ -0,0 +1,7 @@ +package mp.code.exceptions; + +public class DeadlockedException extends CodeMPLibException { + public DeadlockedException(String s) { + super(s); + } +} diff --git a/java/src/mp/code/exceptions/InvalidStateException.java b/java/src/mp/code/exceptions/InvalidStateException.java new file mode 100644 index 0000000..fa0fa49 --- /dev/null +++ b/java/src/mp/code/exceptions/InvalidStateException.java @@ -0,0 +1,7 @@ +package mp.code.exceptions; + +public class InvalidStateException extends CodeMPLibException { + public InvalidStateException(String message) { + super(message); + } +} diff --git a/java/src/mp/code/exceptions/TransportException.java b/java/src/mp/code/exceptions/TransportException.java new file mode 100644 index 0000000..088f42c --- /dev/null +++ b/java/src/mp/code/exceptions/TransportException.java @@ -0,0 +1,7 @@ +package mp.code.exceptions; + +public class TransportException extends CodeMPLibException { + public TransportException(String message) { + super(message); + } +} diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs new file mode 100644 index 0000000..ea12e59 --- /dev/null +++ b/src/ffi/java/client.rs @@ -0,0 +1,86 @@ +use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv}; +use crate::{client::Client, Workspace}; + +use super::{util::JExceptable, RT}; + +/// Called by the Java GC to drop a [Client]. +#[no_mangle] +pub extern "system" fn Java_mp_code_Client_free(_env: JNIEnv, _class: JClass, input: jlong) { + super::util::dereference_and_drop::(input) +} + +/// 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 { + 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) +} + +/// Logs in to a specific [Workspace]. +#[no_mangle] +pub extern "system" fn Java_mp_code_Client_login<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + self_ptr: jlong, + user: JString<'local>, + pwd: JString<'local>, + workspace: JString<'local> +) { + let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) }; + let user: String = env.get_string(&user).expect("Couldn't get java string!").into(); + let pwd: String = env.get_string(&pwd).expect("Couldn't get java string!").into(); + let workspace: String = env.get_string(&workspace).expect("Couldn't get java string!").into(); + RT.block_on(client.login(user, pwd, Some(workspace))) + .jexcept(&mut env) +} + +/// Joins a [Workspace] and returns a pointer to it. +#[no_mangle] +pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>( + mut 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!") }; + 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) +} + +// 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 { + loop { + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + w.fetch_buffers().await.unwrap(); + w.fetch_users().await.unwrap(); + } + }); + workspace +} diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 5c058d3..3fb5f40 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -1,333 +1,33 @@ -use api::Controller; -use codemp_proto::{cursor::{RowCol, CursorPosition}, files::BufferNode}; -use rifgen::rifgen_attr::{generate_access_methods, generate_interface, generate_interface_doc}; -use std::sync::Arc; -use std::time::Duration; -use uuid::Uuid; - -use crate::{ - Client, - Error, - Result, - api::TextChange, - buffer::{self, tools}, - cursor, - workspace::Workspace -}; - -//rifgen generated code -include!(concat!(env!("OUT_DIR"), "/glue.rs")); +pub mod client; +pub mod workspace; +pub mod util; lazy_static::lazy_static! { - /// the tokio runtime, since we can't easily have Java and Rust async work together - static ref RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new() - .expect("could not start tokio runtime"); + pub(crate) static ref RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("could not create tokio runtime"); } -#[generate_interface_doc] -/// the handler class that represent an instance of a CodeMP client -struct ClientHandler { - client: Client, - url: String, -} +pub(crate) fn setup_logger(debug: bool, path: Option) { + let format = tracing_subscriber::fmt::format() + .with_level(true) + .with_target(true) + .with_thread_ids(false) + .with_thread_names(false) + .with_ansi(false) + .with_file(false) + .with_line_number(false) + .with_source_location(false) + .compact(); -impl ClientHandler { - #[generate_interface(constructor)] - /// construct a new [ClientHandler] - fn new(address: &str) -> ClientHandler { - ClientHandler { - client: RT.block_on(Client::new(address)).unwrap(), - url: address.to_string(), - } - } + let level = if debug { tracing::Level::DEBUG } else {tracing::Level::INFO }; - #[generate_interface] - /// join a workspace by name - fn join_workspace(&mut self, workspace_id: &str) -> Result { - RT.block_on(self.client.join_workspace(workspace_id)) - .map(|workspace| { - Self::spawn_updater(workspace.clone()); - WorkspaceHandler { workspace } - }) - } + let builder = tracing_subscriber::fmt() + .event_format(format) + .with_max_level(level); - fn spawn_updater(workspace: Arc) { - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(60)).await; - workspace.fetch_buffers().await.unwrap(); - workspace.fetch_users().await.unwrap(); - } - }); - } - - #[generate_interface] - /// get the url you are currently connected to - fn get_url(&self) -> String { - self.url.clone() - } -} - -#[generate_interface_doc] -/// wraps a [codemp::workspace::Workspace] to be handled by Java -struct WorkspaceHandler { - workspace: Arc, -} - -impl WorkspaceHandler { // TODO: workspace leave / buffer detach ? - #[generate_interface(constructor)] - /// constructor required by flapigen, DO NOT CALL THIS - fn new() -> WorkspaceHandler { - unimplemented!() - } - - #[generate_interface] - /// create a new buffer in current workspace - fn create_buffer(&mut self, path: &str) -> Result { - RT.block_on(self.workspace.create(path))?; - Ok(self.get_buffer(path).unwrap()) - } - - #[generate_interface] - /// attach to a buffer and get a [crate::BufferHandler] for it - fn attach_to_buffer(&mut self, path: &str) -> Result { - RT.block_on(self.workspace.attach(path)) - .map(|buffer| BufferHandler { buffer }) - } - - #[generate_interface] - /// updates the local list of the workspace's buffers - fn fetch_buffers(&mut self) -> Result<()> { - RT.block_on(self.workspace.fetch_buffers()) - } - - #[generate_interface] - /// updates the local list of the workspace's users - fn fetch_users(&mut self) -> Result<()> { - RT.block_on(self.workspace.fetch_users()) - } - - #[generate_interface] - /// gets a list of all users in a buffer - fn list_buffer_users(&mut self, path: &str) -> Result { - let mut res = StringVec::new(); - RT.block_on(self.workspace.list_buffer_users(path))? - .iter() - .for_each(|u| res.push(Uuid::from(u.clone()).to_string())); - Ok(res) - } - - #[generate_interface] - /// delete a buffer - fn delete_buffer(&mut self, path: &str) -> Result<()> { - RT.block_on(self.workspace.delete(path)) - } - - #[generate_interface] - /// get the workspace id - fn get_workspace_id(&self) -> String { - self.workspace.id().clone() - } - - #[generate_interface] - /// get a [crate::CursorHandler] for the workspace's cursor - fn get_cursor(&self) -> CursorHandler { - CursorHandler { - cursor: self.workspace.cursor().clone(), - } - } - - #[generate_interface] - /// get a [crate::BufferHandler] for one of the workspace's buffers - fn get_buffer(&self, path: &str) -> Option { - self.workspace - .buffer_by_name(path) - .map(|buffer| BufferHandler { buffer }) - } - - #[generate_interface] - /// get the names of all buffers available in the workspace - fn get_filetree(&self) -> StringVec { - StringVec { - v: self.workspace.filetree() - } - } - - #[generate_interface] - /// polls a list of buffers, returning the first ready one - fn select_buffer( - &mut self, - mut buffer_ids: StringVec, - timeout: i64, - ) -> Result> { - let mut buffers = Vec::new(); - for id in buffer_ids.v.iter_mut() { - match self.get_buffer(id.as_str()) { - Some(buf) => buffers.push(buf.buffer), - None => continue, - } - } - - let result = RT.block_on(tools::select_buffer( - buffers.as_slice(), - Some(Duration::from_millis(timeout as u64)), - )); - - match result { - Err(e) => Err(e), - Ok(buffer) => Ok(buffer.map(|buffer| BufferHandler { buffer })), - } - } -} - -#[generate_interface_doc] -#[generate_access_methods] -/// wraps a [codemp::proto::cursor::CursorEvent] to be handled by Java -struct CursorEventWrapper { - user: String, - buffer: String, - start_row: i32, - start_col: i32, - end_row: i32, - end_col: i32, -} - -#[generate_interface_doc] -/// a handler providing Java access to [codemp::cursor::Controller] methods -struct CursorHandler { - pub cursor: Arc, -} - -impl CursorHandler { - #[generate_interface(constructor)] - /// constructor required by flapigen, DO NOT CALL THIS - fn new() -> CursorHandler { - unimplemented!() - } - - #[generate_interface] - /// get next cursor event from current workspace, or block until one is available - fn recv(&self) -> Result { - match RT.block_on(self.cursor.recv()) { - Err(err) => Err(err), - Ok(event) => Ok(CursorEventWrapper { - user: Uuid::from(event.user).to_string(), - buffer: event.position.buffer.path.clone(), - start_row: event.position.start.row, - start_col: event.position.start.col, - end_row: event.position.end.row, - end_col: event.position.end.col, - }), - } - } - - #[generate_interface] - /// broadcast a cursor event - /// will automatically fix start and end if they are accidentally inverted - fn send( - &self, - buffer: String, - start_row: i32, - start_col: i32, - end_row: i32, - end_col: i32, - ) -> Result<()> { - self.cursor.send(CursorPosition { - buffer: BufferNode { path: buffer }, - start: RowCol::from((start_row, start_col)), - end: RowCol::from((end_row, end_col)), - }) - } -} - -#[generate_interface_doc] -#[generate_access_methods] -/// wraps a [codemp::api::change::TextChange] to make it accessible from Java -struct TextChangeWrapper { - start: usize, - end: usize, //not inclusive - content: String, -} - -#[generate_interface_doc] -/// a handler providing Java access to [codemp::buffer::Controller] methods -struct BufferHandler { - pub buffer: Arc, -} - -impl BufferHandler { - #[generate_interface(constructor)] - /// constructor required by flapigen, DO NOT CALL THIS - fn new() -> BufferHandler { - unimplemented!() - } - - #[generate_interface] - /// get the name of the buffer - fn get_name(&self) -> String { - self.buffer.name().to_string() - } - - #[generate_interface] - /// get the contents of the buffer - fn get_content(&self) -> String { - self.buffer.content() - } - - #[generate_interface] - /// if a text change is available on the buffer, return it immediately - fn try_recv(&self) -> Result> { - match self.buffer.try_recv() { - Err(err) => Err(err), - Ok(None) => Ok(None), - Ok(Some(change)) => Ok(Some(TextChangeWrapper { - start: change.span.start, - end: change.span.end, - content: change.content.clone(), - })), - } - } - - #[generate_interface] - /// broadcast a text change on the buffer - fn send(&self, start_offset: usize, end_offset: usize, content: String) -> Result<()> { - self.buffer.send(TextChange { - span: start_offset..end_offset, - content, - }) - } -} - -#[generate_interface_doc] -/// a convenience struct allowing Java access to a Rust vector -struct StringVec { - //jni moment - v: Vec, -} - -impl StringVec { - #[generate_interface(constructor)] - /// initialize an empty vector - fn new() -> StringVec { - Self { v: Vec::new() } - } - - #[generate_interface] - /// push a new value onto the vector - fn push(&mut self, s: String) { - self.v.push(s); - } - - #[generate_interface] - /// get the length of the underlying vector - fn length(&self) -> i64 { - self.v.len() as i64 - } - - #[generate_interface] - /// access the element at a given index - fn get(&self, idx: i64) -> Option { - let elem: Option<&String> = self.v.get(idx as usize); - elem.map(|s| s.clone()) + if let Some(path) = path { + let logfile = std::fs::File::create(path).expect("failed creating logfile"); + builder.with_writer(std::sync::Mutex::new(logfile)).init(); + } else { + builder.with_writer(std::sync::Mutex::new(std::io::stdout())).init(); } } diff --git a/src/ffi/java/typemap.in b/src/ffi/java/typemap.in deleted file mode 100644 index 76920f8..0000000 --- a/src/ffi/java/typemap.in +++ /dev/null @@ -1,39 +0,0 @@ -foreign_typemap!( //thanks @tasn on GitHub for the idea - ($p:r_type) Result => swig_i_type!(T) { - $out = match $p { - Ok(x) => { - swig_from_rust_to_i_type!(T, x, ret) - ret - } - Err(err) => { - let (msg, exception_class) = match err { - Error::Filler { message } => ( - message, - swig_jni_find_class!(CODEMP_EXCEPTION, "com/codemp/intellij/exceptions/CodeMPException") - ), - Error::Transport { status, message } => ( - format!("Status {}: {}", status, message), - swig_jni_find_class!(TRANSPORT_EXCEPTION, "com/codemp/intellij/exceptions/lib/TransportException") - ), - Error::InvalidState { msg } => ( - msg, swig_jni_find_class!(INVALID_STATE_EXCEPTION, "com/codemp/intellij/exceptions/lib/InvalidStateException") - ), - Error::Deadlocked => ( - "WOOT deadlocked (safe to retry)!".to_string(), - swig_jni_find_class!(DEADLOCKED_EXCEPTION, "com/codemp/intellij/exceptions/lib/DeadlockedException") - ), - Error::Channel { send } => { - let verb = if send { "sending" } else { "reading" }; - ( - format!("Error while {} message on channel: the channel was closed!", verb), - swig_jni_find_class!(CHANNEL_EXCEPTION, "com/codemp/intellij/exceptions/lib/ChannelException") - ) - } - }; - jni_throw(env, exception_class, &msg); - return ::jni_invalid_value(); - } - }; - }; - ($p:f_type, unique_prefix="/*err*/") => "/*err*/swig_f_type!(T)" "swig_foreign_from_i_type!(T, $p)"; -); diff --git a/src/ffi/java/util.rs b/src/ffi/java/util.rs new file mode 100644 index 0000000..ca0a6c3 --- /dev/null +++ b/src/ffi/java/util.rs @@ -0,0 +1,29 @@ +use jni::{JNIEnv, sys::jlong}; + +/// A simple utility method that converts a pointer back into a [Box] and then drops it. +pub(crate) fn dereference_and_drop(ptr: jlong) { + let client : Box = unsafe { Box::from_raw(ptr as *mut T) }; + std::mem::drop(client) +} + +/// A trait meant for our [crate::Result] type to make converting it to Java easier. +pub(crate) trait JExceptable { + /// Unwraps it and throws an appropriate Java exception if it's an error. + /// Theoretically it returns the type's default value, but the exception makes the value ignored. + fn jexcept(self, env: &mut JNIEnv) -> T; +} + +impl JExceptable for crate::Result where T: Default { + fn jexcept(self, env: &mut JNIEnv) -> T { + if let Err(err) = &self { + let msg = format!("{err}"); + match err { + crate::Error::InvalidState { .. } => env.throw_new("mp/code/exceptions/InvalidStateException", msg), + crate::Error::Deadlocked => env.throw_new("mp/code/exceptions/DeadlockedException", msg), + crate::Error::Transport { .. } => env.throw_new("mp/code/exceptions/TransportException", msg), + crate::Error::Channel { .. } => env.throw_new("mp/code/exceptions/ChannelException", msg) + }.expect("Failed to throw exception!"); + } + self.unwrap_or_default() + } +} diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs new file mode 100644 index 0000000..09522a9 --- /dev/null +++ b/src/ffi/java/workspace.rs @@ -0,0 +1,23 @@ +use jni::{objects::{JClass, JString}, sys::jlong, JNIEnv}; +use crate::{Client, Workspace}; + +use super::{util::JExceptable, RT}; + +/// Called by the Java GC to drop a [Workspace]. +#[no_mangle] +pub extern "system" fn Java_mp_code_Workspace_free(_env: JNIEnv, _class: JClass, input: jlong) { + super::util::dereference_and_drop::(input) +} + +/// Creates a [Buffer] +#[no_mangle] +pub extern "system" fn Java_mp_code_Workspace_create_1buffer<'local>( + mut env: JNIEnv, + _class: JClass<'local>, + self_ptr: jlong, + input: JString<'local> +) { + let ws: Box = unsafe { Box::from_raw(self_ptr as *mut Workspace) }; + let path: String = env.get_string(&input).expect("Couldn't get java string!").into(); + RT.block_on(ws.create(&path)).jexcept(&mut env); +}