blob: b712a65c1153f9448d1f2485cfdc665d06ba534d [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.
#![allow(incomplete_features)]
#![allow(non_camel_case_types)]
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::result_large_err)]
// For prost generated struct
#![allow(clippy::derive_partial_eq_without_eq)]
// The clippy throws an error if the reference clone not wrapped into `Arc::clone`
// The lint makes easier for code reader/reviewer separate references clones from more heavyweight ones
#![deny(clippy::clone_on_ref_ptr)]
extern crate core;
#[macro_use]
extern crate datafusion_comet_jni_bridge;
use jni::{
objects::{JClass, JString},
EnvUnowned,
};
use log::info;
use log4rs::{
append::console::{ConsoleAppender, Target},
config::{load_config_file, Appender, Deserializers, Root},
encode::pattern::PatternEncoder,
Config,
};
// Re-export from jvm-bridge crate for internal use
pub use datafusion_comet_jni_bridge::errors;
pub use datafusion_comet_jni_bridge::JAVA_VM;
/// Re-export jvm-bridge items under the `jvm_bridge` name for convenience.
pub mod jvm_bridge {
pub use datafusion_comet_jni_bridge::*;
}
use errors::{try_unwrap_or_throw, CometError, CometResult};
pub mod alloc_accounting;
pub mod cloud;
pub mod comet_native_udf_bridge;
pub mod execution;
pub mod parquet;
// this module is for non release only. Intended for debugging/profiling purposes
#[cfg(debug_assertions)]
pub mod debug;
// Global allocator selection. `backend` names the allocator the feature set asks for: jemalloc
// where it builds, otherwise mimalloc, otherwise the system allocator. The three cfgs partition
// every feature combination, so exactly one `backend` exists and a combination matching none would
// fail to compile. Whichever it is, it is installed wrapped in the `AccountingAllocator`, which
// counts the bytes Rust code holds for the memory usage log and the `native_allocated` metric.
/// jemalloc, on targets where it builds, unless mimalloc was also requested.
#[cfg(all(
not(target_env = "msvc"),
feature = "jemalloc",
not(feature = "mimalloc")
))]
mod backend {
pub type Backend = tikv_jemallocator::Jemalloc;
pub const BACKEND: Backend = tikv_jemallocator::Jemalloc;
pub const NAME: &str = "jemalloc";
}
/// mimalloc, unless a usable jemalloc was also requested.
#[cfg(all(
feature = "mimalloc",
not(all(not(target_env = "msvc"), feature = "jemalloc"))
))]
mod backend {
pub type Backend = mimalloc::MiMalloc;
pub const BACKEND: Backend = mimalloc::MiMalloc;
pub const NAME: &str = "mimalloc";
}
/// The system allocator: the complement of the two cases above, which covers neither feature, a
/// jemalloc request on MSVC, and both features together.
#[cfg(not(any(
all(
not(target_env = "msvc"),
feature = "jemalloc",
not(feature = "mimalloc")
),
all(
feature = "mimalloc",
not(all(not(target_env = "msvc"), feature = "jemalloc"))
)
)))]
mod backend {
pub type Backend = std::alloc::System;
pub const BACKEND: Backend = std::alloc::System;
pub const NAME: &str = "system";
}
/// The allocator backend this build selected, its name (`"jemalloc"`, `"mimalloc"` or
/// `"system"`), and an instance of it. The selection is decided here and nowhere else, so the
/// `alloc_overhead` benchmark takes the backend from here to measure it with and without the
/// accounting wrapper.
#[doc(hidden)]
pub use backend::{
Backend as AllocatorBackend, BACKEND as BACKEND_ALLOCATOR, NAME as ALLOCATOR_BACKEND,
};
#[global_allocator]
static GLOBAL: alloc_accounting::AccountingAllocator<backend::Backend> =
alloc_accounting::AccountingAllocator::new(backend::BACKEND);
#[no_mangle]
pub extern "system" fn Java_org_apache_comet_NativeBase_init(
e: EnvUnowned,
_: JClass,
log_conf_path: JString,
log_level: JString,
) {
// Initialize the error handling to capture panic backtraces
errors::init();
try_unwrap_or_throw(&e, |env| {
let path: String = log_conf_path.try_to_string(env)?;
// The log level only applies without a custom log4rs config file.
let log_level: String = if path.is_empty() {
match log_level.try_to_string(env) {
Ok(level) => level,
Err(_) => "info".to_string(),
}
} else {
String::new()
};
init_logging(&path, &log_level)?;
// Initialize the global Java VM
let java_vm = env.get_java_vm()?;
JAVA_VM.get_or_init(|| java_vm);
let comet_version = env!("CARGO_PKG_VERSION");
info!("Comet native library version {comet_version} initialized");
Ok(())
})
}
/// Initializes logging from the log4rs config file at `log_conf_path`, or, when it is empty, to the
/// console at `log_level`. Logging can only be initialized once per process; later calls keep the
/// first configuration. Core of `NativeBase.init`, except for capturing the `JavaVM`.
pub fn init_logging(log_conf_path: &str, log_level: &str) -> CometResult<()> {
let _ = log4rs::init_config(logger_config(log_conf_path, log_level)?)
.map_err(|err| CometError::Config(err.to_string()));
Ok(())
}
/// The log4rs config [`init_logging`] installs.
fn logger_config(log_conf_path: &str, log_level: &str) -> CometResult<Config> {
// empty path means there is no custom log4rs config file provided, so fallback to use
// the default configuration
if log_conf_path.is_empty() {
default_logger_config(log_level)
} else {
load_config_file(log_conf_path, Deserializers::default())
.map_err(|err| CometError::Config(err.to_string()))
}
}
#[no_mangle]
/// Releases the global Tokio runtime used by Comet native execution.
pub extern "system" fn Java_org_apache_comet_NativeBase_release(_e: EnvUnowned, _class: JClass) {
execution::jni_api::release_runtime();
}
const LOG_PATTERN: &str = "{d(%y/%m/%d %H:%M:%S)} {l} {f}: {m}{n}";
/// JNI method to check if a specific feature is enabled in the native Rust code.
/// # Arguments
/// * `feature_name` - The name of the feature to check. Supported features:
/// - "jemalloc" - tikv-jemallocator memory allocator
/// - "hdfs-opendal" - HDFS support via OpenDAL
/// # Returns
/// * `1` (true) if the feature is enabled
/// * `0` (false) if the feature is disabled or unknown
#[no_mangle]
pub extern "system" fn Java_org_apache_comet_NativeBase_isFeatureEnabled(
env: EnvUnowned,
_: JClass,
feature_name: JString,
) -> jni::sys::jboolean {
try_unwrap_or_throw(&env, |env| {
let feature: String = feature_name.try_to_string(env)?;
Ok(is_feature_enabled(&feature))
})
}
/// Whether the native build enables `feature`; unknown features are disabled. Core of
/// `NativeBase.isFeatureEnabled`.
pub fn is_feature_enabled(feature: &str) -> bool {
match feature {
"jemalloc" => cfg!(feature = "jemalloc"),
"hdfs-opendal" => cfg!(feature = "hdfs-opendal"),
_ => false, // Unknown features return false
}
}
/// JNI: can object_store build a store AND an object key for this URL?
///
/// Source of truth for the JVM planner's "can the native reader handle this filesystem?" check.
/// `prepare_object_store_with_configs` dispatches non-hdfs/non-s3 schemes to object_store's
/// `parse_url` (driven by `ObjectStoreScheme::parse`), so answering from the same parser lets the
/// planner decline early without hardcoding the supported set. (hdfs/libhdfs are handled JVM-side.)
///
/// `ObjectStoreScheme::parse` validates the PATH as well as the scheme -- it ends in
/// `Path::from_url_path` -- so a recognized scheme carrying a key object_store forbids (e.g. a
/// directory name with a newline) answers false. `CometScanRule` relies on both halves: it probes
/// a synthetic scheme-only URL for the cached scheme gate and the real URL for the path gate.
///
/// Alias schemes (e.g. `blob`) are NOT answered here: this gate has no config. Their opt-in lives
/// in the JVM `CometScanRule` via `fs.comet.s3Compliant.schemes`; natively they are rewritten to
/// `s3://` by `normalize_object_store_url`, the first step of
/// `prepare_object_store_with_configs`.
#[no_mangle]
pub extern "system" fn Java_org_apache_comet_NativeBase_isObjectStoreSchemeSupported(
env: EnvUnowned,
_: JClass,
url: JString,
) -> jni::sys::jboolean {
try_unwrap_or_throw(&env, |env| {
let url_str: String = url.try_to_string(env)?;
Ok(is_object_store_scheme_supported(&url_str))
})
}
/// Whether object_store can build a store and an object key for `url`. Core of
/// `NativeBase.isObjectStoreSchemeSupported`.
pub fn is_object_store_scheme_supported(url: &str) -> bool {
url::Url::parse(url)
.ok()
.map(|u| object_store::ObjectStoreScheme::parse(&u).is_ok())
.unwrap_or(false)
}
/// JNI: the version of the IANA timezone database native code uses, such as `2025b`. chrono-tz
/// compiles the database into libcomet, so it can differ from the JVM's `tzdb.dat`, and then local
/// times computed natively can differ from Spark's.
#[no_mangle]
pub extern "system" fn Java_org_apache_comet_NativeBase_getTzdataVersion(
env: EnvUnowned,
_: JClass,
) -> jni::sys::jstring {
try_unwrap_or_throw(&env, |env| Ok(env.new_string(tzdata_version())?.into_raw()))
}
/// The version of the IANA timezone database compiled into libcomet. Core of
/// `NativeBase.getTzdataVersion`.
pub fn tzdata_version() -> &'static str {
chrono_tz::IANA_TZDB_VERSION
}
// Creates a default log4rs config, which logs to console with log level.
fn default_logger_config(log_level: &str) -> CometResult<Config> {
let console_append = ConsoleAppender::builder()
.target(Target::Stderr)
.encoder(Box::new(PatternEncoder::new(LOG_PATTERN)))
.build();
let appender = Appender::builder().build("console", Box::new(console_append));
let root = Root::builder().appender("console").build(
log_level
.parse()
.map_err(|err| CometError::Config(format!("{err}")))?,
);
Config::builder()
.appender(appender)
.build(root)
.map_err(|err| CometError::Config(err.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feature_enabled() {
assert_eq!(is_feature_enabled("jemalloc"), cfg!(feature = "jemalloc"));
assert_eq!(
is_feature_enabled("hdfs-opendal"),
cfg!(feature = "hdfs-opendal")
);
assert!(!is_feature_enabled("no-such-feature"));
}
#[test]
fn logger_config_from_level_or_file() {
assert!(logger_config("", "debug").is_ok());
assert!(matches!(
logger_config("", "not-a-level"),
Err(CometError::Config(_))
));
assert!(matches!(
logger_config("/no/such/log4rs.yaml", "info"),
Err(CometError::Config(_))
));
}
#[test]
fn tzdata_version_is_set() {
assert!(!tzdata_version().is_empty());
}
#[test]
fn object_store_scheme_supported() {
assert!(is_object_store_scheme_supported("file:///tmp/data.parquet"));
assert!(is_object_store_scheme_supported("s3://bucket/key"));
assert!(!is_object_store_scheme_supported("unknown://bucket/key"));
assert!(!is_object_store_scheme_supported("not a url"));
}
}