Skip to content
4 changes: 2 additions & 2 deletions pkgs/jni/lib/src/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ final class UseAfterReleaseError extends StateError with _ExplainsRelease {
}

// TODO(#567): Use NullPointerError once it's available.
final class JNullError extends StateError {
JNullError() : super('The reference was null');
final class JNullError extends JniException {
JNullError() : super('The reference was null', '');
}

final class DoubleReleaseError extends StateError with _ExplainsRelease {
Expand Down
5 changes: 5 additions & 0 deletions pkgs/jni/lib/src/jclass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ extension type JInstanceFieldId._fromPointer(JFieldIDPtr pointer) {
JInstanceFieldId._(JClass jClass, String name, String signature)
: pointer = using((arena) {
final jClassRef = jClass.reference;
if (jClassRef.isNull) throw JNullError();
return Jni.env.GetFieldID(
jClassRef.pointer,
name.toNativeChars(arena),
Expand All @@ -66,6 +67,7 @@ extension type JStaticFieldId._fromPointer(JFieldIDPtr pointer) {
JStaticFieldId._(JClass jClass, String name, String signature)
: pointer = using((arena) {
final jClassRef = jClass.reference;
if (jClassRef.isNull) throw JNullError();
return Jni.env.GetStaticFieldID(
jClassRef.pointer,
name.toNativeChars(arena),
Expand Down Expand Up @@ -93,6 +95,7 @@ extension type JInstanceMethodId._fromPointer(JMethodIDPtr pointer) {
String signature,
) : pointer = using((arena) {
final jClassRef = jClass.reference;
if (jClassRef.isNull) throw JNullError();
return Jni.env.GetMethodID(
jClassRef.pointer,
name.toNativeChars(arena),
Expand Down Expand Up @@ -122,6 +125,7 @@ extension type JStaticMethodId._fromPointer(JMethodIDPtr pointer) {
String signature,
) : pointer = using((arena) {
final jClassRef = jClass.reference;
if (jClassRef.isNull) throw JNullError();
return Jni.env.GetStaticMethodID(
jClassRef.pointer,
name.toNativeChars(arena),
Expand All @@ -148,6 +152,7 @@ extension type JConstructorId._fromPointer(JMethodIDPtr pointer) {
String signature,
) : pointer = using((arena) {
final jClassRef = jClass.reference;
if (jClassRef.isNull) throw JNullError();
return Jni.env.GetMethodID(
jClassRef.pointer,
'<init>'.toNativeChars(arena),
Expand Down
2 changes: 1 addition & 1 deletion pkgs/jni/lib/src/jobject.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class JObject {

/// Constructs a [JObject] with the underlying [reference].
JObject.fromReference(this.reference) {
if (reference.isNull) {
if (reference.isNull && runtimeType == JObject) {
throw JNullError();
}
}
Expand Down
1 change: 1 addition & 0 deletions pkgs/jni/lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:meta/meta.dart' show internal;

import 'errors.dart';
import 'jni.dart';
import 'jobject.dart';
import 'jreference.dart';
Expand Down
14 changes: 6 additions & 8 deletions pkgs/jni/src/dartjni.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,6 @@ JniClassLookupResult FindClass(const char* name) {
return result;
}

/// Stores class and method references for obtaining exception details
typedef struct JniExceptionMethods {
jclass objectClass, exceptionClass, printStreamClass;
jclass byteArrayOutputStreamClass;
jmethodID toStringMethod, printStackTraceMethod;
jmethodID byteArrayOutputStreamCtor, printStreamCtor;
} JniExceptionMethods;

// Context and shared global state. Initialized once or if thread-local, initialized once in a thread.
JniContext jni_context = {
.jvm = NULL,
Expand All @@ -81,6 +73,8 @@ void init() {
exceptionMethods.printStreamClass = FindClassUnchecked("java/io/PrintStream");
exceptionMethods.byteArrayOutputStreamClass =
FindClassUnchecked("java/io/ByteArrayOutputStream");
exceptionMethods.runtimeExceptionClass =
FindClassUnchecked("java/lang/RuntimeException");
load_method(exceptionMethods.objectClass, &exceptionMethods.toStringMethod,
"toString", "()Ljava/lang/String;");
load_method(exceptionMethods.exceptionClass,
Expand All @@ -91,6 +85,9 @@ void init() {
load_method(exceptionMethods.printStreamClass,
&exceptionMethods.printStreamCtor, "<init>",
"(Ljava/io/OutputStream;)V");
load_method(exceptionMethods.runtimeExceptionClass,
&exceptionMethods.runtimeExceptionCtor, "<init>",
"(Ljava/lang/String;)V");
}

void deinit() {
Expand All @@ -109,6 +106,7 @@ void deinit() {
(*jniEnv)->DeleteGlobalRef(jniEnv, exceptionMethods.printStreamClass);
(*jniEnv)->DeleteGlobalRef(jniEnv,
exceptionMethods.byteArrayOutputStreamClass);
(*jniEnv)->DeleteGlobalRef(jniEnv, exceptionMethods.runtimeExceptionClass);
}
}

Expand Down
71 changes: 68 additions & 3 deletions pkgs/jni/src/dartjni.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,31 @@ typedef struct CallbackResult {
jobject object;
} CallbackResult;

typedef struct JniLocks {
typedef struct JniContextLocks {
MutexLock classLoadingLock;
} JniLocks;
} JniContextLocks;

/// Stores the global state of the JNI.
typedef struct JniContext {
JavaVM* jvm;
jobject classLoader;
jmethodID loadClassMethod;
JniLocks locks;
JniContextLocks locks;
} JniContext;

// Forward declaration for JniExceptionMethods
typedef struct JniExceptionMethods {
jclass objectClass, exceptionClass, printStreamClass;
jclass byteArrayOutputStreamClass;
jclass runtimeExceptionClass;
jmethodID toStringMethod, printStackTraceMethod;
jmethodID byteArrayOutputStreamCtor, printStreamCtor;
jmethodID runtimeExceptionCtor;
} JniExceptionMethods;

// External reference to exception methods (defined in dartjni.c)
extern JniExceptionMethods exceptionMethods;

// jniEnv for this thread, used by inline functions in this header,
// therefore declared as extern.
extern THREAD_LOCAL JNIEnv* jniEnv;
Expand Down Expand Up @@ -308,6 +321,58 @@ static inline jthrowable check_exception() {
return to_global_ref(exception);
}

/// Creates a RuntimeException for null parameter errors.
/// Returns a global reference to the exception.
/// Uses cached RuntimeException class/constructor initialized during JVM init.
static inline jthrowable create_null_parameter_exception(
const char* param_name) {
// Use cached RuntimeException class and constructor from exceptionMethods
jclass runtime_exception_class = exceptionMethods.runtimeExceptionClass;
jmethodID constructor = exceptionMethods.runtimeExceptionCtor;

// If not initialized yet (shouldn't happen after init), return NULL
// This provides safety during early bootstrap before exception methods are cached
if (runtime_exception_class == NULL || constructor == NULL) {
return NULL;
}

// Create error message
char message[256];
snprintf(message, sizeof(message), "Parameter %s is null", param_name);
jstring message_str = (*jniEnv)->NewStringUTF(jniEnv, message);

if (message_str == NULL) {
// Can't create string - bail out
return NULL;
}

// Clear any pending exceptions before creating our exception
if ((*jniEnv)->ExceptionCheck(jniEnv)) {
(*jniEnv)->ExceptionClear(jniEnv);
}

// Create the RuntimeException instance
jobject exception = (*jniEnv)->NewObject(jniEnv, runtime_exception_class,
constructor, message_str);

// Clean up local reference to message string
(*jniEnv)->DeleteLocalRef(jniEnv, message_str);

if (exception == NULL) {
// Failed to create exception - clear any errors and bail
if ((*jniEnv)->ExceptionCheck(jniEnv)) {
(*jniEnv)->ExceptionClear(jniEnv);
}
return NULL;
}

// Convert to global reference
jthrowable global_exception = (*jniEnv)->NewGlobalRef(jniEnv, exception);
(*jniEnv)->DeleteLocalRef(jniEnv, exception);

return global_exception;
}

static inline JniResult to_global_ref_result(jobject ref) {
JniResult result;
result.exception = check_exception();
Expand Down
Loading
Loading