alef 0.115.3

Opinionated polyglot binding generator for Rust libraries
Documentation
fn runtime() -> &'static Runtime {
    // ~keep 16 MiB: tokio's ~2 MB default worker stack can overflow on a deep extraction
    // future (a nested archive member, a multi-stage OCR pipeline), and a stack overflow
    // aborts the process with SIGBUS instead of raising a catchable panic.
    const SHARED_RUNTIME_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
    static RT: OnceLock<Runtime> = OnceLock::new();
    RT.get_or_init(|| {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .thread_stack_size(SHARED_RUNTIME_STACK_SIZE_BYTES)
            .build()
            .expect("create tokio runtime")
    })
}

fn jstring_to_string(env: &mut Env<'_>, s: JString) -> std::result::Result<String, jni::errors::Error> {
    s.try_to_string(env)
}

fn string_to_jstring(env: &mut Env<'_>, s: impl AsRef<str>) -> jstring {
    match env.new_string(s.as_ref()) {
        Ok(o) => o.into_raw(),
        Err(_) => std::ptr::null_mut(),
    }
}

fn jni_call_string_method(env: &mut Env<'_>, obj: JObject, method_name: &str, method_sig: &str) -> std::result::Result<String, jni::errors::Error> {
    use std::str::FromStr;
    let class = env.get_object_class(&obj)?;
    let name_jni = jni::strings::JNIString::from(method_name);
    let sig_runtime = jni::signature::RuntimeMethodSignature::from_str(method_sig)?;
    let sig = sig_runtime.method_signature();
    let method_id = env.get_method_id(&class, name_jni, sig)?;
    // SAFETY: method_id is valid from the preceding get_method_id call, and the method exists on the class.
    let result = unsafe { env.call_method_unchecked(&obj, method_id, jni::signature::ReturnType::Object, &[])? }
        .l()?;
    // SAFETY: JNI return type guaranteed a String, so the raw jstring pointer is valid.
    let jstring = unsafe { JString::from_raw(env, result.into_raw()) };
    jstring_to_string(env, jstring)
}

/// Kind, HTTP status and retry delay read from a core error. `status` and `retry_after_ms`
/// are -1 when the error carries none.
struct JniErrorParts {
    kind: Option<&'static str>,
    status: i32,
    retry_after_ms: i64,
}

impl JniErrorParts {
    const NONE: JniErrorParts = JniErrorParts { kind: None, status: -1, retry_after_ms: -1 };
}

{% for error in errors %}
fn jni_error_parts_{{ error.snake }}(e: &{{ error.rust_path }}) -> JniErrorParts {
    #[allow(unreachable_patterns)]
    let kind = match e {
{% for variant in error.variants %}
        {{ variant.pattern }} => Some("{{ variant.name }}"),
{% endfor %}
        _ => None,
    };
    JniErrorParts {
        kind,
        status: {{ error.status_expr }},
        retry_after_ms: {{ error.retry_after_expr }},
    }
}

{% endfor %}
/// Read the parts of `error` when it is, or boxes, one of the core error types.
fn jni_error_parts(error: &dyn std::any::Any) -> JniErrorParts {
    let boxed = error.downcast_ref::<Box<dyn std::error::Error + Send + Sync + 'static>>();
{% for error in errors %}
    if let Some(e) = error.downcast_ref::<{{ error.rust_path }}>() {
        return jni_error_parts_{{ error.snake }}(e);
    }
    if let Some(e) = boxed.and_then(|b| b.downcast_ref::<{{ error.rust_path }}>()) {
        return jni_error_parts_{{ error.snake }}(e);
    }
{% endfor %}
    let _ = boxed;
    JniErrorParts::NONE
}

/// Build the declared exception through its `(message, kind, status, retryAfterMs)`
/// constructor and make it the pending exception.
fn throw_typed_jni_exception(
    env: &mut Env<'_>,
    msg: &str,
    kind: &str,
    status: i32,
    retry_after_ms: i64,
) -> std::result::Result<(), jni::errors::Error> {
    use std::str::FromStr;
    let message_j = env.new_string(msg)?;
    let kind_j = env.new_string(kind)?;
    let class_jni = jni::strings::JNIString::from(ERROR_CLASS);
    let sig_runtime = jni::signature::RuntimeMethodSignature::from_str("(Ljava/lang/String;Ljava/lang/String;IJ)V")?;
    let exception = env.new_object(
        &class_jni,
        sig_runtime.method_signature(),
        &[
            jni::objects::JValue::Object(&message_j),
            jni::objects::JValue::Object(&kind_j),
            jni::objects::JValue::Int(status),
            jni::objects::JValue::Long(retry_after_ms),
        ],
    )?;
    let throwable = env.cast_local::<jni::objects::JThrowable>(exception)?;
    env.throw(&throwable)
}

