Add support Java bindings

This add support for Java bindings in the bindings/java directory.
This commit is contained in:
김선우
2025-01-01 21:53:50 +09:00
committed by Pekka Enberg
parent 1c2e074c93
commit 370e1ca5c2
22 changed files with 1253 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
use crate::cursor::Cursor;
use jni::objects::JClass;
use jni::sys::jlong;
use jni::JNIEnv;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Connection {
pub(crate) conn: Arc<Mutex<Rc<limbo_core::Connection>>>,
pub(crate) io: Arc<limbo_core::PlatformIO>,
}
/// Returns a pointer to a `Cursor` object.
///
/// The Java application will pass this pointer to native functions,
/// which will use it to reference the `Cursor` object.
///
/// # Arguments
///
/// * `_env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `connection_ptr` - A pointer to the `Connection` object.
///
/// # Returns
///
/// A `jlong` representing the pointer to the newly created `Cursor` object.
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_cursor<'local>(
_env: JNIEnv<'local>,
_class: JClass<'local>,
connection_ptr: jlong,
) -> jlong {
let connection = to_connection(connection_ptr);
let cursor = Cursor {
array_size: 1,
conn: connection.clone(),
description: None,
rowcount: -1,
smt: None,
};
Box::into_raw(Box::new(cursor)) as jlong
}
/// Closes the connection and releases the associated resources.
///
/// This function is called from the Java side to close the connection
/// and free the memory allocated for the `Connection` object.
///
/// # Arguments
///
/// * `_env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `connection_ptr` - A pointer to the `Connection` object to be closed.
#[no_mangle]
pub unsafe extern "system" fn Java_org_github_tursodatabase_limbo_Connection_close<'local>(
_env: JNIEnv<'local>,
_class: JClass<'local>,
connection_ptr: jlong,
) {
let _boxed_connection = Box::from_raw(connection_ptr as *mut Connection);
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_commit<'local>(
_env: &mut JNIEnv<'local>,
_class: JClass<'local>,
_connection_id: jlong,
) {
unimplemented!()
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_rollback<'local>(
_env: &mut JNIEnv<'local>,
_class: JClass<'local>,
_connection_id: jlong,
) {
unimplemented!()
}
fn to_connection(connection_ptr: jlong) -> &'static mut Connection {
unsafe { &mut *(connection_ptr as *mut Connection) }
}

View File

