[jnigen] Throw DartExceptions in Java (https://github.com/dart-lang/jnigen/issues/376)

diff --git a/pkgs/jni/example/android/app/build.gradle b/pkgs/jni/example/android/app/build.gradle
index 3c6742c..fbcc356 100644
--- a/pkgs/jni/example/android/app/build.gradle
+++ b/pkgs/jni/example/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java
index ff9c076..3caf02a 100644
--- a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java
+++ b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java
@@ -73,10 +73,19 @@
     return obj;
   }
 
+  private static final class DartException extends Exception {
+    private DartException(String message) {
+      super(message);
+    }
+  }
+
   @Override
-  public Object invoke(Object proxy, Method method, Object[] args) {
+  public Object invoke(Object proxy, Method method, Object[] args) throws DartException {
     Object[] result = _invoke(port, isolateId, functionPtr, proxy, getDescriptor(method), args);
     _cleanUp((Long) result[0]);
+    if (result[1] instanceof DartException) {
+      throw (DartException) result[1];
+    }
     return result[1];
   }
 
diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart
index 2957105..d5495bc 100644
--- a/pkgs/jni/lib/src/jni.dart
+++ b/pkgs/jni/lib/src/jni.dart
@@ -299,6 +299,13 @@
     return lookup;
   }
 
+  /// Returns a new DartException.
+  static JObjectPtr newDartException(String message) {
+    return Jni._bindings
+        .DartException__ctor(Jni.env.toJStringPtr(message))
+        .object;
+  }
+
   /// Returns a new PortContinuation.
   static JObjectPtr newPortContinuation(ReceivePort port) {
     return Jni._bindings
@@ -323,7 +330,7 @@
         .object;
   }
 