/// `Env::throw` and `Env::throw_new` report a thrown exception as `Err(JavaException)`, so that
/// error means the exception is pending and is the success case here.
fn jni_throw_succeeded(result: std::result::Result<(), jni::errors::Error>) -> bool {
    matches!(result, Ok(()) | Err(jni::errors::Error::JavaException))
}

fn throw_jni_error_with_parts(env: &mut Env<'_>, msg: &str, parts: &JniErrorParts) {
    if let Some(kind) = parts.kind {
        if jni_throw_succeeded(throw_typed_jni_exception(env, msg, kind, parts.status, parts.retry_after_ms)) {
            return;
        }
        // A failed lookup or constructor call leaves its own exception pending.
        env.exception_clear();
    }
    // If the error class cannot be found (misconfigured AAR), fall back to a
    // generic RuntimeException so the caller always gets *some* exception rather
    // than a silent null/zero return that looks like a valid result.
    let class_jni = jni::strings::JNIString::from(ERROR_CLASS);
    let msg_jni = jni::strings::JNIString::from(msg);
    if !jni_throw_succeeded(env.throw_new(&class_jni, &msg_jni)) {
        env.exception_clear();
        let fallback = jni::strings::JNIString::from("java/lang/RuntimeException");
        let _ = env.throw_new(&fallback, &msg_jni);
    }
}

fn throw_jni_error(env: &mut Env<'_>, msg: &str) {
    throw_jni_error_with_parts(env, msg, &JniErrorParts::NONE);
}

/// Throw a core error with its kind, HTTP status and retry delay.
fn throw_jni_error_from<E: std::fmt::Display + 'static>(env: &mut Env<'_>, e: &E) {
    throw_jni_error_with_parts(env, &e.to_string(), &jni_error_parts(e));
}

fn run_or_throw<T, F>(env: &mut Env<'_>, f: F) -> Option<T>
where
    F: FnOnce(&mut Env<'_>) -> T,
{
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(env))) {
        Ok(v) => Some(v),
        Err(payload) => {
            let msg = payload.downcast_ref::<String>().cloned()
                .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))
                .unwrap_or_else(|| "panic in native code".to_string());
            throw_jni_error(env, &format!("native panic: {msg}"));
            None
        }
    }
}

/// Call a trait-bridge dispatcher's `dispatch(String, String) -> String` entry
/// point. Used by the generated plugin bridges to route trait method calls to
/// the JVM host object as JSON.
#[allow(dead_code)]
fn jni_dispatch_json(
    env: &mut Env<'_>,
    obj: &JObject,
    method: &str,
    args_json: &str,
) -> std::result::Result<String, jni::errors::Error> {
    use std::str::FromStr;
    let class = env.get_object_class(obj)?;
    let name_jni = jni::strings::JNIString::from("dispatch");
    let sig_runtime = jni::signature::RuntimeMethodSignature::from_str(
        "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
    )?;
    let sig = sig_runtime.method_signature();
    let method_id = env.get_method_id(&class, name_jni, sig)?;
    let method_j = env.new_string(method)?;
    let args_j = env.new_string(args_json)?;
    // SAFETY: method_id comes from get_method_id on this object's class and the
    // argument values match the looked-up signature.
    let call = unsafe {
        env.call_method_unchecked(
            obj,
            method_id,
            jni::signature::ReturnType::Object,
            &[
                jni::objects::JValue::Object(&method_j).as_jni(),
                jni::objects::JValue::Object(&args_j).as_jni(),
            ],
        )
    };
    let result = match call.and_then(|v| v.l()) {
        Ok(o) => o,
        Err(e) => {
            // A pending Java exception must be cleared before further JNI calls.
            if env.exception_check() {
                env.exception_clear();
            }
            return Err(e);
        }
    };
    // SAFETY: the dispatcher's `dispatch` method returns a Java String.
    let jstr = unsafe { JString::from_raw(env, result.into_raw()) };
    jstring_to_string(env, jstr)
}