@@ -0,0 +1,240 @@
use crate::connection::Connection;
use crate::errors::ErrorCode;
use crate::utils::row_to_obj_array;
use crate::{eprint_return, eprint_return_null};
use jni::errors::JniError;
use jni::objects::{JClass, JObject, JString};
use jni::sys::jlong;
use jni::JNIEnv;
use limbo_core::IO;
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Cursor {
/// This read/write attribute specifies the number of rows to fetch at a time with `.fetchmany()`.
/// It defaults to `1`, meaning it fetches a single row at a time.
pub(crate) array_size: i64,
pub(crate) conn: Connection,
/// The `.description` attribute is a read-only sequence of 7-item, each describing a column in the result set:
///
/// - `name`: The column's name (always present).
/// - `type_code`: The data type code (always present).
/// - `display_size`: Column's display size (optional).
/// - `internal_size`: Column's internal size (optional).
/// - `precision`: Numeric precision (optional).
/// - `scale`: Numeric scale (optional).
/// - `null_ok`: Indicates if null values are allowed (optional).
///
/// The `name` and `type_code` fields are mandatory; others default to `None` if not applicable.
///
/// This attribute is `None` for operations that do not return rows or if no `.execute*()` method has been invoked.
pub(crate) description: Option<Description>,
/// Read-only attribute that provides the number of modified rows for `INSERT`, `UPDATE`, `DELETE`,
/// and `REPLACE` statements; it is `-1` for other statements, including CTE queries.
/// It is only updated by the `execute()` and `executemany()` methods after the statement has run to completion.
/// This means any resulting rows must be fetched for `rowcount` to be updated.
pub(crate) rowcount: i64,
pub(crate) smt: Option<Arc<Mutex<limbo_core::Statement>>>,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) struct Description {
_name: String,
_type_code: String,
_display_size: Option<String>,
_internal_size: Option<String>,
_precision: Option<String>,
_scale: Option<String>,
_null_ok: Option<String>,
}
impl Debug for Cursor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cursor")
.field("array_size", &self.array_size)
.field("description", &self.description)
.field("rowcount", &self.rowcount)
.finish()
}
}
/// TODO: we should find a way to handle Error thrown by rust and how to handle those errors in java
#[no_mangle]
#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_execute<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
sql: JString<'local>,
) -> Result<(), JniError> {
let sql: String = env
.get_string(&sql)
.expect("Could not extract query")
.into();
let stmt_is_dml = stmt_is_dml(&sql);
if stmt_is_dml {
return eprint_return!(
"DML statements (INSERT/UPDATE/DELETE) are not fully supported in this version",
JniError::Other(ErrorCode::STATEMENT_IS_DML)
);
}
let cursor = to_cursor(cursor_ptr);
let conn_lock = match cursor.conn.conn.lock() {
Ok(lock) => lock,
Err(_) => return eprint_return!("Failed to acquire connection lock", JniError::Other(-1)),
};
match conn_lock.prepare(&sql) {
Ok(statement) => {
cursor.smt = Some(Arc::new(Mutex::new(statement)));
Ok(())
}
Err(e) => {
eprint_return!(
&format!("Failed to prepare statement: {:?}", e),
JniError::Other(-1)
)
}
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchOne<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
) -> JObject<'local> {
let cursor = to_cursor(cursor_ptr);
if let Some(smt) = &cursor.smt {
loop {
let mut smt_lock = match smt.lock() {
Ok(lock) => lock,
Err(_) => {
return eprint_return_null!(
"Failed to acquire statement lock",
JniError::Other(-1)
)
}
};
match smt_lock.step() {
Ok(limbo_core::StepResult::Row(row)) => {
return match row_to_obj_array(&mut env, &row) {
Ok(r) => r,
Err(e) => eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)),
}
}
Ok(limbo_core::StepResult::IO) => {
if let Err(e) = cursor.conn.io.run_once() {
return eprint_return_null!(
&format!("IO Error: {:?}", e),
JniError::Other(-1)
);
}
}
Ok(limbo_core::StepResult::Interrupt) => return JObject::null(),
Ok(limbo_core::StepResult::Done) => return JObject::null(),
Ok(limbo_core::StepResult::Busy) => {
return eprint_return_null!("Busy error", JniError::Other(-1));
}
Err(e) => {
return eprint_return_null!(
format!("Step error: {:?}", e),
JniError::Other(-1)
);
}
};
}
} else {
eprint_return_null!("No statement prepared for execution", JniError::Other(-1))
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchAll<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
) -> JObject<'local> {
let cursor = to_cursor(cursor_ptr);
if let Some(smt) = &cursor.smt {
let mut rows = Vec::new();
loop {
let mut smt_lock = match smt.lock() {
Ok(lock) => lock,
Err(_) => {
return eprint_return_null!(
"Failed to acquire statement lock",
JniError::Other(-1)
)
}
};
match smt_lock.step() {
Ok(limbo_core::StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) {
Ok(r) => rows.push(r),
Err(e) => return eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)),
},
Ok(limbo_core::StepResult::IO) => {
if let Err(e) = cursor.conn.io.run_once() {
return eprint_return_null!(
&format!("IO Error: {:?}", e),
JniError::Other(-1)
);
}
}
Ok(limbo_core::StepResult::Interrupt) => {
return JObject::null();
}
Ok(limbo_core::StepResult::Done) => {
break;
}
Ok(limbo_core::StepResult::Busy) => {
return eprint_return_null!("Busy error", JniError::Other(-1));
}
Err(e) => {
return eprint_return_null!(
format!("Step error: {:?}", e),
JniError::Other(-1)
);
}
};
}
let array_class = env
.find_class("[Ljava/lang/Object;")
.expect("Failed to find Object array class");
let result_array = env
.new_object_array(rows.len() as i32, array_class, JObject::null())
.expect("Failed to create new object array");
for (i, row) in rows.into_iter().enumerate() {
env.set_object_array_element(&result_array, i as i32, row)
.expect("Failed to set object array element");
}
result_array.into()
} else {
eprint_return_null!("No statement prepared for execution", JniError::Other(-1))
}
}
fn to_cursor(cursor_ptr: jlong) -> &'static mut Cursor {
unsafe { &mut *(cursor_ptr as *mut Cursor) }
}
fn stmt_is_dml(sql: &str) -> bool {
let sql = sql.trim();
let sql = sql.to_uppercase();
sql.starts_with("INSERT") || sql.starts_with("UPDATE") || sql.starts_with("DELETE")
}