-  /// Return the result of a callback..
+  /// Returns the result of a callback..
   static void returnResult(
       Pointer<CallbackResult> result, JObjectPtr object) async {
     Jni._bindings.resultFor(result, object);
@@ -334,28 +341,30 @@
   /// Convenience method for converting a [JStringPtr]
   /// to dart string.
   /// if [deleteOriginal] is specified, jstring passed will be deleted using
-  /// DeleteLocalRef.
+  /// DeleteGlobalRef.
   String toDartString(JStringPtr jstringPtr, {bool deleteOriginal = false}) {
     if (jstringPtr == nullptr) {
       throw const JNullException();
     }
-    final chars = GetStringUTFChars(jstringPtr, nullptr);
+    final chars = GetStringChars(jstringPtr, nullptr);
     if (chars == nullptr) {
       throw InvalidJStringException(jstringPtr);
     }
-    final result = chars.cast<Utf8>().toDartString();
-    ReleaseStringUTFChars(jstringPtr, chars);
+    final result = chars.cast<Utf16>().toDartString();
+    ReleaseStringChars(jstringPtr, chars);
     if (deleteOriginal) {
       DeleteGlobalRef(jstringPtr);
     }
     return result;
   }
 
-  /// Return a new [JStringPtr] from contents of [s].
+  /// Returns a new [JStringPtr] from contents of [s].
   JStringPtr toJStringPtr(String s) => using((arena) {
-        final utf = s.toNativeUtf8().cast<Char>();
-        final result = NewStringUTF(utf);
-        malloc.free(utf);
+        final utf = s.toNativeUtf16(allocator: arena).cast<Uint16>();
+        final result = NewString(utf, s.length);
+        if (utf == nullptr) {
+          throw 'Fatal: cannot convert string to Java string: $s';
+        }
         return result;
       });
 
diff --git a/pkgs/jni/lib/src/lang/jstring.dart b/pkgs/jni/lib/src/lang/jstring.dart
index 5bfcce5..f1df76c 100644
--- a/pkgs/jni/lib/src/lang/jstring.dart
+++ b/pkgs/jni/lib/src/lang/jstring.dart
@@ -47,20 +47,11 @@
   /// Construct a new [JString] with [reference] as its underlying reference.
   JString.fromRef(JStringPtr reference) : super.fromRef(reference);
 
-  static JStringPtr _toJavaString(String s) => using((arena) {
-        final chars = s.toNativeUtf16(allocator: arena).cast<Uint16>();
-        final jstr = Jni.env.NewString(chars, s.length);
-        if (jstr == nullptr) {
-          throw 'Fatal: cannot convert string to Java string: $s';
-        }
-        return jstr;
-      });
-
   /// The number of Unicode characters in this Java string.
   int get length => Jni.env.GetStringLength(reference);
 
   /// Construct a [JString] from the contents of Dart string [s].
-  JString.fromString(String s) : super.fromRef(_toJavaString(s));
+  JString.fromString(String s) : super.fromRef(Jni.env.toJStringPtr(s));
 
   /// Returns the contents as a Dart String.
   ///
diff --git a/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart b/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
index bdd6ab7..892f69e 100644
--- a/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
+++ b/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
@@ -178,22 +178,19 @@
   late final _InitDartApiDL =
       _InitDartApiDLPtr.asFunction<int Function(ffi.Pointer<ffi.Void>)>();
 
-  void resultFor(
-    ffi.Pointer<CallbackResult> result,
-    JObjectPtr object,
+  JniResult DartException__ctor(
+    JStringPtr message,
   ) {
-    return _resultFor(
-      result,
-      object,
+    return _DartException__ctor(
+      message,
     );
   }
 
-  late final _resultForPtr = _lookup<
-      ffi.NativeFunction<
-          ffi.Void Function(
-              ffi.Pointer<CallbackResult>, JObjectPtr)>>('resultFor');
-  late final _resultFor = _resultForPtr
-      .asFunction<void Function(ffi.Pointer<CallbackResult>, JObjectPtr)>();
+  late final _DartException__ctorPtr =
+      _lookup<ffi.NativeFunction<JniResult Function(JStringPtr)>>(
+          'DartException__ctor');
+  late final _DartException__ctor =
+      _DartException__ctorPtr.asFunction<JniResult Function(JStringPtr)>();
 
   JniResult PortContinuation__ctor(
     int j,
@@ -228,6 +225,23 @@
   late final _PortProxy__newInstance = _PortProxy__newInstancePtr.asFunction<
       JniResult Function(JObjectPtr, int, int)>();
 
+  void resultFor(
+    ffi.Pointer<CallbackResult> result,
+    JObjectPtr object,
+  ) {
+    return _resultFor(
+      result,
+      object,
+    );
+  }
+
+  late final _resultForPtr = _lookup<
+      ffi.NativeFunction<
+          ffi.Void Function(
+              ffi.Pointer<CallbackResult>, JObjectPtr)>>('resultFor');
+  late final _resultFor = _resultForPtr
+      .asFunction<void Function(ffi.Pointer<CallbackResult>, JObjectPtr)>();
+
   ffi.Pointer<GlobalJniEnvStruct> GetGlobalEnv() {
     return _GetGlobalEnv();
   }
diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c
index dc595a9..7c5951b 100644
--- a/pkgs/jni/src/dartjni.c
+++ b/pkgs/jni/src/dartjni.c
@@ -569,6 +569,29 @@
   return Dart_InitializeApiDL(data);
 }
 
+// com.github.dart_lang.jni.DartException
+jclass _c_DartException = NULL;
+
+jmethodID _m_DartException__ctor = NULL;
+FFI_PLUGIN_EXPORT JniResult DartException__ctor(jstring message) {
+  attach_thread();
+  load_class_global_ref(&_c_DartException,
+                        "com/github/dart_lang/jni/PortProxy$DartException");
+  if (_c_DartException == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_method(_c_DartException, &_m_DartException__ctor, "<init>",
+              "(Ljava/lang/String;)V");
+  if (_m_DartException__ctor == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  jobject _result = (*jniEnv)->NewObject(jniEnv, _c_DartException,
+                                         _m_DartException__ctor, message);
+  jthrowable exception = check_exception();
+  if (exception == NULL) {
+    _result = to_global_ref(_result);
+  }
+  return (JniResult){.value = {.l = _result}, .exception = check_exception()};
+}
+
 JNIEXPORT void JNICALL
 Java_com_github_dart_1lang_jni_PortContinuation__1resumeWith(JNIEnv* env,
                                                              jclass clazz,
diff --git a/pkgs/jni/src/dartjni.h b/pkgs/jni/src/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jni/src/dartjni.h
+++ b/pkgs/jni/src/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/android_test_runner/android/app/build.gradle b/pkgs/jnigen/android_test_runner/android/app/build.gradle
index e0c777f..58cd27a 100644
--- a/pkgs/jnigen/android_test_runner/android/app/build.gradle
+++ b/pkgs/jnigen/android_test_runner/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jnigen/example/in_app_java/android/app/build.gradle b/pkgs/jnigen/example/in_app_java/android/app/build.gradle
index 8f38e43..59ebf16 100644
--- a/pkgs/jnigen/example/in_app_java/android/app/build.gradle
+++ b/pkgs/jnigen/example/in_app_java/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jnigen/example/in_app_java/src/android_utils/dartjni.h b/pkgs/jnigen/example/in_app_java/src/android_utils/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/example/in_app_java/src/android_utils/dartjni.h
+++ b/pkgs/jnigen/example/in_app_java/src/android_utils/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/example/kotlin_plugin/example/android/app/build.gradle b/pkgs/jnigen/example/kotlin_plugin/example/android/app/build.gradle
index f371e65..7f1cb3e 100644
--- a/pkgs/jnigen/example/kotlin_plugin/example/android/app/build.gradle
+++ b/pkgs/jnigen/example/kotlin_plugin/example/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jnigen/example/kotlin_plugin/src/dartjni.h b/pkgs/jnigen/example/kotlin_plugin/src/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/example/kotlin_plugin/src/dartjni.h
+++ b/pkgs/jnigen/example/kotlin_plugin/src/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/example/notification_plugin/example/android/app/build.gradle b/pkgs/jnigen/example/notification_plugin/example/android/app/build.gradle
index ee3c62f..7f66900 100644
--- a/pkgs/jnigen/example/notification_plugin/example/android/app/build.gradle
+++ b/pkgs/jnigen/example/notification_plugin/example/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jnigen/example/notification_plugin/src/dartjni.h b/pkgs/jnigen/example/notification_plugin/src/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/example/notification_plugin/src/dartjni.h
+++ b/pkgs/jnigen/example/notification_plugin/src/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/example/pdfbox_plugin/example/android/app/build.gradle b/pkgs/jnigen/example/pdfbox_plugin/example/android/app/build.gradle
index 162ad5b..c5791ba 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/example/android/app/build.gradle
+++ b/pkgs/jnigen/example/pdfbox_plugin/example/android/app/build.gradle
@@ -8,7 +8,7 @@
 
 def flutterRoot = localProperties.getProperty('flutter.sdk')
 if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+    throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
diff --git a/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h b/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h
+++ b/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
index 3bc72cc..d21665c 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
@@ -439,14 +439,18 @@
     int $p,
     $MethodInvocation $i,
   ) {
-    final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
-    final $a = $i.args;
+    try {
+      final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
+      final $a = $i.args;
     ''');
       final proxyMethodIf = _InterfaceMethodIf(resolver, s);
       for (final method in node.methods) {
         method.accept(proxyMethodIf);
       }
       s.write('''
+    } catch (e) {
+      return $_protectedExtension.newDartException(e.toString());
+    }
     return jni.nullptr;
   }
 
@@ -469,19 +473,17 @@
     ).._\$p = \$p;
     final \$a = \$p.sendPort.nativePort; 
     _\$impls[\$a] = \$impl;
-''');
-      s.write(r'''
-    $p.listen(($m) {
-      if ($m == null) {
-        _$impls.remove($p.sendPort.nativePort);
-        $p.close();
+    \$p.listen((\$m) {
+      if (\$m == null) {
+        _\$impls.remove(\$p.sendPort.nativePort);
+        \$p.close();
         return;
       }
-      final $i = $MethodInvocation.fromMessage($m);
-      final $r = _$invokeMethod($p.sendPort.nativePort, $i);
-      ProtectedJniExtensions.returnResult($i.result, $r);
+      final \$i = \$MethodInvocation.fromMessage(\$m);
+      final \$r = _\$invokeMethod(\$p.sendPort.nativePort, \$i);
+      $_protectedExtension.returnResult(\$i.result, \$r);
     });
-    return $x;
+    return \$x;
   }
   ''');
     }
@@ -1645,17 +1647,17 @@
     final saveResult = isVoid ? '' : 'final \$r = ';
     final name = node.finalName;
     s.write('''
-      if (\$d == r"$signature") {
-        ${saveResult}_\$impls[\$p]!.$name(
+        if (\$d == r"$signature") {
+          ${saveResult}_\$impls[\$p]!.$name(
 ''');
     for (var i = 0; i < node.params.length; ++i) {
       node.params[i].accept(_InterfaceParamCast(resolver, s, paramIndex: i));
     }
     const returnBox = _InterfaceReturnBox();
     s.write('''
-        );
-        return ${node.returnType.accept(returnBox)};
-      }
+          );
+          return ${node.returnType.accept(returnBox)};
+        }
 ''');
   }
 }
diff --git a/pkgs/jnigen/lib/src/config/experiments.dart b/pkgs/jnigen/lib/src/config/experiments.dart
index d0eb39d..6bb65c1 100644
--- a/pkgs/jnigen/lib/src/config/experiments.dart
+++ b/pkgs/jnigen/lib/src/config/experiments.dart
@@ -31,7 +31,7 @@
     }
     final result = search.single;
     if (result.isExpired) {
-      throw 'The experiment $s can no longer be used in this version. ';
+      throw 'The experiment $s can no longer be used in this version.';
     }
     return result;
   }
diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/dartjni.h b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/dartjni.h
+++ b/pkgs/jnigen/test/jackson_core_test/third_party/c_based/c_bindings/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/test/kotlin_test/c_based/c_bindings/dartjni.h b/pkgs/jnigen/test/kotlin_test/c_based/c_bindings/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/test/kotlin_test/c_based/c_bindings/dartjni.h
+++ b/pkgs/jnigen/test/kotlin_test/c_based/c_bindings/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/test/regenerate_examples_test.dart b/pkgs/jnigen/test/regenerate_examples_test.dart
index 5afeebe..35ab2e0 100644
--- a/pkgs/jnigen/test/regenerate_examples_test.dart
+++ b/pkgs/jnigen/test/regenerate_examples_test.dart
@@ -29,7 +29,7 @@
     {bool isLargeTest = false}) {
   test(
     'Generate and compare bindings for $exampleName',
-    timeout: const Timeout.factor(2),
+    timeout: const Timeout.factor(3),
     () async {
       final examplePath = join('example', exampleName);
       final configPath = join(examplePath, 'jnigen.yaml');
diff --git a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/dartjni.h b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/dartjni.h
index 0a7f669..8f1dc74 100644
--- a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/dartjni.h
+++ b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/dartjni.h
@@ -410,7 +410,8 @@
 
 FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data);
 
-FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
+FFI_PLUGIN_EXPORT
+JniResult DartException__ctor(jstring message);
 
 FFI_PLUGIN_EXPORT
 JniResult PortContinuation__ctor(int64_t j);
@@ -419,3 +420,5 @@
 JniResult PortProxy__newInstance(jobject binaryName,
                                  int64_t port,
                                  int64_t functionPtr);
+
+FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object);
diff --git a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c
index 62b150e..20c7e7e 100644
--- a/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c
+++ b/pkgs/jnigen/test/simple_package_test/c_based/c_bindings/simple_package.c
@@ -2786,6 +2786,111 @@
   return (JniResult){.value = {.j = 0}, .exception = check_exception()};
 }
 
+// com.github.dart_lang.jnigen.interfaces.MyRunnable
+jclass _c_MyRunnable = NULL;
+
+jmethodID _m_MyRunnable__run = NULL;
+FFI_PLUGIN_EXPORT
+JniResult MyRunnable__run(jobject self_) {
+  load_env();
+  load_class_global_ref(&_c_MyRunnable,
+                        "com/github/dart_lang/jnigen/interfaces/MyRunnable");
+  if (_c_MyRunnable == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_method(_c_MyRunnable, &_m_MyRunnable__run, "run", "()V");
+  if (_m_MyRunnable__run == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  (*jniEnv)->CallVoidMethod(jniEnv, self_, _m_MyRunnable__run);
+  return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+}
+
+// com.github.dart_lang.jnigen.interfaces.MyRunnableRunner
+jclass _c_MyRunnableRunner = NULL;
+
+jmethodID _m_MyRunnableRunner__new0 = NULL;
+FFI_PLUGIN_EXPORT
+JniResult MyRunnableRunner__new0(jobject runnable) {
+  load_env();
+  load_class_global_ref(
+      &_c_MyRunnableRunner,
+      "com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+  if (_c_MyRunnableRunner == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_method(_c_MyRunnableRunner, &_m_MyRunnableRunner__new0, "<init>",
+              "(Lcom/github/dart_lang/jnigen/interfaces/MyRunnable;)V");
+  if (_m_MyRunnableRunner__new0 == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  jobject _result = (*jniEnv)->NewObject(jniEnv, _c_MyRunnableRunner,
+                                         _m_MyRunnableRunner__new0, runnable);
+  return to_global_ref_result(_result);
+}
+
+jmethodID _m_MyRunnableRunner__runOnSameThread = NULL;
+FFI_PLUGIN_EXPORT
+JniResult MyRunnableRunner__runOnSameThread(jobject self_) {
+  load_env();
+  load_class_global_ref(
+      &_c_MyRunnableRunner,
+      "com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+  if (_c_MyRunnableRunner == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_method(_c_MyRunnableRunner, &_m_MyRunnableRunner__runOnSameThread,
+              "runOnSameThread", "()V");
+  if (_m_MyRunnableRunner__runOnSameThread == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  (*jniEnv)->CallVoidMethod(jniEnv, self_,
+                            _m_MyRunnableRunner__runOnSameThread);
+  return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+}
+
+jmethodID _m_MyRunnableRunner__runOnAnotherThread = NULL;
+FFI_PLUGIN_EXPORT
+JniResult MyRunnableRunner__runOnAnotherThread(jobject self_) {
+  load_env();
+  load_class_global_ref(
+      &_c_MyRunnableRunner,
+      "com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+  if (_c_MyRunnableRunner == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_method(_c_MyRunnableRunner, &_m_MyRunnableRunner__runOnAnotherThread,
+              "runOnAnotherThread", "()V");
+  if (_m_MyRunnableRunner__runOnAnotherThread == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  (*jniEnv)->CallVoidMethod(jniEnv, self_,
+                            _m_MyRunnableRunner__runOnAnotherThread);
+  return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+}
+
+jfieldID _f_MyRunnableRunner__error = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_MyRunnableRunner__error(jobject self_) {
+  load_env();
+  load_class_global_ref(
+      &_c_MyRunnableRunner,
+      "com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+  if (_c_MyRunnableRunner == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_field(_c_MyRunnableRunner, &_f_MyRunnableRunner__error, "error",
+             "Ljava/lang/Throwable;");
+  jobject _result =
+      (*jniEnv)->GetObjectField(jniEnv, self_, _f_MyRunnableRunner__error);
+  return to_global_ref_result(_result);
+}
+
+FFI_PLUGIN_EXPORT
+JniResult set_MyRunnableRunner__error(jobject self_, jobject value) {
+  load_env();
+  load_class_global_ref(
+      &_c_MyRunnableRunner,
+      "com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+  if (_c_MyRunnableRunner == NULL)
+    return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+  load_field(_c_MyRunnableRunner, &_f_MyRunnableRunner__error, "error",
+             "Ljava/lang/Throwable;");
+  (*jniEnv)->SetObjectField(jniEnv, self_, _f_MyRunnableRunner__error, value);
+  return (JniResult){.value = {.j = 0}, .exception = check_exception()};
+}
+
 // com.github.dart_lang.jnigen.annotations.JsonSerializable$Case
 jclass _c_JsonSerializable_Case = NULL;
 
diff --git a/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart
index cd47f44..5154b75 100644
--- a/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart
+++ b/pkgs/jnigen/test/simple_package_test/c_based/dart_bindings/simple_package.dart
@@ -3371,42 +3371,46 @@
     int $p,
     $MethodInvocation $i,
   ) {
-    final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
-    final $a = $i.args;
-    if ($d == r"voidCallback(Ljava/lang/String;)V") {
-      _$impls[$p]!.voidCallback(
-        $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
-      );
-      return jni.nullptr;
-    }
-    if ($d == r"stringCallback(Ljava/lang/String;)Ljava/lang/String;") {
-      final $r = _$impls[$p]!.stringCallback(
-        $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
-      );
-      return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
-    }
-    if ($d == r"varCallback(Ljava/lang/Object;)Ljava/lang/Object;") {
-      final $r = _$impls[$p]!.varCallback(
-        $a[0].castTo(_$impls[$p]!.T, deleteOriginal: true),
-      );
-      return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
-    }
-    if ($d == r"manyPrimitives(IZCD)J") {
-      final $r = _$impls[$p]!.manyPrimitives(
-        $a[0]
-            .castTo(const jni.JIntegerType(), deleteOriginal: true)
-            .intValue(deleteOriginal: true),
-        $a[1]
-            .castTo(const jni.JBooleanType(), deleteOriginal: true)
-            .booleanValue(deleteOriginal: true),
-        $a[2]
-            .castTo(const jni.JCharacterType(), deleteOriginal: true)
-            .charValue(deleteOriginal: true),
-        $a[3]
-            .castTo(const jni.JDoubleType(), deleteOriginal: true)
-            .doubleValue(deleteOriginal: true),
-      );
-      return jni.JLong($r).toPointer();
+    try {
+      final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
+      final $a = $i.args;
+      if ($d == r"voidCallback(Ljava/lang/String;)V") {
+        _$impls[$p]!.voidCallback(
+          $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
+        );
+        return jni.nullptr;
+      }
+      if ($d == r"stringCallback(Ljava/lang/String;)Ljava/lang/String;") {
+        final $r = _$impls[$p]!.stringCallback(
+          $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
+        );
+        return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
+      }
+      if ($d == r"varCallback(Ljava/lang/Object;)Ljava/lang/Object;") {
+        final $r = _$impls[$p]!.varCallback(
+          $a[0].castTo(_$impls[$p]!.T, deleteOriginal: true),
+        );
+        return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
+      }
+      if ($d == r"manyPrimitives(IZCD)J") {
+        final $r = _$impls[$p]!.manyPrimitives(
+          $a[0]
+              .castTo(const jni.JIntegerType(), deleteOriginal: true)
+              .intValue(deleteOriginal: true),
+          $a[1]
+              .castTo(const jni.JBooleanType(), deleteOriginal: true)
+              .booleanValue(deleteOriginal: true),
+          $a[2]
+              .castTo(const jni.JCharacterType(), deleteOriginal: true)
+              .charValue(deleteOriginal: true),
+          $a[3]
+              .castTo(const jni.JDoubleType(), deleteOriginal: true)
+              .doubleValue(deleteOriginal: true),
+        );
+        return jni.JLong($r).toPointer();
+      }
+    } catch (e) {
+      return ProtectedJniExtensions.newDartException(e.toString());
     }
     return jni.nullptr;
   }
@@ -3646,6 +3650,243 @@
   }
 }
 
+/// from: com.github.dart_lang.jnigen.interfaces.MyRunnable
+class MyRunnable extends jni.JObject {
+  @override
+  late final jni.JObjType<MyRunnable> $type = type;
+
+  MyRunnable.fromRef(
+    jni.JObjectPtr ref,
+  ) : super.fromRef(ref);
+
+  /// The type which includes information such as the signature of this class.
+  static const type = $MyRunnableType();
+  static final _run = jniLookup<
+              ffi
+              .NativeFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>>(
+          "MyRunnable__run")
+      .asFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>();
+
+  /// from: public abstract void run()
+  void run() {
+    return _run(reference).check();
+  }
+
+  /// Maps a specific port to the implemented interface.
+  static final Map<int, $MyRunnableImpl> _$impls = {};
+  ReceivePort? _$p;
+
+  static jni.JObjectPtr _$invoke(
+    int port,
+    jni.JObjectPtr descriptor,
+    jni.JObjectPtr args,
+  ) {
+    return _$invokeMethod(
+      port,
+      $MethodInvocation.fromAddresses(
+        0,
+        descriptor.address,
+        args.address,
+      ),
+    );
+  }
+
+  static final ffi.Pointer<
+          ffi.NativeFunction<
+              jni.JObjectPtr Function(
+                  ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>>
+      _$invokePointer = ffi.Pointer.fromFunction(_$invoke);
+
+  static ffi.Pointer<ffi.Void> _$invokeMethod(
+    int $p,
+    $MethodInvocation $i,
+  ) {
+    try {
+      final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
+      final $a = $i.args;
+      if ($d == r"run()V") {
+        _$impls[$p]!.run();
+        return jni.nullptr;
+      }
+    } catch (e) {
+      return ProtectedJniExtensions.newDartException(e.toString());
+    }
+    return jni.nullptr;
+  }
+
+  factory MyRunnable.implement(
+    $MyRunnableImpl $impl,
+  ) {
+    final $p = ReceivePort();
+    final $x = MyRunnable.fromRef(
+      ProtectedJniExtensions.newPortProxy(
+        r"com.github.dart_lang.jnigen.interfaces.MyRunnable",
+        $p,
+        _$invokePointer,
+      ),
+    ).._$p = $p;
+    final $a = $p.sendPort.nativePort;
+    _$impls[$a] = $impl;
+    $p.listen(($m) {
+      if ($m == null) {
+        _$impls.remove($p.sendPort.nativePort);
+        $p.close();
+        return;
+      }
+      final $i = $MethodInvocation.fromMessage($m);
+      final $r = _$invokeMethod($p.sendPort.nativePort, $i);
+      ProtectedJniExtensions.returnResult($i.result, $r);
+    });
+    return $x;
+  }
+}
+
+abstract class $MyRunnableImpl {
+  factory $MyRunnableImpl({
+    required void Function() run,
+  }) = _$MyRunnableImpl;
+
+  void run();
+}
+
+class _$MyRunnableImpl implements $MyRunnableImpl {
+  _$MyRunnableImpl({
+    required void Function() run,
+  }) : _run = run;
+
+  final void Function() _run;
+
+  void run() {
+    return _run();
+  }
+}
+
+class $MyRunnableType extends jni.JObjType<MyRunnable> {
+  const $MyRunnableType();
+
+  @override
+  String get signature =>
+      r"Lcom/github/dart_lang/jnigen/interfaces/MyRunnable;";
+
+  @override
+  MyRunnable fromRef(jni.JObjectPtr ref) => MyRunnable.fromRef(ref);
+
+  @override
+  jni.JObjType get superType => const jni.JObjectType();
+
+  @override
+  final superCount = 1;
+
+  @override
+  int get hashCode => ($MyRunnableType).hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    return other.runtimeType == ($MyRunnableType) && other is $MyRunnableType;
+  }
+}
+
+/// from: com.github.dart_lang.jnigen.interfaces.MyRunnableRunner
+class MyRunnableRunner extends jni.JObject {
+  @override
+  late final jni.JObjType<MyRunnableRunner> $type = type;
+
+  MyRunnableRunner.fromRef(
+    jni.JObjectPtr ref,
+  ) : super.fromRef(ref);
+
+  /// The type which includes information such as the signature of this class.
+  static const type = $MyRunnableRunnerType();
+  static final _get_error = jniLookup<
+          ffi.NativeFunction<
+              jni.JniResult Function(
+                jni.JObjectPtr,
+              )>>("get_MyRunnableRunner__error")
+      .asFunction<
+          jni.JniResult Function(
+            jni.JObjectPtr,
+          )>();
+
+  static final _set_error = jniLookup<
+          ffi.NativeFunction<
+              jni.JniResult Function(jni.JObjectPtr,
+                  ffi.Pointer<ffi.Void>)>>("set_MyRunnableRunner__error")
+      .asFunction<
+          jni.JniResult Function(jni.JObjectPtr, ffi.Pointer<ffi.Void>)>();
+
+  /// from: public java.lang.Throwable error
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  jni.JObject get error =>
+      const jni.JObjectType().fromRef(_get_error(reference).object);
+
+  /// from: public java.lang.Throwable error
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  set error(jni.JObject value) =>
+      _set_error(reference, value.reference).check();
+
+  static final _new0 = jniLookup<
+              ffi
+              .NativeFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>>(
+          "MyRunnableRunner__new0")
+      .asFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>();
+
+  /// from: public void <init>(com.github.dart_lang.jnigen.interfaces.MyRunnable runnable)
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  factory MyRunnableRunner(
+    MyRunnable runnable,
+  ) {
+    return MyRunnableRunner.fromRef(_new0(runnable.reference).object);
+  }
+
+  static final _runOnSameThread = jniLookup<
+              ffi
+              .NativeFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>>(
+          "MyRunnableRunner__runOnSameThread")
+      .asFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>();
+
+  /// from: public void runOnSameThread()
+  void runOnSameThread() {
+    return _runOnSameThread(reference).check();
+  }
+
+  static final _runOnAnotherThread = jniLookup<
+              ffi
+              .NativeFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>>(
+          "MyRunnableRunner__runOnAnotherThread")
+      .asFunction<jni.JniResult Function(ffi.Pointer<ffi.Void>)>();
+
+  /// from: public void runOnAnotherThread()
+  void runOnAnotherThread() {
+    return _runOnAnotherThread(reference).check();
+  }
+}
+
+class $MyRunnableRunnerType extends jni.JObjType<MyRunnableRunner> {
+  const $MyRunnableRunnerType();
+
+  @override
+  String get signature =>
+      r"Lcom/github/dart_lang/jnigen/interfaces/MyRunnableRunner;";
+
+  @override
+  MyRunnableRunner fromRef(jni.JObjectPtr ref) => MyRunnableRunner.fromRef(ref);
+
+  @override
+  jni.JObjType get superType => const jni.JObjectType();
+
+  @override
+  final superCount = 1;
+
+  @override
+  int get hashCode => ($MyRunnableRunnerType).hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    return other.runtimeType == ($MyRunnableRunnerType) &&
+        other is $MyRunnableRunnerType;
+  }
+}
+
 /// from: com.github.dart_lang.jnigen.annotations.JsonSerializable$Case
 class JsonSerializable_Case extends jni.JObject {
   @override
diff --git a/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart
index 9ca0857..7b36cdd 100644
--- a/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart
+++ b/pkgs/jnigen/test/simple_package_test/dart_only/dart_bindings/simple_package.dart
@@ -3180,42 +3180,46 @@
     int $p,
     $MethodInvocation $i,
   ) {
-    final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
-    final $a = $i.args;
-    if ($d == r"voidCallback(Ljava/lang/String;)V") {
-      _$impls[$p]!.voidCallback(
-        $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
-      );
-      return jni.nullptr;
-    }
-    if ($d == r"stringCallback(Ljava/lang/String;)Ljava/lang/String;") {
-      final $r = _$impls[$p]!.stringCallback(
-        $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
-      );
-      return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
-    }
-    if ($d == r"varCallback(Ljava/lang/Object;)Ljava/lang/Object;") {
-      final $r = _$impls[$p]!.varCallback(
-        $a[0].castTo(_$impls[$p]!.T, deleteOriginal: true),
-      );
-      return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
-    }
-    if ($d == r"manyPrimitives(IZCD)J") {
-      final $r = _$impls[$p]!.manyPrimitives(
-        $a[0]
-            .castTo(const jni.JIntegerType(), deleteOriginal: true)
-            .intValue(deleteOriginal: true),
-        $a[1]
-            .castTo(const jni.JBooleanType(), deleteOriginal: true)
-            .booleanValue(deleteOriginal: true),
-        $a[2]
-            .castTo(const jni.JCharacterType(), deleteOriginal: true)
-            .charValue(deleteOriginal: true),
-        $a[3]
-            .castTo(const jni.JDoubleType(), deleteOriginal: true)
-            .doubleValue(deleteOriginal: true),
-      );
-      return jni.JLong($r).toPointer();
+    try {
+      final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
+      final $a = $i.args;
+      if ($d == r"voidCallback(Ljava/lang/String;)V") {
+        _$impls[$p]!.voidCallback(
+          $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
+        );
+        return jni.nullptr;
+      }
+      if ($d == r"stringCallback(Ljava/lang/String;)Ljava/lang/String;") {
+        final $r = _$impls[$p]!.stringCallback(
+          $a[0].castTo(const jni.JStringType(), deleteOriginal: true),
+        );
+        return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
+      }
+      if ($d == r"varCallback(Ljava/lang/Object;)Ljava/lang/Object;") {
+        final $r = _$impls[$p]!.varCallback(
+          $a[0].castTo(_$impls[$p]!.T, deleteOriginal: true),
+        );
+        return ($r as jni.JObject).castTo(const jni.JObjectType()).toPointer();
+      }
+      if ($d == r"manyPrimitives(IZCD)J") {
+        final $r = _$impls[$p]!.manyPrimitives(
+          $a[0]
+              .castTo(const jni.JIntegerType(), deleteOriginal: true)
+              .intValue(deleteOriginal: true),
+          $a[1]
+              .castTo(const jni.JBooleanType(), deleteOriginal: true)
+              .booleanValue(deleteOriginal: true),
+          $a[2]
+              .castTo(const jni.JCharacterType(), deleteOriginal: true)
+              .charValue(deleteOriginal: true),
+          $a[3]
+              .castTo(const jni.JDoubleType(), deleteOriginal: true)
+              .doubleValue(deleteOriginal: true),
+        );
+        return jni.JLong($r).toPointer();
+      }
+    } catch (e) {
+      return ProtectedJniExtensions.newDartException(e.toString());
     }
     return jni.nullptr;
   }
@@ -3452,6 +3456,231 @@
   }
 }
 
+/// from: com.github.dart_lang.jnigen.interfaces.MyRunnable
+class MyRunnable extends jni.JObject {
+  @override
+  late final jni.JObjType<MyRunnable> $type = type;
+
+  MyRunnable.fromRef(
+    jni.JObjectPtr ref,
+  ) : super.fromRef(ref);
+
+  static final _class =
+      jni.Jni.findJClass(r"com/github/dart_lang/jnigen/interfaces/MyRunnable");
+
+  /// The type which includes information such as the signature of this class.
+  static const type = $MyRunnableType();
+  static final _id_run =
+      jni.Jni.accessors.getMethodIDOf(_class.reference, r"run", r"()V");
+
+  /// from: public abstract void run()
+  void run() {
+    return jni.Jni.accessors.callMethodWithArgs(
+        reference, _id_run, jni.JniCallType.voidType, []).check();
+  }
+
+  /// Maps a specific port to the implemented interface.
+  static final Map<int, $MyRunnableImpl> _$impls = {};
+  ReceivePort? _$p;
+
+  static jni.JObjectPtr _$invoke(
+    int port,
+    jni.JObjectPtr descriptor,
+    jni.JObjectPtr args,
+  ) {
+    return _$invokeMethod(
+      port,
+      $MethodInvocation.fromAddresses(
+        0,
+        descriptor.address,
+        args.address,
+      ),
+    );
+  }
+
+  static final ffi.Pointer<
+          ffi.NativeFunction<
+              jni.JObjectPtr Function(
+                  ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>>
+      _$invokePointer = ffi.Pointer.fromFunction(_$invoke);
+
+  static ffi.Pointer<ffi.Void> _$invokeMethod(
+    int $p,
+    $MethodInvocation $i,
+  ) {
+    try {
+      final $d = $i.methodDescriptor.toDartString(deleteOriginal: true);
+      final $a = $i.args;
+      if ($d == r"run()V") {
+        _$impls[$p]!.run();
+        return jni.nullptr;
+      }
+    } catch (e) {
+      return ProtectedJniExtensions.newDartException(e.toString());
+    }
+    return jni.nullptr;
+  }
+
+  factory MyRunnable.implement(
+    $MyRunnableImpl $impl,
+  ) {
+    final $p = ReceivePort();
+    final $x = MyRunnable.fromRef(
+      ProtectedJniExtensions.newPortProxy(
+        r"com.github.dart_lang.jnigen.interfaces.MyRunnable",
+        $p,
+        _$invokePointer,
+      ),
+    ).._$p = $p;
+    final $a = $p.sendPort.nativePort;
+    _$impls[$a] = $impl;
+    $p.listen(($m) {
+      if ($m == null) {
+        _$impls.remove($p.sendPort.nativePort);
+        $p.close();
+        return;
+      }
+      final $i = $MethodInvocation.fromMessage($m);
+      final $r = _$invokeMethod($p.sendPort.nativePort, $i);
+      ProtectedJniExtensions.returnResult($i.result, $r);
+    });
+    return $x;
+  }
+}
+
+abstract class $MyRunnableImpl {
+  factory $MyRunnableImpl({
+    required void Function() run,
+  }) = _$MyRunnableImpl;
+
+  void run();
+}
+
+class _$MyRunnableImpl implements $MyRunnableImpl {
+  _$MyRunnableImpl({
+    required void Function() run,
+  }) : _run = run;
+
+  final void Function() _run;
+
+  void run() {
+    return _run();
+  }
+}
+
+class $MyRunnableType extends jni.JObjType<MyRunnable> {
+  const $MyRunnableType();
+
+  @override
+  String get signature =>
+      r"Lcom/github/dart_lang/jnigen/interfaces/MyRunnable;";
+
+  @override
+  MyRunnable fromRef(jni.JObjectPtr ref) => MyRunnable.fromRef(ref);
+
+  @override
+  jni.JObjType get superType => const jni.JObjectType();
+
+  @override
+  final superCount = 1;
+
+  @override
+  int get hashCode => ($MyRunnableType).hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    return other.runtimeType == ($MyRunnableType) && other is $MyRunnableType;
+  }
+}
+
+/// from: com.github.dart_lang.jnigen.interfaces.MyRunnableRunner
+class MyRunnableRunner extends jni.JObject {
+  @override
+  late final jni.JObjType<MyRunnableRunner> $type = type;
+
+  MyRunnableRunner.fromRef(
+    jni.JObjectPtr ref,
+  ) : super.fromRef(ref);
+
+  static final _class = jni.Jni.findJClass(
+      r"com/github/dart_lang/jnigen/interfaces/MyRunnableRunner");
+
+  /// The type which includes information such as the signature of this class.
+  static const type = $MyRunnableRunnerType();
+  static final _id_error = jni.Jni.accessors.getFieldIDOf(
+    _class.reference,
+    r"error",
+    r"Ljava/lang/Throwable;",
+  );
+
+  /// from: public java.lang.Throwable error
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  jni.JObject get error => const jni.JObjectType().fromRef(jni.Jni.accessors
+      .getField(reference, _id_error, jni.JniCallType.objectType)
+      .object);
+
+  /// from: public java.lang.Throwable error
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  set error(jni.JObject value) =>
+      jni.Jni.env.SetObjectField(reference, _id_error, value.reference);
+
+  static final _id_new0 = jni.Jni.accessors.getMethodIDOf(_class.reference,
+      r"<init>", r"(Lcom/github/dart_lang/jnigen/interfaces/MyRunnable;)V");
+
+  /// from: public void <init>(com.github.dart_lang.jnigen.interfaces.MyRunnable runnable)
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  factory MyRunnableRunner(
+    MyRunnable runnable,
+  ) {
+    return MyRunnableRunner.fromRef(jni.Jni.accessors.newObjectWithArgs(
+        _class.reference, _id_new0, [runnable.reference]).object);
+  }
+
+  static final _id_runOnSameThread = jni.Jni.accessors
+      .getMethodIDOf(_class.reference, r"runOnSameThread", r"()V");
+
+  /// from: public void runOnSameThread()
+  void runOnSameThread() {
+    return jni.Jni.accessors.callMethodWithArgs(
+        reference, _id_runOnSameThread, jni.JniCallType.voidType, []).check();
+  }
+
+  static final _id_runOnAnotherThread = jni.Jni.accessors
+      .getMethodIDOf(_class.reference, r"runOnAnotherThread", r"()V");
+
+  /// from: public void runOnAnotherThread()
+  void runOnAnotherThread() {
+    return jni.Jni.accessors.callMethodWithArgs(reference,
+        _id_runOnAnotherThread, jni.JniCallType.voidType, []).check();
+  }
+}
+
+class $MyRunnableRunnerType extends jni.JObjType<MyRunnableRunner> {
+  const $MyRunnableRunnerType();
+
+  @override
+  String get signature =>
+      r"Lcom/github/dart_lang/jnigen/interfaces/MyRunnableRunner;";
+
+  @override
+  MyRunnableRunner fromRef(jni.JObjectPtr ref) => MyRunnableRunner.fromRef(ref);
+
+  @override
+  jni.JObjType get superType => const jni.JObjectType();
+
+  @override
+  final superCount = 1;
+
+  @override
+  int get hashCode => ($MyRunnableRunnerType).hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    return other.runtimeType == ($MyRunnableRunnerType) &&
+        other is $MyRunnableRunnerType;
+  }
+}
+
 /// from: com.github.dart_lang.jnigen.annotations.JsonSerializable$Case
 class JsonSerializable_Case extends jni.JObject {
   @override
diff --git a/pkgs/jnigen/test/simple_package_test/generate.dart b/pkgs/jnigen/test/simple_package_test/generate.dart
index 446cfb2..9f59888 100644
--- a/pkgs/jnigen/test/simple_package_test/generate.dart
+++ b/pkgs/jnigen/test/simple_package_test/generate.dart
@@ -32,6 +32,8 @@
   join(javaPrefix, 'generics', 'StringStack.java'),
   join(javaPrefix, 'generics', 'StringValuedMap.java'),
   join(javaPrefix, 'generics', 'StringKeyedMap.java'),
+  join(javaPrefix, 'interfaces', 'MyRunnable.java'),
+  join(javaPrefix, 'interfaces', 'MyRunnableRunner.java'),
   join(javaPrefix, 'interfaces', 'MyInterface.java'),
   join(javaPrefix, 'interfaces', 'MyInterfaceConsumer.java'),
   join(javaPrefix, 'annotations', 'JsonSerializable.java'),
diff --git a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnable.java b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnable.java
new file mode 100644
index 0000000..4a39f81
--- /dev/null
+++ b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnable.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.github.dart_lang.jnigen.interfaces;
+
+public interface MyRunnable {
+  void run();
+}
diff --git a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java
new file mode 100644
index 0000000..2903f37
--- /dev/null
+++ b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java
@@ -0,0 +1,28 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.github.dart_lang.jnigen.interfaces;
+
+public class MyRunnableRunner {
+  final MyRunnable runnable;
+
+  public Throwable error;
+
+  public MyRunnableRunner(MyRunnable runnable) {
+    this.runnable = runnable;
+  }
+
+  public void runOnSameThread() {
+    try {
+      runnable.run();
+    } catch (Throwable e) {
+      error = e;
+    }
+  }
+
+  public void runOnAnotherThread() {
+    var thread = new Thread(this::runOnSameThread);
+    thread.start();
+  }
+}
diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart
index cae9758..07ed1d3 100644
--- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart
+++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart
@@ -24,8 +24,8 @@
   final pid = bean.callMethodByName<int>('getPid', '()J', []);
   ProcessResult result;
   do {
-    sleep(const Duration(milliseconds: 100));
     result = Process.runSync('jcmd', [pid.toString(), 'GC.run']);
+    sleep(const Duration(milliseconds: 100));
   } while (result.exitCode != 0);
 }
 
@@ -624,6 +624,50 @@
         expect(MyInterface.$impls, isEmpty);
       });
     }
+    group('Dart exceptions are handled', () {
+      for (final exception in [UnimplementedError(), 'Hello!']) {
+        for (final sameThread in [true, false]) {
+          test(
+              'on ${sameThread ? 'the same thread' : 'another thread'}'
+              ' throwing $exception', () async {
+            final runnable = MyRunnable.implement(
+              $MyRunnableImpl(
+                run: () {
+                  throw exception;
+                },
+              ),
+            );
+            final runner = MyRunnableRunner(runnable);
+            if (sameThread) {
+              runner.runOnSameThread();
+            } else {
+              runner.runOnAnotherThread();
+            }
+            while (runner.error.isNull) {
+              await Future<void>.delayed(const Duration(milliseconds: 100));
+            }
+            expect(
+              Jni.env.IsInstanceOf(
+                runner.error.reference,
+                Jni.findClass('java/lang/reflect/UndeclaredThrowableException'),
+              ),
+              isTrue,
+            );
+            final cause = runner.error.callMethodByName<JObject>(
+                'getCause', '()Ljava/lang/Throwable;', []);
+            expect(
+              Jni.env.IsInstanceOf(
+                cause.reference,
+                Jni.findClass(
+                    'com/github/dart_lang/jni/PortProxy\$DartException'),
+              ),
+              isTrue,
+            );
+            expect(cause.toString(), contains(exception.toString()));
+          });
+        }
+      }
+    });
   });
 
   group('$groupName (load tests)', () {