blob: 62d0e24516a013babf87d6ebb9c135de9784bb3a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//! Java-backed scalar UDF support.
use std::any::Any;
use std::fmt;
use std::sync::Arc;
use datafusion::arrow::array::{make_array, Array, ArrayRef, StructArray};
use datafusion::arrow::datatypes::{DataType, Field, Fields};
use datafusion::arrow::ffi::{from_ffi, to_ffi, FFI_ArrowArray, FFI_ArrowSchema};
use datafusion::error::DataFusionError;
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, Volatility,
};
use jni::objects::{GlobalRef, JStaticMethodID, JThrowable};
use jni::signature::{Primitive, ReturnType};
use jni::sys::{jlong, jvalue};
use jni::JNIEnv;
pub(crate) struct JavaScalarUdf {
pub(crate) name: String,
pub(crate) signature: Signature,
pub(crate) return_type: DataType,
/// Global ref to the user's `org.apache.datafusion.ScalarFunction` instance.
pub(crate) udf_global_ref: GlobalRef,
/// Global ref to the `org.apache.datafusion.internal.JniBridge` class.
pub(crate) bridge_class: GlobalRef,
/// Method ID for `JniBridge.invokeScalarUdf`.
pub(crate) invoke_method: JStaticMethodID,
}
// SAFETY: JStaticMethodID is a JNI handle that's safe to share because the
// class it points to is held alive by `bridge_class`. We never mutate
// `invoke_method` after construction; DataFusion requires `Send + Sync` on
// `ScalarUDFImpl`.
unsafe impl Send for JavaScalarUdf {}
unsafe impl Sync for JavaScalarUdf {}
impl fmt::Debug for JavaScalarUdf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JavaScalarUdf")
.field("name", &self.name)
.field("return_type", &self.return_type)
.finish()
}
}
impl PartialEq for JavaScalarUdf {
fn eq(&self, other: &Self) -> bool {
// Two Java UDFs are equal iff they wrap the same registered name.
self.name == other.name
}
}
impl Eq for JavaScalarUdf {}
impl std::hash::Hash for JavaScalarUdf {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl ScalarUDFImpl for JavaScalarUdf {
fn as_any(&self) -> &dyn Any {
self
}
fn name(&self) -> &str {
&self.name
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _arg_types: &[DataType]) -> datafusion::error::Result<DataType> {
Ok(self.return_type.clone())
}
fn invoke_with_args(
&self,
args: ScalarFunctionArgs,
) -> datafusion::error::Result<ColumnarValue> {
let number_rows = args.number_rows;
// 1. Materialise scalars to arrays so all columns are length-N.
let arrays: Vec<ArrayRef> = args
.args
.iter()
.map(|cv| cv.clone().into_array(number_rows))
.collect::<datafusion::error::Result<Vec<_>>>()?;
// 2. Build a single struct array carrying all arg columns. Field names/types come
// from the signature's Exact type list (matches what the Java caller declared).
let signature_fields: Vec<Arc<Field>> = match &self.signature.type_signature {
TypeSignature::Exact(types) => types
.iter()
.enumerate()
.map(|(i, ty)| Arc::new(Field::new(format!("arg{}", i), ty.clone(), true)))
.collect(),
_ => {
return Err(DataFusionError::Internal(
"JavaScalarUdf signature is not Exact; only Signature::exact is supported"
.to_string(),
))
}
};
let fields = Fields::from(
signature_fields
.iter()
.map(|f| f.as_ref().clone())
.collect::<Vec<Field>>(),
);
let struct_array = StructArray::try_new_with_length(fields, arrays, None, number_rows)
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
let args_data = struct_array.into_data();
let (args_ffi_array, args_ffi_schema) =
to_ffi(&args_data).map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
// 3. Pre-allocate empty FFI structs for the result.
let result_ffi_array = FFI_ArrowArray::empty();
let result_ffi_schema = FFI_ArrowSchema::empty();
// 4. Box for stable addresses across the JNI call.
let mut args_array_box = Box::new(args_ffi_array);
let mut args_schema_box = Box::new(args_ffi_schema);
let mut result_array_box = Box::new(result_ffi_array);
let mut result_schema_box = Box::new(result_ffi_schema);
let args_array_addr = args_array_box.as_mut() as *mut _ as jlong;
let args_schema_addr = args_schema_box.as_mut() as *mut _ as jlong;
let result_array_addr = result_array_box.as_mut() as *mut _ as jlong;
let result_schema_addr = result_schema_box.as_mut() as *mut _ as jlong;
// 5. Attach JNI to current thread.
let mut env = crate::jvm()
.attach_current_thread()
.map_err(|e| DataFusionError::Execution(format!("JNI attach failed: {}", e)))?;
// 6. Call JniBridge.invokeScalarUdf(udf, args*, result*, expectedRowCount).
//
// Build the jvalue argument array for call_static_method_unchecked.
// SAFETY: we build the args inline and pass them immediately; the JObject
// pointed to by udf_global_ref is alive for the duration of this call.
let expected_rows = i32::try_from(number_rows).map_err(|_| {
DataFusionError::Execution(format!(
"batch row count {} exceeds i32::MAX; UDFs cannot handle batches larger than 2^31 - 1 rows",
number_rows
))
})?;
let udf_jobject = self.udf_global_ref.as_obj();
// SAFETY: udf_jobject is derived from a GlobalRef alive for the duration of this
// function. The raw pointer is only read by the JNI call below, which happens
// before any code that could drop udf_global_ref.
let call_args: [jvalue; 6] = [
// ScalarFunction instance
jvalue {
l: udf_jobject.as_raw(),
},
// argsArrayAddr
jvalue { j: args_array_addr },
// argsSchemaAddr
jvalue {
j: args_schema_addr,
},
// resultArrayAddr
jvalue {
j: result_array_addr,
},
// resultSchemaAddr
jvalue {
j: result_schema_addr,
},
// expectedRowCount
jvalue { i: expected_rows },
];
let call_result = unsafe {
env.call_static_method_unchecked(
&self.bridge_class,
self.invoke_method,
ReturnType::Primitive(Primitive::Void),
&call_args,
)
};
// 7. If Java threw, translate to DataFusionError. Always check exception_check first.
if env.exception_check().unwrap_or(false) {
let throwable = env.exception_occurred().map_err(|e| {
DataFusionError::Execution(format!("exception_occurred failed: {}", e))
})?;
env.exception_clear().ok();
let message = jthrowable_to_string(&mut env, &throwable, &self.name);
return Err(DataFusionError::Execution(message));
}
call_result.map_err(|e| DataFusionError::Execution(format!("JNI call failed: {}", e)))?;
// 8. Import result. from_ffi consumes the FFI_ArrowArray.
let result_array = *result_array_box;
let result_schema = *result_schema_box;
// SAFETY: Java's `Data.exportVector` populated `result_array_box` and
// `result_schema_box` in place via the C Data Interface, and the
// exception check above guarantees the call succeeded without
// throwing — so the FFI structs are fully initialized.
let result_data = unsafe { from_ffi(result_array, &result_schema) }
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
// 9. Validate type.
if result_data.data_type() != &self.return_type {
return Err(DataFusionError::Execution(format!(
"Java UDF '{}' returned vector of type {:?}; declared return type was {:?}",
self.name,
result_data.data_type(),
self.return_type
)));
}
let array: ArrayRef = make_array(result_data);
Ok(ColumnarValue::Array(array))
}
}
pub(crate) fn volatility_from_byte(byte: u8) -> datafusion::error::Result<Volatility> {
match byte {
0 => Ok(Volatility::Immutable),
1 => Ok(Volatility::Stable),
2 => Ok(Volatility::Volatile),
other => Err(DataFusionError::Execution(format!(
"unknown volatility byte: {}",
other
))),
}
}
/// Best-effort: extract class name and getMessage() from a Java throwable.
/// Anything that goes wrong collapses to a generic message so we don't
/// double-throw inside an error path.
fn jthrowable_to_string(env: &mut JNIEnv, throwable: &JThrowable, udf_name: &str) -> String {
let class_name_result = (|| -> jni::errors::Result<String> {
let class = env.call_method(throwable, "getClass", "()Ljava/lang/Class;", &[])?;
let class_obj = class.l()?;
let name = env.call_method(&class_obj, "getName", "()Ljava/lang/String;", &[])?;
let name_obj = name.l()?;
let name_str: String = env.get_string(&name_obj.into())?.into();
Ok(name_str)
})();
let class_name = match class_name_result {
Ok(s) => s,
Err(_) => {
// A reflective call itself threw — clear that secondary exception so the
// thread is in a clean state when we return to the JVM.
env.exception_clear().ok();
"<unknown exception class>".to_string()
}
};
let message_result = (|| -> jni::errors::Result<String> {
let msg = env.call_method(throwable, "getMessage", "()Ljava/lang/String;", &[])?;
let msg_obj = msg.l()?;
if msg_obj.is_null() {
return Ok("<no message>".to_string());
}
let s: String = env.get_string(&msg_obj.into())?.into();
Ok(s)
})();
let message = match message_result {
Ok(s) => s,
Err(_) => {
env.exception_clear().ok();
"<no message>".to_string()
}
};
format!("Java UDF '{}' threw {}: {}", udf_name, class_name, message)
}