View File

@@ -0,0 +1,35 @@
use jni::errors::{Error, JniError};
#[derive(Debug, Clone)]
pub struct CustomError {
pub message: String,
}
/// This struct defines error codes that correspond to the constants defined in the
/// Java package `org.github.tursodatabase.exceptions.ErrorCode`.
///
/// These error codes are used to handle and represent specific error conditions
/// that may occur within the Rust code and need to be communicated to the Java side.
#[derive(Clone)]
pub struct ErrorCode;
impl ErrorCode {
pub const CONNECTION_FAILURE: i32 = -1;
pub const STATEMENT_IS_DML: i32 = -1;
}
impl From<jni::errors::Error> for CustomError {
fn from(value: Error) -> Self {
CustomError {
message: value.to_string(),
}
}
}
impl From<CustomError> for JniError {
fn from(value: CustomError) -> Self {
eprintln!("Error occurred: {:?}", value.message);
JniError::Other(-1)
}
}

View File

@@ -0,0 +1,66 @@
mod connection;
mod cursor;
mod errors;
mod macros;
mod utils;
use crate::connection::Connection;
use crate::errors::ErrorCode;
use jni::errors::JniError;
use jni::objects::{JClass, JString};
use jni::sys::jlong;
use jni::JNIEnv;
use std::sync::{Arc, Mutex};
/// Establishes a connection to the database specified by the given path.
///
/// This function is called from the Java side to create a connection to the database.
/// It returns a pointer to the `Connection` object, which can be used in subsequent
/// native function calls.
///
/// # Arguments
///
/// * `env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `path` - A `JString` representing the path to the database file.
///
/// # Returns
///
/// A `jlong` representing the pointer to the newly created `Connection` object,
/// or [ErrorCode::CONNECTION_FAILURE] if the connection could not be established.
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Limbo_connect<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
path: JString<'local>,
) -> jlong {
connect_internal(&mut env, path).unwrap_or_else(|_| ErrorCode::CONNECTION_FAILURE as jlong)
}
#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)] // TODO: remove
fn connect_internal<'local>(
env: &mut JNIEnv<'local>,
path: JString<'local>,
) -> Result<jlong, JniError> {
let io = Arc::new(limbo_core::PlatformIO::new().map_err(|e| {
println!("IO initialization failed: {:?}", e);
JniError::Unknown
})?);
let path: String = env
.get_string(&path)
.expect("Failed to convert JString to Rust String")
.into();
let db = limbo_core::Database::open_file(io.clone(), &path).map_err(|e| {
println!("Failed to open database: {:?}", e);
JniError::Unknown
})?;
let conn = db.connect().clone();
let connection = Connection {
conn: Arc::new(Mutex::new(conn)),
io,
};
Ok(Box::into_raw(Box::new(connection)) as jlong)
}

View File

@@ -0,0 +1,16 @@
// bindings/java/src/macros.rs
#[macro_export]
macro_rules! eprint_return {
($log:expr, $error:expr) => {{
eprintln!("{}", $log);
Err($error)
}};
}
#[macro_export]
macro_rules! eprint_return_null {
($log:expr, $error:expr) => {{
eprintln!("{}", $log);
JObject::null()
}};
}

View File

@@ -0,0 +1,30 @@
use crate::errors::CustomError;
use jni::objects::{JObject, JValue};
use jni::JNIEnv;
pub(crate) fn row_to_obj_array<'local>(
env: &mut JNIEnv<'local>,
row: &limbo_core::Row,
) -> Result<JObject<'local>, CustomError> {
let obj_array =
env.new_object_array(row.values.len() as i32, "java/lang/Object", JObject::null())?;
for (i, value) in row.values.iter().enumerate() {
let obj = match value {
limbo_core::Value::Null => JObject::null(),
limbo_core::Value::Integer(i) => {
env.new_object("java/lang/Long", "(J)V", &[JValue::Long(*i)])?
}
limbo_core::Value::Float(f) => {
env.new_object("java/lang/Double", "(D)V", &[JValue::Double(*f)])?
}
limbo_core::Value::Text(s) => env.new_string(s)?.into(),
limbo_core::Value::Blob(b) => env.byte_array_from_slice(b)?.into(),
};
if let Err(e) = env.set_object_array_element(&obj_array, i as i32, obj) {
eprintln!("Error on parsing row: {:?}", e);
}
}
Ok(obj_array.into())
}