risingwavelabs/risingwave · error · BindingError::Jni

JniError {error}

Error message

JniError {error}

What it means

BindingError::Jni is the variant of the risingwave_jni_core BindingError enum that wraps any jni::errors::Error produced while calling into the JNI API from Rust (e.g. env.get_string, array access, class lookups). The jni crate returns these errors instead of panicking when a JNI call fails, such as when a passed Java object handle is invalid, a type is wrong, or an exception is pending on the thread. The BindingError is converted by execute_and_catch into a thrown Java exception on the calling side.

Source

Thrown at src/jni_core/src/lib.rs:84

use tokio::sync::mpsc::{Receiver, Sender};
use tracing_slf4j::*;

/// Enable JVM and Java libraries.
///
/// This macro forces this crate to be linked, which registers the JVM builder.
#[macro_export]
macro_rules! enable {
    () => {
        use risingwave_jni_core as _;
    };
}

pub static JAVA_BINDING_ASYNC_RUNTIME: LazyLock<Runtime> =
    LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());

#[derive(Error, Debug)]
pub enum BindingError {
    #[error("JniError {error}")]
    Jni {
        #[from]
        error: jni::errors::Error,
        backtrace: Backtrace,
    },

    #[error("StorageError {error}")]
    Storage {
        #[from]
        error: anyhow::Error,
        backtrace: Backtrace,
    },

    #[error("DecodeError {error}")]
    Decode {
        #[from]
        error: DecodeError,
        backtrace: Backtrace,

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Check the Java call site for null or wrongly-typed arguments before invoking the native binding; the message names the wrapped jni::errors::Error which identifies the exact failing JNI operation
  2. If the error is JavaException, inspect the pending Java exception on the thread — it was thrown by Java code invoked from Rust and must be handled/cleared there
  3. Rebuild/verify JVM and binding versions match: stale JAR/so combinations cause JniError::MethodNotFound/ClassNotFound
  4. Wrap the JNI call site with env.exception_check()/exception_clear() diagnostics to find the pending exception

Example fix

// before
let msg: String = env.get_string(&jstr)?.into(); // panics/throws on null jstr
// after
if jstr.is_null() {
    return Err(BindingError::Jni { error: jni::errors::Error::NullPtr, backtrace: Backtrace::capture() });
}
let msg: String = env.get_string(&jstr)?.into();
Defensive patterns

Strategy: try-catch

Validate before calling

// Java, before calling any binding method
if (jstr == null) throw new IllegalArgumentException("binding argument must not be null");

Type guard

// Java
static boolean isValidJStringArg(Object arg) {
    return arg instanceof String && !((String) arg).isEmpty();
}

Try / catch

try {
    Binding.someNativeMethod(arg);
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().contains("JniError")) {
        // inspect wrapped jni::errors::Error report, log and rethrow as domain error
    }
    throw e;
}

Prevention

When it happens

Trigger: Any JNI entry point in the binding (string extraction via env.get_string, byte-array access via to_guarded_slice/get_array_elements, JObject casts) receives an invalid, null, or wrong-typed Java object, or a Java exception is already pending when the native code makes another JNI call.

Common situations: Java side passes null where a String or byte[] is expected; calling a binding after the JVM changed the type of a field; a previously thrown Java exception was not cleared before re-entering native code; stale local references used across calls.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/f177c32ca417ca96. Report an issue: GitHub.