Skip to content

Commit

Permalink
chore(java): provide conversion utilities between jni objects and rus…
Browse files Browse the repository at this point in the history
…t objects (#2009)

Add conversions between Errors, Java/Rust objects.
Modeled after `pyo3`
  • Loading branch information
eddyxu authored Feb 28, 2024
1 parent e9cd804 commit 3defe20
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 231 deletions.
45 changes: 42 additions & 3 deletions java/lance-jni/src/blocking_dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

use arrow::array::RecordBatchReader;

use jni::objects::JObject;
use jni::JNIEnv;
use lance::dataset::{Dataset, WriteParams};
use lance::Result;

use crate::RT;
use crate::{traits::IntoJava, Result, RT};

pub struct BlockingDataset {
inner: Dataset,
Expand All @@ -39,8 +40,46 @@ impl BlockingDataset {
}

pub fn count_rows(&self) -> Result<usize> {
RT.block_on(self.inner.count_rows())
Ok(RT.block_on(self.inner.count_rows())?)
}

pub fn close(&self) {}
}

impl IntoJava for BlockingDataset {
fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> JObject<'a> {
attach_native_dataset(env, self)
}
}

fn attach_native_dataset<'local>(
env: &mut JNIEnv<'local>,
dataset: BlockingDataset,
) -> JObject<'local> {
let j_dataset = create_java_dataset_object(env);
// This block sets a native Rust object (dataset) as a field in the Java object (j_dataset).
// Caution: This creates a potential for memory leaks. The Rust object (dataset) is not
// automatically garbage-collected by Java, and its memory will not be freed unless
// explicitly handled.
//
// To prevent memory leaks, ensure the following:
// 1. The Java object (`j_dataset`) should implement the `java.io.Closeable` interface.
// 2. Users of this Java object should be instructed to always use it within a try-with-resources
// statement (or manually call the `close()` method) to ensure that `self.close()` is invoked.
match unsafe { env.set_rust_field(&j_dataset, "nativeDatasetHandle", dataset) } {
Ok(_) => j_dataset,
Err(err) => {
env.throw_new(
"java/lang/RuntimeException",
format!("Failed to set native handle: {}", err),
)
.expect("Error throwing exception");
JObject::null()
}
}
}

fn create_java_dataset_object<'a>(env: &mut JNIEnv<'a>) -> JObject<'a> {
env.new_object("com/lancedb/lance/Dataset", "()V", &[])
.expect("Failed to create Java Dataset instance")
}
104 changes: 104 additions & 0 deletions java/lance-jni/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2024 Lance Developers.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::str::Utf8Error;

use jni::errors::Error as JniError;
use snafu::{Location, Snafu};

/// Java Exception types
pub enum JavaException {
IllegalArgumentException,
IOException,
RuntimeException,
}

impl JavaException {
pub fn as_str(&self) -> &str {
match self {
Self::IllegalArgumentException => "java/lang/IllegalArgumentException",
Self::IOException => "java/io/IOException",
Self::RuntimeException => "java/lang/RuntimeException",
}
}
}

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum Error {
#[snafu(display("JNI error: {}", message))]
Jni { message: String },
#[snafu(display("Invalid argument: {}", message))]
InvalidArgument { message: String },
#[snafu(display("IO error: {}, location: {}", message, location))]
IO { message: String, location: Location },
#[snafu(display("Arrow error: {}", message))]
Arrow { message: String, location: Location },
#[snafu(display("Index error: {}, location", message))]
Index { message: String, location: Location },
#[snafu(display("Unknown error"))]
Other { message: String },
}

impl Error {
/// Throw as Java Exception
pub fn throw(&self, env: &mut jni::JNIEnv) {
match self {
Self::InvalidArgument { .. } => {
self.throw_as(env, JavaException::IllegalArgumentException)
}
Self::IO { .. } | Self::Index { .. } => self.throw_as(env, JavaException::IOException),
Self::Arrow { .. } | Self::Other { .. } | Self::Jni { .. } => {
self.throw_as(env, JavaException::RuntimeException)
}
}
}

/// Throw as an concrete Java Exception
pub fn throw_as(&self, env: &mut jni::JNIEnv, exception: JavaException) {
env.throw_new(exception.as_str(), self.to_string())
.expect("Error when throwing Java exception");
}
}

pub type Result<T> = std::result::Result<T, Error>;

impl From<JniError> for Error {
fn from(source: JniError) -> Self {
Self::Jni {
message: source.to_string(),
}
}
}

impl From<Utf8Error> for Error {
fn from(source: Utf8Error) -> Self {
Self::InvalidArgument {
message: source.to_string(),
}
}
}

impl From<lance::Error> for Error {
fn from(source: lance::Error) -> Self {
match source {
lance::Error::IO { message, location } => Self::IO { message, location },
lance::Error::Arrow { message, location } => Self::Arrow { message, location },
lance::Error::Index { message, location } => Self::Index { message, location },
_ => Self::Other {
message: source.to_string(),
},
}
}
}
87 changes: 0 additions & 87 deletions java/lance-jni/src/jni_helpers.rs

This file was deleted.

Loading

0 comments on commit 3defe20

Please sign in to comment.