[jnigen] General cleanup (https://github.com/dart-lang/jnigen/issues/80)

diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index d3d44e7..060f8e4 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -121,11 +121,18 @@
         with:
           distribution: 'zulu'
           java-version: '11'
+      - name: install clang tools
+        run: |
+          sudo apt-get update -y
+          sudo apt-get install -y clang-format
       - run: flutter pub get
       - name: Check formatting
         run: flutter format --output=none --set-exit-if-changed .
       - name: Run lints
         run: flutter analyze --fatal-infos
+      - name: Check C code formatting using clang-format
+        run: clang-format --dry-run -Werror dartjni.c dartjni.h third_party/*.c third_party/*.h
+        working-directory: ./pkgs/jni/src
 
   test_jni:
     runs-on: ubuntu-latest
@@ -192,7 +199,9 @@
       - run: Add-Content $env:GITHUB_PATH "$env:JAVA_HOME\bin\server"
       - run: dart pub get
       - run: dart run jni:setup
-      - run: dart test
+      ## TODO(#51): -j 1 is required to avoid a rare race condition which is caused by
+      ## all tests which spawn JVM being run in same process.
+      - run: dart test -j 1
 
   test_jnigen_windows_minimal:
     needs: [analyze_jnigen]
@@ -362,7 +371,7 @@
           diff -qr _c src/
           diff -qr _dart lib/third_party
       - name: Generate full bindings
-        run: dart run jnigen --config jnigen_full.yaml
+        run: dart run jnigen --config jnigen.yaml --override classes="org.apache.pdfbox.pdmodel;org.apache.pdfbox.text"
       - name: Analyze generated bindings
         run: |
           flutter pub get # dart-analyze errors on flutter example
diff --git a/pkgs/jni/README.md b/pkgs/jni/README.md
index 3f4ba5d..79ce932 100644
--- a/pkgs/jni/README.md
+++ b/pkgs/jni/README.md
@@ -17,7 +17,7 @@
 __To generate type-safe bindings from Java libraries, use `jnigen`.__
 
 ## SDK Note
-Dart standalone is supported, but due to some current limitations of the `pubspec` format, `dart` command must be from Flutter SDK and not dart SDK. See dart-lang/pub#3563.
+Dart standalone is supported, but due to some current limitations of the `pubspec` format, `dart` command must be from Flutter SDK and not dart SDK.
 
 ## Version note
 This library is at an early stage of development and we do not provide backwards compatibility of the API at this point.
diff --git a/pkgs/jni/android/README.md b/pkgs/jni/android/README.md
new file mode 100644
index 0000000..7d56c69
--- /dev/null
+++ b/pkgs/jni/android/README.md
@@ -0,0 +1 @@
+This folder contains the [Android plugin class](src/main/java/com/github/dart_lang/jni/JniPlugin.java) which takes care of initializing JNI on Android.
diff --git a/pkgs/jni/bin/setup.dart b/pkgs/jni/bin/setup.dart
index ec62bb1..8f12c6b 100644
--- a/pkgs/jni/bin/setup.dart
+++ b/pkgs/jni/bin/setup.dart
@@ -7,111 +7,141 @@
 import 'package:args/args.dart';
 import 'package:package_config/package_config.dart';
 
-const _buildDir = "build-dir";
-const _srcDir = "source-dir";
+const ansiRed = '\x1b[31m';
+const ansiDefault = '\x1b[39;49m';
+
+const _defaultRelativeBuildPath = "build/jni_libs";
+
+const _buildPath = "build-path";
+const _srcPath = "source-path";
 const _packageName = 'package-name';
 const _verbose = "verbose";
 const _cmakeArgs = "cmake-args";
-const _clean = "clean";
 
-const _cmakeTemporaryFiles = [
-  'CMakeCache.txt',
-  'CMakeFiles/',
-  'cmake_install.cmake',
-  'Makefile'
-];
-
-void deleteCMakeTemps(Uri buildDir) async {
-  for (var filename in _cmakeTemporaryFiles) {
-    if (options.verbose) {
-      stderr.writeln('remove $filename');
-    }
-    await File(buildDir.resolve(filename).toFilePath()).delete(recursive: true);
+Future<void> runCommand(
+    String exec, List<String> args, String workingDir) async {
+  // For printing relative path always.
+  var current = Directory.current.path;
+  if (!current.endsWith(Platform.pathSeparator)) {
+    current += Platform.pathSeparator;
   }
-}
+  if (workingDir.startsWith(current)) {
+    workingDir.replaceFirst(current, "");
+  }
 
-// Sets up input output channels and maintains state.
-class CommandRunner {
-  CommandRunner({this.printCmds = false});
-  bool printCmds = false;
-  int? time;
-  Future<CommandRunner> run(
-      String exec, List<String> args, String workingDir) async {
-    if (printCmds) {
-      final cmd = "$exec ${args.join(" ")}";
-      stderr.writeln("\n+ [$workingDir] $cmd");
-    }
-    final process = await Process.start(exec, args,
-        workingDirectory: workingDir,
-        runInShell: true,
-        mode: ProcessStartMode.inheritStdio);
-    final exitCode = await process.exitCode;
+  final cmd = "$exec ${args.join(" ")}";
+  stderr.writeln("+ [$workingDir] $cmd");
+  int exitCode;
+  if (options.verbose) {
+    final process = await Process.start(
+      exec, args,
+      workingDirectory: workingDir,
+      mode: ProcessStartMode.inheritStdio,
+      // without `runInShell`, sometimes cmake doesn't run on windows.
+      runInShell: true,
+    );
+    exitCode = await process.exitCode;
+  } else {
+    // ProcessStartMode.normal sometimes hangs on windows. No idea why.
+    final process = await Process.run(exec, args,
+        runInShell: true, workingDirectory: workingDir);
+    exitCode = process.exitCode;
     if (exitCode != 0) {
-      stderr.writeln("command exited with $exitCode");
+      var out = process.stdout;
+      var err = process.stderr;
+      if (stdout.supportsAnsiEscapes) {
+        out = "$ansiRed$out$ansiDefault";
+        err = "$ansiRed$err$ansiDefault";
+      }
+      stdout.writeln(out);
+      stderr.writeln(err);
     }
-    return this;
+  }
+  if (exitCode != 0) {
+    stderr.writeln("Command exited with $exitCode.");
   }
 }
 
 class Options {
   Options(ArgResults arg)
-      : buildDir = arg[_buildDir],
-        srcDir = arg[_srcDir],
+      : buildPath = arg[_buildPath],
+        srcPath = arg[_srcPath],
         packageName = arg[_packageName] ?? 'jni',
         cmakeArgs = arg[_cmakeArgs],
-        verbose = arg[_verbose] ?? false,
-        clean = arg[_clean] ?? false;
+        verbose = arg[_verbose] ?? false;
 
-  String? buildDir, srcDir;
+  String? buildPath, srcPath;
   String packageName;
   List<String> cmakeArgs;
-  bool verbose, clean;
+  bool verbose;
 }
 
 late Options options;
-void log(String msg) {
+
+void verboseLog(String msg) {
   if (options.verbose) {
     stderr.writeln(msg);
   }
 }
 
-/// tries to find package:jni's source folder in pub cache
-/// if not possible, returns null.
-Future<String?> findSources() async {
+/// Find path to C sources in pub cache for package specified by [packageName].
+///
+/// It's assumed C FFI sources are in "src/" relative to package root.
+/// If package cannot be found, null is returned.
+Future<String?> findSources(String packageName) async {
   final packageConfig = await findPackageConfig(Directory.current);
   if (packageConfig == null) {
     return null;
   }
-  final packages = packageConfig.packages;
-  for (var package in packages) {
-    if (package.name == options.packageName) {
-      return package.root.resolve("src/").toFilePath();
+  final package = packageConfig[options.packageName];
+  if (package == null) {
+    return null;
+  }
+  return package.root.resolve("src").toFilePath();
+}
+
+/// Returns true if [artifact] does not exist, or any file in [sourceDir] is
+/// newer than [artifact].
+bool needsBuild(File artifact, Directory sourceDir) {
+  if (!artifact.existsSync()) return true;
+  final fileLastModified = artifact.lastModifiedSync();
+  for (final entry in sourceDir.listSync(recursive: true)) {
+    if (entry.statSync().modified.isAfter(fileLastModified)) {
+      return true;
     }
   }
-  return null;
+  return false;
+}
+
+/// Returns the name of file built using sources in [cDir]
+String getTargetName(Directory cDir) {
+  for (final file in cDir.listSync(recursive: true)) {
+    if (file.path.endsWith(".c")) {
+      final cFileName = file.uri.pathSegments.last;
+      final librarySuffix = Platform.isWindows ? "dll" : "so";
+      return cFileName.substring(0, cFileName.length - 1) + librarySuffix;
+    }
+  }
+  throw Exception("Could not find a C file in ${cDir.path}");
 }
 
 void main(List<String> arguments) async {
   final parser = ArgParser()
-    ..addOption(_buildDir,
-        abbr: 'B', help: 'Directory to place built artifacts')
-    ..addOption(_srcDir,
-        abbr: 'S', help: 'alternative path to package:jni sources')
+    ..addOption(_buildPath,
+        abbr: 'b', help: 'Directory to place built artifacts')
+    ..addOption(_srcPath,
+        abbr: 's', help: 'alternative path to package:jni sources')
     ..addOption(_packageName,
         abbr: 'p',
         help: 'package for which native'
             'library should be built',
         defaultsTo: 'jni')
     ..addFlag(_verbose, abbr: 'v', help: 'Enable verbose output')
-    ..addFlag(_clean,
-        negatable: false,
-        abbr: 'C',
-        help: 'Clear built artifacts instead of running a build')
     ..addMultiOption(_cmakeArgs,
-        abbr: 'm', help: 'additional argument to pass to CMake');
-  final cli = parser.parse(arguments);
-  options = Options(cli);
-  final rest = cli.rest;
+        abbr: 'm', help: 'Pass additional argument to CMake');
+  final argResults = parser.parse(arguments);
+  options = Options(argResults);
+  final rest = argResults.rest;
 
   if (rest.isNotEmpty) {
     stderr.writeln("one or more unrecognized arguments: $rest");
@@ -121,79 +151,69 @@
     return;
   }
 
-  final srcPath = options.srcDir ?? await findSources();
+  final srcPath = options.srcPath ?? await findSources(options.packageName);
 
   if (srcPath == null) {
-    stderr.writeln("No sources specified and current directory is not a "
-        "package root.");
+    stderr.writeln("Cannot find sources for package ${options.packageName} "
+        "and no sources were manually specified.");
     exitCode = 1;
     return;
   }
 
   final srcDir = Directory(srcPath);
-  if (!await srcDir.exists() && !options.clean) {
-    throw 'Directory $srcPath does not exist';
+  if (!srcDir.existsSync()) {
+    stderr.writeln('Directory $srcPath does not exist');
+    exitCode = 1;
+    return;
   }
 
-  log("srcPath: $srcPath");
+  verboseLog("srcPath: $srcPath");
 
-  final currentDirUri = Uri.file(".");
-  final buildPath =
-      options.buildDir ?? currentDirUri.resolve("build/jni_libs").toFilePath();
+  final currentDirUri = Uri.directory(".");
+  final buildPath = options.buildPath ??
+      currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
   final buildDir = Directory(buildPath);
   await buildDir.create(recursive: true);
-  log("buildPath: $buildPath");
+  verboseLog("buildPath: $buildPath");
 
   if (buildDir.absolute.uri == srcDir.absolute.uri) {
     stderr.writeln("Please build in a directory different than source.");
-    exit(2);
+    exitCode = 1;
+    return;
   }
 
-  if (options.clean) {
-    await cleanup(options, srcDir.absolute.path, buildDir.absolute.path);
-  } else {
-    // pass srcDir absolute path because it will be passed to CMake as arg
-    // which will be running in different directory
-    final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
-    final jniDir = Directory.fromUri(jniDirUri);
-    await jniDir.create(recursive: true);
-    final tempDir = await jniDir.createTemp("jni_native_build_");
-    await build(options, srcDir.absolute.path, tempDir.path);
-    final dllDirUri =
-        Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
-    final dllDir = Directory.fromUri(dllDirUri);
-    for (var entry in dllDir.listSync()) {
-      final dllSuffix = Platform.isWindows ? "dll" : "so";
-      if (entry.path.endsWith(dllSuffix)) {
-        final dllName = entry.uri.pathSegments.last;
-        final target = buildDir.uri.resolve(dllName);
-        entry.renameSync(target.toFilePath());
-      }
-    }
-    await tempDir.delete(recursive: true);
+  final targetFileUri = buildDir.uri.resolve(getTargetName(srcDir));
+  final targetFile = File.fromUri(targetFileUri);
+  if (!needsBuild(targetFile, srcDir)) {
+    verboseLog("last modified of ${targetFile.path}: "
+        "${targetFile.lastModifiedSync()}");
+    stderr.writeln("target newer than source, skipping build");
+    return;
   }
-}
 
-Future<void> build(Options options, String srcPath, String buildPath) async {
-  final runner = CommandRunner(printCmds: true);
+  // Note: creating temp dir in .dart_tool/jni instead of SystemTemp
+  // because latter can fail tests on Windows CI, when system temp is on
+  // separate drive or something.
+  final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
+  final jniDir = Directory.fromUri(jniDirUri);
+  await jniDir.create(recursive: true);
+  final tempDir = await jniDir.createTemp("jni_native_build_");
   final cmakeArgs = <String>[];
   cmakeArgs.addAll(options.cmakeArgs);
-  cmakeArgs.add(srcPath);
-  await runner.run("cmake", cmakeArgs, buildPath);
-  await runner.run("cmake", ["--build", "."], buildPath);
-}
-
-Future<void> cleanup(Options options, String srcPath, String buildPath) async {
-  if (srcPath == buildPath) {
-    stderr.writeln('Error: build path is same as source path.');
+  // Pass absolute path of srcDir because cmake command is run in temp dir
+  cmakeArgs.add(srcDir.absolute.path);
+  await runCommand("cmake", cmakeArgs, tempDir.path);
+  await runCommand("cmake", ["--build", "."], tempDir.path);
+  final dllDirUri =
+      Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
+  final dllDir = Directory.fromUri(dllDirUri);
+  for (var entry in dllDir.listSync()) {
+    final dllSuffix = Platform.isWindows ? "dll" : "so";
+    if (entry.path.endsWith(dllSuffix)) {
+      final dllName = entry.uri.pathSegments.last;
+      final target = buildDir.uri.resolve(dllName);
+      entry.renameSync(target.toFilePath());
+    }
   }
-
-  stderr.writeln("deleting $buildPath");
-
-  try {
-    await Directory(buildPath).delete(recursive: true);
-  } catch (e) {
-    stderr.writeln("Error: cannot be deleted");
-    stderr.writeln(e);
-  }
+  await tempDir.delete(recursive: true);
 }
diff --git a/pkgs/jni/example/integration_test/jni_object_test.dart b/pkgs/jni/example/integration_test/on_device_jni_test.dart
similarity index 85%
rename from pkgs/jni/example/integration_test/jni_object_test.dart
rename to pkgs/jni/example/integration_test/on_device_jni_test.dart
index aee60d9..5b0fd84 100644
--- a/pkgs/jni/example/integration_test/jni_object_test.dart
+++ b/pkgs/jni/example/integration_test/on_device_jni_test.dart
@@ -2,6 +2,11 @@
 // 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.
 
+// This file contains mostly the same tests from package:jni's
+// test/jni_object_test.dart. This file can be run on android device using
+// flutter test integration_test/ and therefore useful for checking the
+// working of JNI on an Android device (or emulator).
+
 import 'dart:io';
 
 import 'package:flutter_test/flutter_test.dart';
@@ -80,6 +85,15 @@
     expect(maxLong, equals(32767));
   });
 
+  testWidgets("Call method with null argument, expect exception",
+      (tester) async {
+    final integerClass = Jni.findJniClass("java/lang/Integer");
+    expect(
+        () => integerClass.callStaticMethodByName<int>(
+            "parseInt", "(Ljava/lang/String;)I", [nullptr]),
+        throwsException);
+  });
+
   testWidgets("callStaticStringMethod", (t) async {
     final longClass = Jni.findJniClass("java/lang/Long");
     const n = 1223334444;
diff --git a/pkgs/jni/example/lib/main.dart b/pkgs/jni/example/lib/main.dart
index aaa5341..69f5933 100644
--- a/pkgs/jni/example/lib/main.dart
+++ b/pkgs/jni/example/lib/main.dart
@@ -13,6 +13,7 @@
 
 // An example of calling JNI methods using low level primitives.
 // GlobalJniEnv is a thin abstraction over JNIEnv in JNI C API.
+//
 // For a more ergonomic API for common use cases of calling methods and
 // accessing fields, see next examples using JniObject and JniClass.
 String toJavaStringUsingEnv(int n) => using((arena) {
@@ -78,7 +79,7 @@
         Jni.getCachedApplicationContext(),
         "😀",
         0,
-      ]).use((toast) => toast.callMethodByName("show", "()V", []));
+      ]).callMethodByName<void>("show", "()V", []);
 }
 
 void main() {
diff --git a/pkgs/jni/example/pubspec.lock b/pkgs/jni/example/pubspec.lock
index cb53cc4..129041a 100644
--- a/pkgs/jni/example/pubspec.lock
+++ b/pkgs/jni/example/pubspec.lock
@@ -130,7 +130,7 @@
       path: ".."
       relative: true
     source: path
-    version: "0.1.0"
+    version: "0.1.1"
   lints:
     dependency: transitive
     description:
diff --git a/pkgs/jni/ffigen.yaml b/pkgs/jni/ffigen.yaml
index 8fdb4f1..3833215 100644
--- a/pkgs/jni/ffigen.yaml
+++ b/pkgs/jni/ffigen.yaml
@@ -11,19 +11,17 @@
 output: 'lib/src/third_party/jni_bindings_generated.dart'
 headers:
   entry-points:
-    - 'src/global_jni_env.h'
+    - 'src/dartjni.h' # Exports majority of JNI functions
+    - 'src/third_party/global_jni_env.h' # Exports GlobalJniEnv type
   include-directives:
     - 'src/dartjni.h'
-    - 'src/global_jni_env.h'
-    - 'third_party/jni.h'
+    - 'src/third_party/global_jni_env.h'
+    - 'third_party/jni.h' # jni.h from Android NDK
 compiler-opts:
   - '-Ithird_party/'
 functions:
   exclude: # Exclude init functions supposed to be defined in loaded DLL, not JNI
-    - 'JNI_OnLoad'
-    - 'JNI_OnUnload'
-    - 'JNI_OnLoad_L'
-    - 'JNI_OnUnload_L'
+    - 'JNI_.*'
     - 'GetJniContext'
     - 'setJniGetters'
     - 'jni_log'
@@ -33,6 +31,7 @@
     - 'JNIEnv'
     - '_JNIEnv'
     - 'JNIInvokeInterface'
+    - '__va_list_tag'
   rename:
     ## opaque struct definitions, base types of jfieldID and jmethodID
     '_jfieldID': 'jfieldID_'
@@ -41,9 +40,6 @@
 unions:
   rename:
     'jvalue': 'JValue'
-enums:
-  rename:
-    'DartJniLogLevel': 'JniLogLevel'
 globals:
   exclude:
     - 'jni'
@@ -51,6 +47,8 @@
     - 'context_getter'
     - 'env_getter'
 typedefs:
+  exclude:
+    - 'va_list'
   rename:
     'JNI(.*)': 'Jni$1'
     'jint': 'JInt'
diff --git a/pkgs/jni/lib/internal_helpers_for_jnigen.dart b/pkgs/jni/lib/internal_helpers_for_jnigen.dart
index fb0f36c..9f44e7a 100644
--- a/pkgs/jni/lib/internal_helpers_for_jnigen.dart
+++ b/pkgs/jni/lib/internal_helpers_for_jnigen.dart
@@ -8,3 +8,4 @@
 
 export 'src/jni.dart' show ProtectedJniExtensions;
 export 'src/jni_object.dart' show JniReference;
+export 'src/accessors.dart';
diff --git a/pkgs/jni/lib/jni.dart b/pkgs/jni/lib/jni.dart
index bb5a5ae..7306fd8 100644
--- a/pkgs/jni/lib/jni.dart
+++ b/pkgs/jni/lib/jni.dart
@@ -60,7 +60,7 @@
 library jni;
 
 export 'src/third_party/jni_bindings_generated.dart'
-    hide JNI_LOG_TAG, JniBindings, JniEnv, JniEnv1;
+    hide JniBindings, JniEnv, JniEnv1;
 export 'src/jni.dart' hide ProtectedJniExtensions;
 export 'src/jvalues.dart' hide JValueArgs, toJValues;
 export 'src/env_extensions.dart'
diff --git a/pkgs/jni/lib/src/accessors.dart b/pkgs/jni/lib/src/accessors.dart
new file mode 100644
index 0000000..030d87c
--- /dev/null
+++ b/pkgs/jni/lib/src/accessors.dart
@@ -0,0 +1,131 @@
+// Copyright (c) 2022, 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.
+
+import 'dart:ffi';
+import 'package:ffi/ffi.dart' show using;
+
+import 'package:jni/src/jvalues.dart';
+
+import 'third_party/jni_bindings_generated.dart';
+import 'jni.dart';
+import 'env_extensions.dart';
+
+Pointer<GlobalJniEnv> env = Jni.env;
+
+void _check(JThrowable exception) {
+  if (exception != nullptr) {
+    env.throwException(exception);
+  }
+}
+
+extension JniResultMethods on JniResult {
+  void check() => _check(exception);
+
+  int get byte {
+    check();
+    return result.b;
+  }
+
+  int get short {
+    check();
+    return result.s;
+  }
+
+  int get char {
+    check();
+    return result.c;
+  }
+
+  int get integer {
+    check();
+    return result.i;
+  }
+
+  int get long {
+    check();
+    return result.j;
+  }
+
+  double get float {
+    check();
+    return result.f;
+  }
+
+  double get doubleFloat {
+    check();
+    return result.d;
+  }
+
+  JObject get object {
+    check();
+    return result.l;
+  }
+
+  bool get boolean {
+    check();
+    return result.z != 0;
+  }
+}
+
+extension JniIdLookupResultMethods on JniPointerResult {
+  JMethodID get methodID {
+    _check(exception);
+    return id.cast<jmethodID_>();
+  }
+
+  JFieldID get fieldID {
+    _check(exception);
+    return id.cast<jfieldID_>();
+  }
+}
+
+extension JniClassLookupResultMethods on JniClassLookupResult {
+  JClass get checkedClassRef {
+    _check(exception);
+    return classRef;
+  }
+}
+
+extension JniAccessorWrappers on Pointer<JniAccessors> {
+  // TODO(PR): How to name these methods? These only wrap toNativeChars()
+  // so that generated bindings are less verbose.
+  JClass getClassOf(String internalName) =>
+      using((arena) => getClass(internalName.toNativeChars(arena)))
+          .checkedClassRef;
+
+  JMethodID getMethodIDOf(JClass cls, String name, String signature) =>
+      using((arena) => getMethodID(
+              cls, name.toNativeChars(arena), signature.toNativeChars(arena)))
+          .methodID;
+
+  JMethodID getStaticMethodIDOf(JClass cls, String name, String signature) =>
+      using((arena) => getStaticMethodID(
+              cls, name.toNativeChars(arena), signature.toNativeChars(arena)))
+          .methodID;
+
+  JFieldID getFieldIDOf(JClass cls, String name, String signature) =>
+      using((arena) => getFieldID(
+              cls, name.toNativeChars(arena), signature.toNativeChars(arena)))
+          .fieldID;
+
+  JFieldID getStaticFieldIDOf(JClass cls, String name, String signature) =>
+      using((arena) => getStaticFieldID(
+              cls, name.toNativeChars(arena), signature.toNativeChars(arena)))
+          .fieldID;
+
+  JniResult newObjectWithArgs(JClass cls, JMethodID ctor, List<dynamic> args) =>
+      using((arena) {
+        return newObject(cls, ctor, toJValues(args, allocator: arena));
+      });
+
+  JniResult callMethodWithArgs(
+          JObject obj, JMethodID id, int callType, List<dynamic> args) =>
+      using((arena) =>
+          callMethod(obj, id, callType, toJValues(args, allocator: arena)));
+
+  JniResult callStaticMethodWithArgs(
+          JClass cls, JMethodID id, int callType, List<dynamic> args) =>
+      using((arena) => callStaticMethod(
+          cls, id, callType, toJValues(args, allocator: arena)));
+}
diff --git a/pkgs/jni/lib/src/env_extensions.dart b/pkgs/jni/lib/src/env_extensions.dart
index 63fddd1..c9164ef 100644
--- a/pkgs/jni/lib/src/env_extensions.dart
+++ b/pkgs/jni/lib/src/env_extensions.dart
@@ -47,26 +47,25 @@
   ///
   /// If [describe] is true, a description is printed to screen.
   /// To access actual exception object, use `ExceptionOccurred`.
+  ///
+  /// TODO(#64): Do not keep reference to exception, store stack trace & error.
   void checkException({bool describe = false}) {
     final exc = ExceptionOccurred();
-    if (exc != nullptr) {
-      // TODO(#13): Doing this every time is expensive.
-      // Should lookup and cache method reference,
-      // and keep it alive by keeping a reference to Exception class.
-      final ecls = GetObjectClass(exc);
-      final toStr = GetMethodID(ecls, _toString, _toStringSig);
-      final jstr = CallObjectMethod(exc, toStr);
-      final dstr = asDartString(jstr);
-      for (final i in [jstr, ecls]) {
-        DeleteGlobalRef(i);
-      }
-      if (describe) {
-        ExceptionDescribe();
-      } else {
-        ExceptionClear();
-      }
-      throw JniException(exc, dstr);
+    throwException(exc);
+  }
+
+  /// Throw [exception] from JNI in Dart as a [JniException].
+  void throwException(JObject exception, {bool describe = false}) {
+    if (exception == nullptr) {
+      return;
     }
+    final exceptionClass = GetObjectClass(exception);
+    final toStringMethod = GetMethodID(exceptionClass, _toString, _toStringSig);
+    final javaDescription = CallObjectMethod(exception, toStringMethod);
+    final dartDescription = asDartString(javaDescription);
+    DeleteGlobalRef(javaDescription);
+    DeleteGlobalRef(exceptionClass);
+    throw JniException(exception, dartDescription);
   }
 
   /// Calls the printStackTrace on exception object
diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart
index 115a74a..2b8d10f 100644
--- a/pkgs/jni/lib/src/jni.dart
+++ b/pkgs/jni/lib/src/jni.dart
@@ -13,6 +13,7 @@
 import 'jvalues.dart';
 import 'jni_exceptions.dart';
 import 'jni_object.dart';
+import 'accessors.dart';
 
 String _getLibraryFileName(String base) {
   if (Platform.isLinux || Platform.isAndroid) {
@@ -146,10 +147,11 @@
   /// It provides an indirection over [JniEnv] so that it can be used from
   /// any thread, and always returns global object references.
   static Pointer<GlobalJniEnv> get env {
-    _env ??= _fetchGlobalEnv();
-    return _env!;
+    return _env ??= _fetchGlobalEnv();
   }
 
+  static Pointer<JniAccessors> get accessors => _bindings.GetAccessors();
+
   /// Returns current application context on Android.
   static JObject getCachedApplicationContext() {
     return _bindings.GetApplicationContext();
@@ -167,12 +169,8 @@
 
   /// Returns class reference found through system-specific mechanism
   static JClass findClass(String qualifiedName) => using((arena) {
-        final nameChars = qualifiedName.toNativeChars(arena);
-        final cls = _bindings.LoadClass(nameChars);
-        if (cls == nullptr) {
-          env.checkException();
-        }
-        return cls;
+        final cls = accessors.getClass(qualifiedName.toNativeChars(arena));
+        return cls.checkedClassRef;
       });
 
   /// Returns class for [qualifiedName] found by platform-specific mechanism,
@@ -257,7 +255,4 @@
     final lookup = dl.lookup;
     return lookup;
   }
-
-  /// Checks for and rethrows any pending exception in JNI as a [JniException].
-  static void checkException() => Jni.env.checkException();
 }
diff --git a/pkgs/jni/lib/src/jni_exceptions.dart b/pkgs/jni/lib/src/jni_exceptions.dart
index 0cb5806..939c465 100644
--- a/pkgs/jni/lib/src/jni_exceptions.dart
+++ b/pkgs/jni/lib/src/jni_exceptions.dart
@@ -45,7 +45,7 @@
 
 extension JniTypeNames on int {
   static const _names = {
-    JniType.boolType: 'bool',
+    JniType.booleanType: 'bool',
     JniType.byteType: 'byte',
     JniType.shortType: 'short',
     JniType.charType: 'char',
@@ -88,5 +88,6 @@
   String toString() => "Lookup for helper library $path failed.\n"
       "Please ensure that `dartjni` shared library is built.\n"
       "Provided jni:setup script can be used to build the shared library."
-      "If the library is already built, double check the path.";
+      "If the library is already built, ensure that the JVM libraries can be "
+      "loaded from Dart.";
 }
diff --git a/pkgs/jni/lib/src/jni_object.dart b/pkgs/jni/lib/src/jni_object.dart
index fc66c6a..80cf707 100644
--- a/pkgs/jni/lib/src/jni_object.dart
+++ b/pkgs/jni/lib/src/jni_object.dart
@@ -10,6 +10,7 @@
 import 'jni_exceptions.dart';
 import 'jni.dart';
 import 'env_extensions.dart';
+import 'accessors.dart';
 import 'jvalues.dart';
 
 // This typedef is needed because void is a keyword and cannot be used in
@@ -71,89 +72,23 @@
   }
 }
 
-class _CallGetMethods {
-  _CallGetMethods(this.getField, this.getStaticField, this.callMethod,
-      this.callStaticMethod);
-  Function(JObject, JFieldID) getField;
-  Function(JClass, JFieldID) getStaticField;
-  Function(JObject, JMethodID, Pointer<JValue>) callMethod;
-  Function(JClass, JMethodID, Pointer<JValue>) callStaticMethod;
-}
+final Pointer<JniAccessors> _accessors = Jni.accessors;
 
 final Pointer<GlobalJniEnv> _env = Jni.env;
 
-final Map<int, _CallGetMethods> _accessors = {
-  JniType.boolType: _CallGetMethods(
-    _env.GetBooleanField,
-    _env.GetStaticBooleanField,
-    _env.CallBooleanMethodA,
-    _env.CallStaticBooleanMethodA,
-  ),
-  JniType.byteType: _CallGetMethods(
-    _env.GetByteField,
-    _env.GetStaticByteField,
-    _env.CallByteMethodA,
-    _env.CallStaticByteMethodA,
-  ),
-  JniType.shortType: _CallGetMethods(
-    _env.GetShortField,
-    _env.GetStaticShortField,
-    _env.CallShortMethodA,
-    _env.CallStaticShortMethodA,
-  ),
-  JniType.charType: _CallGetMethods(
-    _env.GetCharField,
-    _env.GetStaticCharField,
-    _env.CallCharMethodA,
-    _env.CallStaticCharMethodA,
-  ),
-  JniType.intType: _CallGetMethods(
-    _env.GetIntField,
-    _env.GetStaticIntField,
-    _env.CallIntMethodA,
-    _env.CallStaticIntMethodA,
-  ),
-  JniType.longType: _CallGetMethods(
-    _env.GetLongField,
-    _env.GetStaticLongField,
-    _env.CallLongMethodA,
-    _env.CallStaticLongMethodA,
-  ),
-  JniType.floatType: _CallGetMethods(
-    _env.GetFloatField,
-    _env.GetStaticFloatField,
-    _env.CallFloatMethodA,
-    _env.CallStaticFloatMethodA,
-  ),
-  JniType.doubleType: _CallGetMethods(
-    _env.GetDoubleField,
-    _env.GetStaticDoubleField,
-    _env.CallDoubleMethodA,
-    _env.CallStaticDoubleMethodA,
-  ),
-  JniType.objectType: _CallGetMethods(
-    _env.GetObjectField,
-    _env.GetStaticObjectField,
-    _env.CallObjectMethodA,
-    _env.CallStaticObjectMethodA,
-  ),
-  JniType.voidType: _CallGetMethods(
-    (x, y) => throw ArgumentError('void passed as field type'),
-    (x, y) => throw ArgumentError('void passed as static field type'),
-    _env.CallVoidMethodA,
-    _env.CallStaticVoidMethodA,
-  ),
-};
-
-T _getID<T>(
-    T Function(Pointer<Void> ptr, Pointer<Char> name, Pointer<Char> sig) f,
+Pointer<T> _getID<T extends NativeType>(
+    JniPointerResult Function(
+            Pointer<Void> ptr, Pointer<Char> name, Pointer<Char> sig)
+        f,
     Pointer<Void> ptr,
     String name,
     String sig) {
   final result = using(
       (arena) => f(ptr, name.toNativeChars(arena), sig.toNativeChars(arena)));
-  _env.checkException();
-  return result;
+  if (result.exception != nullptr) {
+    env.throwException(result.exception);
+  }
+  return result.id.cast<T>();
 }
 
 int _getCallType(int? callType, int defaultType, Set<int> allowed) {
@@ -162,14 +97,14 @@
   throw InvalidCallTypeException(callType, allowed);
 }
 
-T _callOrGet<T>(int? callType, Function(int) f) {
+T _callOrGet<T>(int? callType, JniResult Function(int) function) {
   final int finalCallType;
-  T result;
+  late T result;
   switch (T) {
     case bool:
       finalCallType =
-          _getCallType(callType, JniType.boolType, {JniType.boolType});
-      result = (f(finalCallType) as int != 0) as T;
+          _getCallType(callType, JniType.booleanType, {JniType.booleanType});
+      result = function(finalCallType).boolean as T;
       break;
     case int:
       finalCallType = _getCallType(callType, JniType.intType, {
@@ -179,22 +114,44 @@
         JniType.intType,
         JniType.longType,
       });
-      result = f(finalCallType) as T;
+      final jniResult = function(finalCallType);
+      switch (finalCallType) {
+        case JniType.byteType:
+          result = jniResult.byte as T;
+          break;
+        case JniType.shortType:
+          result = jniResult.short as T;
+          break;
+        case JniType.charType:
+          result = jniResult.char as T;
+          break;
+        case JniType.intType:
+          result = jniResult.integer as T;
+          break;
+        case JniType.longType:
+          result = jniResult.long as T;
+          break;
+      }
       break;
     case double:
       finalCallType = _getCallType(callType, JniType.doubleType,
           {JniType.floatType, JniType.doubleType});
-      result = f(finalCallType) as T;
+      final jniResult = function(finalCallType);
+      switch (finalCallType) {
+        case JniType.floatType:
+          result = jniResult.float as T;
+          break;
+        case JniType.doubleType:
+          result = jniResult.doubleFloat as T;
+          break;
+      }
       break;
     case String:
     case JniObject:
     case JniString:
       finalCallType =
           _getCallType(callType, JniType.objectType, {JniType.objectType});
-      final ref = f(finalCallType) as JObject;
-      if (ref == nullptr) {
-        _env.checkException();
-      }
+      final ref = function(finalCallType).object;
       final ctor = T == String
           ? (ref) => _env.asDartString(ref, deleteOriginal: true)
           : (T == JniObject ? JniObject.fromRef : JniString.fromRef);
@@ -203,33 +160,27 @@
     case _VoidType:
       finalCallType =
           _getCallType(callType, JniType.voidType, {JniType.voidType});
-      f(finalCallType);
+      function(finalCallType).check();
       result = null as T;
       break;
     case dynamic:
-      result = f(callType ?? JniType.voidType) as T;
-      break;
+      throw UnsupportedError("Return type not specified for JNI call");
     default:
       throw UnsupportedError('Unknown type $T');
   }
   return result;
 }
 
-T _callMethod<T>(
-        int? callType, List<dynamic> args, Function(int, Pointer<JValue>) f) =>
+T _callMethod<T>(int? callType, List<dynamic> args,
+        JniResult Function(int, Pointer<JValue>) f) =>
     using((arena) {
       final jArgs = JValueArgs(args, arena);
-      final result = _callOrGet<T>(callType, (ct) => f(ct, jArgs.values));
-      jArgs.dispose();
-      if (result == 0 || result == 0.0 || result == null) {
-        _env.checkException();
-      }
-      return result;
+      arena.onReleaseAll(jArgs.dispose);
+      return _callOrGet<T>(callType, (ct) => f(ct, jArgs.values));
     });
 
-T _getField<T>(int? callType, Function(int) f) {
+T _getField<T>(int? callType, JniResult Function(int) f) {
   final result = _callOrGet<T>(callType, f);
-  _env.checkException();
   return result;
 }
 
@@ -243,8 +194,7 @@
   JniClass? _jniClass;
 
   JniClass get _class {
-    _jniClass ??= getClass();
-    return _jniClass!;
+    return _jniClass ??= getClass();
   }
 
   /// Deletes the JNI reference and marks this object as deleted. Any further
@@ -270,27 +220,29 @@
   /// Get [JFieldID] of instance field identified by [fieldName] & [signature].
   JFieldID getFieldID(String fieldName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetFieldID, _class.reference, fieldName, signature);
+    return _getID(
+        _accessors.getFieldID, _class.reference, fieldName, signature);
   }
 
   /// Get [JFieldID] of static field identified by [fieldName] & [signature].
   JFieldID getStaticFieldID(String fieldName, String signature) {
     _ensureNotDeleted();
-    return _getID(
-        _env.GetStaticFieldID, _class.reference, fieldName, signature);
+    return _getID<jfieldID_>(
+        _accessors.getStaticFieldID, _class.reference, fieldName, signature);
   }
 
   /// Get [JMethodID] of instance method [methodName] with [signature].
   JMethodID getMethodID(String methodName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetMethodID, _class.reference, methodName, signature);
+    return _getID<jmethodID_>(
+        _accessors.getMethodID, _class.reference, methodName, signature);
   }
 
   /// Get [JMethodID] of static method [methodName] with [signature].
   JMethodID getStaticMethodID(String methodName, String signature) {
     _ensureNotDeleted();
-    return _getID(
-        _env.GetStaticMethodID, _class.reference, methodName, signature);
+    return _getID<jmethodID_>(
+        _accessors.getStaticMethodID, _class.reference, methodName, signature);
   }
 
   /// Retrieve the value of the field using [fieldID].
@@ -304,8 +256,11 @@
   /// final value is returned.
   T getField<T>(JFieldID fieldID, [int? callType]) {
     _ensureNotDeleted();
+    if (callType == JniType.voidType) {
+      throw ArgumentError("void is not a valid field type.");
+    }
     return _getField<T>(
-        callType, (ct) => _accessors[ct]!.getField(reference, fieldID));
+        callType, (ct) => _accessors.getField(reference, fieldID, ct));
   }
 
   /// Get value of the field identified by [name] and [signature].
@@ -320,9 +275,12 @@
   ///
   /// See [getField] for an explanation about [callType] parameter.
   T getStaticField<T>(JFieldID fieldID, [int? callType]) {
+    if (callType == JniType.voidType) {
+      throw ArgumentError("void is not a valid field type.");
+    }
     _ensureNotDeleted();
     return _getField<T>(callType,
-        (ct) => _accessors[ct]!.getStaticField(_class.reference, fieldID));
+        (ct) => _accessors.getStaticField(_class.reference, fieldID, ct));
   }
 
   /// Get value of the static field identified by [name] and [signature].
@@ -342,7 +300,7 @@
   T callMethod<T>(JMethodID methodID, List<dynamic> args, [int? callType]) {
     _ensureNotDeleted();
     return _callMethod<T>(callType, args,
-        (ct, jvs) => _accessors[ct]!.callMethod(reference, methodID, jvs));
+        (ct, jvs) => _accessors.callMethod(reference, methodID, ct, jvs));
   }
 
   /// Call instance method identified by [name] and [signature].
@@ -359,11 +317,8 @@
   T callStaticMethod<T>(JMethodID methodID, List<dynamic> args,
       [int? callType]) {
     _ensureNotDeleted();
-    return _callMethod<T>(
-        callType,
-        args,
-        (ct, jvs) =>
-            _accessors[ct]!.callStaticMethod(reference, methodID, jvs));
+    return _callMethod<T>(callType, args,
+        (ct, jvs) => _accessors.callStaticMethod(reference, methodID, ct, jvs));
   }
 
   /// Call static method identified by [name] and [signature].
@@ -384,25 +339,29 @@
   /// Get [JFieldID] of static field [fieldName] with [signature].
   JFieldID getStaticFieldID(String fieldName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetStaticFieldID, reference, fieldName, signature);
+    return _getID<jfieldID_>(
+        _accessors.getStaticFieldID, reference, fieldName, signature);
   }
 
   /// Get [JMethodID] of static method [methodName] with [signature].
   JMethodID getStaticMethodID(String methodName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetStaticMethodID, reference, methodName, signature);
+    return _getID<jmethodID_>(
+        _accessors.getStaticMethodID, reference, methodName, signature);
   }
 
   /// Get [JFieldID] of field [fieldName] with [signature].
   JFieldID getFieldID(String fieldName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetFieldID, reference, fieldName, signature);
+    return _getID<jfieldID_>(
+        _accessors.getFieldID, reference, fieldName, signature);
   }
 
   /// Get [JMethodID] of method [methodName] with [signature].
   JMethodID getMethodID(String methodName, String signature) {
     _ensureNotDeleted();
-    return _getID(_env.GetMethodID, reference, methodName, signature);
+    return _getID<jmethodID_>(
+        _accessors.getMethodID, reference, methodName, signature);
   }
 
   /// Get [JMethodID] of constructor with [signature].
@@ -412,9 +371,12 @@
   ///
   /// See [JniObject.getField] for more explanation about [callType].
   T getStaticField<T>(JFieldID fieldID, [int? callType]) {
+    if (callType == JniType.voidType) {
+      throw ArgumentError("void is not a valid field type.");
+    }
     _ensureNotDeleted();
     return _getField<T>(
-        callType, (ct) => _accessors[ct]!.getStaticField(reference, fieldID));
+        callType, (ct) => _accessors.getStaticField(reference, fieldID, ct));
   }
 
   /// Get the value of static field identified by [name] and [signature].
@@ -432,11 +394,8 @@
   T callStaticMethod<T>(JMethodID methodID, List<dynamic> args,
       [int? callType]) {
     _ensureNotDeleted();
-    return _callMethod<T>(
-        callType,
-        args,
-        (ct, jvs) =>
-            _accessors[ct]!.callStaticMethod(reference, methodID, jvs));
+    return _callMethod<T>(callType, args,
+        (ct, jvs) => _accessors.callStaticMethod(reference, methodID, ct, jvs));
   }
 
   /// Call the static method identified by [name] and [signature].
@@ -452,11 +411,8 @@
   JniObject newInstance(JMethodID ctor, List<dynamic> args) => using((arena) {
         _ensureNotDeleted();
         final jArgs = JValueArgs(args, arena);
-        final res = _env.NewObjectA(reference, ctor, jArgs.values);
-        jArgs.dispose();
-        if (res == nullptr) {
-          _env.checkException();
-        }
+        arena.onReleaseAll(jArgs.dispose);
+        final res = _accessors.newObject(reference, ctor, jArgs.values).object;
         return JniObject.fromRef(res);
       });
 }
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 9b95e21..a93b135 100644
--- a/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
+++ b/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
@@ -62,64 +62,15 @@
           lookup)
       : _lookup = lookup;
 
-  /// VM initialization functions.
-  ///
-  /// Note these are the only symbols exported for JNI by the VM.
-  int JNI_GetDefaultJavaVMInitArgs(
-    ffi.Pointer<ffi.Void> arg0,
-  ) {
-    return _JNI_GetDefaultJavaVMInitArgs(
-      arg0,
-    );
+  ffi.Pointer<JniAccessors> GetAccessors() {
+    return _GetAccessors();
   }
 
-  late final _JNI_GetDefaultJavaVMInitArgsPtr =
-      _lookup<ffi.NativeFunction<JInt Function(ffi.Pointer<ffi.Void>)>>(
-          'JNI_GetDefaultJavaVMInitArgs');
-  late final _JNI_GetDefaultJavaVMInitArgs = _JNI_GetDefaultJavaVMInitArgsPtr
-      .asFunction<int Function(ffi.Pointer<ffi.Void>)>();
-
-  int JNI_CreateJavaVM(
-    ffi.Pointer<ffi.Pointer<JavaVM>> arg0,
-    ffi.Pointer<ffi.Pointer<JniEnv>> arg1,
-    ffi.Pointer<ffi.Void> arg2,
-  ) {
-    return _JNI_CreateJavaVM(
-      arg0,
-      arg1,
-      arg2,
-    );
-  }
-
-  late final _JNI_CreateJavaVMPtr = _lookup<
-      ffi.NativeFunction<
-          JInt Function(
-              ffi.Pointer<ffi.Pointer<JavaVM>>,
-              ffi.Pointer<ffi.Pointer<JniEnv>>,
-              ffi.Pointer<ffi.Void>)>>('JNI_CreateJavaVM');
-  late final _JNI_CreateJavaVM = _JNI_CreateJavaVMPtr.asFunction<
-      int Function(ffi.Pointer<ffi.Pointer<JavaVM>>,
-          ffi.Pointer<ffi.Pointer<JniEnv>>, ffi.Pointer<ffi.Void>)>();
-
-  int JNI_GetCreatedJavaVMs(
-    ffi.Pointer<ffi.Pointer<JavaVM>> arg0,
-    int arg1,
-    ffi.Pointer<JSize> arg2,
-  ) {
-    return _JNI_GetCreatedJavaVMs(
-      arg0,
-      arg1,
-      arg2,
-    );
-  }
-
-  late final _JNI_GetCreatedJavaVMsPtr = _lookup<
-      ffi.NativeFunction<
-          JInt Function(ffi.Pointer<ffi.Pointer<JavaVM>>, JSize,
-              ffi.Pointer<JSize>)>>('JNI_GetCreatedJavaVMs');
-  late final _JNI_GetCreatedJavaVMs = _JNI_GetCreatedJavaVMsPtr.asFunction<
-      int Function(
-          ffi.Pointer<ffi.Pointer<JavaVM>>, int, ffi.Pointer<JSize>)>();
+  late final _GetAccessorsPtr =
+      _lookup<ffi.NativeFunction<ffi.Pointer<JniAccessors> Function()>>(
+          'GetAccessors');
+  late final _GetAccessors =
+      _GetAccessorsPtr.asFunction<ffi.Pointer<JniAccessors> Function()>();
 
   ffi.Pointer<JavaVM> GetJavaVM() {
     return _GetJavaVM();
@@ -130,14 +81,6 @@
   late final _GetJavaVM =
       _GetJavaVMPtr.asFunction<ffi.Pointer<JavaVM> Function()>();
 
-  int DestroyJavaVM() {
-    return _DestroyJavaVM();
-  }
-
-  late final _DestroyJavaVMPtr =
-      _lookup<ffi.NativeFunction<ffi.Int Function()>>('DestroyJavaVM');
-  late final _DestroyJavaVM = _DestroyJavaVMPtr.asFunction<int Function()>();
-
   ffi.Pointer<JniEnv> GetJniEnv() {
     return _GetJniEnv();
   }
@@ -203,11 +146,6 @@
   late final _GetCurrentActivity =
       _GetCurrentActivityPtr.asFunction<JObject Function()>();
 
-  late final ffi.Pointer<GlobalJniEnv> _globalEnv =
-      _lookup<GlobalJniEnv>('globalEnv');
-
-  GlobalJniEnv get globalEnv => _globalEnv.ref;
-
   ffi.Pointer<GlobalJniEnv> GetGlobalEnv() {
     return _GetGlobalEnv();
   }
@@ -470,10 +408,7 @@
       ffi.NativeFunction<
           JObject Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>> NewObject;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JObject Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _NewObjectV;
+  external ffi.Pointer<ffi.Void> _NewObjectV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -499,10 +434,7 @@
               JObject Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallObjectMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JObject Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallObjectMethodV;
+  external ffi.Pointer<ffi.Void> _CallObjectMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -514,10 +446,7 @@
               JBoolean Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallBooleanMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JBoolean Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallBooleanMethodV;
+  external ffi.Pointer<ffi.Void> _CallBooleanMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -529,10 +458,7 @@
               JByte Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallByteMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JByte Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallByteMethodV;
+  external ffi.Pointer<ffi.Void> _CallByteMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -544,10 +470,7 @@
               JChar Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallCharMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JChar Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallCharMethodV;
+  external ffi.Pointer<ffi.Void> _CallCharMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -559,10 +482,7 @@
               JShort Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallShortMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JShort Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallShortMethodV;
+  external ffi.Pointer<ffi.Void> _CallShortMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -574,10 +494,7 @@
               JInt Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallIntMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JInt Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallIntMethodV;
+  external ffi.Pointer<ffi.Void> _CallIntMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -589,10 +506,7 @@
               JLong Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallLongMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JLong Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallLongMethodV;
+  external ffi.Pointer<ffi.Void> _CallLongMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -604,10 +518,7 @@
               JFloat Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallFloatMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JFloat Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallFloatMethodV;
+  external ffi.Pointer<ffi.Void> _CallFloatMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -619,10 +530,7 @@
               JDouble Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallDoubleMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JDouble Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallDoubleMethodV;
+  external ffi.Pointer<ffi.Void> _CallDoubleMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -634,10 +542,7 @@
               ffi.Void Function(ffi.Pointer<JniEnv1>, JObject, JMethodID)>>
       CallVoidMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          ffi.Void Function(ffi.Pointer<JniEnv1>, JObject, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallVoidMethodV;
+  external ffi.Pointer<ffi.Void> _CallVoidMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -650,10 +555,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualObjectMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JObject Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualObjectMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualObjectMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -666,10 +568,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualBooleanMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JBoolean Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualBooleanMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualBooleanMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -681,10 +580,7 @@
               JByte Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualByteMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JByte Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualByteMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualByteMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -696,10 +592,7 @@
               JChar Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualCharMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JChar Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualCharMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualCharMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -712,10 +605,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualShortMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JShort Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualShortMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualShortMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -727,10 +617,7 @@
               JInt Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualIntMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JInt Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualIntMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualIntMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -742,10 +629,7 @@
               JLong Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualLongMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JLong Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualLongMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualLongMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -758,10 +642,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualFloatMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JFloat Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualFloatMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualFloatMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -774,10 +655,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualDoubleMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JDouble Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualDoubleMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualDoubleMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -790,10 +668,7 @@
                   ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID)>>
       CallNonvirtualVoidMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          ffi.Void Function(ffi.Pointer<JniEnv1>, JObject, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallNonvirtualVoidMethodV;
+  external ffi.Pointer<ffi.Void> _CallNonvirtualVoidMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -907,10 +782,7 @@
               JObject Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticObjectMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JObject Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticObjectMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticObjectMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -922,10 +794,7 @@
               JBoolean Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticBooleanMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JBoolean Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticBooleanMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticBooleanMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -937,10 +806,7 @@
               JByte Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticByteMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JByte Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticByteMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticByteMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -952,10 +818,7 @@
               JChar Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticCharMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JChar Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticCharMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticCharMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -967,10 +830,7 @@
               JShort Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticShortMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JShort Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticShortMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticShortMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -982,10 +842,7 @@
               JInt Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticIntMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JInt Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticIntMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticIntMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -997,10 +854,7 @@
               JLong Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticLongMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JLong Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticLongMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticLongMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -1012,10 +866,7 @@
               JFloat Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticFloatMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JFloat Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticFloatMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticFloatMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -1027,10 +878,7 @@
               JDouble Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticDoubleMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          JDouble Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticDoubleMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticDoubleMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -1042,10 +890,7 @@
               ffi.Void Function(ffi.Pointer<JniEnv1>, JClass, JMethodID)>>
       CallStaticVoidMethod;
 
-  external ffi.Pointer<
-      ffi.NativeFunction<
-          ffi.Void Function(ffi.Pointer<JniEnv1>, JClass, JMethodID,
-              ffi.Pointer<__va_list_tag>)>> _CallStaticVoidMethodV;
+  external ffi.Pointer<ffi.Void> _CallStaticVoidMethodV;
 
   external ffi.Pointer<
       ffi.NativeFunction<
@@ -1517,19 +1362,6 @@
 typedef JMethodID = ffi.Pointer<jmethodID_>;
 typedef JFieldID = ffi.Pointer<jfieldID_>;
 typedef JThrowable = JObject;
-
-class __va_list_tag extends ffi.Struct {
-  @ffi.UnsignedInt()
-  external int gp_offset;
-
-  @ffi.UnsignedInt()
-  external int fp_offset;
-
-  external ffi.Pointer<ffi.Void> overflow_arg_area;
-
-  external ffi.Pointer<ffi.Void> reg_save_area;
-}
-
 typedef JString = JObject;
 typedef JArray = JObject;
 typedef JObjectArray = JArray;
@@ -1577,16 +1409,9 @@
   external int ignoreUnrecognized;
 }
 
-abstract class JniLogLevel {
-  static const int JNI_VERBOSE = 2;
-  static const int JNI_DEBUG = 3;
-  static const int JNI_INFO = 4;
-  static const int JNI_WARN = 5;
-  static const int JNI_ERROR = 6;
-}
-
+/// Types used by JNI API to distinguish between primitive types.
 abstract class JniType {
-  static const int boolType = 0;
+  static const int booleanType = 0;
   static const int byteType = 1;
   static const int shortType = 2;
   static const int charType = 3;
@@ -1598,6 +1423,158 @@
   static const int voidType = 9;
 }
 
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+class JniResult extends ffi.Struct {
+  external JValue result;
+
+  external JThrowable exception;
+}
+
+/// Similar to [JniResult] but for class lookups.
+class JniClassLookupResult extends ffi.Struct {
+  external JClass classRef;
+
+  external JThrowable exception;
+}
+
+/// Similar to [JniResult] but for method/field ID lookups.
+class JniPointerResult extends ffi.Struct {
+  external ffi.Pointer<ffi.Void> id;
+
+  external JThrowable exception;
+}
+
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+class JniAccessors extends ffi.Struct {
+  external ffi.Pointer<
+      ffi.NativeFunction<
+          JniClassLookupResult Function(ffi.Pointer<ffi.Char>)>> getClass;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniPointerResult Function(
+                  JClass, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>>
+      getFieldID;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniPointerResult Function(
+                  JClass, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>>
+      getStaticFieldID;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniPointerResult Function(
+                  JClass, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>>
+      getMethodID;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniPointerResult Function(
+                  JClass, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>>
+      getStaticMethodID;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniResult Function(JClass, JMethodID, ffi.Pointer<JValue>)>>
+      newObject;
+
+  external ffi.Pointer<
+      ffi.NativeFunction<
+          JniResult Function(
+              JObject, JMethodID, ffi.Int, ffi.Pointer<JValue>)>> callMethod;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<
+              JniResult Function(
+                  JClass, JMethodID, ffi.Int, ffi.Pointer<JValue>)>>
+      callStaticMethod;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<JniResult Function(JObject, JFieldID, ffi.Int)>>
+      getField;
+
+  external ffi.Pointer<
+          ffi.NativeFunction<JniResult Function(JClass, JFieldID, ffi.Int)>>
+      getStaticField;
+}
+
+extension JniAccessorsExtension on ffi.Pointer<JniAccessors> {
+  JniClassLookupResult getClass(ffi.Pointer<ffi.Char> internalName) {
+    return ref.getClass
+            .asFunction<JniClassLookupResult Function(ffi.Pointer<ffi.Char>)>()(
+        internalName);
+  }
+
+  JniPointerResult getFieldID(JClass cls, ffi.Pointer<ffi.Char> fieldName,
+      ffi.Pointer<ffi.Char> signature) {
+    return ref.getFieldID.asFunction<
+        JniPointerResult Function(JClass, ffi.Pointer<ffi.Char>,
+            ffi.Pointer<ffi.Char>)>()(cls, fieldName, signature);
+  }
+
+  JniPointerResult getStaticFieldID(JClass cls, ffi.Pointer<ffi.Char> fieldName,
+      ffi.Pointer<ffi.Char> signature) {
+    return ref.getStaticFieldID.asFunction<
+        JniPointerResult Function(JClass, ffi.Pointer<ffi.Char>,
+            ffi.Pointer<ffi.Char>)>()(cls, fieldName, signature);
+  }
+
+  JniPointerResult getMethodID(JClass cls, ffi.Pointer<ffi.Char> methodName,
+      ffi.Pointer<ffi.Char> signature) {
+    return ref.getMethodID.asFunction<
+        JniPointerResult Function(JClass, ffi.Pointer<ffi.Char>,
+            ffi.Pointer<ffi.Char>)>()(cls, methodName, signature);
+  }
+
+  JniPointerResult getStaticMethodID(JClass cls,
+      ffi.Pointer<ffi.Char> methodName, ffi.Pointer<ffi.Char> signature) {
+    return ref.getStaticMethodID.asFunction<
+        JniPointerResult Function(JClass, ffi.Pointer<ffi.Char>,
+            ffi.Pointer<ffi.Char>)>()(cls, methodName, signature);
+  }
+
+  JniResult newObject(JClass cls, JMethodID ctor, ffi.Pointer<JValue> args) {
+    return ref.newObject.asFunction<
+        JniResult Function(
+            JClass, JMethodID, ffi.Pointer<JValue>)>()(cls, ctor, args);
+  }
+
+  JniResult callMethod(
+      JObject obj, JMethodID methodID, int callType, ffi.Pointer<JValue> args) {
+    return ref.callMethod.asFunction<
+            JniResult Function(JObject, JMethodID, int, ffi.Pointer<JValue>)>()(
+        obj, methodID, callType, args);
+  }
+
+  JniResult callStaticMethod(
+      JClass cls, JMethodID methodID, int callType, ffi.Pointer<JValue> args) {
+    return ref.callStaticMethod.asFunction<
+            JniResult Function(JClass, JMethodID, int, ffi.Pointer<JValue>)>()(
+        cls, methodID, callType, args);
+  }
+
+  JniResult getField(JObject obj, JFieldID fieldID, int callType) {
+    return ref.getField
+            .asFunction<JniResult Function(JObject, JFieldID, int)>()(
+        obj, fieldID, callType);
+  }
+
+  JniResult getStaticField(JClass cls, JFieldID fieldID, int callType) {
+    return ref.getStaticField
+            .asFunction<JniResult Function(JClass, JFieldID, int)>()(
+        cls, fieldID, callType);
+  }
+}
+
 /// Wrapper over JNIEnv in the JNI API, which can be used from multiple Dart
 /// Threads.
 ///
@@ -3542,5 +3519,3 @@
 const int JNI_COMMIT = 1;
 
 const int JNI_ABORT = 2;
-
-const String JNI_LOG_TAG = 'Dart-JNI';
diff --git a/pkgs/jni/src/.clang-format b/pkgs/jni/src/.clang-format
new file mode 100644
index 0000000..a256c2f
--- /dev/null
+++ b/pkgs/jni/src/.clang-format
@@ -0,0 +1,15 @@
+# From dart SDK: https://github.com/dart-lang/sdk/blob/main/.clang-format
+
+# Defines the Chromium style for automatic reformatting.
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
+BasedOnStyle: Chromium
+
+# clang-format doesn't seem to do a good job of this for longer comments.
+ReflowComments: 'false'
+
+# We have lots of these. Though we need to put them all in curly braces,
+# clang-format can't do that.
+AllowShortIfStatementsOnASingleLine: 'true'
+
+# Put escaped newlines into the rightmost column.
+AlignEscapedNewlinesLeft: false
diff --git a/pkgs/jni/src/CMakeLists.txt b/pkgs/jni/src/CMakeLists.txt
index 3b4739e..23ac23b 100644
--- a/pkgs/jni/src/CMakeLists.txt
+++ b/pkgs/jni/src/CMakeLists.txt
@@ -7,7 +7,7 @@
 
 add_library(jni SHARED
   "dartjni.c"
-  "global_jni_env.c"
+  "third_party/global_jni_env.c"
 )
 
 set_target_properties(jni PROPERTIES
diff --git a/pkgs/jni/src/README.md b/pkgs/jni/src/README.md
index c0c3119..6516f65 100644
--- a/pkgs/jni/src/README.md
+++ b/pkgs/jni/src/README.md
@@ -1,27 +1,5 @@
+Native code for jni library.
+
 ## LSP instructions
 
-If using an LSP based editor plugin and the syntax highlighting / code completion is not working, there are 2 ways to fix that.
-
-* __Create a compile_flags.txt with following content__:
-
-`-I<path_to_folder_containing_jni_headers>`
-
-This might need the OS specific include folder as well, so that transitively included headers can be found. Example:
-
-```
--I/usr/lib/jvm/java-11-openjdk-amd64/include
--I/usr/lib/jvm/java-11-openjdk-amd64/include/linux
-```
-
-Note that this file should contain one compilation flag per line.
-
-* __create a compilation database by prefixing `bear` to your cmake build__.
-
-Run `cmake --build` command from your source, prefixed with `bear`.
-
-`bear -- cmake --build <build_dir>`
-
-On some distro versions of `bear` command, the `--` needs to be omitted.
-
-`bear cmake --build <build_dir>`
-
+see https://releases.llvm.org/8.0.0/tools/clang/tools/extra/docs/clangd/Installation.html#project-setup for creating compile_commands.json for use with LSP based editor plugins (in VS Code, VIM etc..).
\ No newline at end of file
diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c
index d30689d..36af423 100644
--- a/pkgs/jni/src/dartjni.c
+++ b/pkgs/jni/src/dartjni.c
@@ -3,39 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 
 #include <jni.h>
+#include <stdarg.h>
 #include <stdint.h>
 
 #include "dartjni.h"
 
+// Context and shared global state. Initialized once or if thread-local, initialized once in a thread.
 JniContext jni = {NULL, NULL, NULL, NULL, NULL};
 
-thread_local JNIEnv *jniEnv = NULL;
-
-FFI_PLUGIN_EXPORT JniContext GetJniContext() { return jni; }
+thread_local JNIEnv* jniEnv = NULL;
 
 /// Get JVM associated with current process.
 /// Returns NULL if no JVM is running.
 FFI_PLUGIN_EXPORT
-JavaVM *GetJavaVM() { return jni.jvm; }
-
-/// Destroys the JVM.
-///
-/// Returns 0 on success and appropriate error code on failure.
-FFI_PLUGIN_EXPORT
-int DestroyJavaVM() {
-	int result = (*jni.jvm)->DestroyJavaVM(jni.jvm);
-	jni.jvm = NULL;
-	return result;
-}
-
-/// Returns Application classLoader (on Android), 
-/// which can be used to load application and platform classes.
-/// ...
-/// On other platforms, NULL is returned.
-FFI_PLUGIN_EXPORT
-jobject GetClassLoader() {
-	attach_thread();
-	return (*jniEnv)->NewLocalRef(jniEnv, jni.classLoader);
+JavaVM* GetJavaVM() {
+  return jni.jvm;
 }
 
 /// Load class through platform-specific mechanism.
@@ -43,20 +25,23 @@
 /// Currently uses application classloader on android,
 /// and JNIEnv->FindClass on other platforms.
 FFI_PLUGIN_EXPORT
-jclass LoadClass(const char *name) {
-	jclass cls = NULL;
-	attach_thread();
-	load_class(&cls, name);
-	return to_global_ref(cls);
+jclass LoadClass(const char* name) {
+  jclass cls = NULL;
+  attach_thread();
+  load_class(&cls, name);
+  return to_global_ref(cls);
 };
 
+// Android specifics
+
+/// Returns Application classLoader (on Android),
+/// which can be used to load application and platform classes.
+/// ...
+/// On other platforms, NULL is returned.
 FFI_PLUGIN_EXPORT
-JNIEnv *GetJniEnv() {
-	if (jni.jvm == NULL) {
-		return NULL;
-	}
-	attach_thread();
-	return jniEnv;
+jobject GetClassLoader() {
+  attach_thread();
+  return (*jniEnv)->NewGlobalRef(jniEnv, jni.classLoader);
 }
 
 /// Returns application context on Android.
@@ -64,64 +49,330 @@
 /// On other platforms, NULL is returned.
 FFI_PLUGIN_EXPORT
 jobject GetApplicationContext() {
-	attach_thread();
-	return (*jniEnv)->NewGlobalRef(jniEnv, jni.appContext);
+  attach_thread();
+  return (*jniEnv)->NewGlobalRef(jniEnv, jni.appContext);
 }
 
 /// Returns current activity of the app
 FFI_PLUGIN_EXPORT
 jobject GetCurrentActivity() {
-	attach_thread();
-	return (*jniEnv)->NewGlobalRef(jniEnv, jni.currentActivity);
+  attach_thread();
+  return (*jniEnv)->NewGlobalRef(jniEnv, jni.currentActivity);
 }
 
+// JNI Initialization
+
 #ifdef __ANDROID__
-JNIEXPORT void JNICALL Java_com_github_dart_1lang_jni_JniPlugin_initializeJni(
-    JNIEnv *env, jobject obj, jobject appContext, jobject classLoader) {
-	jniEnv = env;
-	(*env)->GetJavaVM(env, &jni.jvm);
-	jni.classLoader = (*env)->NewGlobalRef(env, classLoader);
-	jni.appContext = (*env)->NewGlobalRef(env, appContext);
-	jclass classLoaderClass = (*env)->GetObjectClass(env, classLoader);
-	jni.loadClassMethod =
-	    (*env)->GetMethodID(env, classLoaderClass, "loadClass",
-	                        "(Ljava/lang/String;)Ljava/lang/Class;");
+JNIEXPORT void JNICALL
+Java_com_github_dart_1lang_jni_JniPlugin_initializeJni(JNIEnv* env,
+                                                       jobject obj,
+                                                       jobject appContext,
+                                                       jobject classLoader) {
+  jniEnv = env;
+  (*env)->GetJavaVM(env, &jni.jvm);
+  jni.classLoader = (*env)->NewGlobalRef(env, classLoader);
+  jni.appContext = (*env)->NewGlobalRef(env, appContext);
+  jclass classLoaderClass = (*env)->GetObjectClass(env, classLoader);
+  jni.loadClassMethod =
+      (*env)->GetMethodID(env, classLoaderClass, "loadClass",
+                          "(Ljava/lang/String;)Ljava/lang/Class;");
 }
 
-JNIEXPORT void JNICALL Java_com_github_dart_1lang_jni_JniPlugin_setJniActivity(JNIEnv *env, jobject obj, jobject activity, jobject context) {
-	jniEnv = env;
-	if (jni.currentActivity != NULL) {
-		(*env)->DeleteGlobalRef(env, jni.currentActivity);
-	}
-	jni.currentActivity = (*env)->NewGlobalRef(env, activity);
-	if (jni.appContext != NULL) {
-		(*env)->DeleteGlobalRef(env, jni.appContext);
-	}
-	jni.appContext = (*env)->NewGlobalRef(env, context);
+JNIEXPORT void JNICALL
+Java_com_github_dart_1lang_jni_JniPlugin_setJniActivity(JNIEnv* env,
+                                                        jobject obj,
+                                                        jobject activity,
+                                                        jobject context) {
+  jniEnv = env;
+  if (jni.currentActivity != NULL) {
+    (*env)->DeleteGlobalRef(env, jni.currentActivity);
+  }
+  jni.currentActivity = (*env)->NewGlobalRef(env, activity);
+  if (jni.appContext != NULL) {
+    (*env)->DeleteGlobalRef(env, jni.appContext);
+  }
+  jni.appContext = (*env)->NewGlobalRef(env, context);
 }
 
 // Sometimes you may get linker error trying to link JNI_CreateJavaVM APIs
 // on Android NDK. So IFDEF is required.
 #else
 FFI_PLUGIN_EXPORT
-JNIEnv *SpawnJvm(JavaVMInitArgs *initArgs) {
-	JavaVMOption jvmopt[1];
-	char class_path[] = "-Djava.class.path=.";
-	jvmopt[0].optionString = class_path;
-	JavaVMInitArgs vmArgs;
-	if (!initArgs) {
-		vmArgs.version = JNI_VERSION_1_2;
-		vmArgs.nOptions = 1;
-		vmArgs.options = jvmopt;
-		vmArgs.ignoreUnrecognized = JNI_TRUE;
-		initArgs = &vmArgs;
-	}
-	const long flag =
-	    JNI_CreateJavaVM(&jni.jvm, __ENVP_CAST &jniEnv, initArgs);
-	if (flag == JNI_ERR) {
-		return NULL;
-	}
-	return jniEnv;
+JNIEnv* SpawnJvm(JavaVMInitArgs* initArgs) {
+  JavaVMOption jvmopt[1];
+  char class_path[] = "-Djava.class.path=.";
+  jvmopt[0].optionString = class_path;
+  JavaVMInitArgs vmArgs;
+  if (!initArgs) {
+    vmArgs.version = JNI_VERSION_1_6;
+    vmArgs.nOptions = 1;
+    vmArgs.options = jvmopt;
+    vmArgs.ignoreUnrecognized = JNI_TRUE;
+    initArgs = &vmArgs;
+  }
+  const long flag = JNI_CreateJavaVM(&jni.jvm, __ENVP_CAST & jniEnv, initArgs);
+  if (flag == JNI_ERR) {
+    return NULL;
+  }
+  return jniEnv;
 }
 #endif
 
+// accessors - a bunch of functions which are directly called by jnigen generated bindings
+// and also package:jni reflective method access.
+
+jthrowable exceptionCheck() {
+  jthrowable exception = (*jniEnv)->ExceptionOccurred(jniEnv);
+  if (exception != NULL) (*jniEnv)->ExceptionClear(jniEnv);
+  if (exception == NULL) return NULL;
+  return to_global_ref(exception);
+}
+
+JniClassLookupResult getClass(char* internalName) {
+  JniClassLookupResult result = {NULL, NULL};
+  result.classRef = LoadClass(internalName);
+  result.exception = exceptionCheck();
+  return result;
+}
+
+static inline JniPointerResult _getId(
+    void* (*getter)(JNIEnv*, jclass, char*, char*),
+    jclass cls,
+    char* name,
+    char* sig) {
+  JniPointerResult result = {NULL, NULL};
+  attach_thread();
+  result.id = getter(jniEnv, cls, name, sig);
+  result.exception = exceptionCheck();
+  return result;
+}
+
+JniPointerResult getMethodID(jclass cls, char* name, char* sig) {
+  return _getId((*jniEnv)->GetMethodID, cls, name, sig);
+}
+
+JniPointerResult getStaticMethodID(jclass cls, char* name, char* sig) {
+  return _getId((*jniEnv)->GetStaticMethodID, cls, name, sig);
+}
+
+JniPointerResult getFieldID(jclass cls, char* name, char* sig) {
+  return _getId((*jniEnv)->GetFieldID, cls, name, sig);
+}
+
+JniPointerResult getStaticFieldID(jclass cls, char* name, char* sig) {
+  return _getId((*jniEnv)->GetStaticFieldID, cls, name, sig);
+}
+
+JniResult callMethod(jobject obj,
+                     jmethodID fieldID,
+                     int callType,
+                     jvalue* args) {
+  attach_thread();
+  jvalue result = {.j = 0};
+  switch (callType) {
+    case booleanType:
+      result.z = (*jniEnv)->CallBooleanMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case byteType:
+      result.b = (*jniEnv)->CallByteMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case shortType:
+      result.s = (*jniEnv)->CallShortMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case charType:
+      result.c = (*jniEnv)->CallCharMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case intType:
+      result.i = (*jniEnv)->CallIntMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case longType:
+      result.j = (*jniEnv)->CallLongMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case floatType:
+      result.f = (*jniEnv)->CallFloatMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case doubleType:
+      result.d = (*jniEnv)->CallDoubleMethodA(jniEnv, obj, fieldID, args);
+      break;
+    case objectType:
+      result.l = to_global_ref(
+          (*jniEnv)->CallObjectMethodA(jniEnv, obj, fieldID, args));
+      break;
+    case voidType:
+      (*jniEnv)->CallVoidMethodA(jniEnv, obj, fieldID, args);
+      break;
+  }
+  JniResult jniResult = {.result = result, .exception = NULL};
+  jniResult.exception = exceptionCheck();
+  return jniResult;
+}
+
+// TODO(#60): Any way to reduce this boilerplate?
+JniResult callStaticMethod(jclass cls,
+                           jmethodID methodID,
+                           int callType,
+                           jvalue* args) {
+  attach_thread();
+  jvalue result = {.j = 0};
+  switch (callType) {
+    case booleanType:
+      result.z =
+          (*jniEnv)->CallStaticBooleanMethodA(jniEnv, cls, methodID, args);
+      break;
+    case byteType:
+      result.b = (*jniEnv)->CallStaticByteMethodA(jniEnv, cls, methodID, args);
+      break;
+    case shortType:
+      result.s = (*jniEnv)->CallStaticShortMethodA(jniEnv, cls, methodID, args);
+      break;
+    case charType:
+      result.c = (*jniEnv)->CallStaticCharMethodA(jniEnv, cls, methodID, args);
+      break;
+    case intType:
+      result.i = (*jniEnv)->CallStaticIntMethodA(jniEnv, cls, methodID, args);
+      break;
+    case longType:
+      result.j = (*jniEnv)->CallStaticLongMethodA(jniEnv, cls, methodID, args);
+      break;
+    case floatType:
+      result.f = (*jniEnv)->CallStaticFloatMethodA(jniEnv, cls, methodID, args);
+      break;
+    case doubleType:
+      result.d =
+          (*jniEnv)->CallStaticDoubleMethodA(jniEnv, cls, methodID, args);
+      break;
+    case objectType:
+      result.l = to_global_ref(
+          (*jniEnv)->CallStaticObjectMethodA(jniEnv, cls, methodID, args));
+      break;
+    case voidType:
+      (*jniEnv)->CallStaticVoidMethodA(jniEnv, cls, methodID, args);
+      break;
+  }
+  JniResult jniResult = {.result = result, .exception = NULL};
+  jniResult.exception = exceptionCheck();
+  return jniResult;
+}
+
+JniResult getField(jobject obj, jfieldID fieldID, int callType) {
+  attach_thread();
+  jvalue result = {.j = 0};
+  switch (callType) {
+    case booleanType:
+      result.z = (*jniEnv)->GetBooleanField(jniEnv, obj, fieldID);
+      break;
+    case byteType:
+      result.b = (*jniEnv)->GetByteField(jniEnv, obj, fieldID);
+      break;
+    case shortType:
+      result.s = (*jniEnv)->GetShortField(jniEnv, obj, fieldID);
+      break;
+    case charType:
+      result.c = (*jniEnv)->GetCharField(jniEnv, obj, fieldID);
+      break;
+    case intType:
+      result.i = (*jniEnv)->GetIntField(jniEnv, obj, fieldID);
+      break;
+    case longType:
+      result.j = (*jniEnv)->GetLongField(jniEnv, obj, fieldID);
+      break;
+    case floatType:
+      result.f = (*jniEnv)->GetFloatField(jniEnv, obj, fieldID);
+      break;
+    case doubleType:
+      result.d = (*jniEnv)->GetDoubleField(jniEnv, obj, fieldID);
+      break;
+    case objectType:
+      result.l = to_global_ref((*jniEnv)->GetObjectField(jniEnv, obj, fieldID));
+      break;
+    case voidType:
+      // This error should have been handled in Dart.
+      break;
+  }
+  JniResult jniResult = {.result = result, .exception = NULL};
+  jniResult.exception = exceptionCheck();
+  return jniResult;
+}
+
+FFI_PLUGIN_EXPORT
+JniResult getStaticField(jclass cls, jfieldID fieldID, int callType) {
+  attach_thread();
+  jvalue result = {.j = 0};
+  switch (callType) {
+    case booleanType:
+      result.z = (*jniEnv)->GetStaticBooleanField(jniEnv, cls, fieldID);
+      break;
+    case byteType:
+      result.b = (*jniEnv)->GetStaticByteField(jniEnv, cls, fieldID);
+      break;
+    case shortType:
+      result.s = (*jniEnv)->GetStaticShortField(jniEnv, cls, fieldID);
+      break;
+    case charType:
+      result.c = (*jniEnv)->GetStaticCharField(jniEnv, cls, fieldID);
+      break;
+    case intType:
+      result.i = (*jniEnv)->GetStaticIntField(jniEnv, cls, fieldID);
+      break;
+    case longType:
+      result.j = (*jniEnv)->GetStaticLongField(jniEnv, cls, fieldID);
+      break;
+    case floatType:
+      result.f = (*jniEnv)->GetStaticFloatField(jniEnv, cls, fieldID);
+      break;
+    case doubleType:
+      result.d = (*jniEnv)->GetStaticDoubleField(jniEnv, cls, fieldID);
+      break;
+    case objectType:
+      result.l =
+          to_global_ref((*jniEnv)->GetStaticObjectField(jniEnv, cls, fieldID));
+      break;
+    case voidType:
+      // This error should have been handled in dart.
+      // is there a way to mark this as unreachable?
+      // or throw exception in Dart using Dart's C API.
+      break;
+  }
+  JniResult jniResult = {.result = result, .exception = NULL};
+  jniResult.exception = exceptionCheck();
+  return jniResult;
+}
+
+JniResult newObject(jclass cls, jmethodID ctor, jvalue* args) {
+  attach_thread();
+  JniResult jniResult;
+  jniResult.result.l =
+      to_global_ref((*jniEnv)->NewObjectA(jniEnv, cls, ctor, args));
+  jniResult.exception = exceptionCheck();
+  return jniResult;
+}
+
+JniAccessors accessors = {
+    .getClass = getClass,
+    .getFieldID = getFieldID,
+    .getStaticFieldID = getStaticFieldID,
+    .getMethodID = getMethodID,
+    .getStaticMethodID = getStaticMethodID,
+    .newObject = newObject,
+    .callMethod = callMethod,
+    .callStaticMethod = callStaticMethod,
+    .getField = getField,
+    .getStaticField = getStaticField,
+};
+
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors() {
+  return &accessors;
+}
+
+// These will not be required after migrating to Dart-only bindings.
+FFI_PLUGIN_EXPORT JniContext GetJniContext() {
+  return jni;
+}
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv() {
+  if (jni.jvm == NULL) {
+    return NULL;
+  }
+  attach_thread();
+  return jniEnv;
+}
diff --git a/pkgs/jni/src/dartjni.h b/pkgs/jni/src/dartjni.h
index 0ce5069..bc72baa 100644
--- a/pkgs/jni/src/dartjni.h
+++ b/pkgs/jni/src/dartjni.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jni/src/global_jni_env.c b/pkgs/jni/src/global_jni_env.c
deleted file mode 100644
index d7da0af..0000000
--- a/pkgs/jni/src/global_jni_env.c
+++ /dev/null
@@ -1,1188 +0,0 @@
-// Copyright (c) 2022, 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.
-
-#include "global_jni_env.h"
-jint globalEnv_GetVersion() {
-    attach_thread();
-    return (*jniEnv)->GetVersion(jniEnv);
-}
-
-jclass globalEnv_DefineClass(const char * name, jobject loader, const jbyte * buf, jsize bufLen) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->DefineClass(jniEnv, name, loader, buf, bufLen));
-}
-
-jclass globalEnv_FindClass(const char * name) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->FindClass(jniEnv, name));
-}
-
-jmethodID globalEnv_FromReflectedMethod(jobject method) {
-    attach_thread();
-    return (*jniEnv)->FromReflectedMethod(jniEnv, method);
-}
-
-jfieldID globalEnv_FromReflectedField(jobject field) {
-    attach_thread();
-    return (*jniEnv)->FromReflectedField(jniEnv, field);
-}
-
-jobject globalEnv_ToReflectedMethod(jclass cls, jmethodID methodId, jboolean isStatic) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->ToReflectedMethod(jniEnv, cls, methodId, isStatic));
-}
-
-jclass globalEnv_GetSuperclass(jclass clazz) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->GetSuperclass(jniEnv, clazz));
-}
-
-jboolean globalEnv_IsAssignableFrom(jclass clazz1, jclass clazz2) {
-    attach_thread();
-    return (*jniEnv)->IsAssignableFrom(jniEnv, clazz1, clazz2);
-}
-
-jobject globalEnv_ToReflectedField(jclass cls, jfieldID fieldID, jboolean isStatic) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->ToReflectedField(jniEnv, cls, fieldID, isStatic));
-}
-
-jint globalEnv_Throw(jthrowable obj) {
-    attach_thread();
-    return (*jniEnv)->Throw(jniEnv, obj);
-}
-
-jint globalEnv_ThrowNew(jclass clazz, const char * message) {
-    attach_thread();
-    return (*jniEnv)->ThrowNew(jniEnv, clazz, message);
-}
-
-jthrowable globalEnv_ExceptionOccurred() {
-    attach_thread();
-    return to_global_ref((*jniEnv)->ExceptionOccurred(jniEnv));
-}
-
-void globalEnv_ExceptionDescribe() {
-    attach_thread();
-    (*jniEnv)->ExceptionDescribe(jniEnv);
-}
-
-void globalEnv_ExceptionClear() {
-    attach_thread();
-    (*jniEnv)->ExceptionClear(jniEnv);
-}
-
-void globalEnv_FatalError(const char * msg) {
-    attach_thread();
-    (*jniEnv)->FatalError(jniEnv, msg);
-}
-
-jint globalEnv_PushLocalFrame(jint capacity) {
-    attach_thread();
-    return (*jniEnv)->PushLocalFrame(jniEnv, capacity);
-}
-
-jobject globalEnv_PopLocalFrame(jobject result) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->PopLocalFrame(jniEnv, result));
-}
-
-jobject globalEnv_NewGlobalRef(jobject obj) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewGlobalRef(jniEnv, obj));
-}
-
-void globalEnv_DeleteGlobalRef(jobject globalRef) {
-    attach_thread();
-    (*jniEnv)->DeleteGlobalRef(jniEnv, globalRef);
-}
-
-jboolean globalEnv_IsSameObject(jobject ref1, jobject ref2) {
-    attach_thread();
-    return (*jniEnv)->IsSameObject(jniEnv, ref1, ref2);
-}
-
-jint globalEnv_EnsureLocalCapacity(jint capacity) {
-    attach_thread();
-    return (*jniEnv)->EnsureLocalCapacity(jniEnv, capacity);
-}
-
-jobject globalEnv_AllocObject(jclass clazz) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->AllocObject(jniEnv, clazz));
-}
-
-jobject globalEnv_NewObject(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewObject(jniEnv, arg1, arg2));
-}
-
-jobject globalEnv_NewObjectA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewObjectA(jniEnv, clazz, methodID, args));
-}
-
-jclass globalEnv_GetObjectClass(jobject obj) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->GetObjectClass(jniEnv, obj));
-}
-
-jboolean globalEnv_IsInstanceOf(jobject obj, jclass clazz) {
-    attach_thread();
-    return (*jniEnv)->IsInstanceOf(jniEnv, obj, clazz);
-}
-
-jmethodID globalEnv_GetMethodID(jclass clazz, const char * name, const char * sig) {
-    attach_thread();
-    return (*jniEnv)->GetMethodID(jniEnv, clazz, name, sig);
-}
-
-jobject globalEnv_CallObjectMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallObjectMethod(jniEnv, arg1, arg2));
-}
-
-jobject globalEnv_CallObjectMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallObjectMethodA(jniEnv, obj, methodID, args));
-}
-
-jboolean globalEnv_CallBooleanMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallBooleanMethod(jniEnv, arg1, arg2);
-}
-
-jboolean globalEnv_CallBooleanMethodA(jobject obj, jmethodID methodId, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallBooleanMethodA(jniEnv, obj, methodId, args);
-}
-
-jbyte globalEnv_CallByteMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallByteMethod(jniEnv, arg1, arg2);
-}
-
-jbyte globalEnv_CallByteMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallByteMethodA(jniEnv, obj, methodID, args);
-}
-
-jchar globalEnv_CallCharMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallCharMethod(jniEnv, arg1, arg2);
-}
-
-jchar globalEnv_CallCharMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallCharMethodA(jniEnv, obj, methodID, args);
-}
-
-jshort globalEnv_CallShortMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallShortMethod(jniEnv, arg1, arg2);
-}
-
-jshort globalEnv_CallShortMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallShortMethodA(jniEnv, obj, methodID, args);
-}
-
-jint globalEnv_CallIntMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallIntMethod(jniEnv, arg1, arg2);
-}
-
-jint globalEnv_CallIntMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallIntMethodA(jniEnv, obj, methodID, args);
-}
-
-jlong globalEnv_CallLongMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallLongMethod(jniEnv, arg1, arg2);
-}
-
-jlong globalEnv_CallLongMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallLongMethodA(jniEnv, obj, methodID, args);
-}
-
-jfloat globalEnv_CallFloatMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallFloatMethod(jniEnv, arg1, arg2);
-}
-
-jfloat globalEnv_CallFloatMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallFloatMethodA(jniEnv, obj, methodID, args);
-}
-
-jdouble globalEnv_CallDoubleMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallDoubleMethod(jniEnv, arg1, arg2);
-}
-
-jdouble globalEnv_CallDoubleMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallDoubleMethodA(jniEnv, obj, methodID, args);
-}
-
-void globalEnv_CallVoidMethod(jobject arg1, jmethodID arg2) {
-    attach_thread();
-    (*jniEnv)->CallVoidMethod(jniEnv, arg1, arg2);
-}
-
-void globalEnv_CallVoidMethodA(jobject obj, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    (*jniEnv)->CallVoidMethodA(jniEnv, obj, methodID, args);
-}
-
-jobject globalEnv_CallNonvirtualObjectMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallNonvirtualObjectMethod(jniEnv, arg1, arg2, arg3));
-}
-
-jobject globalEnv_CallNonvirtualObjectMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallNonvirtualObjectMethodA(jniEnv, obj, clazz, methodID, args));
-}
-
-jboolean globalEnv_CallNonvirtualBooleanMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualBooleanMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jboolean globalEnv_CallNonvirtualBooleanMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualBooleanMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jbyte globalEnv_CallNonvirtualByteMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualByteMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jbyte globalEnv_CallNonvirtualByteMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualByteMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jchar globalEnv_CallNonvirtualCharMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualCharMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jchar globalEnv_CallNonvirtualCharMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualCharMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jshort globalEnv_CallNonvirtualShortMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualShortMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jshort globalEnv_CallNonvirtualShortMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualShortMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jint globalEnv_CallNonvirtualIntMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualIntMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jint globalEnv_CallNonvirtualIntMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualIntMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jlong globalEnv_CallNonvirtualLongMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualLongMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jlong globalEnv_CallNonvirtualLongMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualLongMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jfloat globalEnv_CallNonvirtualFloatMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualFloatMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jfloat globalEnv_CallNonvirtualFloatMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualFloatMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jdouble globalEnv_CallNonvirtualDoubleMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualDoubleMethod(jniEnv, arg1, arg2, arg3);
-}
-
-jdouble globalEnv_CallNonvirtualDoubleMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallNonvirtualDoubleMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-void globalEnv_CallNonvirtualVoidMethod(jobject arg1, jclass arg2, jmethodID arg3) {
-    attach_thread();
-    (*jniEnv)->CallNonvirtualVoidMethod(jniEnv, arg1, arg2, arg3);
-}
-
-void globalEnv_CallNonvirtualVoidMethodA(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    (*jniEnv)->CallNonvirtualVoidMethodA(jniEnv, obj, clazz, methodID, args);
-}
-
-jfieldID globalEnv_GetFieldID(jclass clazz, const char * name, const char * sig) {
-    attach_thread();
-    return (*jniEnv)->GetFieldID(jniEnv, clazz, name, sig);
-}
-
-jobject globalEnv_GetObjectField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->GetObjectField(jniEnv, obj, fieldID));
-}
-
-jboolean globalEnv_GetBooleanField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetBooleanField(jniEnv, obj, fieldID);
-}
-
-jbyte globalEnv_GetByteField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetByteField(jniEnv, obj, fieldID);
-}
-
-jchar globalEnv_GetCharField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetCharField(jniEnv, obj, fieldID);
-}
-
-jshort globalEnv_GetShortField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetShortField(jniEnv, obj, fieldID);
-}
-
-jint globalEnv_GetIntField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetIntField(jniEnv, obj, fieldID);
-}
-
-jlong globalEnv_GetLongField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetLongField(jniEnv, obj, fieldID);
-}
-
-jfloat globalEnv_GetFloatField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetFloatField(jniEnv, obj, fieldID);
-}
-
-jdouble globalEnv_GetDoubleField(jobject obj, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetDoubleField(jniEnv, obj, fieldID);
-}
-
-void globalEnv_SetObjectField(jobject obj, jfieldID fieldID, jobject val) {
-    attach_thread();
-    (*jniEnv)->SetObjectField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetBooleanField(jobject obj, jfieldID fieldID, jboolean val) {
-    attach_thread();
-    (*jniEnv)->SetBooleanField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetByteField(jobject obj, jfieldID fieldID, jbyte val) {
-    attach_thread();
-    (*jniEnv)->SetByteField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetCharField(jobject obj, jfieldID fieldID, jchar val) {
-    attach_thread();
-    (*jniEnv)->SetCharField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetShortField(jobject obj, jfieldID fieldID, jshort val) {
-    attach_thread();
-    (*jniEnv)->SetShortField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetIntField(jobject obj, jfieldID fieldID, jint val) {
-    attach_thread();
-    (*jniEnv)->SetIntField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetLongField(jobject obj, jfieldID fieldID, jlong val) {
-    attach_thread();
-    (*jniEnv)->SetLongField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetFloatField(jobject obj, jfieldID fieldID, jfloat val) {
-    attach_thread();
-    (*jniEnv)->SetFloatField(jniEnv, obj, fieldID, val);
-}
-
-void globalEnv_SetDoubleField(jobject obj, jfieldID fieldID, jdouble val) {
-    attach_thread();
-    (*jniEnv)->SetDoubleField(jniEnv, obj, fieldID, val);
-}
-
-jmethodID globalEnv_GetStaticMethodID(jclass clazz, const char * name, const char * sig) {
-    attach_thread();
-    return (*jniEnv)->GetStaticMethodID(jniEnv, clazz, name, sig);
-}
-
-jobject globalEnv_CallStaticObjectMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallStaticObjectMethod(jniEnv, arg1, arg2));
-}
-
-jobject globalEnv_CallStaticObjectMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->CallStaticObjectMethodA(jniEnv, clazz, methodID, args));
-}
-
-jboolean globalEnv_CallStaticBooleanMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticBooleanMethod(jniEnv, arg1, arg2);
-}
-
-jboolean globalEnv_CallStaticBooleanMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticBooleanMethodA(jniEnv, clazz, methodID, args);
-}
-
-jbyte globalEnv_CallStaticByteMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticByteMethod(jniEnv, arg1, arg2);
-}
-
-jbyte globalEnv_CallStaticByteMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticByteMethodA(jniEnv, clazz, methodID, args);
-}
-
-jchar globalEnv_CallStaticCharMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticCharMethod(jniEnv, arg1, arg2);
-}
-
-jchar globalEnv_CallStaticCharMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticCharMethodA(jniEnv, clazz, methodID, args);
-}
-
-jshort globalEnv_CallStaticShortMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticShortMethod(jniEnv, arg1, arg2);
-}
-
-jshort globalEnv_CallStaticShortMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticShortMethodA(jniEnv, clazz, methodID, args);
-}
-
-jint globalEnv_CallStaticIntMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticIntMethod(jniEnv, arg1, arg2);
-}
-
-jint globalEnv_CallStaticIntMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticIntMethodA(jniEnv, clazz, methodID, args);
-}
-
-jlong globalEnv_CallStaticLongMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticLongMethod(jniEnv, arg1, arg2);
-}
-
-jlong globalEnv_CallStaticLongMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticLongMethodA(jniEnv, clazz, methodID, args);
-}
-
-jfloat globalEnv_CallStaticFloatMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticFloatMethod(jniEnv, arg1, arg2);
-}
-
-jfloat globalEnv_CallStaticFloatMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticFloatMethodA(jniEnv, clazz, methodID, args);
-}
-
-jdouble globalEnv_CallStaticDoubleMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    return (*jniEnv)->CallStaticDoubleMethod(jniEnv, arg1, arg2);
-}
-
-jdouble globalEnv_CallStaticDoubleMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    return (*jniEnv)->CallStaticDoubleMethodA(jniEnv, clazz, methodID, args);
-}
-
-void globalEnv_CallStaticVoidMethod(jclass arg1, jmethodID arg2) {
-    attach_thread();
-    (*jniEnv)->CallStaticVoidMethod(jniEnv, arg1, arg2);
-}
-
-void globalEnv_CallStaticVoidMethodA(jclass clazz, jmethodID methodID, const jvalue * args) {
-    attach_thread();
-    (*jniEnv)->CallStaticVoidMethodA(jniEnv, clazz, methodID, args);
-}
-
-jfieldID globalEnv_GetStaticFieldID(jclass clazz, const char * name, const char * sig) {
-    attach_thread();
-    return (*jniEnv)->GetStaticFieldID(jniEnv, clazz, name, sig);
-}
-
-jobject globalEnv_GetStaticObjectField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->GetStaticObjectField(jniEnv, clazz, fieldID));
-}
-
-jboolean globalEnv_GetStaticBooleanField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticBooleanField(jniEnv, clazz, fieldID);
-}
-
-jbyte globalEnv_GetStaticByteField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticByteField(jniEnv, clazz, fieldID);
-}
-
-jchar globalEnv_GetStaticCharField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticCharField(jniEnv, clazz, fieldID);
-}
-
-jshort globalEnv_GetStaticShortField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticShortField(jniEnv, clazz, fieldID);
-}
-
-jint globalEnv_GetStaticIntField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticIntField(jniEnv, clazz, fieldID);
-}
-
-jlong globalEnv_GetStaticLongField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticLongField(jniEnv, clazz, fieldID);
-}
-
-jfloat globalEnv_GetStaticFloatField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticFloatField(jniEnv, clazz, fieldID);
-}
-
-jdouble globalEnv_GetStaticDoubleField(jclass clazz, jfieldID fieldID) {
-    attach_thread();
-    return (*jniEnv)->GetStaticDoubleField(jniEnv, clazz, fieldID);
-}
-
-void globalEnv_SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject val) {
-    attach_thread();
-    (*jniEnv)->SetStaticObjectField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticBooleanField(jclass clazz, jfieldID fieldID, jboolean val) {
-    attach_thread();
-    (*jniEnv)->SetStaticBooleanField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticByteField(jclass clazz, jfieldID fieldID, jbyte val) {
-    attach_thread();
-    (*jniEnv)->SetStaticByteField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticCharField(jclass clazz, jfieldID fieldID, jchar val) {
-    attach_thread();
-    (*jniEnv)->SetStaticCharField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticShortField(jclass clazz, jfieldID fieldID, jshort val) {
-    attach_thread();
-    (*jniEnv)->SetStaticShortField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticIntField(jclass clazz, jfieldID fieldID, jint val) {
-    attach_thread();
-    (*jniEnv)->SetStaticIntField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticLongField(jclass clazz, jfieldID fieldID, jlong val) {
-    attach_thread();
-    (*jniEnv)->SetStaticLongField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticFloatField(jclass clazz, jfieldID fieldID, jfloat val) {
-    attach_thread();
-    (*jniEnv)->SetStaticFloatField(jniEnv, clazz, fieldID, val);
-}
-
-void globalEnv_SetStaticDoubleField(jclass clazz, jfieldID fieldID, jdouble val) {
-    attach_thread();
-    (*jniEnv)->SetStaticDoubleField(jniEnv, clazz, fieldID, val);
-}
-
-jstring globalEnv_NewString(const jchar * unicodeChars, jsize len) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewString(jniEnv, unicodeChars, len));
-}
-
-jsize globalEnv_GetStringLength(jstring string) {
-    attach_thread();
-    return (*jniEnv)->GetStringLength(jniEnv, string);
-}
-
-const jchar * globalEnv_GetStringChars(jstring string, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetStringChars(jniEnv, string, isCopy);
-}
-
-void globalEnv_ReleaseStringChars(jstring string, const jchar * isCopy) {
-    attach_thread();
-    (*jniEnv)->ReleaseStringChars(jniEnv, string, isCopy);
-}
-
-jstring globalEnv_NewStringUTF(const char * bytes) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewStringUTF(jniEnv, bytes));
-}
-
-jsize globalEnv_GetStringUTFLength(jstring string) {
-    attach_thread();
-    return (*jniEnv)->GetStringUTFLength(jniEnv, string);
-}
-
-const char * globalEnv_GetStringUTFChars(jstring string, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetStringUTFChars(jniEnv, string, isCopy);
-}
-
-void globalEnv_ReleaseStringUTFChars(jstring string, const char * utf) {
-    attach_thread();
-    (*jniEnv)->ReleaseStringUTFChars(jniEnv, string, utf);
-}
-
-jsize globalEnv_GetArrayLength(jarray array) {
-    attach_thread();
-    return (*jniEnv)->GetArrayLength(jniEnv, array);
-}
-
-jobjectArray globalEnv_NewObjectArray(jsize length, jclass elementClass, jobject initialElement) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewObjectArray(jniEnv, length, elementClass, initialElement));
-}
-
-jobject globalEnv_GetObjectArrayElement(jobjectArray array, jsize index) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->GetObjectArrayElement(jniEnv, array, index));
-}
-
-void globalEnv_SetObjectArrayElement(jobjectArray array, jsize index, jobject val) {
-    attach_thread();
-    (*jniEnv)->SetObjectArrayElement(jniEnv, array, index, val);
-}
-
-jbooleanArray globalEnv_NewBooleanArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewBooleanArray(jniEnv, length));
-}
-
-jbyteArray globalEnv_NewByteArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewByteArray(jniEnv, length));
-}
-
-jcharArray globalEnv_NewCharArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewCharArray(jniEnv, length));
-}
-
-jshortArray globalEnv_NewShortArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewShortArray(jniEnv, length));
-}
-
-jintArray globalEnv_NewIntArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewIntArray(jniEnv, length));
-}
-
-jlongArray globalEnv_NewLongArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewLongArray(jniEnv, length));
-}
-
-jfloatArray globalEnv_NewFloatArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewFloatArray(jniEnv, length));
-}
-
-jdoubleArray globalEnv_NewDoubleArray(jsize length) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewDoubleArray(jniEnv, length));
-}
-
-jboolean * globalEnv_GetBooleanArrayElements(jbooleanArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetBooleanArrayElements(jniEnv, array, isCopy);
-}
-
-jbyte * globalEnv_GetByteArrayElements(jbyteArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetByteArrayElements(jniEnv, array, isCopy);
-}
-
-jchar * globalEnv_GetCharArrayElements(jcharArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetCharArrayElements(jniEnv, array, isCopy);
-}
-
-jshort * globalEnv_GetShortArrayElements(jshortArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetShortArrayElements(jniEnv, array, isCopy);
-}
-
-jint * globalEnv_GetIntArrayElements(jintArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetIntArrayElements(jniEnv, array, isCopy);
-}
-
-jlong * globalEnv_GetLongArrayElements(jlongArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetLongArrayElements(jniEnv, array, isCopy);
-}
-
-jfloat * globalEnv_GetFloatArrayElements(jfloatArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetFloatArrayElements(jniEnv, array, isCopy);
-}
-
-jdouble * globalEnv_GetDoubleArrayElements(jdoubleArray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetDoubleArrayElements(jniEnv, array, isCopy);
-}
-
-void globalEnv_ReleaseBooleanArrayElements(jbooleanArray array, jboolean * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseBooleanArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseByteArrayElements(jbyteArray array, jbyte * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseByteArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseCharArrayElements(jcharArray array, jchar * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseCharArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseShortArrayElements(jshortArray array, jshort * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseShortArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseIntArrayElements(jintArray array, jint * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseIntArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseLongArrayElements(jlongArray array, jlong * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseLongArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseFloatArrayElements(jfloatArray array, jfloat * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseFloatArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_ReleaseDoubleArrayElements(jdoubleArray array, jdouble * elems, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleaseDoubleArrayElements(jniEnv, array, elems, mode);
-}
-
-void globalEnv_GetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, jboolean * buf) {
-    attach_thread();
-    (*jniEnv)->GetBooleanArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetByteArrayRegion(jbyteArray array, jsize start, jsize len, jbyte * buf) {
-    attach_thread();
-    (*jniEnv)->GetByteArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetCharArrayRegion(jcharArray array, jsize start, jsize len, jchar * buf) {
-    attach_thread();
-    (*jniEnv)->GetCharArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetShortArrayRegion(jshortArray array, jsize start, jsize len, jshort * buf) {
-    attach_thread();
-    (*jniEnv)->GetShortArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetIntArrayRegion(jintArray array, jsize start, jsize len, jint * buf) {
-    attach_thread();
-    (*jniEnv)->GetIntArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetLongArrayRegion(jlongArray array, jsize start, jsize len, jlong * buf) {
-    attach_thread();
-    (*jniEnv)->GetLongArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetFloatArrayRegion(jfloatArray array, jsize start, jsize len, jfloat * buf) {
-    attach_thread();
-    (*jniEnv)->GetFloatArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_GetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, jdouble * buf) {
-    attach_thread();
-    (*jniEnv)->GetDoubleArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, const jboolean * buf) {
-    attach_thread();
-    (*jniEnv)->SetBooleanArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetByteArrayRegion(jbyteArray array, jsize start, jsize len, const jbyte * buf) {
-    attach_thread();
-    (*jniEnv)->SetByteArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetCharArrayRegion(jcharArray array, jsize start, jsize len, const jchar * buf) {
-    attach_thread();
-    (*jniEnv)->SetCharArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetShortArrayRegion(jshortArray array, jsize start, jsize len, const jshort * buf) {
-    attach_thread();
-    (*jniEnv)->SetShortArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetIntArrayRegion(jintArray array, jsize start, jsize len, const jint * buf) {
-    attach_thread();
-    (*jniEnv)->SetIntArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetLongArrayRegion(jlongArray array, jsize start, jsize len, const jlong * buf) {
-    attach_thread();
-    (*jniEnv)->SetLongArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetFloatArrayRegion(jfloatArray array, jsize start, jsize len, const jfloat * buf) {
-    attach_thread();
-    (*jniEnv)->SetFloatArrayRegion(jniEnv, array, start, len, buf);
-}
-
-void globalEnv_SetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, const jdouble * buf) {
-    attach_thread();
-    (*jniEnv)->SetDoubleArrayRegion(jniEnv, array, start, len, buf);
-}
-
-jint globalEnv_RegisterNatives(jclass clazz, const JNINativeMethod * methods, jint nMethods) {
-    attach_thread();
-    return (*jniEnv)->RegisterNatives(jniEnv, clazz, methods, nMethods);
-}
-
-jint globalEnv_UnregisterNatives(jclass clazz) {
-    attach_thread();
-    return (*jniEnv)->UnregisterNatives(jniEnv, clazz);
-}
-
-jint globalEnv_MonitorEnter(jobject obj) {
-    attach_thread();
-    return (*jniEnv)->MonitorEnter(jniEnv, obj);
-}
-
-jint globalEnv_MonitorExit(jobject obj) {
-    attach_thread();
-    return (*jniEnv)->MonitorExit(jniEnv, obj);
-}
-
-jint globalEnv_GetJavaVM(JavaVM ** vm) {
-    attach_thread();
-    return (*jniEnv)->GetJavaVM(jniEnv, vm);
-}
-
-void globalEnv_GetStringRegion(jstring str, jsize start, jsize len, jchar * buf) {
-    attach_thread();
-    (*jniEnv)->GetStringRegion(jniEnv, str, start, len, buf);
-}
-
-void globalEnv_GetStringUTFRegion(jstring str, jsize start, jsize len, char * buf) {
-    attach_thread();
-    (*jniEnv)->GetStringUTFRegion(jniEnv, str, start, len, buf);
-}
-
-void * globalEnv_GetPrimitiveArrayCritical(jarray array, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetPrimitiveArrayCritical(jniEnv, array, isCopy);
-}
-
-void globalEnv_ReleasePrimitiveArrayCritical(jarray array, void * carray, jint mode) {
-    attach_thread();
-    (*jniEnv)->ReleasePrimitiveArrayCritical(jniEnv, array, carray, mode);
-}
-
-const jchar * globalEnv_GetStringCritical(jstring str, jboolean * isCopy) {
-    attach_thread();
-    return (*jniEnv)->GetStringCritical(jniEnv, str, isCopy);
-}
-
-void globalEnv_ReleaseStringCritical(jstring str, const jchar * carray) {
-    attach_thread();
-    (*jniEnv)->ReleaseStringCritical(jniEnv, str, carray);
-}
-
-jweak globalEnv_NewWeakGlobalRef(jobject obj) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewWeakGlobalRef(jniEnv, obj));
-}
-
-void globalEnv_DeleteWeakGlobalRef(jweak obj) {
-    attach_thread();
-    (*jniEnv)->DeleteWeakGlobalRef(jniEnv, obj);
-}
-
-jboolean globalEnv_ExceptionCheck() {
-    attach_thread();
-    return (*jniEnv)->ExceptionCheck(jniEnv);
-}
-
-jobject globalEnv_NewDirectByteBuffer(void * address, jlong capacity) {
-    attach_thread();
-    return to_global_ref((*jniEnv)->NewDirectByteBuffer(jniEnv, address, capacity));
-}
-
-void * globalEnv_GetDirectBufferAddress(jobject buf) {
-    attach_thread();
-    return (*jniEnv)->GetDirectBufferAddress(jniEnv, buf);
-}
-
-jlong globalEnv_GetDirectBufferCapacity(jobject buf) {
-    attach_thread();
-    return (*jniEnv)->GetDirectBufferCapacity(jniEnv, buf);
-}
-
-jobjectRefType globalEnv_GetObjectRefType(jobject obj) {
-    attach_thread();
-    return (*jniEnv)->GetObjectRefType(jniEnv, obj);
-}
-
-struct GlobalJniEnv globalEnv = {
-    .GetVersion = globalEnv_GetVersion,
-    .DefineClass = globalEnv_DefineClass,
-    .FindClass = globalEnv_FindClass,
-    .FromReflectedMethod = globalEnv_FromReflectedMethod,
-    .FromReflectedField = globalEnv_FromReflectedField,
-    .ToReflectedMethod = globalEnv_ToReflectedMethod,
-    .GetSuperclass = globalEnv_GetSuperclass,
-    .IsAssignableFrom = globalEnv_IsAssignableFrom,
-    .ToReflectedField = globalEnv_ToReflectedField,
-    .Throw = globalEnv_Throw,
-    .ThrowNew = globalEnv_ThrowNew,
-    .ExceptionOccurred = globalEnv_ExceptionOccurred,
-    .ExceptionDescribe = globalEnv_ExceptionDescribe,
-    .ExceptionClear = globalEnv_ExceptionClear,
-    .FatalError = globalEnv_FatalError,
-    .PushLocalFrame = globalEnv_PushLocalFrame,
-    .PopLocalFrame = globalEnv_PopLocalFrame,
-    .NewGlobalRef = globalEnv_NewGlobalRef,
-    .DeleteGlobalRef = globalEnv_DeleteGlobalRef,
-    .IsSameObject = globalEnv_IsSameObject,
-    .EnsureLocalCapacity = globalEnv_EnsureLocalCapacity,
-    .AllocObject = globalEnv_AllocObject,
-    .NewObject = globalEnv_NewObject,
-    .NewObjectA = globalEnv_NewObjectA,
-    .GetObjectClass = globalEnv_GetObjectClass,
-    .IsInstanceOf = globalEnv_IsInstanceOf,
-    .GetMethodID = globalEnv_GetMethodID,
-    .CallObjectMethod = globalEnv_CallObjectMethod,
-    .CallObjectMethodA = globalEnv_CallObjectMethodA,
-    .CallBooleanMethod = globalEnv_CallBooleanMethod,
-    .CallBooleanMethodA = globalEnv_CallBooleanMethodA,
-    .CallByteMethod = globalEnv_CallByteMethod,
-    .CallByteMethodA = globalEnv_CallByteMethodA,
-    .CallCharMethod = globalEnv_CallCharMethod,
-    .CallCharMethodA = globalEnv_CallCharMethodA,
-    .CallShortMethod = globalEnv_CallShortMethod,
-    .CallShortMethodA = globalEnv_CallShortMethodA,
-    .CallIntMethod = globalEnv_CallIntMethod,
-    .CallIntMethodA = globalEnv_CallIntMethodA,
-    .CallLongMethod = globalEnv_CallLongMethod,
-    .CallLongMethodA = globalEnv_CallLongMethodA,
-    .CallFloatMethod = globalEnv_CallFloatMethod,
-    .CallFloatMethodA = globalEnv_CallFloatMethodA,
-    .CallDoubleMethod = globalEnv_CallDoubleMethod,
-    .CallDoubleMethodA = globalEnv_CallDoubleMethodA,
-    .CallVoidMethod = globalEnv_CallVoidMethod,
-    .CallVoidMethodA = globalEnv_CallVoidMethodA,
-    .CallNonvirtualObjectMethod = globalEnv_CallNonvirtualObjectMethod,
-    .CallNonvirtualObjectMethodA = globalEnv_CallNonvirtualObjectMethodA,
-    .CallNonvirtualBooleanMethod = globalEnv_CallNonvirtualBooleanMethod,
-    .CallNonvirtualBooleanMethodA = globalEnv_CallNonvirtualBooleanMethodA,
-    .CallNonvirtualByteMethod = globalEnv_CallNonvirtualByteMethod,
-    .CallNonvirtualByteMethodA = globalEnv_CallNonvirtualByteMethodA,
-    .CallNonvirtualCharMethod = globalEnv_CallNonvirtualCharMethod,
-    .CallNonvirtualCharMethodA = globalEnv_CallNonvirtualCharMethodA,
-    .CallNonvirtualShortMethod = globalEnv_CallNonvirtualShortMethod,
-    .CallNonvirtualShortMethodA = globalEnv_CallNonvirtualShortMethodA,
-    .CallNonvirtualIntMethod = globalEnv_CallNonvirtualIntMethod,
-    .CallNonvirtualIntMethodA = globalEnv_CallNonvirtualIntMethodA,
-    .CallNonvirtualLongMethod = globalEnv_CallNonvirtualLongMethod,
-    .CallNonvirtualLongMethodA = globalEnv_CallNonvirtualLongMethodA,
-    .CallNonvirtualFloatMethod = globalEnv_CallNonvirtualFloatMethod,
-    .CallNonvirtualFloatMethodA = globalEnv_CallNonvirtualFloatMethodA,
-    .CallNonvirtualDoubleMethod = globalEnv_CallNonvirtualDoubleMethod,
-    .CallNonvirtualDoubleMethodA = globalEnv_CallNonvirtualDoubleMethodA,
-    .CallNonvirtualVoidMethod = globalEnv_CallNonvirtualVoidMethod,
-    .CallNonvirtualVoidMethodA = globalEnv_CallNonvirtualVoidMethodA,
-    .GetFieldID = globalEnv_GetFieldID,
-    .GetObjectField = globalEnv_GetObjectField,
-    .GetBooleanField = globalEnv_GetBooleanField,
-    .GetByteField = globalEnv_GetByteField,
-    .GetCharField = globalEnv_GetCharField,
-    .GetShortField = globalEnv_GetShortField,
-    .GetIntField = globalEnv_GetIntField,
-    .GetLongField = globalEnv_GetLongField,
-    .GetFloatField = globalEnv_GetFloatField,
-    .GetDoubleField = globalEnv_GetDoubleField,
-    .SetObjectField = globalEnv_SetObjectField,
-    .SetBooleanField = globalEnv_SetBooleanField,
-    .SetByteField = globalEnv_SetByteField,
-    .SetCharField = globalEnv_SetCharField,
-    .SetShortField = globalEnv_SetShortField,
-    .SetIntField = globalEnv_SetIntField,
-    .SetLongField = globalEnv_SetLongField,
-    .SetFloatField = globalEnv_SetFloatField,
-    .SetDoubleField = globalEnv_SetDoubleField,
-    .GetStaticMethodID = globalEnv_GetStaticMethodID,
-    .CallStaticObjectMethod = globalEnv_CallStaticObjectMethod,
-    .CallStaticObjectMethodA = globalEnv_CallStaticObjectMethodA,
-    .CallStaticBooleanMethod = globalEnv_CallStaticBooleanMethod,
-    .CallStaticBooleanMethodA = globalEnv_CallStaticBooleanMethodA,
-    .CallStaticByteMethod = globalEnv_CallStaticByteMethod,
-    .CallStaticByteMethodA = globalEnv_CallStaticByteMethodA,
-    .CallStaticCharMethod = globalEnv_CallStaticCharMethod,
-    .CallStaticCharMethodA = globalEnv_CallStaticCharMethodA,
-    .CallStaticShortMethod = globalEnv_CallStaticShortMethod,
-    .CallStaticShortMethodA = globalEnv_CallStaticShortMethodA,
-    .CallStaticIntMethod = globalEnv_CallStaticIntMethod,
-    .CallStaticIntMethodA = globalEnv_CallStaticIntMethodA,
-    .CallStaticLongMethod = globalEnv_CallStaticLongMethod,
-    .CallStaticLongMethodA = globalEnv_CallStaticLongMethodA,
-    .CallStaticFloatMethod = globalEnv_CallStaticFloatMethod,
-    .CallStaticFloatMethodA = globalEnv_CallStaticFloatMethodA,
-    .CallStaticDoubleMethod = globalEnv_CallStaticDoubleMethod,
-    .CallStaticDoubleMethodA = globalEnv_CallStaticDoubleMethodA,
-    .CallStaticVoidMethod = globalEnv_CallStaticVoidMethod,
-    .CallStaticVoidMethodA = globalEnv_CallStaticVoidMethodA,
-    .GetStaticFieldID = globalEnv_GetStaticFieldID,
-    .GetStaticObjectField = globalEnv_GetStaticObjectField,
-    .GetStaticBooleanField = globalEnv_GetStaticBooleanField,
-    .GetStaticByteField = globalEnv_GetStaticByteField,
-    .GetStaticCharField = globalEnv_GetStaticCharField,
-    .GetStaticShortField = globalEnv_GetStaticShortField,
-    .GetStaticIntField = globalEnv_GetStaticIntField,
-    .GetStaticLongField = globalEnv_GetStaticLongField,
-    .GetStaticFloatField = globalEnv_GetStaticFloatField,
-    .GetStaticDoubleField = globalEnv_GetStaticDoubleField,
-    .SetStaticObjectField = globalEnv_SetStaticObjectField,
-    .SetStaticBooleanField = globalEnv_SetStaticBooleanField,
-    .SetStaticByteField = globalEnv_SetStaticByteField,
-    .SetStaticCharField = globalEnv_SetStaticCharField,
-    .SetStaticShortField = globalEnv_SetStaticShortField,
-    .SetStaticIntField = globalEnv_SetStaticIntField,
-    .SetStaticLongField = globalEnv_SetStaticLongField,
-    .SetStaticFloatField = globalEnv_SetStaticFloatField,
-    .SetStaticDoubleField = globalEnv_SetStaticDoubleField,
-    .NewString = globalEnv_NewString,
-    .GetStringLength = globalEnv_GetStringLength,
-    .GetStringChars = globalEnv_GetStringChars,
-    .ReleaseStringChars = globalEnv_ReleaseStringChars,
-    .NewStringUTF = globalEnv_NewStringUTF,
-    .GetStringUTFLength = globalEnv_GetStringUTFLength,
-    .GetStringUTFChars = globalEnv_GetStringUTFChars,
-    .ReleaseStringUTFChars = globalEnv_ReleaseStringUTFChars,
-    .GetArrayLength = globalEnv_GetArrayLength,
-    .NewObjectArray = globalEnv_NewObjectArray,
-    .GetObjectArrayElement = globalEnv_GetObjectArrayElement,
-    .SetObjectArrayElement = globalEnv_SetObjectArrayElement,
-    .NewBooleanArray = globalEnv_NewBooleanArray,
-    .NewByteArray = globalEnv_NewByteArray,
-    .NewCharArray = globalEnv_NewCharArray,
-    .NewShortArray = globalEnv_NewShortArray,
-    .NewIntArray = globalEnv_NewIntArray,
-    .NewLongArray = globalEnv_NewLongArray,
-    .NewFloatArray = globalEnv_NewFloatArray,
-    .NewDoubleArray = globalEnv_NewDoubleArray,
-    .GetBooleanArrayElements = globalEnv_GetBooleanArrayElements,
-    .GetByteArrayElements = globalEnv_GetByteArrayElements,
-    .GetCharArrayElements = globalEnv_GetCharArrayElements,
-    .GetShortArrayElements = globalEnv_GetShortArrayElements,
-    .GetIntArrayElements = globalEnv_GetIntArrayElements,
-    .GetLongArrayElements = globalEnv_GetLongArrayElements,
-    .GetFloatArrayElements = globalEnv_GetFloatArrayElements,
-    .GetDoubleArrayElements = globalEnv_GetDoubleArrayElements,
-    .ReleaseBooleanArrayElements = globalEnv_ReleaseBooleanArrayElements,
-    .ReleaseByteArrayElements = globalEnv_ReleaseByteArrayElements,
-    .ReleaseCharArrayElements = globalEnv_ReleaseCharArrayElements,
-    .ReleaseShortArrayElements = globalEnv_ReleaseShortArrayElements,
-    .ReleaseIntArrayElements = globalEnv_ReleaseIntArrayElements,
-    .ReleaseLongArrayElements = globalEnv_ReleaseLongArrayElements,
-    .ReleaseFloatArrayElements = globalEnv_ReleaseFloatArrayElements,
-    .ReleaseDoubleArrayElements = globalEnv_ReleaseDoubleArrayElements,
-    .GetBooleanArrayRegion = globalEnv_GetBooleanArrayRegion,
-    .GetByteArrayRegion = globalEnv_GetByteArrayRegion,
-    .GetCharArrayRegion = globalEnv_GetCharArrayRegion,
-    .GetShortArrayRegion = globalEnv_GetShortArrayRegion,
-    .GetIntArrayRegion = globalEnv_GetIntArrayRegion,
-    .GetLongArrayRegion = globalEnv_GetLongArrayRegion,
-    .GetFloatArrayRegion = globalEnv_GetFloatArrayRegion,
-    .GetDoubleArrayRegion = globalEnv_GetDoubleArrayRegion,
-    .SetBooleanArrayRegion = globalEnv_SetBooleanArrayRegion,
-    .SetByteArrayRegion = globalEnv_SetByteArrayRegion,
-    .SetCharArrayRegion = globalEnv_SetCharArrayRegion,
-    .SetShortArrayRegion = globalEnv_SetShortArrayRegion,
-    .SetIntArrayRegion = globalEnv_SetIntArrayRegion,
-    .SetLongArrayRegion = globalEnv_SetLongArrayRegion,
-    .SetFloatArrayRegion = globalEnv_SetFloatArrayRegion,
-    .SetDoubleArrayRegion = globalEnv_SetDoubleArrayRegion,
-    .RegisterNatives = globalEnv_RegisterNatives,
-    .UnregisterNatives = globalEnv_UnregisterNatives,
-    .MonitorEnter = globalEnv_MonitorEnter,
-    .MonitorExit = globalEnv_MonitorExit,
-    .GetJavaVM = globalEnv_GetJavaVM,
-    .GetStringRegion = globalEnv_GetStringRegion,
-    .GetStringUTFRegion = globalEnv_GetStringUTFRegion,
-    .GetPrimitiveArrayCritical = globalEnv_GetPrimitiveArrayCritical,
-    .ReleasePrimitiveArrayCritical = globalEnv_ReleasePrimitiveArrayCritical,
-    .GetStringCritical = globalEnv_GetStringCritical,
-    .ReleaseStringCritical = globalEnv_ReleaseStringCritical,
-    .NewWeakGlobalRef = globalEnv_NewWeakGlobalRef,
-    .DeleteWeakGlobalRef = globalEnv_DeleteWeakGlobalRef,
-    .ExceptionCheck = globalEnv_ExceptionCheck,
-    .NewDirectByteBuffer = globalEnv_NewDirectByteBuffer,
-    .GetDirectBufferAddress = globalEnv_GetDirectBufferAddress,
-    .GetDirectBufferCapacity = globalEnv_GetDirectBufferCapacity,
-    .GetObjectRefType = globalEnv_GetObjectRefType,
-};
-
-FFI_PLUGIN_EXPORT struct GlobalJniEnv *GetGlobalEnv(void) {
-    if (jni.jvm == NULL) return NULL;
-    return &globalEnv;
-}
diff --git a/pkgs/jni/src/global_jni_env.h b/pkgs/jni/src/global_jni_env.h
deleted file mode 100644
index befaf4b..0000000
--- a/pkgs/jni/src/global_jni_env.h
+++ /dev/null
@@ -1,219 +0,0 @@
-// Copyright (c) 2022, 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.
-
-#include "dartjni.h"
-/// Wrapper over JNIEnv in the JNI API, which can be used from multiple Dart
-/// Threads.
-///
-/// It consists of wrappers to JNIEnv methods which manage the thread-local
-/// JNIEnv pointer in C code. Additionally, any returned local reference value
-/// is converted to global reference.
-///
-/// For the documentation on methods themselves, see the JNI Specification at
-/// https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html
-///
-/// Apart from the specification, the Android NDK's JNI page consists of useful
-/// information about using the JNI:
-/// https://developer.android.com/training/articles/perf-jni
-typedef struct GlobalJniEnv {
-    jint (*GetVersion)();
-    jclass (*DefineClass)(const char * name, jobject loader, const jbyte * buf, jsize bufLen);
-    jclass (*FindClass)(const char * name);
-    jmethodID (*FromReflectedMethod)(jobject method);
-    jfieldID (*FromReflectedField)(jobject field);
-    jobject (*ToReflectedMethod)(jclass cls, jmethodID methodId, jboolean isStatic);
-    jclass (*GetSuperclass)(jclass clazz);
-    jboolean (*IsAssignableFrom)(jclass clazz1, jclass clazz2);
-    jobject (*ToReflectedField)(jclass cls, jfieldID fieldID, jboolean isStatic);
-    jint (*Throw)(jthrowable obj);
-    jint (*ThrowNew)(jclass clazz, const char * message);
-    jthrowable (*ExceptionOccurred)();
-    void (*ExceptionDescribe)();
-    void (*ExceptionClear)();
-    void (*FatalError)(const char * msg);
-    jint (*PushLocalFrame)(jint capacity);
-    jobject (*PopLocalFrame)(jobject result);
-    jobject (*NewGlobalRef)(jobject obj);
-    void (*DeleteGlobalRef)(jobject globalRef);
-    jboolean (*IsSameObject)(jobject ref1, jobject ref2);
-    jint (*EnsureLocalCapacity)(jint capacity);
-    jobject (*AllocObject)(jclass clazz);
-    jobject (*NewObject)(jclass arg1, jmethodID arg2);
-    jobject (*NewObjectA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jclass (*GetObjectClass)(jobject obj);
-    jboolean (*IsInstanceOf)(jobject obj, jclass clazz);
-    jmethodID (*GetMethodID)(jclass clazz, const char * name, const char * sig);
-    jobject (*CallObjectMethod)(jobject arg1, jmethodID arg2);
-    jobject (*CallObjectMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jboolean (*CallBooleanMethod)(jobject arg1, jmethodID arg2);
-    jboolean (*CallBooleanMethodA)(jobject obj, jmethodID methodId, const jvalue * args);
-    jbyte (*CallByteMethod)(jobject arg1, jmethodID arg2);
-    jbyte (*CallByteMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jchar (*CallCharMethod)(jobject arg1, jmethodID arg2);
-    jchar (*CallCharMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jshort (*CallShortMethod)(jobject arg1, jmethodID arg2);
-    jshort (*CallShortMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jint (*CallIntMethod)(jobject arg1, jmethodID arg2);
-    jint (*CallIntMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jlong (*CallLongMethod)(jobject arg1, jmethodID arg2);
-    jlong (*CallLongMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jfloat (*CallFloatMethod)(jobject arg1, jmethodID arg2);
-    jfloat (*CallFloatMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jdouble (*CallDoubleMethod)(jobject arg1, jmethodID arg2);
-    jdouble (*CallDoubleMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    void (*CallVoidMethod)(jobject arg1, jmethodID arg2);
-    void (*CallVoidMethodA)(jobject obj, jmethodID methodID, const jvalue * args);
-    jobject (*CallNonvirtualObjectMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jobject (*CallNonvirtualObjectMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jboolean (*CallNonvirtualBooleanMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jboolean (*CallNonvirtualBooleanMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jbyte (*CallNonvirtualByteMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jbyte (*CallNonvirtualByteMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jchar (*CallNonvirtualCharMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jchar (*CallNonvirtualCharMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jshort (*CallNonvirtualShortMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jshort (*CallNonvirtualShortMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jint (*CallNonvirtualIntMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jint (*CallNonvirtualIntMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jlong (*CallNonvirtualLongMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jlong (*CallNonvirtualLongMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jfloat (*CallNonvirtualFloatMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jfloat (*CallNonvirtualFloatMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jdouble (*CallNonvirtualDoubleMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    jdouble (*CallNonvirtualDoubleMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    void (*CallNonvirtualVoidMethod)(jobject arg1, jclass arg2, jmethodID arg3);
-    void (*CallNonvirtualVoidMethodA)(jobject obj, jclass clazz, jmethodID methodID, const jvalue * args);
-    jfieldID (*GetFieldID)(jclass clazz, const char * name, const char * sig);
-    jobject (*GetObjectField)(jobject obj, jfieldID fieldID);
-    jboolean (*GetBooleanField)(jobject obj, jfieldID fieldID);
-    jbyte (*GetByteField)(jobject obj, jfieldID fieldID);
-    jchar (*GetCharField)(jobject obj, jfieldID fieldID);
-    jshort (*GetShortField)(jobject obj, jfieldID fieldID);
-    jint (*GetIntField)(jobject obj, jfieldID fieldID);
-    jlong (*GetLongField)(jobject obj, jfieldID fieldID);
-    jfloat (*GetFloatField)(jobject obj, jfieldID fieldID);
-    jdouble (*GetDoubleField)(jobject obj, jfieldID fieldID);
-    void (*SetObjectField)(jobject obj, jfieldID fieldID, jobject val);
-    void (*SetBooleanField)(jobject obj, jfieldID fieldID, jboolean val);
-    void (*SetByteField)(jobject obj, jfieldID fieldID, jbyte val);
-    void (*SetCharField)(jobject obj, jfieldID fieldID, jchar val);
-    void (*SetShortField)(jobject obj, jfieldID fieldID, jshort val);
-    void (*SetIntField)(jobject obj, jfieldID fieldID, jint val);
-    void (*SetLongField)(jobject obj, jfieldID fieldID, jlong val);
-    void (*SetFloatField)(jobject obj, jfieldID fieldID, jfloat val);
-    void (*SetDoubleField)(jobject obj, jfieldID fieldID, jdouble val);
-    jmethodID (*GetStaticMethodID)(jclass clazz, const char * name, const char * sig);
-    jobject (*CallStaticObjectMethod)(jclass arg1, jmethodID arg2);
-    jobject (*CallStaticObjectMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jboolean (*CallStaticBooleanMethod)(jclass arg1, jmethodID arg2);
-    jboolean (*CallStaticBooleanMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jbyte (*CallStaticByteMethod)(jclass arg1, jmethodID arg2);
-    jbyte (*CallStaticByteMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jchar (*CallStaticCharMethod)(jclass arg1, jmethodID arg2);
-    jchar (*CallStaticCharMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jshort (*CallStaticShortMethod)(jclass arg1, jmethodID arg2);
-    jshort (*CallStaticShortMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jint (*CallStaticIntMethod)(jclass arg1, jmethodID arg2);
-    jint (*CallStaticIntMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jlong (*CallStaticLongMethod)(jclass arg1, jmethodID arg2);
-    jlong (*CallStaticLongMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jfloat (*CallStaticFloatMethod)(jclass arg1, jmethodID arg2);
-    jfloat (*CallStaticFloatMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jdouble (*CallStaticDoubleMethod)(jclass arg1, jmethodID arg2);
-    jdouble (*CallStaticDoubleMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    void (*CallStaticVoidMethod)(jclass arg1, jmethodID arg2);
-    void (*CallStaticVoidMethodA)(jclass clazz, jmethodID methodID, const jvalue * args);
-    jfieldID (*GetStaticFieldID)(jclass clazz, const char * name, const char * sig);
-    jobject (*GetStaticObjectField)(jclass clazz, jfieldID fieldID);
-    jboolean (*GetStaticBooleanField)(jclass clazz, jfieldID fieldID);
-    jbyte (*GetStaticByteField)(jclass clazz, jfieldID fieldID);
-    jchar (*GetStaticCharField)(jclass clazz, jfieldID fieldID);
-    jshort (*GetStaticShortField)(jclass clazz, jfieldID fieldID);
-    jint (*GetStaticIntField)(jclass clazz, jfieldID fieldID);
-    jlong (*GetStaticLongField)(jclass clazz, jfieldID fieldID);
-    jfloat (*GetStaticFloatField)(jclass clazz, jfieldID fieldID);
-    jdouble (*GetStaticDoubleField)(jclass clazz, jfieldID fieldID);
-    void (*SetStaticObjectField)(jclass clazz, jfieldID fieldID, jobject val);
-    void (*SetStaticBooleanField)(jclass clazz, jfieldID fieldID, jboolean val);
-    void (*SetStaticByteField)(jclass clazz, jfieldID fieldID, jbyte val);
-    void (*SetStaticCharField)(jclass clazz, jfieldID fieldID, jchar val);
-    void (*SetStaticShortField)(jclass clazz, jfieldID fieldID, jshort val);
-    void (*SetStaticIntField)(jclass clazz, jfieldID fieldID, jint val);
-    void (*SetStaticLongField)(jclass clazz, jfieldID fieldID, jlong val);
-    void (*SetStaticFloatField)(jclass clazz, jfieldID fieldID, jfloat val);
-    void (*SetStaticDoubleField)(jclass clazz, jfieldID fieldID, jdouble val);
-    jstring (*NewString)(const jchar * unicodeChars, jsize len);
-    jsize (*GetStringLength)(jstring string);
-    const jchar * (*GetStringChars)(jstring string, jboolean * isCopy);
-    void (*ReleaseStringChars)(jstring string, const jchar * isCopy);
-    jstring (*NewStringUTF)(const char * bytes);
-    jsize (*GetStringUTFLength)(jstring string);
-    const char * (*GetStringUTFChars)(jstring string, jboolean * isCopy);
-    void (*ReleaseStringUTFChars)(jstring string, const char * utf);
-    jsize (*GetArrayLength)(jarray array);
-    jobjectArray (*NewObjectArray)(jsize length, jclass elementClass, jobject initialElement);
-    jobject (*GetObjectArrayElement)(jobjectArray array, jsize index);
-    void (*SetObjectArrayElement)(jobjectArray array, jsize index, jobject val);
-    jbooleanArray (*NewBooleanArray)(jsize length);
-    jbyteArray (*NewByteArray)(jsize length);
-    jcharArray (*NewCharArray)(jsize length);
-    jshortArray (*NewShortArray)(jsize length);
-    jintArray (*NewIntArray)(jsize length);
-    jlongArray (*NewLongArray)(jsize length);
-    jfloatArray (*NewFloatArray)(jsize length);
-    jdoubleArray (*NewDoubleArray)(jsize length);
-    jboolean * (*GetBooleanArrayElements)(jbooleanArray array, jboolean * isCopy);
-    jbyte * (*GetByteArrayElements)(jbyteArray array, jboolean * isCopy);
-    jchar * (*GetCharArrayElements)(jcharArray array, jboolean * isCopy);
-    jshort * (*GetShortArrayElements)(jshortArray array, jboolean * isCopy);
-    jint * (*GetIntArrayElements)(jintArray array, jboolean * isCopy);
-    jlong * (*GetLongArrayElements)(jlongArray array, jboolean * isCopy);
-    jfloat * (*GetFloatArrayElements)(jfloatArray array, jboolean * isCopy);
-    jdouble * (*GetDoubleArrayElements)(jdoubleArray array, jboolean * isCopy);
-    void (*ReleaseBooleanArrayElements)(jbooleanArray array, jboolean * elems, jint mode);
-    void (*ReleaseByteArrayElements)(jbyteArray array, jbyte * elems, jint mode);
-    void (*ReleaseCharArrayElements)(jcharArray array, jchar * elems, jint mode);
-    void (*ReleaseShortArrayElements)(jshortArray array, jshort * elems, jint mode);
-    void (*ReleaseIntArrayElements)(jintArray array, jint * elems, jint mode);
-    void (*ReleaseLongArrayElements)(jlongArray array, jlong * elems, jint mode);
-    void (*ReleaseFloatArrayElements)(jfloatArray array, jfloat * elems, jint mode);
-    void (*ReleaseDoubleArrayElements)(jdoubleArray array, jdouble * elems, jint mode);
-    void (*GetBooleanArrayRegion)(jbooleanArray array, jsize start, jsize len, jboolean * buf);
-    void (*GetByteArrayRegion)(jbyteArray array, jsize start, jsize len, jbyte * buf);
-    void (*GetCharArrayRegion)(jcharArray array, jsize start, jsize len, jchar * buf);
-    void (*GetShortArrayRegion)(jshortArray array, jsize start, jsize len, jshort * buf);
-    void (*GetIntArrayRegion)(jintArray array, jsize start, jsize len, jint * buf);
-    void (*GetLongArrayRegion)(jlongArray array, jsize start, jsize len, jlong * buf);
-    void (*GetFloatArrayRegion)(jfloatArray array, jsize start, jsize len, jfloat * buf);
-    void (*GetDoubleArrayRegion)(jdoubleArray array, jsize start, jsize len, jdouble * buf);
-    void (*SetBooleanArrayRegion)(jbooleanArray array, jsize start, jsize len, const jboolean * buf);
-    void (*SetByteArrayRegion)(jbyteArray array, jsize start, jsize len, const jbyte * buf);
-    void (*SetCharArrayRegion)(jcharArray array, jsize start, jsize len, const jchar * buf);
-    void (*SetShortArrayRegion)(jshortArray array, jsize start, jsize len, const jshort * buf);
-    void (*SetIntArrayRegion)(jintArray array, jsize start, jsize len, const jint * buf);
-    void (*SetLongArrayRegion)(jlongArray array, jsize start, jsize len, const jlong * buf);
-    void (*SetFloatArrayRegion)(jfloatArray array, jsize start, jsize len, const jfloat * buf);
-    void (*SetDoubleArrayRegion)(jdoubleArray array, jsize start, jsize len, const jdouble * buf);
-    jint (*RegisterNatives)(jclass clazz, const JNINativeMethod * methods, jint nMethods);
-    jint (*UnregisterNatives)(jclass clazz);
-    jint (*MonitorEnter)(jobject obj);
-    jint (*MonitorExit)(jobject obj);
-    jint (*GetJavaVM)(JavaVM ** vm);
-    void (*GetStringRegion)(jstring str, jsize start, jsize len, jchar * buf);
-    void (*GetStringUTFRegion)(jstring str, jsize start, jsize len, char * buf);
-    void * (*GetPrimitiveArrayCritical)(jarray array, jboolean * isCopy);
-    void (*ReleasePrimitiveArrayCritical)(jarray array, void * carray, jint mode);
-    const jchar * (*GetStringCritical)(jstring str, jboolean * isCopy);
-    void (*ReleaseStringCritical)(jstring str, const jchar * carray);
-    jweak (*NewWeakGlobalRef)(jobject obj);
-    void (*DeleteWeakGlobalRef)(jweak obj);
-    jboolean (*ExceptionCheck)();
-    jobject (*NewDirectByteBuffer)(void * address, jlong capacity);
-    void * (*GetDirectBufferAddress)(jobject buf);
-    jlong (*GetDirectBufferCapacity)(jobject buf);
-    jobjectRefType (*GetObjectRefType)(jobject obj);
-} GlobalJniEnv;
-
-extern GlobalJniEnv globalEnv;
-FFI_PLUGIN_EXPORT struct GlobalJniEnv *GetGlobalEnv(void);
diff --git a/pkgs/jni/src/third_party/global_jni_env.c b/pkgs/jni/src/third_party/global_jni_env.c
new file mode 100644
index 0000000..961f16f
--- /dev/null
+++ b/pkgs/jni/src/third_party/global_jni_env.c
@@ -0,0 +1,1417 @@
+// Generated from an annotated version of jni.h provided in Android NDK.
+// (NDK Version 23.1.7779620)
+// The license for original file is provided below:
+
+/*
+   * Copyright (C) 2006 The Android Open Source Project
+   *
+   * Licensed 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.
+   */
+
+#include "global_jni_env.h"
+#include "../dartjni.h"
+
+jint globalEnv_GetVersion() {
+  attach_thread();
+  return (*jniEnv)->GetVersion(jniEnv);
+}
+
+jclass globalEnv_DefineClass(const char* name,
+                             jobject loader,
+                             const jbyte* buf,
+                             jsize bufLen) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->DefineClass(jniEnv, name, loader, buf, bufLen));
+}
+
+jclass globalEnv_FindClass(const char* name) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->FindClass(jniEnv, name));
+}
+
+jmethodID globalEnv_FromReflectedMethod(jobject method) {
+  attach_thread();
+  return (*jniEnv)->FromReflectedMethod(jniEnv, method);
+}
+
+jfieldID globalEnv_FromReflectedField(jobject field) {
+  attach_thread();
+  return (*jniEnv)->FromReflectedField(jniEnv, field);
+}
+
+jobject globalEnv_ToReflectedMethod(jclass cls,
+                                    jmethodID methodId,
+                                    jboolean isStatic) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->ToReflectedMethod(jniEnv, cls, methodId, isStatic));
+}
+
+jclass globalEnv_GetSuperclass(jclass clazz) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->GetSuperclass(jniEnv, clazz));
+}
+
+jboolean globalEnv_IsAssignableFrom(jclass clazz1, jclass clazz2) {
+  attach_thread();
+  return (*jniEnv)->IsAssignableFrom(jniEnv, clazz1, clazz2);
+}
+
+jobject globalEnv_ToReflectedField(jclass cls,
+                                   jfieldID fieldID,
+                                   jboolean isStatic) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->ToReflectedField(jniEnv, cls, fieldID, isStatic));
+}
+
+jint globalEnv_Throw(jthrowable obj) {
+  attach_thread();
+  return (*jniEnv)->Throw(jniEnv, obj);
+}
+
+jint globalEnv_ThrowNew(jclass clazz, const char* message) {
+  attach_thread();
+  return (*jniEnv)->ThrowNew(jniEnv, clazz, message);
+}
+
+jthrowable globalEnv_ExceptionOccurred() {
+  attach_thread();
+  return to_global_ref((*jniEnv)->ExceptionOccurred(jniEnv));
+}
+
+void globalEnv_ExceptionDescribe() {
+  attach_thread();
+  (*jniEnv)->ExceptionDescribe(jniEnv);
+}
+
+void globalEnv_ExceptionClear() {
+  attach_thread();
+  (*jniEnv)->ExceptionClear(jniEnv);
+}
+
+void globalEnv_FatalError(const char* msg) {
+  attach_thread();
+  (*jniEnv)->FatalError(jniEnv, msg);
+}
+
+jint globalEnv_PushLocalFrame(jint capacity) {
+  attach_thread();
+  return (*jniEnv)->PushLocalFrame(jniEnv, capacity);
+}
+
+jobject globalEnv_PopLocalFrame(jobject result) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->PopLocalFrame(jniEnv, result));
+}
+
+jobject globalEnv_NewGlobalRef(jobject obj) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewGlobalRef(jniEnv, obj));
+}
+
+void globalEnv_DeleteGlobalRef(jobject globalRef) {
+  attach_thread();
+  (*jniEnv)->DeleteGlobalRef(jniEnv, globalRef);
+}
+
+jboolean globalEnv_IsSameObject(jobject ref1, jobject ref2) {
+  attach_thread();
+  return (*jniEnv)->IsSameObject(jniEnv, ref1, ref2);
+}
+
+jint globalEnv_EnsureLocalCapacity(jint capacity) {
+  attach_thread();
+  return (*jniEnv)->EnsureLocalCapacity(jniEnv, capacity);
+}
+
+jobject globalEnv_AllocObject(jclass clazz) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->AllocObject(jniEnv, clazz));
+}
+
+jobject globalEnv_NewObject(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewObject(jniEnv, arg1, arg2));
+}
+
+jobject globalEnv_NewObjectA(jclass clazz,
+                             jmethodID methodID,
+                             const jvalue* args) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewObjectA(jniEnv, clazz, methodID, args));
+}
+
+jclass globalEnv_GetObjectClass(jobject obj) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->GetObjectClass(jniEnv, obj));
+}
+
+jboolean globalEnv_IsInstanceOf(jobject obj, jclass clazz) {
+  attach_thread();
+  return (*jniEnv)->IsInstanceOf(jniEnv, obj, clazz);
+}
+
+jmethodID globalEnv_GetMethodID(jclass clazz,
+                                const char* name,
+                                const char* sig) {
+  attach_thread();
+  return (*jniEnv)->GetMethodID(jniEnv, clazz, name, sig);
+}
+
+jobject globalEnv_CallObjectMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->CallObjectMethod(jniEnv, arg1, arg2));
+}
+
+jobject globalEnv_CallObjectMethodA(jobject obj,
+                                    jmethodID methodID,
+                                    const jvalue* args) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->CallObjectMethodA(jniEnv, obj, methodID, args));
+}
+
+jboolean globalEnv_CallBooleanMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallBooleanMethod(jniEnv, arg1, arg2);
+}
+
+jboolean globalEnv_CallBooleanMethodA(jobject obj,
+                                      jmethodID methodId,
+                                      const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallBooleanMethodA(jniEnv, obj, methodId, args);
+}
+
+jbyte globalEnv_CallByteMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallByteMethod(jniEnv, arg1, arg2);
+}
+
+jbyte globalEnv_CallByteMethodA(jobject obj,
+                                jmethodID methodID,
+                                const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallByteMethodA(jniEnv, obj, methodID, args);
+}
+
+jchar globalEnv_CallCharMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallCharMethod(jniEnv, arg1, arg2);
+}
+
+jchar globalEnv_CallCharMethodA(jobject obj,
+                                jmethodID methodID,
+                                const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallCharMethodA(jniEnv, obj, methodID, args);
+}
+
+jshort globalEnv_CallShortMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallShortMethod(jniEnv, arg1, arg2);
+}
+
+jshort globalEnv_CallShortMethodA(jobject obj,
+                                  jmethodID methodID,
+                                  const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallShortMethodA(jniEnv, obj, methodID, args);
+}
+
+jint globalEnv_CallIntMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallIntMethod(jniEnv, arg1, arg2);
+}
+
+jint globalEnv_CallIntMethodA(jobject obj,
+                              jmethodID methodID,
+                              const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallIntMethodA(jniEnv, obj, methodID, args);
+}
+
+jlong globalEnv_CallLongMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallLongMethod(jniEnv, arg1, arg2);
+}
+
+jlong globalEnv_CallLongMethodA(jobject obj,
+                                jmethodID methodID,
+                                const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallLongMethodA(jniEnv, obj, methodID, args);
+}
+
+jfloat globalEnv_CallFloatMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallFloatMethod(jniEnv, arg1, arg2);
+}
+
+jfloat globalEnv_CallFloatMethodA(jobject obj,
+                                  jmethodID methodID,
+                                  const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallFloatMethodA(jniEnv, obj, methodID, args);
+}
+
+jdouble globalEnv_CallDoubleMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallDoubleMethod(jniEnv, arg1, arg2);
+}
+
+jdouble globalEnv_CallDoubleMethodA(jobject obj,
+                                    jmethodID methodID,
+                                    const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallDoubleMethodA(jniEnv, obj, methodID, args);
+}
+
+void globalEnv_CallVoidMethod(jobject arg1, jmethodID arg2) {
+  attach_thread();
+  (*jniEnv)->CallVoidMethod(jniEnv, arg1, arg2);
+}
+
+void globalEnv_CallVoidMethodA(jobject obj,
+                               jmethodID methodID,
+                               const jvalue* args) {
+  attach_thread();
+  (*jniEnv)->CallVoidMethodA(jniEnv, obj, methodID, args);
+}
+
+jobject globalEnv_CallNonvirtualObjectMethod(jobject arg1,
+                                             jclass arg2,
+                                             jmethodID arg3) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->CallNonvirtualObjectMethod(jniEnv, arg1, arg2, arg3));
+}
+
+jobject globalEnv_CallNonvirtualObjectMethodA(jobject obj,
+                                              jclass clazz,
+                                              jmethodID methodID,
+                                              const jvalue* args) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->CallNonvirtualObjectMethodA(
+      jniEnv, obj, clazz, methodID, args));
+}
+
+jboolean globalEnv_CallNonvirtualBooleanMethod(jobject arg1,
+                                               jclass arg2,
+                                               jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualBooleanMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jboolean globalEnv_CallNonvirtualBooleanMethodA(jobject obj,
+                                                jclass clazz,
+                                                jmethodID methodID,
+                                                const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualBooleanMethodA(jniEnv, obj, clazz, methodID,
+                                                 args);
+}
+
+jbyte globalEnv_CallNonvirtualByteMethod(jobject arg1,
+                                         jclass arg2,
+                                         jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualByteMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jbyte globalEnv_CallNonvirtualByteMethodA(jobject obj,
+                                          jclass clazz,
+                                          jmethodID methodID,
+                                          const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualByteMethodA(jniEnv, obj, clazz, methodID,
+                                              args);
+}
+
+jchar globalEnv_CallNonvirtualCharMethod(jobject arg1,
+                                         jclass arg2,
+                                         jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualCharMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jchar globalEnv_CallNonvirtualCharMethodA(jobject obj,
+                                          jclass clazz,
+                                          jmethodID methodID,
+                                          const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualCharMethodA(jniEnv, obj, clazz, methodID,
+                                              args);
+}
+
+jshort globalEnv_CallNonvirtualShortMethod(jobject arg1,
+                                           jclass arg2,
+                                           jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualShortMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jshort globalEnv_CallNonvirtualShortMethodA(jobject obj,
+                                            jclass clazz,
+                                            jmethodID methodID,
+                                            const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualShortMethodA(jniEnv, obj, clazz, methodID,
+                                               args);
+}
+
+jint globalEnv_CallNonvirtualIntMethod(jobject arg1,
+                                       jclass arg2,
+                                       jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualIntMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jint globalEnv_CallNonvirtualIntMethodA(jobject obj,
+                                        jclass clazz,
+                                        jmethodID methodID,
+                                        const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualIntMethodA(jniEnv, obj, clazz, methodID,
+                                             args);
+}
+
+jlong globalEnv_CallNonvirtualLongMethod(jobject arg1,
+                                         jclass arg2,
+                                         jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualLongMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jlong globalEnv_CallNonvirtualLongMethodA(jobject obj,
+                                          jclass clazz,
+                                          jmethodID methodID,
+                                          const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualLongMethodA(jniEnv, obj, clazz, methodID,
+                                              args);
+}
+
+jfloat globalEnv_CallNonvirtualFloatMethod(jobject arg1,
+                                           jclass arg2,
+                                           jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualFloatMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jfloat globalEnv_CallNonvirtualFloatMethodA(jobject obj,
+                                            jclass clazz,
+                                            jmethodID methodID,
+                                            const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualFloatMethodA(jniEnv, obj, clazz, methodID,
+                                               args);
+}
+
+jdouble globalEnv_CallNonvirtualDoubleMethod(jobject arg1,
+                                             jclass arg2,
+                                             jmethodID arg3) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualDoubleMethod(jniEnv, arg1, arg2, arg3);
+}
+
+jdouble globalEnv_CallNonvirtualDoubleMethodA(jobject obj,
+                                              jclass clazz,
+                                              jmethodID methodID,
+                                              const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallNonvirtualDoubleMethodA(jniEnv, obj, clazz, methodID,
+                                                args);
+}
+
+void globalEnv_CallNonvirtualVoidMethod(jobject arg1,
+                                        jclass arg2,
+                                        jmethodID arg3) {
+  attach_thread();
+  (*jniEnv)->CallNonvirtualVoidMethod(jniEnv, arg1, arg2, arg3);
+}
+
+void globalEnv_CallNonvirtualVoidMethodA(jobject obj,
+                                         jclass clazz,
+                                         jmethodID methodID,
+                                         const jvalue* args) {
+  attach_thread();
+  (*jniEnv)->CallNonvirtualVoidMethodA(jniEnv, obj, clazz, methodID, args);
+}
+
+jfieldID globalEnv_GetFieldID(jclass clazz, const char* name, const char* sig) {
+  attach_thread();
+  return (*jniEnv)->GetFieldID(jniEnv, clazz, name, sig);
+}
+
+jobject globalEnv_GetObjectField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->GetObjectField(jniEnv, obj, fieldID));
+}
+
+jboolean globalEnv_GetBooleanField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetBooleanField(jniEnv, obj, fieldID);
+}
+
+jbyte globalEnv_GetByteField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetByteField(jniEnv, obj, fieldID);
+}
+
+jchar globalEnv_GetCharField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetCharField(jniEnv, obj, fieldID);
+}
+
+jshort globalEnv_GetShortField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetShortField(jniEnv, obj, fieldID);
+}
+
+jint globalEnv_GetIntField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetIntField(jniEnv, obj, fieldID);
+}
+
+jlong globalEnv_GetLongField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetLongField(jniEnv, obj, fieldID);
+}
+
+jfloat globalEnv_GetFloatField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetFloatField(jniEnv, obj, fieldID);
+}
+
+jdouble globalEnv_GetDoubleField(jobject obj, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetDoubleField(jniEnv, obj, fieldID);
+}
+
+void globalEnv_SetObjectField(jobject obj, jfieldID fieldID, jobject val) {
+  attach_thread();
+  (*jniEnv)->SetObjectField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetBooleanField(jobject obj, jfieldID fieldID, jboolean val) {
+  attach_thread();
+  (*jniEnv)->SetBooleanField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetByteField(jobject obj, jfieldID fieldID, jbyte val) {
+  attach_thread();
+  (*jniEnv)->SetByteField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetCharField(jobject obj, jfieldID fieldID, jchar val) {
+  attach_thread();
+  (*jniEnv)->SetCharField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetShortField(jobject obj, jfieldID fieldID, jshort val) {
+  attach_thread();
+  (*jniEnv)->SetShortField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetIntField(jobject obj, jfieldID fieldID, jint val) {
+  attach_thread();
+  (*jniEnv)->SetIntField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetLongField(jobject obj, jfieldID fieldID, jlong val) {
+  attach_thread();
+  (*jniEnv)->SetLongField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetFloatField(jobject obj, jfieldID fieldID, jfloat val) {
+  attach_thread();
+  (*jniEnv)->SetFloatField(jniEnv, obj, fieldID, val);
+}
+
+void globalEnv_SetDoubleField(jobject obj, jfieldID fieldID, jdouble val) {
+  attach_thread();
+  (*jniEnv)->SetDoubleField(jniEnv, obj, fieldID, val);
+}
+
+jmethodID globalEnv_GetStaticMethodID(jclass clazz,
+                                      const char* name,
+                                      const char* sig) {
+  attach_thread();
+  return (*jniEnv)->GetStaticMethodID(jniEnv, clazz, name, sig);
+}
+
+jobject globalEnv_CallStaticObjectMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->CallStaticObjectMethod(jniEnv, arg1, arg2));
+}
+
+jobject globalEnv_CallStaticObjectMethodA(jclass clazz,
+                                          jmethodID methodID,
+                                          const jvalue* args) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->CallStaticObjectMethodA(jniEnv, clazz, methodID, args));
+}
+
+jboolean globalEnv_CallStaticBooleanMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticBooleanMethod(jniEnv, arg1, arg2);
+}
+
+jboolean globalEnv_CallStaticBooleanMethodA(jclass clazz,
+                                            jmethodID methodID,
+                                            const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticBooleanMethodA(jniEnv, clazz, methodID, args);
+}
+
+jbyte globalEnv_CallStaticByteMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticByteMethod(jniEnv, arg1, arg2);
+}
+
+jbyte globalEnv_CallStaticByteMethodA(jclass clazz,
+                                      jmethodID methodID,
+                                      const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticByteMethodA(jniEnv, clazz, methodID, args);
+}
+
+jchar globalEnv_CallStaticCharMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticCharMethod(jniEnv, arg1, arg2);
+}
+
+jchar globalEnv_CallStaticCharMethodA(jclass clazz,
+                                      jmethodID methodID,
+                                      const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticCharMethodA(jniEnv, clazz, methodID, args);
+}
+
+jshort globalEnv_CallStaticShortMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticShortMethod(jniEnv, arg1, arg2);
+}
+
+jshort globalEnv_CallStaticShortMethodA(jclass clazz,
+                                        jmethodID methodID,
+                                        const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticShortMethodA(jniEnv, clazz, methodID, args);
+}
+
+jint globalEnv_CallStaticIntMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticIntMethod(jniEnv, arg1, arg2);
+}
+
+jint globalEnv_CallStaticIntMethodA(jclass clazz,
+                                    jmethodID methodID,
+                                    const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticIntMethodA(jniEnv, clazz, methodID, args);
+}
+
+jlong globalEnv_CallStaticLongMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticLongMethod(jniEnv, arg1, arg2);
+}
+
+jlong globalEnv_CallStaticLongMethodA(jclass clazz,
+                                      jmethodID methodID,
+                                      const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticLongMethodA(jniEnv, clazz, methodID, args);
+}
+
+jfloat globalEnv_CallStaticFloatMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticFloatMethod(jniEnv, arg1, arg2);
+}
+
+jfloat globalEnv_CallStaticFloatMethodA(jclass clazz,
+                                        jmethodID methodID,
+                                        const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticFloatMethodA(jniEnv, clazz, methodID, args);
+}
+
+jdouble globalEnv_CallStaticDoubleMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  return (*jniEnv)->CallStaticDoubleMethod(jniEnv, arg1, arg2);
+}
+
+jdouble globalEnv_CallStaticDoubleMethodA(jclass clazz,
+                                          jmethodID methodID,
+                                          const jvalue* args) {
+  attach_thread();
+  return (*jniEnv)->CallStaticDoubleMethodA(jniEnv, clazz, methodID, args);
+}
+
+void globalEnv_CallStaticVoidMethod(jclass arg1, jmethodID arg2) {
+  attach_thread();
+  (*jniEnv)->CallStaticVoidMethod(jniEnv, arg1, arg2);
+}
+
+void globalEnv_CallStaticVoidMethodA(jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args) {
+  attach_thread();
+  (*jniEnv)->CallStaticVoidMethodA(jniEnv, clazz, methodID, args);
+}
+
+jfieldID globalEnv_GetStaticFieldID(jclass clazz,
+                                    const char* name,
+                                    const char* sig) {
+  attach_thread();
+  return (*jniEnv)->GetStaticFieldID(jniEnv, clazz, name, sig);
+}
+
+jobject globalEnv_GetStaticObjectField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->GetStaticObjectField(jniEnv, clazz, fieldID));
+}
+
+jboolean globalEnv_GetStaticBooleanField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticBooleanField(jniEnv, clazz, fieldID);
+}
+
+jbyte globalEnv_GetStaticByteField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticByteField(jniEnv, clazz, fieldID);
+}
+
+jchar globalEnv_GetStaticCharField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticCharField(jniEnv, clazz, fieldID);
+}
+
+jshort globalEnv_GetStaticShortField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticShortField(jniEnv, clazz, fieldID);
+}
+
+jint globalEnv_GetStaticIntField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticIntField(jniEnv, clazz, fieldID);
+}
+
+jlong globalEnv_GetStaticLongField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticLongField(jniEnv, clazz, fieldID);
+}
+
+jfloat globalEnv_GetStaticFloatField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticFloatField(jniEnv, clazz, fieldID);
+}
+
+jdouble globalEnv_GetStaticDoubleField(jclass clazz, jfieldID fieldID) {
+  attach_thread();
+  return (*jniEnv)->GetStaticDoubleField(jniEnv, clazz, fieldID);
+}
+
+void globalEnv_SetStaticObjectField(jclass clazz,
+                                    jfieldID fieldID,
+                                    jobject val) {
+  attach_thread();
+  (*jniEnv)->SetStaticObjectField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticBooleanField(jclass clazz,
+                                     jfieldID fieldID,
+                                     jboolean val) {
+  attach_thread();
+  (*jniEnv)->SetStaticBooleanField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticByteField(jclass clazz, jfieldID fieldID, jbyte val) {
+  attach_thread();
+  (*jniEnv)->SetStaticByteField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticCharField(jclass clazz, jfieldID fieldID, jchar val) {
+  attach_thread();
+  (*jniEnv)->SetStaticCharField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticShortField(jclass clazz, jfieldID fieldID, jshort val) {
+  attach_thread();
+  (*jniEnv)->SetStaticShortField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticIntField(jclass clazz, jfieldID fieldID, jint val) {
+  attach_thread();
+  (*jniEnv)->SetStaticIntField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticLongField(jclass clazz, jfieldID fieldID, jlong val) {
+  attach_thread();
+  (*jniEnv)->SetStaticLongField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticFloatField(jclass clazz, jfieldID fieldID, jfloat val) {
+  attach_thread();
+  (*jniEnv)->SetStaticFloatField(jniEnv, clazz, fieldID, val);
+}
+
+void globalEnv_SetStaticDoubleField(jclass clazz,
+                                    jfieldID fieldID,
+                                    jdouble val) {
+  attach_thread();
+  (*jniEnv)->SetStaticDoubleField(jniEnv, clazz, fieldID, val);
+}
+
+jstring globalEnv_NewString(const jchar* unicodeChars, jsize len) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewString(jniEnv, unicodeChars, len));
+}
+
+jsize globalEnv_GetStringLength(jstring string) {
+  attach_thread();
+  return (*jniEnv)->GetStringLength(jniEnv, string);
+}
+
+const jchar* globalEnv_GetStringChars(jstring string, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetStringChars(jniEnv, string, isCopy);
+}
+
+void globalEnv_ReleaseStringChars(jstring string, const jchar* isCopy) {
+  attach_thread();
+  (*jniEnv)->ReleaseStringChars(jniEnv, string, isCopy);
+}
+
+jstring globalEnv_NewStringUTF(const char* bytes) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewStringUTF(jniEnv, bytes));
+}
+
+jsize globalEnv_GetStringUTFLength(jstring string) {
+  attach_thread();
+  return (*jniEnv)->GetStringUTFLength(jniEnv, string);
+}
+
+const char* globalEnv_GetStringUTFChars(jstring string, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetStringUTFChars(jniEnv, string, isCopy);
+}
+
+void globalEnv_ReleaseStringUTFChars(jstring string, const char* utf) {
+  attach_thread();
+  (*jniEnv)->ReleaseStringUTFChars(jniEnv, string, utf);
+}
+
+jsize globalEnv_GetArrayLength(jarray array) {
+  attach_thread();
+  return (*jniEnv)->GetArrayLength(jniEnv, array);
+}
+
+jobjectArray globalEnv_NewObjectArray(jsize length,
+                                      jclass elementClass,
+                                      jobject initialElement) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->NewObjectArray(jniEnv, length, elementClass, initialElement));
+}
+
+jobject globalEnv_GetObjectArrayElement(jobjectArray array, jsize index) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->GetObjectArrayElement(jniEnv, array, index));
+}
+
+void globalEnv_SetObjectArrayElement(jobjectArray array,
+                                     jsize index,
+                                     jobject val) {
+  attach_thread();
+  (*jniEnv)->SetObjectArrayElement(jniEnv, array, index, val);
+}
+
+jbooleanArray globalEnv_NewBooleanArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewBooleanArray(jniEnv, length));
+}
+
+jbyteArray globalEnv_NewByteArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewByteArray(jniEnv, length));
+}
+
+jcharArray globalEnv_NewCharArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewCharArray(jniEnv, length));
+}
+
+jshortArray globalEnv_NewShortArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewShortArray(jniEnv, length));
+}
+
+jintArray globalEnv_NewIntArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewIntArray(jniEnv, length));
+}
+
+jlongArray globalEnv_NewLongArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewLongArray(jniEnv, length));
+}
+
+jfloatArray globalEnv_NewFloatArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewFloatArray(jniEnv, length));
+}
+
+jdoubleArray globalEnv_NewDoubleArray(jsize length) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewDoubleArray(jniEnv, length));
+}
+
+jboolean* globalEnv_GetBooleanArrayElements(jbooleanArray array,
+                                            jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetBooleanArrayElements(jniEnv, array, isCopy);
+}
+
+jbyte* globalEnv_GetByteArrayElements(jbyteArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetByteArrayElements(jniEnv, array, isCopy);
+}
+
+jchar* globalEnv_GetCharArrayElements(jcharArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetCharArrayElements(jniEnv, array, isCopy);
+}
+
+jshort* globalEnv_GetShortArrayElements(jshortArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetShortArrayElements(jniEnv, array, isCopy);
+}
+
+jint* globalEnv_GetIntArrayElements(jintArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetIntArrayElements(jniEnv, array, isCopy);
+}
+
+jlong* globalEnv_GetLongArrayElements(jlongArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetLongArrayElements(jniEnv, array, isCopy);
+}
+
+jfloat* globalEnv_GetFloatArrayElements(jfloatArray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetFloatArrayElements(jniEnv, array, isCopy);
+}
+
+jdouble* globalEnv_GetDoubleArrayElements(jdoubleArray array,
+                                          jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetDoubleArrayElements(jniEnv, array, isCopy);
+}
+
+void globalEnv_ReleaseBooleanArrayElements(jbooleanArray array,
+                                           jboolean* elems,
+                                           jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseBooleanArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseByteArrayElements(jbyteArray array,
+                                        jbyte* elems,
+                                        jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseByteArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseCharArrayElements(jcharArray array,
+                                        jchar* elems,
+                                        jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseCharArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseShortArrayElements(jshortArray array,
+                                         jshort* elems,
+                                         jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseShortArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseIntArrayElements(jintArray array,
+                                       jint* elems,
+                                       jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseIntArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseLongArrayElements(jlongArray array,
+                                        jlong* elems,
+                                        jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseLongArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseFloatArrayElements(jfloatArray array,
+                                         jfloat* elems,
+                                         jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseFloatArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_ReleaseDoubleArrayElements(jdoubleArray array,
+                                          jdouble* elems,
+                                          jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleaseDoubleArrayElements(jniEnv, array, elems, mode);
+}
+
+void globalEnv_GetBooleanArrayRegion(jbooleanArray array,
+                                     jsize start,
+                                     jsize len,
+                                     jboolean* buf) {
+  attach_thread();
+  (*jniEnv)->GetBooleanArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetByteArrayRegion(jbyteArray array,
+                                  jsize start,
+                                  jsize len,
+                                  jbyte* buf) {
+  attach_thread();
+  (*jniEnv)->GetByteArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetCharArrayRegion(jcharArray array,
+                                  jsize start,
+                                  jsize len,
+                                  jchar* buf) {
+  attach_thread();
+  (*jniEnv)->GetCharArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetShortArrayRegion(jshortArray array,
+                                   jsize start,
+                                   jsize len,
+                                   jshort* buf) {
+  attach_thread();
+  (*jniEnv)->GetShortArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetIntArrayRegion(jintArray array,
+                                 jsize start,
+                                 jsize len,
+                                 jint* buf) {
+  attach_thread();
+  (*jniEnv)->GetIntArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetLongArrayRegion(jlongArray array,
+                                  jsize start,
+                                  jsize len,
+                                  jlong* buf) {
+  attach_thread();
+  (*jniEnv)->GetLongArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetFloatArrayRegion(jfloatArray array,
+                                   jsize start,
+                                   jsize len,
+                                   jfloat* buf) {
+  attach_thread();
+  (*jniEnv)->GetFloatArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_GetDoubleArrayRegion(jdoubleArray array,
+                                    jsize start,
+                                    jsize len,
+                                    jdouble* buf) {
+  attach_thread();
+  (*jniEnv)->GetDoubleArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetBooleanArrayRegion(jbooleanArray array,
+                                     jsize start,
+                                     jsize len,
+                                     const jboolean* buf) {
+  attach_thread();
+  (*jniEnv)->SetBooleanArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetByteArrayRegion(jbyteArray array,
+                                  jsize start,
+                                  jsize len,
+                                  const jbyte* buf) {
+  attach_thread();
+  (*jniEnv)->SetByteArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetCharArrayRegion(jcharArray array,
+                                  jsize start,
+                                  jsize len,
+                                  const jchar* buf) {
+  attach_thread();
+  (*jniEnv)->SetCharArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetShortArrayRegion(jshortArray array,
+                                   jsize start,
+                                   jsize len,
+                                   const jshort* buf) {
+  attach_thread();
+  (*jniEnv)->SetShortArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetIntArrayRegion(jintArray array,
+                                 jsize start,
+                                 jsize len,
+                                 const jint* buf) {
+  attach_thread();
+  (*jniEnv)->SetIntArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetLongArrayRegion(jlongArray array,
+                                  jsize start,
+                                  jsize len,
+                                  const jlong* buf) {
+  attach_thread();
+  (*jniEnv)->SetLongArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetFloatArrayRegion(jfloatArray array,
+                                   jsize start,
+                                   jsize len,
+                                   const jfloat* buf) {
+  attach_thread();
+  (*jniEnv)->SetFloatArrayRegion(jniEnv, array, start, len, buf);
+}
+
+void globalEnv_SetDoubleArrayRegion(jdoubleArray array,
+                                    jsize start,
+                                    jsize len,
+                                    const jdouble* buf) {
+  attach_thread();
+  (*jniEnv)->SetDoubleArrayRegion(jniEnv, array, start, len, buf);
+}
+
+jint globalEnv_RegisterNatives(jclass clazz,
+                               const JNINativeMethod* methods,
+                               jint nMethods) {
+  attach_thread();
+  return (*jniEnv)->RegisterNatives(jniEnv, clazz, methods, nMethods);
+}
+
+jint globalEnv_UnregisterNatives(jclass clazz) {
+  attach_thread();
+  return (*jniEnv)->UnregisterNatives(jniEnv, clazz);
+}
+
+jint globalEnv_MonitorEnter(jobject obj) {
+  attach_thread();
+  return (*jniEnv)->MonitorEnter(jniEnv, obj);
+}
+
+jint globalEnv_MonitorExit(jobject obj) {
+  attach_thread();
+  return (*jniEnv)->MonitorExit(jniEnv, obj);
+}
+
+jint globalEnv_GetJavaVM(JavaVM** vm) {
+  attach_thread();
+  return (*jniEnv)->GetJavaVM(jniEnv, vm);
+}
+
+void globalEnv_GetStringRegion(jstring str,
+                               jsize start,
+                               jsize len,
+                               jchar* buf) {
+  attach_thread();
+  (*jniEnv)->GetStringRegion(jniEnv, str, start, len, buf);
+}
+
+void globalEnv_GetStringUTFRegion(jstring str,
+                                  jsize start,
+                                  jsize len,
+                                  char* buf) {
+  attach_thread();
+  (*jniEnv)->GetStringUTFRegion(jniEnv, str, start, len, buf);
+}
+
+void* globalEnv_GetPrimitiveArrayCritical(jarray array, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetPrimitiveArrayCritical(jniEnv, array, isCopy);
+}
+
+void globalEnv_ReleasePrimitiveArrayCritical(jarray array,
+                                             void* carray,
+                                             jint mode) {
+  attach_thread();
+  (*jniEnv)->ReleasePrimitiveArrayCritical(jniEnv, array, carray, mode);
+}
+
+const jchar* globalEnv_GetStringCritical(jstring str, jboolean* isCopy) {
+  attach_thread();
+  return (*jniEnv)->GetStringCritical(jniEnv, str, isCopy);
+}
+
+void globalEnv_ReleaseStringCritical(jstring str, const jchar* carray) {
+  attach_thread();
+  (*jniEnv)->ReleaseStringCritical(jniEnv, str, carray);
+}
+
+jweak globalEnv_NewWeakGlobalRef(jobject obj) {
+  attach_thread();
+  return to_global_ref((*jniEnv)->NewWeakGlobalRef(jniEnv, obj));
+}
+
+void globalEnv_DeleteWeakGlobalRef(jweak obj) {
+  attach_thread();
+  (*jniEnv)->DeleteWeakGlobalRef(jniEnv, obj);
+}
+
+jboolean globalEnv_ExceptionCheck() {
+  attach_thread();
+  return (*jniEnv)->ExceptionCheck(jniEnv);
+}
+
+jobject globalEnv_NewDirectByteBuffer(void* address, jlong capacity) {
+  attach_thread();
+  return to_global_ref(
+      (*jniEnv)->NewDirectByteBuffer(jniEnv, address, capacity));
+}
+
+void* globalEnv_GetDirectBufferAddress(jobject buf) {
+  attach_thread();
+  return (*jniEnv)->GetDirectBufferAddress(jniEnv, buf);
+}
+
+jlong globalEnv_GetDirectBufferCapacity(jobject buf) {
+  attach_thread();
+  return (*jniEnv)->GetDirectBufferCapacity(jniEnv, buf);
+}
+
+jobjectRefType globalEnv_GetObjectRefType(jobject obj) {
+  attach_thread();
+  return (*jniEnv)->GetObjectRefType(jniEnv, obj);
+}
+
+GlobalJniEnv globalJniEnv = {
+    .GetVersion = globalEnv_GetVersion,
+    .DefineClass = globalEnv_DefineClass,
+    .FindClass = globalEnv_FindClass,
+    .FromReflectedMethod = globalEnv_FromReflectedMethod,
+    .FromReflectedField = globalEnv_FromReflectedField,
+    .ToReflectedMethod = globalEnv_ToReflectedMethod,
+    .GetSuperclass = globalEnv_GetSuperclass,
+    .IsAssignableFrom = globalEnv_IsAssignableFrom,
+    .ToReflectedField = globalEnv_ToReflectedField,
+    .Throw = globalEnv_Throw,
+    .ThrowNew = globalEnv_ThrowNew,
+    .ExceptionOccurred = globalEnv_ExceptionOccurred,
+    .ExceptionDescribe = globalEnv_ExceptionDescribe,
+    .ExceptionClear = globalEnv_ExceptionClear,
+    .FatalError = globalEnv_FatalError,
+    .PushLocalFrame = globalEnv_PushLocalFrame,
+    .PopLocalFrame = globalEnv_PopLocalFrame,
+    .NewGlobalRef = globalEnv_NewGlobalRef,
+    .DeleteGlobalRef = globalEnv_DeleteGlobalRef,
+    .IsSameObject = globalEnv_IsSameObject,
+    .EnsureLocalCapacity = globalEnv_EnsureLocalCapacity,
+    .AllocObject = globalEnv_AllocObject,
+    .NewObject = globalEnv_NewObject,
+    .NewObjectA = globalEnv_NewObjectA,
+    .GetObjectClass = globalEnv_GetObjectClass,
+    .IsInstanceOf = globalEnv_IsInstanceOf,
+    .GetMethodID = globalEnv_GetMethodID,
+    .CallObjectMethod = globalEnv_CallObjectMethod,
+    .CallObjectMethodA = globalEnv_CallObjectMethodA,
+    .CallBooleanMethod = globalEnv_CallBooleanMethod,
+    .CallBooleanMethodA = globalEnv_CallBooleanMethodA,
+    .CallByteMethod = globalEnv_CallByteMethod,
+    .CallByteMethodA = globalEnv_CallByteMethodA,
+    .CallCharMethod = globalEnv_CallCharMethod,
+    .CallCharMethodA = globalEnv_CallCharMethodA,
+    .CallShortMethod = globalEnv_CallShortMethod,
+    .CallShortMethodA = globalEnv_CallShortMethodA,
+    .CallIntMethod = globalEnv_CallIntMethod,
+    .CallIntMethodA = globalEnv_CallIntMethodA,
+    .CallLongMethod = globalEnv_CallLongMethod,
+    .CallLongMethodA = globalEnv_CallLongMethodA,
+    .CallFloatMethod = globalEnv_CallFloatMethod,
+    .CallFloatMethodA = globalEnv_CallFloatMethodA,
+    .CallDoubleMethod = globalEnv_CallDoubleMethod,
+    .CallDoubleMethodA = globalEnv_CallDoubleMethodA,
+    .CallVoidMethod = globalEnv_CallVoidMethod,
+    .CallVoidMethodA = globalEnv_CallVoidMethodA,
+    .CallNonvirtualObjectMethod = globalEnv_CallNonvirtualObjectMethod,
+    .CallNonvirtualObjectMethodA = globalEnv_CallNonvirtualObjectMethodA,
+    .CallNonvirtualBooleanMethod = globalEnv_CallNonvirtualBooleanMethod,
+    .CallNonvirtualBooleanMethodA = globalEnv_CallNonvirtualBooleanMethodA,
+    .CallNonvirtualByteMethod = globalEnv_CallNonvirtualByteMethod,
+    .CallNonvirtualByteMethodA = globalEnv_CallNonvirtualByteMethodA,
+    .CallNonvirtualCharMethod = globalEnv_CallNonvirtualCharMethod,
+    .CallNonvirtualCharMethodA = globalEnv_CallNonvirtualCharMethodA,
+    .CallNonvirtualShortMethod = globalEnv_CallNonvirtualShortMethod,
+    .CallNonvirtualShortMethodA = globalEnv_CallNonvirtualShortMethodA,
+    .CallNonvirtualIntMethod = globalEnv_CallNonvirtualIntMethod,
+    .CallNonvirtualIntMethodA = globalEnv_CallNonvirtualIntMethodA,
+    .CallNonvirtualLongMethod = globalEnv_CallNonvirtualLongMethod,
+    .CallNonvirtualLongMethodA = globalEnv_CallNonvirtualLongMethodA,
+    .CallNonvirtualFloatMethod = globalEnv_CallNonvirtualFloatMethod,
+    .CallNonvirtualFloatMethodA = globalEnv_CallNonvirtualFloatMethodA,
+    .CallNonvirtualDoubleMethod = globalEnv_CallNonvirtualDoubleMethod,
+    .CallNonvirtualDoubleMethodA = globalEnv_CallNonvirtualDoubleMethodA,
+    .CallNonvirtualVoidMethod = globalEnv_CallNonvirtualVoidMethod,
+    .CallNonvirtualVoidMethodA = globalEnv_CallNonvirtualVoidMethodA,
+    .GetFieldID = globalEnv_GetFieldID,
+    .GetObjectField = globalEnv_GetObjectField,
+    .GetBooleanField = globalEnv_GetBooleanField,
+    .GetByteField = globalEnv_GetByteField,
+    .GetCharField = globalEnv_GetCharField,
+    .GetShortField = globalEnv_GetShortField,
+    .GetIntField = globalEnv_GetIntField,
+    .GetLongField = globalEnv_GetLongField,
+    .GetFloatField = globalEnv_GetFloatField,
+    .GetDoubleField = globalEnv_GetDoubleField,
+    .SetObjectField = globalEnv_SetObjectField,
+    .SetBooleanField = globalEnv_SetBooleanField,
+    .SetByteField = globalEnv_SetByteField,
+    .SetCharField = globalEnv_SetCharField,
+    .SetShortField = globalEnv_SetShortField,
+    .SetIntField = globalEnv_SetIntField,
+    .SetLongField = globalEnv_SetLongField,
+    .SetFloatField = globalEnv_SetFloatField,
+    .SetDoubleField = globalEnv_SetDoubleField,
+    .GetStaticMethodID = globalEnv_GetStaticMethodID,
+    .CallStaticObjectMethod = globalEnv_CallStaticObjectMethod,
+    .CallStaticObjectMethodA = globalEnv_CallStaticObjectMethodA,
+    .CallStaticBooleanMethod = globalEnv_CallStaticBooleanMethod,
+    .CallStaticBooleanMethodA = globalEnv_CallStaticBooleanMethodA,
+    .CallStaticByteMethod = globalEnv_CallStaticByteMethod,
+    .CallStaticByteMethodA = globalEnv_CallStaticByteMethodA,
+    .CallStaticCharMethod = globalEnv_CallStaticCharMethod,
+    .CallStaticCharMethodA = globalEnv_CallStaticCharMethodA,
+    .CallStaticShortMethod = globalEnv_CallStaticShortMethod,
+    .CallStaticShortMethodA = globalEnv_CallStaticShortMethodA,
+    .CallStaticIntMethod = globalEnv_CallStaticIntMethod,
+    .CallStaticIntMethodA = globalEnv_CallStaticIntMethodA,
+    .CallStaticLongMethod = globalEnv_CallStaticLongMethod,
+    .CallStaticLongMethodA = globalEnv_CallStaticLongMethodA,
+    .CallStaticFloatMethod = globalEnv_CallStaticFloatMethod,
+    .CallStaticFloatMethodA = globalEnv_CallStaticFloatMethodA,
+    .CallStaticDoubleMethod = globalEnv_CallStaticDoubleMethod,
+    .CallStaticDoubleMethodA = globalEnv_CallStaticDoubleMethodA,
+    .CallStaticVoidMethod = globalEnv_CallStaticVoidMethod,
+    .CallStaticVoidMethodA = globalEnv_CallStaticVoidMethodA,
+    .GetStaticFieldID = globalEnv_GetStaticFieldID,
+    .GetStaticObjectField = globalEnv_GetStaticObjectField,
+    .GetStaticBooleanField = globalEnv_GetStaticBooleanField,
+    .GetStaticByteField = globalEnv_GetStaticByteField,
+    .GetStaticCharField = globalEnv_GetStaticCharField,
+    .GetStaticShortField = globalEnv_GetStaticShortField,
+    .GetStaticIntField = globalEnv_GetStaticIntField,
+    .GetStaticLongField = globalEnv_GetStaticLongField,
+    .GetStaticFloatField = globalEnv_GetStaticFloatField,
+    .GetStaticDoubleField = globalEnv_GetStaticDoubleField,
+    .SetStaticObjectField = globalEnv_SetStaticObjectField,
+    .SetStaticBooleanField = globalEnv_SetStaticBooleanField,
+    .SetStaticByteField = globalEnv_SetStaticByteField,
+    .SetStaticCharField = globalEnv_SetStaticCharField,
+    .SetStaticShortField = globalEnv_SetStaticShortField,
+    .SetStaticIntField = globalEnv_SetStaticIntField,
+    .SetStaticLongField = globalEnv_SetStaticLongField,
+    .SetStaticFloatField = globalEnv_SetStaticFloatField,
+    .SetStaticDoubleField = globalEnv_SetStaticDoubleField,
+    .NewString = globalEnv_NewString,
+    .GetStringLength = globalEnv_GetStringLength,
+    .GetStringChars = globalEnv_GetStringChars,
+    .ReleaseStringChars = globalEnv_ReleaseStringChars,
+    .NewStringUTF = globalEnv_NewStringUTF,
+    .GetStringUTFLength = globalEnv_GetStringUTFLength,
+    .GetStringUTFChars = globalEnv_GetStringUTFChars,
+    .ReleaseStringUTFChars = globalEnv_ReleaseStringUTFChars,
+    .GetArrayLength = globalEnv_GetArrayLength,
+    .NewObjectArray = globalEnv_NewObjectArray,
+    .GetObjectArrayElement = globalEnv_GetObjectArrayElement,
+    .SetObjectArrayElement = globalEnv_SetObjectArrayElement,
+    .NewBooleanArray = globalEnv_NewBooleanArray,
+    .NewByteArray = globalEnv_NewByteArray,
+    .NewCharArray = globalEnv_NewCharArray,
+    .NewShortArray = globalEnv_NewShortArray,
+    .NewIntArray = globalEnv_NewIntArray,
+    .NewLongArray = globalEnv_NewLongArray,
+    .NewFloatArray = globalEnv_NewFloatArray,
+    .NewDoubleArray = globalEnv_NewDoubleArray,
+    .GetBooleanArrayElements = globalEnv_GetBooleanArrayElements,
+    .GetByteArrayElements = globalEnv_GetByteArrayElements,
+    .GetCharArrayElements = globalEnv_GetCharArrayElements,
+    .GetShortArrayElements = globalEnv_GetShortArrayElements,
+    .GetIntArrayElements = globalEnv_GetIntArrayElements,
+    .GetLongArrayElements = globalEnv_GetLongArrayElements,
+    .GetFloatArrayElements = globalEnv_GetFloatArrayElements,
+    .GetDoubleArrayElements = globalEnv_GetDoubleArrayElements,
+    .ReleaseBooleanArrayElements = globalEnv_ReleaseBooleanArrayElements,
+    .ReleaseByteArrayElements = globalEnv_ReleaseByteArrayElements,
+    .ReleaseCharArrayElements = globalEnv_ReleaseCharArrayElements,
+    .ReleaseShortArrayElements = globalEnv_ReleaseShortArrayElements,
+    .ReleaseIntArrayElements = globalEnv_ReleaseIntArrayElements,
+    .ReleaseLongArrayElements = globalEnv_ReleaseLongArrayElements,
+    .ReleaseFloatArrayElements = globalEnv_ReleaseFloatArrayElements,
+    .ReleaseDoubleArrayElements = globalEnv_ReleaseDoubleArrayElements,
+    .GetBooleanArrayRegion = globalEnv_GetBooleanArrayRegion,
+    .GetByteArrayRegion = globalEnv_GetByteArrayRegion,
+    .GetCharArrayRegion = globalEnv_GetCharArrayRegion,
+    .GetShortArrayRegion = globalEnv_GetShortArrayRegion,
+    .GetIntArrayRegion = globalEnv_GetIntArrayRegion,
+    .GetLongArrayRegion = globalEnv_GetLongArrayRegion,
+    .GetFloatArrayRegion = globalEnv_GetFloatArrayRegion,
+    .GetDoubleArrayRegion = globalEnv_GetDoubleArrayRegion,
+    .SetBooleanArrayRegion = globalEnv_SetBooleanArrayRegion,
+    .SetByteArrayRegion = globalEnv_SetByteArrayRegion,
+    .SetCharArrayRegion = globalEnv_SetCharArrayRegion,
+    .SetShortArrayRegion = globalEnv_SetShortArrayRegion,
+    .SetIntArrayRegion = globalEnv_SetIntArrayRegion,
+    .SetLongArrayRegion = globalEnv_SetLongArrayRegion,
+    .SetFloatArrayRegion = globalEnv_SetFloatArrayRegion,
+    .SetDoubleArrayRegion = globalEnv_SetDoubleArrayRegion,
+    .RegisterNatives = globalEnv_RegisterNatives,
+    .UnregisterNatives = globalEnv_UnregisterNatives,
+    .MonitorEnter = globalEnv_MonitorEnter,
+    .MonitorExit = globalEnv_MonitorExit,
+    .GetJavaVM = globalEnv_GetJavaVM,
+    .GetStringRegion = globalEnv_GetStringRegion,
+    .GetStringUTFRegion = globalEnv_GetStringUTFRegion,
+    .GetPrimitiveArrayCritical = globalEnv_GetPrimitiveArrayCritical,
+    .ReleasePrimitiveArrayCritical = globalEnv_ReleasePrimitiveArrayCritical,
+    .GetStringCritical = globalEnv_GetStringCritical,
+    .ReleaseStringCritical = globalEnv_ReleaseStringCritical,
+    .NewWeakGlobalRef = globalEnv_NewWeakGlobalRef,
+    .DeleteWeakGlobalRef = globalEnv_DeleteWeakGlobalRef,
+    .ExceptionCheck = globalEnv_ExceptionCheck,
+    .NewDirectByteBuffer = globalEnv_NewDirectByteBuffer,
+    .GetDirectBufferAddress = globalEnv_GetDirectBufferAddress,
+    .GetDirectBufferCapacity = globalEnv_GetDirectBufferCapacity,
+    .GetObjectRefType = globalEnv_GetObjectRefType,
+};
+
+FFI_PLUGIN_EXPORT
+GlobalJniEnv* GetGlobalEnv() {
+  if (jni.jvm == NULL) {
+    return NULL;
+  }
+  return &globalJniEnv;
+}
\ No newline at end of file
diff --git a/pkgs/jni/src/third_party/global_jni_env.h b/pkgs/jni/src/third_party/global_jni_env.h
new file mode 100644
index 0000000..e4229c6
--- /dev/null
+++ b/pkgs/jni/src/third_party/global_jni_env.h
@@ -0,0 +1,375 @@
+// Generated from an annotated version of jni.h provided in Android NDK.
+// (NDK Version 23.1.7779620)
+// The license for original file is provided below:
+
+/*
+   * Copyright (C) 2006 The Android Open Source Project
+   *
+   * Licensed 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.
+   */
+
+#include <jni.h>
+/// Wrapper over JNIEnv in the JNI API, which can be used from multiple Dart
+/// Threads.
+///
+/// It consists of wrappers to JNIEnv methods which manage the thread-local
+/// JNIEnv pointer in C code. Additionally, any returned local reference value
+/// is converted to global reference.
+///
+/// For the documentation on methods themselves, see the JNI Specification at
+/// https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html
+///
+/// Apart from the specification, the Android NDK's JNI page consists of useful
+/// information about using the JNI:
+/// https://developer.android.com/training/articles/perf-jni
+typedef struct GlobalJniEnv {
+  jint (*GetVersion)();
+  jclass (*DefineClass)(const char* name,
+                        jobject loader,
+                        const jbyte* buf,
+                        jsize bufLen);
+  jclass (*FindClass)(const char* name);
+  jmethodID (*FromReflectedMethod)(jobject method);
+  jfieldID (*FromReflectedField)(jobject field);
+  jobject (*ToReflectedMethod)(jclass cls,
+                               jmethodID methodId,
+                               jboolean isStatic);
+  jclass (*GetSuperclass)(jclass clazz);
+  jboolean (*IsAssignableFrom)(jclass clazz1, jclass clazz2);
+  jobject (*ToReflectedField)(jclass cls, jfieldID fieldID, jboolean isStatic);
+  jint (*Throw)(jthrowable obj);
+  jint (*ThrowNew)(jclass clazz, const char* message);
+  jthrowable (*ExceptionOccurred)();
+  void (*ExceptionDescribe)();
+  void (*ExceptionClear)();
+  void (*FatalError)(const char* msg);
+  jint (*PushLocalFrame)(jint capacity);
+  jobject (*PopLocalFrame)(jobject result);
+  jobject (*NewGlobalRef)(jobject obj);
+  void (*DeleteGlobalRef)(jobject globalRef);
+  jboolean (*IsSameObject)(jobject ref1, jobject ref2);
+  jint (*EnsureLocalCapacity)(jint capacity);
+  jobject (*AllocObject)(jclass clazz);
+  jobject (*NewObject)(jclass arg1, jmethodID arg2);
+  jobject (*NewObjectA)(jclass clazz, jmethodID methodID, const jvalue* args);
+  jclass (*GetObjectClass)(jobject obj);
+  jboolean (*IsInstanceOf)(jobject obj, jclass clazz);
+  jmethodID (*GetMethodID)(jclass clazz, const char* name, const char* sig);
+  jobject (*CallObjectMethod)(jobject arg1, jmethodID arg2);
+  jobject (*CallObjectMethodA)(jobject obj,
+                               jmethodID methodID,
+                               const jvalue* args);
+  jboolean (*CallBooleanMethod)(jobject arg1, jmethodID arg2);
+  jboolean (*CallBooleanMethodA)(jobject obj,
+                                 jmethodID methodId,
+                                 const jvalue* args);
+  jbyte (*CallByteMethod)(jobject arg1, jmethodID arg2);
+  jbyte (*CallByteMethodA)(jobject obj, jmethodID methodID, const jvalue* args);
+  jchar (*CallCharMethod)(jobject arg1, jmethodID arg2);
+  jchar (*CallCharMethodA)(jobject obj, jmethodID methodID, const jvalue* args);
+  jshort (*CallShortMethod)(jobject arg1, jmethodID arg2);
+  jshort (*CallShortMethodA)(jobject obj,
+                             jmethodID methodID,
+                             const jvalue* args);
+  jint (*CallIntMethod)(jobject arg1, jmethodID arg2);
+  jint (*CallIntMethodA)(jobject obj, jmethodID methodID, const jvalue* args);
+  jlong (*CallLongMethod)(jobject arg1, jmethodID arg2);
+  jlong (*CallLongMethodA)(jobject obj, jmethodID methodID, const jvalue* args);
+  jfloat (*CallFloatMethod)(jobject arg1, jmethodID arg2);
+  jfloat (*CallFloatMethodA)(jobject obj,
+                             jmethodID methodID,
+                             const jvalue* args);
+  jdouble (*CallDoubleMethod)(jobject arg1, jmethodID arg2);
+  jdouble (*CallDoubleMethodA)(jobject obj,
+                               jmethodID methodID,
+                               const jvalue* args);
+  void (*CallVoidMethod)(jobject arg1, jmethodID arg2);
+  void (*CallVoidMethodA)(jobject obj, jmethodID methodID, const jvalue* args);
+  jobject (*CallNonvirtualObjectMethod)(jobject arg1,
+                                        jclass arg2,
+                                        jmethodID arg3);
+  jobject (*CallNonvirtualObjectMethodA)(jobject obj,
+                                         jclass clazz,
+                                         jmethodID methodID,
+                                         const jvalue* args);
+  jboolean (*CallNonvirtualBooleanMethod)(jobject arg1,
+                                          jclass arg2,
+                                          jmethodID arg3);
+  jboolean (*CallNonvirtualBooleanMethodA)(jobject obj,
+                                           jclass clazz,
+                                           jmethodID methodID,
+                                           const jvalue* args);
+  jbyte (*CallNonvirtualByteMethod)(jobject arg1, jclass arg2, jmethodID arg3);
+  jbyte (*CallNonvirtualByteMethodA)(jobject obj,
+                                     jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args);
+  jchar (*CallNonvirtualCharMethod)(jobject arg1, jclass arg2, jmethodID arg3);
+  jchar (*CallNonvirtualCharMethodA)(jobject obj,
+                                     jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args);
+  jshort (*CallNonvirtualShortMethod)(jobject arg1,
+                                      jclass arg2,
+                                      jmethodID arg3);
+  jshort (*CallNonvirtualShortMethodA)(jobject obj,
+                                       jclass clazz,
+                                       jmethodID methodID,
+                                       const jvalue* args);
+  jint (*CallNonvirtualIntMethod)(jobject arg1, jclass arg2, jmethodID arg3);
+  jint (*CallNonvirtualIntMethodA)(jobject obj,
+                                   jclass clazz,
+                                   jmethodID methodID,
+                                   const jvalue* args);
+  jlong (*CallNonvirtualLongMethod)(jobject arg1, jclass arg2, jmethodID arg3);
+  jlong (*CallNonvirtualLongMethodA)(jobject obj,
+                                     jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args);
+  jfloat (*CallNonvirtualFloatMethod)(jobject arg1,
+                                      jclass arg2,
+                                      jmethodID arg3);
+  jfloat (*CallNonvirtualFloatMethodA)(jobject obj,
+                                       jclass clazz,
+                                       jmethodID methodID,
+                                       const jvalue* args);
+  jdouble (*CallNonvirtualDoubleMethod)(jobject arg1,
+                                        jclass arg2,
+                                        jmethodID arg3);
+  jdouble (*CallNonvirtualDoubleMethodA)(jobject obj,
+                                         jclass clazz,
+                                         jmethodID methodID,
+                                         const jvalue* args);
+  void (*CallNonvirtualVoidMethod)(jobject arg1, jclass arg2, jmethodID arg3);
+  void (*CallNonvirtualVoidMethodA)(jobject obj,
+                                    jclass clazz,
+                                    jmethodID methodID,
+                                    const jvalue* args);
+  jfieldID (*GetFieldID)(jclass clazz, const char* name, const char* sig);
+  jobject (*GetObjectField)(jobject obj, jfieldID fieldID);
+  jboolean (*GetBooleanField)(jobject obj, jfieldID fieldID);
+  jbyte (*GetByteField)(jobject obj, jfieldID fieldID);
+  jchar (*GetCharField)(jobject obj, jfieldID fieldID);
+  jshort (*GetShortField)(jobject obj, jfieldID fieldID);
+  jint (*GetIntField)(jobject obj, jfieldID fieldID);
+  jlong (*GetLongField)(jobject obj, jfieldID fieldID);
+  jfloat (*GetFloatField)(jobject obj, jfieldID fieldID);
+  jdouble (*GetDoubleField)(jobject obj, jfieldID fieldID);
+  void (*SetObjectField)(jobject obj, jfieldID fieldID, jobject val);
+  void (*SetBooleanField)(jobject obj, jfieldID fieldID, jboolean val);
+  void (*SetByteField)(jobject obj, jfieldID fieldID, jbyte val);
+  void (*SetCharField)(jobject obj, jfieldID fieldID, jchar val);
+  void (*SetShortField)(jobject obj, jfieldID fieldID, jshort val);
+  void (*SetIntField)(jobject obj, jfieldID fieldID, jint val);
+  void (*SetLongField)(jobject obj, jfieldID fieldID, jlong val);
+  void (*SetFloatField)(jobject obj, jfieldID fieldID, jfloat val);
+  void (*SetDoubleField)(jobject obj, jfieldID fieldID, jdouble val);
+  jmethodID (*GetStaticMethodID)(jclass clazz,
+                                 const char* name,
+                                 const char* sig);
+  jobject (*CallStaticObjectMethod)(jclass arg1, jmethodID arg2);
+  jobject (*CallStaticObjectMethodA)(jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args);
+  jboolean (*CallStaticBooleanMethod)(jclass arg1, jmethodID arg2);
+  jboolean (*CallStaticBooleanMethodA)(jclass clazz,
+                                       jmethodID methodID,
+                                       const jvalue* args);
+  jbyte (*CallStaticByteMethod)(jclass arg1, jmethodID arg2);
+  jbyte (*CallStaticByteMethodA)(jclass clazz,
+                                 jmethodID methodID,
+                                 const jvalue* args);
+  jchar (*CallStaticCharMethod)(jclass arg1, jmethodID arg2);
+  jchar (*CallStaticCharMethodA)(jclass clazz,
+                                 jmethodID methodID,
+                                 const jvalue* args);
+  jshort (*CallStaticShortMethod)(jclass arg1, jmethodID arg2);
+  jshort (*CallStaticShortMethodA)(jclass clazz,
+                                   jmethodID methodID,
+                                   const jvalue* args);
+  jint (*CallStaticIntMethod)(jclass arg1, jmethodID arg2);
+  jint (*CallStaticIntMethodA)(jclass clazz,
+                               jmethodID methodID,
+                               const jvalue* args);
+  jlong (*CallStaticLongMethod)(jclass arg1, jmethodID arg2);
+  jlong (*CallStaticLongMethodA)(jclass clazz,
+                                 jmethodID methodID,
+                                 const jvalue* args);
+  jfloat (*CallStaticFloatMethod)(jclass arg1, jmethodID arg2);
+  jfloat (*CallStaticFloatMethodA)(jclass clazz,
+                                   jmethodID methodID,
+                                   const jvalue* args);
+  jdouble (*CallStaticDoubleMethod)(jclass arg1, jmethodID arg2);
+  jdouble (*CallStaticDoubleMethodA)(jclass clazz,
+                                     jmethodID methodID,
+                                     const jvalue* args);
+  void (*CallStaticVoidMethod)(jclass arg1, jmethodID arg2);
+  void (*CallStaticVoidMethodA)(jclass clazz,
+                                jmethodID methodID,
+                                const jvalue* args);
+  jfieldID (*GetStaticFieldID)(jclass clazz, const char* name, const char* sig);
+  jobject (*GetStaticObjectField)(jclass clazz, jfieldID fieldID);
+  jboolean (*GetStaticBooleanField)(jclass clazz, jfieldID fieldID);
+  jbyte (*GetStaticByteField)(jclass clazz, jfieldID fieldID);
+  jchar (*GetStaticCharField)(jclass clazz, jfieldID fieldID);
+  jshort (*GetStaticShortField)(jclass clazz, jfieldID fieldID);
+  jint (*GetStaticIntField)(jclass clazz, jfieldID fieldID);
+  jlong (*GetStaticLongField)(jclass clazz, jfieldID fieldID);
+  jfloat (*GetStaticFloatField)(jclass clazz, jfieldID fieldID);
+  jdouble (*GetStaticDoubleField)(jclass clazz, jfieldID fieldID);
+  void (*SetStaticObjectField)(jclass clazz, jfieldID fieldID, jobject val);
+  void (*SetStaticBooleanField)(jclass clazz, jfieldID fieldID, jboolean val);
+  void (*SetStaticByteField)(jclass clazz, jfieldID fieldID, jbyte val);
+  void (*SetStaticCharField)(jclass clazz, jfieldID fieldID, jchar val);
+  void (*SetStaticShortField)(jclass clazz, jfieldID fieldID, jshort val);
+  void (*SetStaticIntField)(jclass clazz, jfieldID fieldID, jint val);
+  void (*SetStaticLongField)(jclass clazz, jfieldID fieldID, jlong val);
+  void (*SetStaticFloatField)(jclass clazz, jfieldID fieldID, jfloat val);
+  void (*SetStaticDoubleField)(jclass clazz, jfieldID fieldID, jdouble val);
+  jstring (*NewString)(const jchar* unicodeChars, jsize len);
+  jsize (*GetStringLength)(jstring string);
+  const jchar* (*GetStringChars)(jstring string, jboolean* isCopy);
+  void (*ReleaseStringChars)(jstring string, const jchar* isCopy);
+  jstring (*NewStringUTF)(const char* bytes);
+  jsize (*GetStringUTFLength)(jstring string);
+  const char* (*GetStringUTFChars)(jstring string, jboolean* isCopy);
+  void (*ReleaseStringUTFChars)(jstring string, const char* utf);
+  jsize (*GetArrayLength)(jarray array);
+  jobjectArray (*NewObjectArray)(jsize length,
+                                 jclass elementClass,
+                                 jobject initialElement);
+  jobject (*GetObjectArrayElement)(jobjectArray array, jsize index);
+  void (*SetObjectArrayElement)(jobjectArray array, jsize index, jobject val);
+  jbooleanArray (*NewBooleanArray)(jsize length);
+  jbyteArray (*NewByteArray)(jsize length);
+  jcharArray (*NewCharArray)(jsize length);
+  jshortArray (*NewShortArray)(jsize length);
+  jintArray (*NewIntArray)(jsize length);
+  jlongArray (*NewLongArray)(jsize length);
+  jfloatArray (*NewFloatArray)(jsize length);
+  jdoubleArray (*NewDoubleArray)(jsize length);
+  jboolean* (*GetBooleanArrayElements)(jbooleanArray array, jboolean* isCopy);
+  jbyte* (*GetByteArrayElements)(jbyteArray array, jboolean* isCopy);
+  jchar* (*GetCharArrayElements)(jcharArray array, jboolean* isCopy);
+  jshort* (*GetShortArrayElements)(jshortArray array, jboolean* isCopy);
+  jint* (*GetIntArrayElements)(jintArray array, jboolean* isCopy);
+  jlong* (*GetLongArrayElements)(jlongArray array, jboolean* isCopy);
+  jfloat* (*GetFloatArrayElements)(jfloatArray array, jboolean* isCopy);
+  jdouble* (*GetDoubleArrayElements)(jdoubleArray array, jboolean* isCopy);
+  void (*ReleaseBooleanArrayElements)(jbooleanArray array,
+                                      jboolean* elems,
+                                      jint mode);
+  void (*ReleaseByteArrayElements)(jbyteArray array, jbyte* elems, jint mode);
+  void (*ReleaseCharArrayElements)(jcharArray array, jchar* elems, jint mode);
+  void (*ReleaseShortArrayElements)(jshortArray array,
+                                    jshort* elems,
+                                    jint mode);
+  void (*ReleaseIntArrayElements)(jintArray array, jint* elems, jint mode);
+  void (*ReleaseLongArrayElements)(jlongArray array, jlong* elems, jint mode);
+  void (*ReleaseFloatArrayElements)(jfloatArray array,
+                                    jfloat* elems,
+                                    jint mode);
+  void (*ReleaseDoubleArrayElements)(jdoubleArray array,
+                                     jdouble* elems,
+                                     jint mode);
+  void (*GetBooleanArrayRegion)(jbooleanArray array,
+                                jsize start,
+                                jsize len,
+                                jboolean* buf);
+  void (*GetByteArrayRegion)(jbyteArray array,
+                             jsize start,
+                             jsize len,
+                             jbyte* buf);
+  void (*GetCharArrayRegion)(jcharArray array,
+                             jsize start,
+                             jsize len,
+                             jchar* buf);
+  void (*GetShortArrayRegion)(jshortArray array,
+                              jsize start,
+                              jsize len,
+                              jshort* buf);
+  void (*GetIntArrayRegion)(jintArray array, jsize start, jsize len, jint* buf);
+  void (*GetLongArrayRegion)(jlongArray array,
+                             jsize start,
+                             jsize len,
+                             jlong* buf);
+  void (*GetFloatArrayRegion)(jfloatArray array,
+                              jsize start,
+                              jsize len,
+                              jfloat* buf);
+  void (*GetDoubleArrayRegion)(jdoubleArray array,
+                               jsize start,
+                               jsize len,
+                               jdouble* buf);
+  void (*SetBooleanArrayRegion)(jbooleanArray array,
+                                jsize start,
+                                jsize len,
+                                const jboolean* buf);
+  void (*SetByteArrayRegion)(jbyteArray array,
+                             jsize start,
+                             jsize len,
+                             const jbyte* buf);
+  void (*SetCharArrayRegion)(jcharArray array,
+                             jsize start,
+                             jsize len,
+                             const jchar* buf);
+  void (*SetShortArrayRegion)(jshortArray array,
+                              jsize start,
+                              jsize len,
+                              const jshort* buf);
+  void (*SetIntArrayRegion)(jintArray array,
+                            jsize start,
+                            jsize len,
+                            const jint* buf);
+  void (*SetLongArrayRegion)(jlongArray array,
+                             jsize start,
+                             jsize len,
+                             const jlong* buf);
+  void (*SetFloatArrayRegion)(jfloatArray array,
+                              jsize start,
+                              jsize len,
+                              const jfloat* buf);
+  void (*SetDoubleArrayRegion)(jdoubleArray array,
+                               jsize start,
+                               jsize len,
+                               const jdouble* buf);
+  jint (*RegisterNatives)(jclass clazz,
+                          const JNINativeMethod* methods,
+                          jint nMethods);
+  jint (*UnregisterNatives)(jclass clazz);
+  jint (*MonitorEnter)(jobject obj);
+  jint (*MonitorExit)(jobject obj);
+  jint (*GetJavaVM)(JavaVM** vm);
+  void (*GetStringRegion)(jstring str, jsize start, jsize len, jchar* buf);
+  void (*GetStringUTFRegion)(jstring str, jsize start, jsize len, char* buf);
+  void* (*GetPrimitiveArrayCritical)(jarray array, jboolean* isCopy);
+  void (*ReleasePrimitiveArrayCritical)(jarray array, void* carray, jint mode);
+  const jchar* (*GetStringCritical)(jstring str, jboolean* isCopy);
+  void (*ReleaseStringCritical)(jstring str, const jchar* carray);
+  jweak (*NewWeakGlobalRef)(jobject obj);
+  void (*DeleteWeakGlobalRef)(jweak obj);
+  jboolean (*ExceptionCheck)();
+  jobject (*NewDirectByteBuffer)(void* address, jlong capacity);
+  void* (*GetDirectBufferAddress)(jobject buf);
+  jlong (*GetDirectBufferCapacity)(jobject buf);
+  jobjectRefType (*GetObjectRefType)(jobject obj);
+} GlobalJniEnv;
+
+// Note(Mahesh): Redoing some work here, because I want these 2 files separate.
+// including dartjni.h for FFI_PLUGIN_EXPORT leads to more issues.
+
+#if _WIN32
+__declspec(dllexport) GlobalJniEnv* GetGlobalEnv();
+#else
+GlobalJniEnv* GetGlobalEnv();
+#endif
\ No newline at end of file
diff --git a/pkgs/jni/test/jni_object_test.dart b/pkgs/jni/test/jni_object_test.dart
index 1cca591..2a86dd4 100644
--- a/pkgs/jni/test/jni_object_test.dart
+++ b/pkgs/jni/test/jni_object_test.dart
@@ -122,7 +122,7 @@
   });
 
   // One-off access of static field in single call.
-  test("retrieve_", () {
+  test("Get static field directly", () {
     final maxLong = Jni.retrieveStaticField<int>(
         "java/lang/Short", "MAX_VALUE", "S", JniType.shortType);
     expect(maxLong, equals(32767));
@@ -142,15 +142,18 @@
   // you can also pass Dart strings, apart from range of types
   // allowed by Jni.jvalues
   // They will be converted automatically.
-  test("Passing strings in arguments", () {
-    final out = Jni.retrieveStaticField<JniObject>(
-        "java/lang/System", "out", "Ljava/io/PrintStream;");
-    // uncomment next line to see output
-    // (\n because test runner prints first char at end of the line)
-    //out.callMethodByName<Null>(
-    //    "println", "(Ljava/lang/Object;)V", ["\nWorks (Apparently)"]);
-    out.delete();
-  });
+  test(
+    "Passing strings in arguments",
+    () {
+      final out = Jni.retrieveStaticField<JniObject>(
+          "java/lang/System", "out", "Ljava/io/PrintStream;");
+      // uncomment next line to see output
+      // (\n because test runner prints first char at end of the line)
+      //out.callMethodByName<Null>(
+      //    "println", "(Ljava/lang/Object;)V", ["\nWorks (Apparently)"]);
+      out.delete();
+    },
+  );
 
   test("Passing strings in arguments 2", () {
     final twelve = Jni.invokeStaticMethod<int>("java/lang/Byte", "parseByte",
@@ -195,15 +198,15 @@
 }
 
 void doSomeWorkInIsolate(Void? _) {
-  // On standalone target, make sure to call load
-  // when doing getInstance first time in a new isolate.
+  // On standalone target, make sure to call [setDylibDir] before accessing
+  // any JNI function.
   //
   // otherwise getInstance will throw a "library not found" exception.
   Jni.setDylibDir(dylibDir: "build/jni_libs");
   final random = Jni.newInstance("java/util/Random", "()V", []);
-  // final r = random.callIntMethodByName("nextInt", "(I)I", [256]);
+  // final r = random.callMethodByName<int>("nextInt", "(I)I", [256]);
   // expect(r, lessThan(256));
-  // Expect throws an OutsideTestException
+  // Expect throws an [OutsideTestException]
   // but you can uncomment below print and see it works
   // print("\n$r");
   random.delete();
diff --git a/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/compound.dart b/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/compound.dart
index 66592e2..3d477e8 100644
--- a/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/compound.dart
+++ b/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/compound.dart
@@ -15,7 +15,7 @@
 // Y is extension class if it contains function pointer fields which are
 // otherwise equivalent to normal functions, and just packed in a structure
 // for convenience.
-const extensionClasses = {'GlobalJniEnv', 'JniNativeExtensions'};
+const extensionClasses = {'GlobalJniEnv', 'JniAccessors'};
 
 const methodNameRenames = {"throw": "throwException"};
 
@@ -139,6 +139,8 @@
     if (dartDoc != null) {
       s.write(makeDartDoc(dartDoc!));
     }
+    final voidPointer =
+        "${w.ffiLibraryPrefix}.Pointer<${w.ffiLibraryPrefix}.Void>";
 
     /// Adding [enclosingClassName] because dart doesn't allow class member
     /// to have the same name as the class.
@@ -186,8 +188,7 @@
           final fnType = nf.type as FunctionType;
 
           if (hasVarArgListParam) {
-            s.write(
-                '${depth}external ${m.type.getDartType(w)} _${m.name};\n\n');
+            s.write('${depth}external $voidPointer _${m.name};\n\n');
             continue;
           }
 
@@ -228,7 +229,9 @@
           es.write(");\n$depth}\n\n");
         } else {
           final memberName = hasVarArgListParam ? '_${m.name}' : m.name;
-          s.write('${depth}external ${m.type.getDartType(w)} $memberName;\n\n');
+          final memberType =
+              hasVarArgListParam ? voidPointer : m.type.getDartType(w);
+          s.write('${depth}external $memberType $memberName;\n\n');
         }
       }
     }
diff --git a/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/writer.dart b/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/writer.dart
index 77e9e83..0bacf55 100644
--- a/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/writer.dart
+++ b/pkgs/jni/third_party/ffigen_patch_jni/lib/src/code_generator/writer.dart
@@ -239,7 +239,9 @@
 
     /// Write [noLookUpBindings].
     for (final b in noLookUpBindings) {
-      s.write(b.toBindingString(this).string);
+      if (b.name != '__va_list_tag') {
+        s.write(b.toBindingString(this).string);
+      }
     }
 
     // Write neccesary imports.
diff --git a/pkgs/jnigen/.gitignore b/pkgs/jnigen/.gitignore
index 2c67603..c66a8b6 100644
--- a/pkgs/jnigen/.gitignore
+++ b/pkgs/jnigen/.gitignore
@@ -40,3 +40,7 @@
 # Vim
 .*.swp
 
+## Downloaded files and build artifacts
+*.jar
+*.class
+*.exe
diff --git a/pkgs/jnigen/README.md b/pkgs/jnigen/README.md
index 642bcf1..f8b6d44 100644
--- a/pkgs/jnigen/README.md
+++ b/pkgs/jnigen/README.md
@@ -9,16 +9,19 @@
 | [jni](jni/) | Ergonomic C bindings to JNI C API and several helper methods |
 | [jnigen](jnigen/) | Tool to generate Dart bindings to Java code using FFI |
 
-This is a work-in-progress project under Google Summer of Code 2022 program.
+This project is a work-in-progress and highly experimental. Users can check out and experiment with the examples. We do not guarantee stability of the API or commands at this point.
 
 ## SDK Requirements
 Dart standalone target is supported, but due to some problems with pubspec, the `dart` command must be from Flutter SDK and not Dart SDK. See [dart-lang/pub#3563](https://github.com/dart-lang/pub/issues/3563).
 
+Along with JDK, maven (`mvn` command) is required. On Windows, it can be installed using a package manager such as `chocolatey` or `scoop`.
+
 On windows, you need to append the path of `jvm.dll` in your JDK installation to PATH.
 
 For example, on Powershell:
 
-```
+```powershell
 $env:Path += ";${env:JAVA_HOME}\bin\server".
 ```
+
 (If JAVA_HOME not set, find the `java.exe` executable and set the environment variable in Control Panel).
diff --git a/pkgs/jnigen/bin/setup.dart b/pkgs/jnigen/bin/setup.dart
index c2937bf..54df277 100644
--- a/pkgs/jnigen/bin/setup.dart
+++ b/pkgs/jnigen/bin/setup.dart
@@ -4,10 +4,13 @@
 
 import 'dart:io';
 
+import 'package:jnigen/src/logging/logging.dart';
 import 'package:jnigen/src/tools/tools.dart';
+import 'package:logging/logging.dart';
 
 void main(List<String> args) async {
   bool force = false;
+  setLoggingLevel(Level.INFO);
   if (args.isNotEmpty) {
     if (args.length != 1 || args[0] != '-f') {
       stderr.writeln('usage: dart run jnigen:setup [-f]');
diff --git a/pkgs/jnigen/cmake/README.md b/pkgs/jnigen/cmake/README.md
new file mode 100644
index 0000000..65f9951
--- /dev/null
+++ b/pkgs/jnigen/cmake/README.md
@@ -0,0 +1,3 @@
+This directory contains a CMakeLists.txt.tmpl file which will be used to generate build configuration for generated C code.
+
+`{{LIBRARY_NAME}}` in this template will be replaced by `library_name` configuration parameter.
\ No newline at end of file
diff --git a/pkgs/jnigen/example/in_app_java/android/build.gradle b/pkgs/jnigen/example/in_app_java/android/build.gradle
index e42825f..88265a9 100644
--- a/pkgs/jnigen/example/in_app_java/android/build.gradle
+++ b/pkgs/jnigen/example/in_app_java/android/build.gradle
@@ -30,4 +30,3 @@
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
-
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 0ce5069..bc72baa 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
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jnigen/example/notification_plugin/example/pubspec.yaml b/pkgs/jnigen/example/notification_plugin/example/pubspec.yaml
index fad3bdb..127c7dc 100644
--- a/pkgs/jnigen/example/notification_plugin/example/pubspec.yaml
+++ b/pkgs/jnigen/example/notification_plugin/example/pubspec.yaml
@@ -20,7 +20,7 @@
 version: 1.0.0+1
 
 environment:
-  sdk: '>=2.18.0 <3.0.0'
+  sdk: '>=2.17.6 <3.0.0'
 
 dependencies:
   flutter:
diff --git a/pkgs/jnigen/example/notification_plugin/pubspec.yaml b/pkgs/jnigen/example/notification_plugin/pubspec.yaml
index 77cc904..f059a55 100644
--- a/pkgs/jnigen/example/notification_plugin/pubspec.yaml
+++ b/pkgs/jnigen/example/notification_plugin/pubspec.yaml
@@ -5,7 +5,7 @@
 homepage: https://github.com/dart-lang/jnigen
 
 environment:
-  sdk: '>=2.18.0 <3.0.0'
+  sdk: '>=2.17.6 <3.0.0'
   flutter: ">=2.11.0"
 
 dependencies:
diff --git a/pkgs/jnigen/example/notification_plugin/src/dartjni.h b/pkgs/jnigen/example/notification_plugin/src/dartjni.h
index 0ce5069..bc72baa 100644
--- a/pkgs/jnigen/example/notification_plugin/src/dartjni.h
+++ b/pkgs/jnigen/example/notification_plugin/src/dartjni.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jnigen/example/pdfbox_plugin/.gitignore b/pkgs/jnigen/example/pdfbox_plugin/.gitignore
index b3f33fc..a117acd 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/.gitignore
+++ b/pkgs/jnigen/example/pdfbox_plugin/.gitignore
@@ -35,3 +35,8 @@
 ## Sometimes generated by maven.
 ## TODO(#34) This is unwanted.
 /target/
+
+# Builds & downloads
+*.pdf
+*.jar
+*.exe
diff --git a/pkgs/jnigen/example/pdfbox_plugin/dart_example/bin/pdf_info.dart b/pkgs/jnigen/example/pdfbox_plugin/dart_example/bin/pdf_info.dart
index c16ac20..b3affc5 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/dart_example/bin/pdf_info.dart
+++ b/pkgs/jnigen/example/pdfbox_plugin/dart_example/bin/pdf_info.dart
@@ -12,7 +12,6 @@
 void writeInfo(String file) {
   final inputFile = Jni.newInstance(
       "java/io/FileInputStream", "(Ljava/lang/String;)V", [file]);
-
   final pdDoc = PDDocument.load7(inputFile);
   int pages = pdDoc.getNumberOfPages();
   final info = pdDoc.getDocumentInformation();
diff --git a/pkgs/jnigen/example/pdfbox_plugin/jnigen_full.yaml b/pkgs/jnigen/example/pdfbox_plugin/jnigen_full.yaml
deleted file mode 100644
index 2652756..0000000
--- a/pkgs/jnigen/example/pdfbox_plugin/jnigen_full.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-## Name of the generated library, this is required and used as the name of
-## shared library which contains C bindings
-library_name: pdfbox_plugin
-
-## String to be pasted verbatim into generated bindings
-preamble: |
-  // Generated from Apache PDFBox library which is licensed under the Apache License 2.0.
-  // The following copyright from the original authors applies.
-  //
-  // 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.
-
-## Root for generated C bindings.
-c_root: 'src/'
-
-## C files can be stored in a different sub-directory inside c_root.
-##
-## We have a guideline to keep all generated code in third_party/ since original
-## project's license applies to generated code. So we specify third_party/ as
-## c_subdir while keeping generated CMakeLists.txt in src/.
-c_subdir: 'third_party/'
-
-## Root for generated Dart bindings.
-dart_root: 'lib/third_party/'
-
-## Classes / packages for which bindings need to be generated.
-classes:
-  - 'org.apache.pdfbox.pdmodel'
-  - 'org.apache.pdfbox.text'
-
-## Dependencies to be downloaded using Maven (Invokes `mvn` command). These
-## dependencies are always downloaded along with their transitive dependencies.
-##
-## Dependencies should be specified using groupID:artifactID:version format
-## The downloaded dependencies are automatically added to classpath of the
-## Java API scanning process. There's no need to specify these again in
-## sourcepath and classpath.
-##
-## Note that when maven based tooling is used, the first run often has to fetch
-## all the dependencies and might take some time. However, maven caches most
-## artifacts in local repository, thus subsequent runs will be faster.
-maven_downloads:
-  ## For these dependencies, both source and JARs are downloaded.
-  source_deps:
-    - 'org.apache.pdfbox:pdfbox:2.0.26'
-  ## Runtime dependencies for which bindings aren't generated directly.
-  ## Only JARs are downloaded.
-  jar_only_deps:
-    - 'org.bouncycastle:bcmail-jdk15on:1.70'
-    - 'org.bouncycastle:bcprov-jdk15on:1.70'
-
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 0ce5069..bc72baa 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h
+++ b/pkgs/jnigen/example/pdfbox_plugin/src/third_party/dartjni.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jnigen/example/pdfbox_plugin/tool/generate_bindings.dart b/pkgs/jnigen/example/pdfbox_plugin/tool/generate_bindings.dart
deleted file mode 100644
index 615b404..0000000
--- a/pkgs/jnigen/example/pdfbox_plugin/tool/generate_bindings.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) 2022, 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.
-
-import 'package:jnigen/jnigen.dart';
-
-const preamble = '''
-// Generated from Apache PDFBox library which is licensed under the Apache License 2.0.
-// The following copyright from the original authors applies.
-//
-// 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.
-''';
-
-void main(List<String> arguments) async {
-  final config = Config(
-    mavenDownloads: MavenDownloads(
-      sourceDeps: ['org.apache.pdfbox:pdfbox:2.0.26'],
-      jarOnlyDeps: [
-        'org.bouncycastle:bcmail-jdk15on:1.70',
-        'org.bouncycastle:bcprov-jdk15on:1.70',
-      ],
-    ),
-    classes: ['org.apache.pdfbox.pdmodel', 'org.apache.pdfbox.text'],
-    cRoot: Uri.directory('src/'),
-    cSubdir: 'third_party',
-    preamble: preamble,
-    dartRoot: Uri.directory('lib/third_party'),
-    libraryName: 'pdfbox_plugin',
-  );
-  await generateJniBindings(config);
-}
diff --git a/pkgs/jnigen/lib/src/bindings/c_bindings.dart b/pkgs/jnigen/lib/src/bindings/c_bindings.dart
index dda1cd0..bdf20bd 100644
--- a/pkgs/jnigen/lib/src/bindings/c_bindings.dart
+++ b/pkgs/jnigen/lib/src/bindings/c_bindings.dart
@@ -7,6 +7,9 @@
 
 import 'common.dart';
 
+// TODO(#52): Do not use long names in C bindings. Use conflict-renamed short
+// name as class name; Use `__` between class & method name.
+
 // fullName / mangled name =
 // binaryName with replace('.', '_'), replace('$', '__');
 
@@ -14,8 +17,9 @@
   static const _classVarPrefix = '_c';
   static const _methodVarPrefix = '_m';
   static const _fieldVarPrefix = '_f';
-  static const _indent = '    ';
+  static final indent = ' ' * 4;
 
+  // These should be avoided in parameter names.
   static const _cTypeKeywords = {
     'short',
     'char',
@@ -25,19 +29,16 @@
     'double',
   };
 
-  String _cParamRename(String paramName) =>
+  String _renameCParam(String paramName) =>
       _cTypeKeywords.contains(paramName) ? '${paramName}0' : paramName;
 
   CBindingGenerator(this.config);
   Config config;
 
-  String generateBinding(ClassDecl c) {
-    return _class(c);
-  }
+  String generateBinding(ClassDecl c) => _class(c);
 
   String _class(ClassDecl c) {
     final s = StringBuffer();
-
     final fullName = mangledClassName(c);
 
     // global variable in C that holds the reference to class
@@ -74,50 +75,47 @@
     final s = StringBuffer();
     final name = m.finalName;
 
-    final methodVar = '${_methodVarPrefix}_${cClassName}_$name';
-    s.write('jmethodID $methodVar = NULL;\n');
+    final methodID = '${_methodVarPrefix}_${cClassName}_$name';
+    s.write('jmethodID $methodID = NULL;\n');
 
     final returnType = isCtor(m) ? 'jobject' : m.returnType.name;
-    final cReturnType = cType(returnType);
+    final cReturnType = getCType(returnType);
     final cMethodName = '${cClassName}_$name';
     final cParams = _formalArgs(m);
     s.write('FFI_PLUGIN_EXPORT\n');
     s.write('$cReturnType $cMethodName($cParams) {\n');
 
     final classVar = '${_classVarPrefix}_$cClassName';
-    final signature = _signature(m);
+    final jniSignature = getJniSignature(m);
 
     final ifError = '($cReturnType)0';
 
     s.write(_loadEnvCall);
-    s.write(_loadClassCall(classVar, _internalName(c.binaryName), ifError));
+    s.write(_loadClassCall(classVar, getInternalName(c.binaryName), ifError));
 
     final ifStatic = isStatic ? 'static_' : '';
-    s.write('${_indent}load_${ifStatic}method($classVar, '
-        '&$methodVar, "${m.name}", "$signature");\n');
-    s.write('${_indent}if ($methodVar == NULL) return $ifError;\n');
-
-    s.write(_initParams(m));
+    s.write('${indent}load_${ifStatic}method($classVar, '
+        '&$methodID, "${m.name}", "$jniSignature");\n');
+    s.write('${indent}if ($methodID == NULL) return $ifError;\n');
 
     var returnTypeName = m.returnType.name;
     if (isACtor) {
       returnTypeName = c.binaryName;
     }
 
-    s.write(_indent);
+    s.write(indent);
     if (returnTypeName != 'void') {
-      s.write('${cType(returnTypeName)} _result = ');
+      s.write('${getCType(returnTypeName)} _result = ');
     }
 
     final callType = _typeNameAtCallSite(m.returnType);
-    final callArgs = _callArgs(m, classVar, methodVar);
+    final callArgs = _callArgs(m, classVar, methodID);
     if (isACtor) {
       s.write('(*jniEnv)->NewObject($callArgs);\n');
     } else {
       final ifStatic = isStatic ? 'Static' : '';
       s.write('(*jniEnv)->Call$ifStatic${callType}Method($callArgs);\n');
     }
-    s.write(_destroyParams(m));
     if (returnTypeName != 'void') {
       s.write(_result(m));
     }
@@ -144,23 +142,24 @@
     final classVar = '${_classVarPrefix}_$cClassName';
 
     void writeAccessor({bool isSetter = false}) {
-      final ct = isSetter ? 'void' : cType(f.type.name);
+      final ct = isSetter ? 'void' : getCType(f.type.name);
       // Getter
       final prefix = isSetter ? 'set' : 'get';
       s.write('FFI_PLUGIN_EXPORT\n');
       s.write('$ct ${prefix}_${memberNameInC(c, fieldName)}(');
       final formalArgs = <String>[
         if (!isStatic) 'jobject self_',
-        if (isSetter) '${cType(f.type.name)} value',
+        if (isSetter) '${getCType(f.type.name)} value',
       ];
       s.write(formalArgs.join(', '));
       s.write(') {\n');
       s.write(_loadEnvCall);
-      s.write(_loadClassCall(classVar, _internalName(c.binaryName), '($ct)0'));
+      s.write(
+          _loadClassCall(classVar, getInternalName(c.binaryName), '($ct)0'));
 
       var ifStatic = isStatic ? 'static_' : '';
       s.write(
-          '${_indent}load_${ifStatic}field($classVar, &$fieldVar, "$fieldName",'
+          '${indent}load_${ifStatic}field($classVar, &$fieldVar, "$fieldName",'
           '"${_fieldSignature(f)}");\n');
 
       ifStatic = isStatic ? 'Static' : '';
@@ -168,14 +167,13 @@
       final acc = isSetter ? 'Set' : 'Get';
       final ret = isSetter ? '' : 'return ';
       final conv = !isSetter && !isPrimitive(f.type) ? 'to_global_ref' : '';
-      s.write('$_indent$ret$conv((*jniEnv)->$acc$ifStatic${callType}Field');
+      s.write('$indent$ret$conv((*jniEnv)->$acc$ifStatic${callType}Field');
       final secondArg = isStatic ? classVar : 'self_';
       s.write('(jniEnv, $secondArg, $fieldVar');
       if (isSetter) {
         s.write(', value');
       }
       s.write('));\n');
-      // TODO(#25): Check Exceptions.
       s.write('}\n\n');
     }
 
@@ -187,12 +185,12 @@
     return s.toString();
   }
 
-  final String _loadEnvCall = '${_indent}load_env();\n';
+  final String _loadEnvCall = '${indent}load_env();\n';
 
   String _loadClassCall(String classVar, String internalName, String ifError) {
-    return '${_indent}load_class_gr(&$classVar, '
+    return '${indent}load_class_gr(&$classVar, '
         '"$internalName");\n'
-        '${_indent}if ($classVar == NULL) return $ifError;\n';
+        '${indent}if ($classVar == NULL) return $ifError;\n';
   }
 
   String _formalArgs(Method m) {
@@ -204,19 +202,14 @@
     }
 
     for (var param in m.params) {
-      final paramName = _cParamRename(param.name);
-      args.add('${cType(param.type.name)} $paramName');
+      final paramName = _renameCParam(param.name);
+      args.add('${getCType(param.type.name)} $paramName');
     }
 
     return args.join(", ");
   }
 
-  bool _needsTemporaries(String binaryName) {
-    // currently no type needs temporaries.
-    return false;
-  }
-
-  // arguments at call site
+  // Returns arguments at call site, concatenated by `,`.
   String _callArgs(Method m, String classVar, String methodVar) {
     final args = ['jniEnv'];
     if (hasSelfParam(m)) {
@@ -226,117 +219,34 @@
     }
     args.add(methodVar);
     for (var param in m.params) {
-      final paramName = _cParamRename(param.name);
-      if (_needsTemporaries(param.type.name)) {
-        args.add('_$paramName');
-      } else {
-        args.add(paramName);
-      }
+      final paramName = _renameCParam(param.name);
+      args.add(paramName);
     }
     return args.join(', ');
   }
 
-  String _initParams(Method m) {
-    // currently no type needs temporaries, but in future we may add
-    // some options that require temporaries.
-    return '';
-  }
-
-  String _destroyParams(Method m) {
-    final s = StringBuffer();
-    for (var param in m.params) {
-      final paramName = _cParamRename(param.name);
-      if (_needsTemporaries(param.type.name)) {
-        s.write('$_indent(*jniEnv)->DeleteLocalRef(jniEnv, _$paramName);\n');
-      }
-    }
-    return s.toString();
-  }
-
   String _result(Method m) {
-    final cReturnType = cType(m.returnType.name);
+    final cReturnType = getCType(m.returnType.name);
     if (cReturnType == 'jobject' || isCtor(m)) {
-      return '${_indent}return to_global_ref(_result);\n';
+      return '${indent}return to_global_ref(_result);\n';
     } else {
-      return '${_indent}return _result;\n';
+      return '${indent}return _result;\n';
     }
   }
 
   String _fieldSignature(Field f) {
-    final iname = _internalNameOf(f.type);
-    if (iname.length == 1) {
-      return iname;
+    final internalName = getInternalNameOfUsage(f.type);
+    if (internalName.length == 1) {
+      return internalName;
     }
-    return 'L$iname;';
+    return 'L$internalName;';
   }
 
-  String _internalName(String binaryName) {
-    switch (binaryName) {
-      case "void":
-        return "V";
-      case "byte":
-        return "B";
-      case "char":
-        return "C";
-      case "double":
-        return "D";
-      case "float":
-        return "F";
-      case "int":
-        return "I";
-      case "long":
-        return "J";
-      case "short":
-        return "S";
-      case "boolean":
-        return "Z";
-      default:
-        return binaryName.replaceAll(".", "/");
-    }
-  }
-
-  String _internalNameOf(TypeUsage usage) {
-    switch (usage.kind) {
-      case Kind.declared:
-        return _internalName((usage.type as DeclaredType).binaryName);
-      case Kind.primitive:
-        return _internalName((usage.type as PrimitiveType).name);
-      case Kind.typeVariable:
-        // It should be possible to compute the erasure of a type
-        // in parser itself.
-        // TODO(#23): Use erasure of the type variable here.
-        // This is just a (wrong) placeholder
-        return "java/lang/Object";
-      case Kind.array:
-        final inner = _internalNameOf((usage.type as ArrayType).type);
-        return "[$inner";
-      case Kind.wildcard:
-        final extendsBound = (usage.type as Wildcard).extendsBound;
-        if (extendsBound != null) {
-          return _internalNameOf(extendsBound);
-        }
-        return 'java/lang/Object';
-    }
-  }
-
-  /// Returns the JNI signature of the method.
-  String _signature(Method m) {
-    final s = StringBuffer();
-    s.write('(');
-    for (var param in m.params) {
-      final type = _internalNameOf(param.type);
-      s.write(type.length == 1 ? type : 'L$type;');
-    }
-    s.write(')');
-    final returnType = _internalNameOf(m.returnType);
-    s.write(returnType.length == 1 ? returnType : 'L$returnType;');
-    return s.toString();
-  }
-
-  // For call<type>Method or get<type>field calls in JNI.
-  String _typeNameAtCallSite(TypeUsage t) {
-    if (isPrimitive(t)) {
-      return t.name.substring(0, 1).toUpperCase() + t.name.substring(1);
+  /// Returns capitalized java type name to be used as in call${type}Method
+  /// or get${type}Field etc..
+  String _typeNameAtCallSite(TypeUsage type) {
+    if (isPrimitive(type)) {
+      return type.name.substring(0, 1).toUpperCase() + type.name.substring(1);
     }
     return "Object";
   }
diff --git a/pkgs/jnigen/lib/src/bindings/common.dart b/pkgs/jnigen/lib/src/bindings/common.dart
index 69e19b1..a8ecccb 100644
--- a/pkgs/jnigen/lib/src/bindings/common.dart
+++ b/pkgs/jnigen/lib/src/bindings/common.dart
@@ -3,14 +3,208 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:jnigen/src/elements/elements.dart';
+import 'package:jnigen/src/util/rename_conflict.dart';
 
+import 'symbol_resolver.dart';
+
+/// Implements the methods used by both Pure dart & C+Dart generators.
+///
+/// These methods are in a superclass since they usually require access
+/// to resolver, or for consistency with similar methods which require
+/// the resolver.
+class BindingsGenerator {
+  SymbolResolver resolver;
+  BindingsGenerator(this.resolver);
+  // Name for reference in base class.
+  static const selfPointer = 'reference';
+  static final indent = ' ' * 2;
+
+  // import prefixes
+  static const ffi = 'ffi.';
+  static const jni = 'jni.';
+
+  static const String voidPointer = '${ffi}Pointer<${ffi}Void>';
+
+  static const String ffiVoidType = '${ffi}Void';
+
+  static const String jniObjectType = '${jni}JniObject';
+
+  /// Formal parameters list of the generated function.
+  ///
+  /// This is the signature seen by the user.
+  String formalArgs(Method m) {
+    final List<String> args = [];
+    for (var param in m.params) {
+      args.add('${dartOuterType(param.type)} ${kwRename(param.name)}');
+    }
+    return args.join(', ');
+  }
+
+  /// Actual arguments passed to native call.
+  String actualArgs(Method m) {
+    final List<String> args = [if (hasSelfParam(m)) selfPointer];
+    for (var param in m.params) {
+      final paramName = kwRename(param.name);
+      args.add(toNativeArg(paramName, param.type));
+    }
+    return args.join(', ');
+  }
+
+  String dartSigForField(Field f,
+      {bool isSetter = false, required bool isFfiSig}) {
+    final conv = isFfiSig ? dartFfiType : dartInnerType;
+    final voidType = isFfiSig ? ffiVoidType : 'void';
+    final ref = f.modifiers.contains('static') ? '' : '$voidPointer, ';
+    if (isSetter) {
+      return '$voidType Function($ref${conv(f.type)})';
+    }
+    return '${conv(f.type)} Function($ref)';
+  }
+
+  String dartSigForMethod(Method m, {required bool isFfiSig}) {
+    final conv = isFfiSig ? dartFfiType : dartInnerType;
+    final argTypes = [if (hasSelfParam(m)) voidPointer];
+    for (var param in m.params) {
+      argTypes.add(conv(param.type));
+    }
+    final retType = isCtor(m) ? voidPointer : conv(m.returnType);
+    return '$retType Function (${argTypes.join(", ")})';
+  }
+
+  String _dartType(TypeUsage t, {SymbolResolver? resolver}) {
+    // if resolver == null, looking for inner fn type, type of fn reference
+    // else looking for outer fn type, that's what user of the library sees.
+    const primitives = {
+      'byte': 'int',
+      'short': 'int',
+      'char': 'int',
+      'int': 'int',
+      'long': 'int',
+      'float': 'double',
+      'double': 'double',
+      'void': 'void',
+      'boolean': 'bool',
+    };
+    switch (t.kind) {
+      case Kind.primitive:
+        if (t.name == 'boolean' && resolver == null) return 'int';
+        return primitives[(t.type as PrimitiveType).name]!;
+      case Kind.typeVariable:
+      case Kind.wildcard:
+        throw SkipException('Not supported: generics');
+      case Kind.array:
+        if (resolver != null) {
+          return jniObjectType;
+        }
+        return voidPointer;
+      case Kind.declared:
+        if (resolver != null) {
+          return resolver.resolve((t.type as DeclaredType).binaryName) ??
+              jniObjectType;
+        }
+        return voidPointer;
+    }
+  }
+
+  // Type for FFI Function signature
+  String dartFfiType(TypeUsage t) {
+    const primitives = {
+      'byte': 'Int8',
+      'short': 'Int16',
+      'char': 'Int16',
+      'int': 'Int32',
+      'long': 'Int64',
+      'float': 'Float',
+      'double': 'Double',
+      'void': 'Void',
+      'boolean': 'Uint8',
+    };
+    switch (t.kind) {
+      case Kind.primitive:
+        return ffi + primitives[(t.type as PrimitiveType).name]!;
+      case Kind.typeVariable:
+      case Kind.wildcard:
+        throw SkipException(
+            'Generic type parameters are not supported', t.toJson());
+      case Kind.array:
+      case Kind.declared:
+        return voidPointer;
+    }
+  }
+
+  String dartInnerType(TypeUsage t) => _dartType(t);
+  String dartOuterType(TypeUsage t) => _dartType(t, resolver: resolver);
+
+  String literal(dynamic value) {
+    if (value is String) {
+      // TODO(#31): escape string literal.
+      return '"$value"';
+    }
+    if (value is int || value is double || value is bool) {
+      return value.toString();
+    }
+    throw SkipException('Not a constant of a known type.');
+  }
+
+  String originalFieldDecl(Field f) {
+    final declStmt = '${f.type.shorthand} ${f.name}';
+    return [...f.modifiers, declStmt].join(' ');
+  }
+
+  String originalMethodHeader(Method m) {
+    final args = <String>[];
+    for (var p in m.params) {
+      args.add('${p.type.shorthand} ${p.name}');
+    }
+    final declStmt = '${m.returnType.shorthand} ${m.name}'
+        '(${args.join(', ')})';
+    return [...m.modifiers, declStmt].join(' ');
+  }
+
+  String toNativeArg(String name, TypeUsage type) {
+    if (isPrimitive(type)) {
+      return type.name == 'boolean' ? '$name ? 1 : 0' : name;
+    }
+    return '$name.$selfPointer';
+  }
+
+  String toDartResult(String expr, TypeUsage type, String dartType) {
+    if (isPrimitive(type)) {
+      return type.name == 'boolean' ? '$expr != 0' : expr;
+    }
+    return '$dartType.fromRef($expr)';
+  }
+
+  static final deleteInstruction =
+      '$indent/// The returned object must be deleted after use, '
+      'by calling the `delete` method.\n';
+}
+
+String breakDocComment(JavaDocComment? javadoc, {String depth = '    '}) {
+  final link = RegExp('{@link ([^{}]+)}');
+  if (javadoc == null) return '';
+  final comment = javadoc.comment
+      .replaceAllMapped(link, (match) => match.group(1) ?? '')
+      .replaceAll('#', '\\#')
+      .replaceAll('<p>', '')
+      .replaceAll('</p>', '\n')
+      .replaceAll('<b>', '__')
+      .replaceAll('</b>', '__')
+      .replaceAll('<em>', '_')
+      .replaceAll('</em>', '_');
+  return '$depth///\n'
+      '$depth/// ${comment.replaceAll('\n', '\n$depth///')}\n';
+}
+
+/// class name canonicalized for C bindings, by replacing "." with "_" and
+/// "$" with "__".
 String mangledClassName(ClassDecl decl) =>
     decl.binaryName.replaceAll('.', '_').replaceAll('\$', '__');
 
 String memberNameInC(ClassDecl decl, String name) =>
     "${mangledClassName(decl)}_$name";
 
-String cType(String binaryName) {
+String getCType(String binaryName) {
   switch (binaryName) {
     case "void":
       return "void";
@@ -35,6 +229,55 @@
   }
 }
 
+String getInternalName(String binaryName) {
+  switch (binaryName) {
+    case "void":
+      return "V";
+    case "byte":
+      return "B";
+    case "char":
+      return "C";
+    case "double":
+      return "D";
+    case "float":
+      return "F";
+    case "int":
+      return "I";
+    case "long":
+      return "J";
+    case "short":
+      return "S";
+    case "boolean":
+      return "Z";
+    default:
+      return binaryName.replaceAll(".", "/");
+  }
+}
+
+String getInternalNameOfUsage(TypeUsage usage) {
+  switch (usage.kind) {
+    case Kind.declared:
+      return getInternalName((usage.type as DeclaredType).binaryName);
+    case Kind.primitive:
+      return getInternalName((usage.type as PrimitiveType).name);
+    case Kind.typeVariable:
+      // It should be possible to compute the erasure of a type
+      // in parser itself.
+      // TODO(#23): Use erasure of the type variable here.
+      // This is just a (wrong) placeholder
+      return "java/lang/Object";
+    case Kind.array:
+      final inner = getInternalNameOfUsage((usage.type as ArrayType).type);
+      return "[$inner";
+    case Kind.wildcard:
+      final extendsBound = (usage.type as Wildcard).extendsBound;
+      if (extendsBound != null) {
+        return getInternalNameOfUsage(extendsBound);
+      }
+      return 'java/lang/Object';
+  }
+}
+
 bool isPrimitive(TypeUsage t) => t.kind == Kind.primitive;
 bool isVoid(TypeUsage t) => isPrimitive(t) && t.name == 'void';
 
@@ -50,8 +293,11 @@
 bool isObjectField(Field f) => !isPrimitive(f.type);
 bool isObjectMethod(Method m) => !isPrimitive(m.returnType);
 
-const ctorNameC = 'new';
-const ctorNameDart = 'ctor';
+/// Returns class name as useful in dart.
+///
+/// Eg -> a.b.X.Y -> X_Y
+String getSimplifiedClassName(String binaryName) =>
+    binaryName.split('.').last.replaceAll('\$', '_');
 
 // Marker exception when a method or class cannot be translated
 // The inner functions may not know how much context has to be skipped in case
@@ -66,3 +312,54 @@
     return '$message;';
   }
 }
+
+String getTypeNameAtCallSite(TypeUsage t) {
+  if (isPrimitive(t)) {
+    return t.name.substring(0, 1).toUpperCase() + t.name.substring(1);
+  }
+  return "Object";
+}
+
+String getCallType(TypeUsage returnType) {
+  if (isPrimitive(returnType)) {
+    return "jni.JniType.${returnType.name}Type";
+  }
+  return "jni.JniType.objectType";
+}
+
+String getResultGetterName(TypeUsage returnType) {
+  final primitives = {
+    'boolean': 'boolean',
+    'byte': 'byte',
+    'short': 'short',
+    'char': 'char',
+    'int': 'integer',
+    'long': 'long',
+    'float': 'float',
+    'double': 'doubleFloat',
+    'void': 'check()',
+  };
+  return primitives[returnType.name] ?? 'object';
+}
+
+/// Returns the JNI signature of the method.
+String getJniSignature(Method m) {
+  final s = StringBuffer();
+  s.write('(');
+  for (var param in m.params) {
+    final type = getInternalNameOfUsage(param.type);
+    s.write(type.length == 1 ? type : 'L$type;');
+  }
+  s.write(')');
+  final returnType = getInternalNameOfUsage(m.returnType);
+  s.write(returnType.length == 1 ? returnType : 'L$returnType;');
+  return s.toString();
+}
+
+String getJniSignatureForField(Field f) {
+  final internalName = getInternalNameOfUsage(f.type);
+  if (internalName.length == 1) {
+    return internalName;
+  }
+  return 'L$internalName;';
+}
diff --git a/pkgs/jnigen/lib/src/bindings/dart_bindings.dart b/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
index 20c058b..d656745 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
@@ -5,32 +5,28 @@
 import 'package:jnigen/src/elements/elements.dart';
 import 'package:jnigen/src/config/config.dart';
 import 'package:jnigen/src/logging/logging.dart';
-import 'package:jnigen/src/util/rename_conflict.dart';
 
 import 'symbol_resolver.dart';
 import 'common.dart';
 
-final _indent = ' ' * 2;
-
-class DartBindingsGenerator {
-  // Name for reference in base class.
-  static const _self = 'reference';
+class DartBindingsGenerator extends BindingsGenerator {
   // symbol lookup function for generated code.
+  static const selfPointer = BindingsGenerator.selfPointer;
   static const _jniLookup = 'jniLookup';
+  static final indent = ' ' * 2;
 
   // import prefixes
-  static const ffi = 'ffi.';
-  static const jni = 'jni.';
+  static const ffi = BindingsGenerator.ffi;
+  static const jni = BindingsGenerator.jni;
 
-  static const String _voidPtr = '${ffi}Pointer<${ffi}Void>';
+  static const voidPointer = BindingsGenerator.voidPointer;
 
-  static const String _void = '${ffi}Void';
+  static const ffiVoidType = BindingsGenerator.ffiVoidType;
 
-  static const String _jniObject = '${jni}JniObject';
+  static const jniObjectType = BindingsGenerator.jniObjectType;
 
-  DartBindingsGenerator(this.config, this.resolver);
+  DartBindingsGenerator(this.config, SymbolResolver resolver) : super(resolver);
   Config config;
-  SymbolResolver resolver;
 
   String generateBinding(ClassDecl decl) {
     if (!decl.isPreprocessed) {
@@ -49,18 +45,18 @@
     final s = StringBuffer();
 
     s.write('/// from: ${decl.binaryName}\n');
-    s.write(_breakDocComment(decl.javadoc, depth: ''));
+    s.write(breakDocComment(decl.javadoc, depth: ''));
     final name = _getSimpleName(decl.binaryName);
 
-    var superName = _jniObject;
+    var superName = jniObjectType;
     if (decl.superclass != null) {
       superName = resolver
               .resolve((decl.superclass!.type as DeclaredType).binaryName) ??
-          _jniObject;
+          jniObjectType;
     }
 
     s.write('class $name extends $superName {\n');
-    s.write('$_indent$name.fromRef($_voidPtr ref) : '
+    s.write('$indent$name.fromRef($voidPointer ref) : '
         'super.fromRef(ref);\n');
 
     s.writeln();
@@ -94,10 +90,6 @@
     return s.toString();
   }
 
-  static final _deleteInstruction =
-      '$_indent/// The returned object must be deleted after use, '
-      'by calling the `delete` method.\n';
-
   String _method(ClassDecl c, Method m) {
     final name = m.finalName;
     final cName = memberNameInC(c, name);
@@ -105,74 +97,57 @@
     final sym = '_$name';
     final ffiSig = dartSigForMethod(m, isFfiSig: true);
     final dartSig = dartSigForMethod(m, isFfiSig: false);
-    s.write('${_indent}static final $sym = $_jniLookup'
+    s.write('${indent}static final $sym = $_jniLookup'
         '<${ffi}NativeFunction<$ffiSig>>("$cName")\n'
         '.asFunction<$dartSig>();\n');
     // Different logic for constructor and method;
     // For constructor, we want return type to be new object.
     final returnType = dartOuterType(m.returnType);
-    s.write('$_indent/// from: ${_originalMethodHeader(m)}\n');
+    s.write('$indent/// from: ${originalMethodHeader(m)}\n');
     if (!isPrimitive(m.returnType)) {
-      s.write(_deleteInstruction);
+      s.write(BindingsGenerator.deleteInstruction);
     }
-    s.write(_breakDocComment(m.javadoc));
-    s.write(_indent);
+    s.write(breakDocComment(m.javadoc));
+    s.write(indent);
 
     if (isStaticMethod(m)) {
       s.write('static ');
     }
 
     if (isCtor(m)) {
-      final wrapperExpr = '$sym(${_actualArgs(m)})';
+      final wrapperExpr = '$sym(${actualArgs(m)})';
       final className = _getSimpleName(c.binaryName);
       final ctorFnName = name == 'ctor' ? className : '$className.$name';
-      s.write('$ctorFnName(${_formalArgs(m)}) : '
+      s.write('$ctorFnName(${formalArgs(m)}) : '
           'super.fromRef($wrapperExpr) { jni.Jni.env.checkException(); }\n');
       return s.toString();
     }
 
-    var wrapperExpr = '$sym(${_actualArgs(m)})';
-    wrapperExpr = _toDartResult(wrapperExpr, m.returnType, returnType);
-    final depth = '$_indent$_indent';
-    s.write('$returnType $name(${_formalArgs(m)}) {');
+    var wrapperExpr = '$sym(${actualArgs(m)})';
+    wrapperExpr = toDartResult(wrapperExpr, m.returnType, returnType);
+    final depth = '$indent$indent';
+    s.write('$returnType $name(${formalArgs(m)}) {');
     s.write('${depth}final result__ = $wrapperExpr;');
     s.write('${depth}jni.Jni.env.checkException();');
-    s.write('${depth}return result__;\n$_indent}');
+    s.write('${depth}return result__;\n$indent}');
     return s.toString();
   }
 
-  String _formalArgs(Method m) {
-    final List<String> args = [];
-    for (var param in m.params) {
-      args.add('${dartOuterType(param.type)} ${kwRename(param.name)}');
-    }
-    return args.join(', ');
-  }
-
-  String _actualArgs(Method m) {
-    final List<String> args = [if (hasSelfParam(m)) _self];
-    for (var param in m.params) {
-      final paramName = kwRename(param.name);
-      args.add(_toCArg(paramName, param.type));
-    }
-    return args.join(', ');
-  }
-
   String _field(ClassDecl c, Field f) {
     final name = f.finalName;
     final s = StringBuffer();
 
     void writeDocs({bool writeDeleteInstruction = true}) {
-      s.write('$_indent/// from: ${_originalFieldDecl(f)}\n');
+      s.write('$indent/// from: ${originalFieldDecl(f)}\n');
       if (!isPrimitive(f.type) && writeDeleteInstruction) {
-        s.write(_deleteInstruction);
+        s.write(BindingsGenerator.deleteInstruction);
       }
-      s.write(_breakDocComment(f.javadoc));
+      s.write(breakDocComment(f.javadoc));
     }
 
     if (isStaticField(f) && isFinalField(f) && f.defaultValue != null) {
       writeDocs(writeDeleteInstruction: false);
-      s.write('${_indent}static const $name = ${_literal(f.defaultValue)};\n');
+      s.write('${indent}static const $name = ${literal(f.defaultValue)};\n');
       return s.toString();
     }
     final cName = memberNameInC(c, name);
@@ -182,26 +157,26 @@
       final sym = '_${symPrefix}_$name';
       final ffiSig = dartSigForField(f, isSetter: isSetter, isFfiSig: true);
       final dartSig = dartSigForField(f, isSetter: isSetter, isFfiSig: false);
-      s.write('${_indent}static final $sym = $_jniLookup'
+      s.write('${indent}static final $sym = $_jniLookup'
           '<${ffi}NativeFunction<$ffiSig>>("${symPrefix}_$cName")\n'
           '.asFunction<$dartSig>();\n');
       // write original type
       writeDocs();
-      s.write(_indent);
+      s.write(indent);
       if (isStaticField(f)) s.write('static ');
       if (isSetter) {
         s.write('set $name(${dartOuterType(f.type)} value) => $sym(');
         if (!isStaticField(f)) {
-          s.write('$_self, ');
+          s.write('$selfPointer, ');
         }
-        s.write(_toCArg('value', f.type));
+        s.write(toNativeArg('value', f.type));
         s.write(');\n');
       } else {
         // getter
-        final self = isStaticField(f) ? '' : _self;
+        final self = isStaticField(f) ? '' : selfPointer;
         final outer = dartOuterType(f.type);
         final callExpr = '$sym($self)';
-        final resultExpr = _toDartResult(callExpr, f.type, outer);
+        final resultExpr = toDartResult(callExpr, f.type, outer);
         s.write('$outer get $name => $resultExpr;\n');
       }
     }
@@ -216,150 +191,6 @@
     return components.last.replaceAll("\$", "_");
   }
 
-  String dartSigForField(Field f,
-      {bool isSetter = false, required bool isFfiSig}) {
-    final conv = isFfiSig ? dartFfiType : dartInnerType;
-    final voidType = isFfiSig ? _void : 'void';
-    final ref = f.modifiers.contains('static') ? '' : '$_voidPtr, ';
-    if (isSetter) {
-      return '$voidType Function($ref${conv(f.type)})';
-    }
-    return '${conv(f.type)} Function($ref)';
-  }
-
-  String dartSigForMethod(Method m, {required bool isFfiSig}) {
-    final conv = isFfiSig ? dartFfiType : dartInnerType;
-    final argTypes = [if (hasSelfParam(m)) _voidPtr];
-    for (var param in m.params) {
-      argTypes.add(conv(param.type));
-    }
-    final retType = isCtor(m) ? _voidPtr : conv(m.returnType);
-    return '$retType Function (${argTypes.join(", ")})';
-  }
-
-  // Type for FFI Function signature
-  String dartFfiType(TypeUsage t) {
-    const primitives = {
-      'byte': 'Int8',
-      'short': 'Int16',
-      'char': 'Int16',
-      'int': 'Int32',
-      'long': 'Int64',
-      'float': 'Float',
-      'double': 'Double',
-      'void': 'Void',
-      'boolean': 'Uint8',
-    };
-    switch (t.kind) {
-      case Kind.primitive:
-        return ffi + primitives[(t.type as PrimitiveType).name]!;
-      case Kind.typeVariable:
-      case Kind.wildcard:
-        throw SkipException(
-            'Generic type parameters are not supported', t.toJson());
-      case Kind.array:
-      case Kind.declared:
-        return _voidPtr;
-    }
-  }
-
-  String _dartType(TypeUsage t, {SymbolResolver? resolver}) {
-    // if resolver == null, looking for inner fn type, type of fn reference
-    // else looking for outer fn type, that's what user of the library sees.
-    const primitives = {
-      'byte': 'int',
-      'short': 'int',
-      'char': 'int',
-      'int': 'int',
-      'long': 'int',
-      'float': 'double',
-      'double': 'double',
-      'void': 'void',
-      'boolean': 'bool',
-    };
-    switch (t.kind) {
-      case Kind.primitive:
-        if (t.name == 'boolean' && resolver == null) return 'int';
-        return primitives[(t.type as PrimitiveType).name]!;
-      case Kind.typeVariable:
-      case Kind.wildcard:
-        throw SkipException('Not supported: generics');
-      case Kind.array:
-        if (resolver != null) {
-          return _jniObject;
-        }
-        return _voidPtr;
-      case Kind.declared:
-        if (resolver != null) {
-          return resolver.resolve((t.type as DeclaredType).binaryName) ??
-              _jniObject;
-        }
-        return _voidPtr;
-    }
-  }
-
-  String dartInnerType(TypeUsage t) => _dartType(t);
-  String dartOuterType(TypeUsage t) => _dartType(t, resolver: resolver);
-
-  String _literal(dynamic value) {
-    if (value is String) {
-      // TODO(#31): escape string literal.
-      return '"$value"';
-    }
-    if (value is int || value is double || value is bool) {
-      return value.toString();
-    }
-    throw SkipException('Not a constant of a known type.');
-  }
-
-  String _originalFieldDecl(Field f) {
-    final declStmt = '${f.type.shorthand} ${f.name}';
-    return [...f.modifiers, declStmt].join(' ');
-  }
-
-  String _originalMethodHeader(Method m) {
-    final args = <String>[];
-    for (var p in m.params) {
-      args.add('${p.type.shorthand} ${p.name}');
-    }
-    final declStmt = '${m.returnType.shorthand} ${m.name}'
-        '(${args.join(', ')})';
-    return [...m.modifiers, declStmt].join(' ');
-  }
-
-  String _toCArg(String name, TypeUsage type) {
-    if (isPrimitive(type)) {
-      return type.name == 'boolean' ? '$name ? 1 : 0' : name;
-    }
-    return '$name.$_self';
-  }
-
-  String _toDartResult(String expr, TypeUsage type, String dartType) {
-    if (isPrimitive(type)) {
-      return type.name == 'boolean' ? '$expr != 0' : expr;
-    }
-    return '$dartType.fromRef($expr)';
-  }
-
-  static String _breakDocComment(JavaDocComment? javadoc,
-      {String depth = '    '}) {
-    final link = RegExp('{@link ([^{}]+)}');
-    if (javadoc == null) return '';
-    final comment = javadoc.comment
-        .replaceAllMapped(link, (match) => match.group(1) ?? '')
-        .replaceAll('#', '\\#')
-        .replaceAll('<p>', '')
-        .replaceAll('</p>', '\n')
-        .replaceAll('<b>', '__')
-        .replaceAll('</b>', '__')
-        .replaceAll('<em>', '_')
-        .replaceAll('</em>', '_');
-    return '$depth///\n'
-        '$depth/// ${comment.replaceAll('\n', '\n$depth///')}\n';
-  }
-}
-
-class DartPreludes {
   static String initFile(String libraryName) => 'import "dart:ffi";\n'
       'import "package:jni/internal_helpers_for_jnigen.dart";\n'
       '\n'
diff --git a/pkgs/jnigen/lib/src/bindings/preprocessor.dart b/pkgs/jnigen/lib/src/bindings/preprocessor.dart
index 61c2141..8ddff1d 100644
--- a/pkgs/jnigen/lib/src/bindings/preprocessor.dart
+++ b/pkgs/jnigen/lib/src/bindings/preprocessor.dart
@@ -10,9 +10,17 @@
 import 'common.dart';
 
 /// Preprocessor which fills information needed by both Dart and C generators.
-class ApiPreprocessor {
-  static void preprocessAll(Map<String, ClassDecl> classes, Config config) {
+abstract class ApiPreprocessor {
+  static void preprocessAll(Map<String, ClassDecl> classes, Config config,
+      {bool renameClasses = false}) {
+    final Map<String, int> classNameCounts = {};
     for (var c in classes.values) {
+      final className = getSimplifiedClassName(c.binaryName);
+      if (renameClasses) {
+        c.finalName = renameConflict(classNameCounts, className);
+      } else {
+        c.finalName = className;
+      }
       _preprocess(c, classes, config);
     }
   }
@@ -22,7 +30,7 @@
     if (decl.isPreprocessed) return;
     if (!_isClassIncluded(decl, config)) {
       decl.isIncluded = false;
-      log.info('exclude class ${decl.binaryName}');
+      log.fine('exclude class ${decl.binaryName}');
       decl.isPreprocessed = true;
       return;
     }
@@ -42,7 +50,7 @@
     for (var field in decl.fields) {
       if (!_isFieldIncluded(decl, field, config)) {
         field.isIncluded = false;
-        log.info('exclude ${decl.binaryName}#${field.name}');
+        log.fine('exclude ${decl.binaryName}#${field.name}');
         continue;
       }
       field.finalName = renameConflict(decl.nameCounts, field.name);
@@ -51,7 +59,7 @@
     for (var method in decl.methods) {
       if (!_isMethodIncluded(decl, method, config)) {
         method.isIncluded = false;
-        log.info('exclude method ${decl.binaryName}#${method.name}');
+        log.fine('exclude method ${decl.binaryName}#${method.name}');
         continue;
       }
       var realName = method.name;
@@ -62,10 +70,10 @@
       // if method already in super class, assign its number, overriding it.
       final superNum = superclass?.methodNumsAfterRenaming[sig];
       if (superNum != null) {
-        // don't rename if superNum == 0
-        final superNumText = superNum == 0 ? '' : '$superNum';
-        // well, unless the method name is a keyword & superNum == 0.
         // TODO(#29): this logic would better live in a dedicated renamer class.
+        // don't rename if superNum == 0
+        // unless the method name is a keyword.
+        final superNumText = superNum == 0 ? '' : '$superNum';
         final methodName = superNum == 0 ? kwRename(realName) : realName;
         method.finalName = '$methodName$superNumText';
         decl.methodNumsAfterRenaming[sig] = superNum;
@@ -77,7 +85,7 @@
       }
     }
     decl.isPreprocessed = true;
-    log.finest('preprocessed ${decl.binaryName}');
+    log.fine('preprocessed ${decl.binaryName}');
   }
 
   static bool _isFieldIncluded(ClassDecl decl, Field field, Config config) =>
diff --git a/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart b/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
index 45b9e1a..adf065b 100644
--- a/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
+++ b/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
@@ -2,147 +2,12 @@
 // 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.
 
-// A symbol resolver is useful mainly to convert a fully qualified name to
-// a locally meaningful name, when creating dart bindings
-
-import 'dart:math';
-
-import 'package:jnigen/src/logging/logging.dart';
-
-import 'package:jnigen/src/util/name_utils.dart';
-
+/// Resolves types referred to in method signatures etc.. and provides
+/// appropriate imports for them.
 abstract class SymbolResolver {
   /// Resolve the binary name to a String which can be used in dart code.
   String? resolve(String binaryName);
+
+  /// Get all imports for types so far resolved through this resolver.
   List<String> getImportStrings();
 }
-
-class PackagePathResolver implements SymbolResolver {
-  PackagePathResolver(this.importMap, this.currentPackage, this.inputClassNames,
-      {this.predefined = const {}});
-
-  final String currentPackage;
-  final Map<String, String> importMap;
-  final Map<String, String> predefined;
-  final Set<String> inputClassNames;
-
-  final List<String> importStrings = [];
-
-  final Set<String> relativeImportedPackages = {};
-
-  final Map<String, String> _importedNameToPackage = {};
-  final Map<String, String> _packageToImportedName = {};
-
-  /// Returns the dart name of the [binaryName] in current translation context,
-  /// or `null` if the name cannot be resolved.
-  @override
-  String? resolve(String binaryName) {
-    if (predefined.containsKey(binaryName)) {
-      return predefined[binaryName];
-    }
-    final parts = cutFromLast(binaryName, '.');
-    final package = parts[0];
-    final typename = parts[1];
-    final simpleTypeName = typename.replaceAll('\$', '_');
-
-    if (package == currentPackage && inputClassNames.contains(binaryName)) {
-      return simpleTypeName;
-    }
-
-    if (_packageToImportedName.containsKey(package)) {
-      // This package was already resolved
-      // but we still need to check if it was a relative import, in which case
-      // the class not in inputClassNames cannot be mapped here.
-      if (!relativeImportedPackages.contains(package) ||
-          inputClassNames.contains(binaryName)) {
-        final importedName = _packageToImportedName[package];
-        return '$importedName.$simpleTypeName';
-      }
-    }
-
-    final packageImport = getImport(package, binaryName);
-    log.finest('$package resolved to $packageImport for $binaryName');
-    if (packageImport == null) {
-      return null;
-    }
-
-    final pkgName = cutFromLast(package, '.')[1];
-    if (pkgName.isEmpty) {
-      throw UnsupportedError('No package could be deduced from '
-          'qualified binaryName');
-    }
-
-    // We always name imports with an underscore suffix, so that they can be
-    // never shadowed by a parameter or local variable.
-    var importedName = '${pkgName}_';
-    int suffix = 0;
-    while (_importedNameToPackage.containsKey(importedName)) {
-      suffix++;
-      importedName = '$pkgName${suffix}_';
-    }
-
-    _importedNameToPackage[importedName] = package;
-    _packageToImportedName[package] = importedName;
-    importStrings.add('import "$packageImport" as $importedName;\n');
-    return '$importedName.$simpleTypeName';
-  }
-
-  /// Returns import string, or `null` if package not found.
-  String? getImport(String packageToResolve, String binaryName) {
-    final right = <String>[];
-    var prefix = packageToResolve;
-
-    if (prefix.isEmpty) {
-      throw UnsupportedError('unexpected: empty package name.');
-    }
-
-    final dest = packageToResolve.split('.');
-    final src = currentPackage.split('.');
-    if (inputClassNames.contains(binaryName)) {
-      int common = 0;
-      for (int i = 0; i < src.length && i < dest.length; i++) {
-        if (src[i] == dest[i]) {
-          common++;
-        }
-      }
-      // a.b.c => a/b/c.dart
-      // from there
-      // a/b.dart => ../b.dart
-      // a.b.d => d.dart
-      // a.b.c.d => c/d.dart
-      var pathToCommon = '';
-      if (common < src.length) {
-        pathToCommon = '../' * (src.length - common);
-      }
-      final pathToPackage = dest.skip(max(common - 1, 0)).join('/');
-      relativeImportedPackages.add(packageToResolve);
-      return '$pathToCommon$pathToPackage.dart';
-    }
-
-    while (prefix.isNotEmpty) {
-      final split = cutFromLast(prefix, '.');
-      final left = split[0];
-      right.add(split[1]);
-      // eg: packages[org.apache.pdfbox]/org/apache/pdfbox.dart
-      if (importMap.containsKey(prefix)) {
-        final sub = packageToResolve.replaceAll('.', '/');
-        final pkg = _suffix(importMap[prefix]!, '/');
-        return '$pkg$sub.dart';
-      }
-      prefix = left;
-    }
-    return null;
-  }
-
-  String _suffix(String str, String suffix) {
-    if (str.endsWith(suffix)) {
-      return str;
-    }
-    return str + suffix;
-  }
-
-  @override
-  List<String> getImportStrings() {
-    return importStrings;
-  }
-}
diff --git a/pkgs/jnigen/lib/src/config/config.dart b/pkgs/jnigen/lib/src/config/config.dart
index 136845f..12c32fe 100644
--- a/pkgs/jnigen/lib/src/config/config.dart
+++ b/pkgs/jnigen/lib/src/config/config.dart
@@ -65,6 +65,10 @@
     this.addGradleDeps = false,
     this.androidExample,
   }) {
+    if (versions != null && sdkRoot == null) {
+      throw ArgumentError("No SDK Root specified for finding Android SDK "
+          "from version priority list $versions");
+    }
     if (versions == null && !addGradleDeps) {
       throw ArgumentError('Neither any SDK versions nor `addGradleDeps` '
           'is specified. Unable to find Android libraries.');
@@ -128,13 +132,30 @@
   ClassFilter? classes;
 }
 
+enum BindingsType {
+  cBased, // C+Dart bindings
+  singleFile,
+  packageStructured,
+}
+
+BindingsType getBindingsType(String? type, BindingsType defaultType) {
+  const names = {
+    'c_based': BindingsType.cBased,
+    'single_file': BindingsType.singleFile,
+    'package_structured': BindingsType.packageStructured,
+  };
+  return names[type] ?? defaultType;
+}
+
 /// Configuration for jnigen binding generation.
 class Config {
   Config({
     required this.classes,
-    required this.libraryName,
-    required this.cRoot,
-    required this.dartRoot,
+    this.bindingsType = BindingsType.cBased,
+    this.outputPath,
+    this.libraryName,
+    this.cRoot,
+    this.dartRoot,
     this.cSubdir,
     this.exclude,
     this.sourcePath,
@@ -146,7 +167,16 @@
     this.summarizerOptions,
     this.logLevel = Level.INFO,
     this.dumpJsonTo,
-  });
+  }) {
+    if (bindingsType == BindingsType.cBased) {
+      if (cRoot == null || dartRoot == null || libraryName == null) {
+        throw ArgumentError("In c_based mode these values must be specified: "
+            "c_root, dart_root, library_name");
+      }
+    } else {
+      throw UnimplementedError("BindingsType not yet supported: $bindingsType");
+    }
+  }
 
   /// List of classes or packages for which bindings have to be generated.
   ///
@@ -157,27 +187,33 @@
   /// name suffix is `.class`.
   List<String> classes;
 
+  /// Type of bindings to generate.
+  final BindingsType bindingsType;
+
   /// Name of generated library in CMakeLists.txt configuration.
   ///
   /// This will also determine the name of shared object file.
-  String libraryName;
+  final String? libraryName;
 
-  /// Directory to write JNI C Bindings.
+  /// Directory to write JNI C Bindings, in C+Dart mode.
   ///
   /// Strictly speaking, this is the root to place the `CMakeLists.txt` file
   /// for the generated C bindings. It may be desirable to use the [cSubdir]
   /// options to write C files to a subdirectory of [cRoot]. For instance,
   /// when generated code is required to be in `third_party` directory.
-  Uri cRoot;
+  Uri? cRoot;
 
-  /// Directory to write Dart bindings.
-  Uri dartRoot;
+  /// Directory to write Dart bindings, in C + Dart mode.
+  Uri? dartRoot;
 
-  /// Subdirectory relative to [cRoot] to write generated C code.
+  /// Subfolder relative to [cRoot] to write generated C code.
   String? cSubdir;
 
+  /// Output file or folder in non-legacy modes
+  Uri? outputPath;
+
   /// Methods and fields to be excluded from generated bindings.
-  BindingExclusions? exclude;
+  final BindingExclusions? exclude;
 
   /// Paths to search for java source files.
   ///
@@ -193,32 +229,30 @@
   List<Uri>? classPath;
 
   /// Common text to be pasted on top of generated C and Dart files.
-  String? preamble;
+  final String? preamble;
 
   /// Additional java package -> dart package mappings (Experimental).
-  Map<String, String>? importMap;
+  final Map<String, String>? importMap;
 
   /// Configuration to search for Android SDK libraries (Experimental).
-  AndroidSdkConfig? androidSdkConfig;
+  final AndroidSdkConfig? androidSdkConfig;
 
   /// Configuration for auto-downloading JAR / source packages using maven,
   /// along with their transitive dependencies.
-  MavenDownloads? mavenDownloads;
+  final MavenDownloads? mavenDownloads;
 
   /// Additional options for the summarizer component
-  SummarizerOptions? summarizerOptions;
+  final SummarizerOptions? summarizerOptions;
 
   /// Log verbosity. The possible values in decreasing order of verbosity
   /// are verbose > debug > info > warning > error. Defaults to [LogLevel.info]
   Level logLevel = Level.INFO;
 
   /// File to which JSON summary is written before binding generation.
-  String? dumpJsonTo;
+  final String? dumpJsonTo;
 
   static final _levels = Map.fromEntries(
       Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l)));
-  static Uri? _toDirUri(String? path) =>
-      path != null ? Uri.directory(path) : null;
   static List<Uri>? _toUris(List<String>? paths) =>
       paths?.map(Uri.file).toList();
 
@@ -234,6 +268,10 @@
       return res;
     }
 
+    Uri? fileUri(String? path) => path != null ? Uri.file(path) : null;
+    Uri? directoryUri(String? path) =>
+        path != null ? Uri.directory(path) : null;
+
     MemberFilter<T>? regexFilter<T extends ClassMember>(String property) {
       final exclusions = prov.getStringList(property);
       if (exclusions == null) return null;
@@ -252,13 +290,9 @@
       return CombinedMemberFilter<T>(filters);
     }
 
-    String getSdkRoot() {
+    String? getSdkRoot() {
       final root = prov.getString(_Props.androidSdkRoot) ??
           Platform.environment['ANDROID_SDK_ROOT'];
-      if (root == null) {
-        missingValues.add(_Props.androidSdkRoot);
-        return '?';
-      }
       return root;
     }
 
@@ -279,14 +313,17 @@
         extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [],
         backend: prov.getString(_Props.backend),
         workingDirectory:
-            _toDirUri(prov.getString(_Props.summarizerWorkingDir)),
+            directoryUri(prov.getString(_Props.summarizerWorkingDir)),
       ),
       exclude: BindingExclusions(
         methods: regexFilter<Method>(_Props.excludeMethods),
         fields: regexFilter<Field>(_Props.excludeFields),
       ),
-      cRoot: Uri.directory(must(prov.getString, '', _Props.cRoot)),
-      dartRoot: Uri.directory(must(prov.getString, '', _Props.dartRoot)),
+      bindingsType: getBindingsType(
+          prov.getString(_Props.bindingsType), BindingsType.cBased),
+      cRoot: directoryUri(prov.getString(_Props.cRoot)),
+      dartRoot: directoryUri(prov.getString(_Props.dartRoot)),
+      outputPath: fileUri(prov.getString(_Props.outputPath)),
       cSubdir: prov.getString(_Props.cSubdir),
       preamble: prov.getString(_Props.preamble),
       libraryName: must(prov.getString, '', _Props.libraryName),
@@ -317,7 +354,7 @@
       logLevel: logLevelFromString(
         prov.getOneOf(
           _Props.logLevel,
-          {'error', 'warning', 'info', 'debug', 'verbose'},
+          _levels.keys.toSet(),
         ),
       ),
     );
@@ -353,6 +390,8 @@
   static const excludeFields = '$exclude.fields';
 
   static const importMap = 'import_map';
+  static const outputPath = 'output_path';
+  static const bindingsType = 'bindings_type';
   static const cRoot = 'c_root';
   static const cSubdir = 'c_subdir';
   static const dartRoot = 'dart_root';
diff --git a/pkgs/jnigen/lib/src/config/yaml_reader.dart b/pkgs/jnigen/lib/src/config/yaml_reader.dart
index 6765d9d..8f3aa67 100644
--- a/pkgs/jnigen/lib/src/config/yaml_reader.dart
+++ b/pkgs/jnigen/lib/src/config/yaml_reader.dart
@@ -63,7 +63,7 @@
         stderr.writeln('cannot read $configFile: $e');
       }
     }
-    final regex = RegExp('([a-z-_.]+)=(.*)');
+    final regex = RegExp('([a-z-_.]+)=(.+)');
     final properties = <String, String>{};
     for (var prop in results['override']) {
       final match = regex.matchAsPrefix(prop as String);
@@ -98,7 +98,7 @@
   }
 
   List<String>? getStringList(String property) {
-    final configValue = cli[property]?.split(',') ??
+    final configValue = cli[property]?.split(';') ??
         getYamlValue<YamlList>(property)?.cast<String>();
     return configValue;
   }
diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart
index f7773fa..879fade 100644
--- a/pkgs/jnigen/lib/src/elements/elements.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.dart
@@ -91,6 +91,8 @@
       _$ClassDeclFromJson(json);
   Map<String, dynamic> toJson() => _$ClassDeclToJson(this);
 
+  String get internalName => binaryName.replaceAll(".", "/");
+
   // synthesized attributes
   @JsonKey(ignore: true)
   late String finalName;
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index c841d80..1c51b68 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -5,11 +5,13 @@
 import 'dart:io';
 import 'dart:convert';
 
+import 'package:jnigen/src/util/command_output.dart';
+
 import 'elements/elements.dart';
 import 'summary/summary.dart';
 import 'config/config.dart';
 import 'tools/tools.dart';
-import 'writers/writers.dart';
+import 'writers/files_writer.dart';
 import 'logging/logging.dart';
 
 Future<void> generateJniBindings(Config config) async {
@@ -26,6 +28,7 @@
     backend: config.summarizerOptions?.backend,
   );
 
+  // Additional sources added using maven downloads and gradle trickery.
   final extraSources = <Uri>[];
   final extraJars = <Uri>[];
   final mavenDl = config.mavenDownloads;
@@ -70,18 +73,23 @@
   summarizer.addSourcePaths(extraSources);
   summarizer.addClassPaths(extraJars);
 
+  Process process;
   Stream<List<int>> input;
   try {
-    input = await summarizer.getInputStream();
+    process = await summarizer.runProcess();
+    input = process.stdout;
   } on Exception catch (e) {
     log.fatal('Cannot obtain API summary: $e');
     return;
   }
+  final errorLog = StringBuffer();
+  collectOutputStream(process.stderr, errorLog);
   final stream = JsonDecoder().bind(Utf8Decoder().bind(input));
   dynamic json;
   try {
     json = await stream.single;
   } on Exception catch (e) {
+    printError(errorLog);
     log.fatal('Cannot parse summary: $e');
     return;
   }
diff --git a/pkgs/jnigen/lib/src/logging/logging.dart b/pkgs/jnigen/lib/src/logging/logging.dart
index 526981e..db8b713 100644
--- a/pkgs/jnigen/lib/src/logging/logging.dart
+++ b/pkgs/jnigen/lib/src/logging/logging.dart
@@ -21,6 +21,13 @@
   });
 }
 
+void printError(Object? message) {
+  if (stderr.supportsAnsiEscapes) {
+    message = '$_ansiRed$message$_ansiDefault';
+  }
+  stderr.writeln(message);
+}
+
 extension FatalErrors on Logger {
   void fatal(Object? message, {int exitCode = 2}) {
     message = '${_ansiRed}Fatal: $message$_ansiDefault';
diff --git a/pkgs/jnigen/lib/src/summary/summary.dart b/pkgs/jnigen/lib/src/summary/summary.dart
index ab3fdcf..900f7f3 100644
--- a/pkgs/jnigen/lib/src/summary/summary.dart
+++ b/pkgs/jnigen/lib/src/summary/summary.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 import 'dart:io';
 import 'package:jnigen/src/logging/logging.dart';
-import 'package:jnigen/src/util/command_output.dart';
 
 /// A command based summary source which calls the ApiSummarizer command.
 /// [sourcePaths] and [classPaths] can be provided for the summarizer to find
@@ -68,7 +67,7 @@
     }
   }
 
-  Future<Stream<List<int>>> getInputStream() async {
+  Future<Process> runProcess() async {
     final commandSplit = command.split(" ");
     final exec = commandSplit[0];
     final List<String> args = commandSplit.sublist(1);
@@ -80,13 +79,9 @@
     }
     args.addAll(extraArgs);
     args.addAll(classes);
-
     log.info('execute $exec ${args.join(' ')}');
     final proc = await Process.start(exec, args,
         workingDirectory: workingDirectory?.toFilePath() ?? '.');
-    // ignore: unawaited_futures
-    prefixedCommandOutputStream('[ApiSummarizer]', proc.stderr)
-        .forEach(stderr.writeln);
-    return proc.stdout;
+    return proc;
   }
 }
diff --git a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
index 72fda52..e62d1e8 100644
--- a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
@@ -11,9 +11,8 @@
   /// get path for android API sources
   static Future<String?> _getVersionDir(
       String relative, String? sdkRoot, List<int> versionOrder) async {
-    sdkRoot ??= Platform.environment['ANDROID_SDK_ROOT'];
     if (sdkRoot == null) {
-      throw ArgumentError('SDK Root not provided and ANDROID_SDK_ROOT not set');
+      throw ArgumentError('SDK Root not provided');
     }
     final parent = join(sdkRoot, relative);
     for (var version in versionOrder) {
@@ -84,11 +83,16 @@
     log.finer('Writing temporary gradle script with stub function...');
     origBuild.writeAsStringSync('$script\n$_gradleListDepsFunction\n');
     log.finer('Running gradle wrapper...');
-    final procRes = Process.runSync('./gradlew', ['-q', 'listDependencies'],
-        workingDirectory: android);
-    log.finer('Restoring build scripts');
-    origBuild.writeAsStringSync(script);
-    File(buildGradleOld).deleteSync();
+    final gradleCommand = Platform.isWindows ? '.\\gradlew.bat' : './gradlew';
+    ProcessResult procRes;
+    try {
+      procRes = Process.runSync(gradleCommand, ['-q', 'listDependencies'],
+          workingDirectory: android, runInShell: true);
+    } finally {
+      log.finer('Restoring build scripts');
+      origBuild.writeAsStringSync(script);
+      File(buildGradleOld).deleteSync();
+    }
     if (procRes.exitCode != 0) {
       final inAndroidProject =
           (androidProject == '.') ? '' : ' in $androidProject';
@@ -96,7 +100,9 @@
           'This can be related to a known issue with gradle. Please run '
           '`flutter build apk`$inAndroidProject and try again\n');
     }
-    final classpaths = (procRes.stdout as String).trim().split('\n');
+    final classpaths = (procRes.stdout as String)
+        .trim()
+        .split(Platform.isWindows ? '\r\n' : '\n');
     log.info('Found release build classpath with ${classpaths.length} entries');
     return classpaths;
   }
diff --git a/pkgs/jnigen/lib/src/tools/build_summarizer.dart b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
index 1a1033b..d2dd22d 100644
--- a/pkgs/jnigen/lib/src/tools/build_summarizer.dart
+++ b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
@@ -29,17 +29,28 @@
     pom.toFilePath(),
     'assembly:assembly'
   ];
-  log.info('execute mvn $mvnArgs');
-  final mvnProc = await Process.start('mvn', mvnArgs,
-      workingDirectory: toolPath,
-      runInShell: true,
-      mode: ProcessStartMode.inheritStdio);
-  await mvnProc.exitCode;
-  await File(targetJarFile).rename(jarFile);
-  await Directory(mvnTargetDir).delete(recursive: true);
+  log.info('execute mvn ${mvnArgs.join(" ")}');
+  try {
+    final mvnProc = await Process.run('mvn', mvnArgs,
+        workingDirectory: toolPath, runInShell: true);
+    final exitCode = mvnProc.exitCode;
+    if (exitCode == 0) {
+      await File(targetJarFile).rename(jarFile);
+    } else {
+      printError(mvnProc.stdout);
+      printError(mvnProc.stderr);
+      printError("maven exited with $exitCode");
+    }
+  } finally {
+    await Directory(mvnTargetDir).delete(recursive: true);
+  }
 }
 
 Future<void> buildSummarizerIfNotExists({bool force = false}) async {
+  // TODO(#43): This function cannot be invoked concurrently because 2 processes
+  // will start building summarizer at once. Introduce a locking mechnanism so
+  // that when one process is building summarizer JAR, other process waits using
+  // exponential backoff.
   final jarExists = await File(jarFile).exists();
   final isJarStale = jarExists &&
       await isPackageModifiedAfter(
diff --git a/pkgs/jnigen/lib/src/tools/maven_tools.dart b/pkgs/jnigen/lib/src/tools/maven_tools.dart
index 80d2c6f..8d1ccd9 100644
--- a/pkgs/jnigen/lib/src/tools/maven_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/maven_tools.dart
@@ -34,7 +34,7 @@
     log.finer('using POM stub:\n$pom');
     await File(tempPom).writeAsString(pom);
     await Directory(tempTarget).create();
-    await _runCmd('mvn', ['-f', tempPom, ...mvnArgs]);
+    await _runCmd('mvn', ['-q', '-f', tempPom, ...mvnArgs]);
     await File(tempPom).delete();
     await Directory(tempTarget).delete(recursive: true);
   }
diff --git a/pkgs/jnigen/lib/src/util/command_output.dart b/pkgs/jnigen/lib/src/util/command_output.dart
index ba9366a..64deac2 100644
--- a/pkgs/jnigen/lib/src/util/command_output.dart
+++ b/pkgs/jnigen/lib/src/util/command_output.dart
@@ -11,3 +11,6 @@
 Stream<String> prefixedCommandOutputStream(
         String prefix, Stream<List<int>> input) =>
     commandOutputStream((line) => '$prefix $line', input);
+
+void collectOutputStream(Stream<List<int>> stream, StringBuffer buffer) =>
+    stream.transform(Utf8Decoder()).forEach(buffer.write);
diff --git a/pkgs/jnigen/lib/src/util/name_utils.dart b/pkgs/jnigen/lib/src/util/name_utils.dart
index 38d980b..96d0c83 100644
--- a/pkgs/jnigen/lib/src/util/name_utils.dart
+++ b/pkgs/jnigen/lib/src/util/name_utils.dart
@@ -2,10 +2,6 @@
 // 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.
 
-import 'package:jnigen/src/elements/elements.dart';
-
-String getPackageName(String binaryName) => cutFromLast(binaryName, '.')[0];
-
 /// splits [str] into 2 from last occurence of [sep]
 List<String> cutFromLast(String str, String sep) {
   final li = str.lastIndexOf(sep);
@@ -14,17 +10,3 @@
   }
   return [str.substring(0, li), str.substring(li + 1)];
 }
-
-String getLastName(String binaryName) => binaryName.split('.').last;
-
-String getSimpleNameOf(ClassDecl cls) => cls.simpleName;
-
-/// Returns class name as useful in dart.
-///
-/// Eg -> a.b.X.Y -> X_Y
-String simplifiedClassName(String binaryName) =>
-    getLastName(binaryName).replaceAll('\$', '_');
-
-// Utilities to operate on package names.
-
-List<String> getComponents(String packageName) => packageName.split('.');
diff --git a/pkgs/jnigen/lib/src/writers/bindings_writer.dart b/pkgs/jnigen/lib/src/writers/bindings_writer.dart
new file mode 100644
index 0000000..f437492
--- /dev/null
+++ b/pkgs/jnigen/lib/src/writers/bindings_writer.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2022, 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.
+
+import 'dart:io';
+
+import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/src/logging/logging.dart';
+
+abstract class BindingsWriter {
+  Future<void> writeBindings(Iterable<ClassDecl> classes);
+
+  /// Run dart format command on [path].
+  static Future<void> runDartFormat(String path) async {
+    log.info('Running dart format...');
+    final formatRes = await Process.run('dart', ['format', path]);
+    // if negative exit code, likely due to an interrupt.
+    if (formatRes.exitCode > 0) {
+      log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
+          'This usually means there\'s a syntax error in bindings.\n'
+          'Please look at the generated files and report a bug.');
+    }
+  }
+}
diff --git a/pkgs/jnigen/lib/src/writers/files_writer.dart b/pkgs/jnigen/lib/src/writers/files_writer.dart
new file mode 100644
index 0000000..f8e724a
--- /dev/null
+++ b/pkgs/jnigen/lib/src/writers/files_writer.dart
@@ -0,0 +1,279 @@
+// Copyright (c) 2022, 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.
+
+import 'dart:io';
+import 'dart:math';
+
+import 'package:jnigen/src/bindings/bindings.dart';
+import 'package:jnigen/src/logging/logging.dart';
+import 'package:jnigen/src/elements/elements.dart';
+import 'package:jnigen/src/config/config.dart';
+import 'package:jnigen/src/util/find_package.dart';
+import 'package:jnigen/src/util/name_utils.dart';
+import 'package:jnigen/src/writers/bindings_writer.dart';
+
+/// Resolver for file-per-package mapping, in which the java package hierarchy
+/// is mirrored.
+class PackagePathResolver implements SymbolResolver {
+  PackagePathResolver(this.importMap, this.currentPackage, this.inputClassNames,
+      {this.predefined = const {}});
+
+  final String currentPackage;
+  final Map<String, String> importMap;
+  final Map<String, String> predefined;
+  final Set<String> inputClassNames;
+
+  final List<String> importStrings = [];
+
+  final Set<String> relativeImportedPackages = {};
+
+  final Map<String, String> _importedNameToPackage = {};
+  final Map<String, String> _packageToImportedName = {};
+
+  /// Returns the dart name of the [binaryName] in current translation context,
+  /// or `null` if the name cannot be resolved.
+  @override
+  String? resolve(String binaryName) {
+    if (predefined.containsKey(binaryName)) {
+      return predefined[binaryName];
+    }
+    final parts = cutFromLast(binaryName, '.');
+    final package = parts[0];
+    final typename = parts[1];
+    final simpleTypeName = typename.replaceAll('\$', '_');
+
+    if (package == currentPackage && inputClassNames.contains(binaryName)) {
+      return simpleTypeName;
+    }
+
+    if (_packageToImportedName.containsKey(package)) {
+      // This package was already resolved
+      // but we still need to check if it was a relative import, in which case
+      // the class not in inputClassNames cannot be mapped here.
+      if (!relativeImportedPackages.contains(package) ||
+          inputClassNames.contains(binaryName)) {
+        final importedName = _packageToImportedName[package];
+        return '$importedName.$simpleTypeName';
+      }
+    }
+
+    final packageImport = getImport(package, binaryName);
+    log.finest('$package resolved to $packageImport for $binaryName');
+    if (packageImport == null) {
+      return null;
+    }
+
+    final pkgName = cutFromLast(package, '.')[1];
+    if (pkgName.isEmpty) {
+      throw UnsupportedError('No package could be deduced from '
+          'qualified binaryName');
+    }
+
+    // We always name imports with an underscore suffix, so that they can be
+    // never shadowed by a parameter or local variable.
+    var importedName = '${pkgName}_';
+    int suffix = 0;
+    while (_importedNameToPackage.containsKey(importedName)) {
+      suffix++;
+      importedName = '$pkgName${suffix}_';
+    }
+
+    _importedNameToPackage[importedName] = package;
+    _packageToImportedName[package] = importedName;
+    importStrings.add('import "$packageImport" as $importedName;\n');
+    return '$importedName.$simpleTypeName';
+  }
+
+  /// Returns import string, or `null` if package not found.
+  String? getImport(String packageToResolve, String binaryName) {
+    final right = <String>[];
+    var prefix = packageToResolve;
+
+    if (prefix.isEmpty) {
+      throw UnsupportedError('unexpected: empty package name.');
+    }
+
+    final dest = packageToResolve.split('.');
+    final src = currentPackage.split('.');
+    if (inputClassNames.contains(binaryName)) {
+      int common = 0;
+      for (int i = 0; i < src.length && i < dest.length; i++) {
+        if (src[i] == dest[i]) {
+          common++;
+        }
+      }
+      // a.b.c => a/b/c.dart
+      // from there
+      // a/b.dart => ../b.dart
+      // a.b.d => d.dart
+      // a.b.c.d => c/d.dart
+      var pathToCommon = '';
+      if (common < src.length) {
+        pathToCommon = '../' * (src.length - common);
+      }
+      final pathToPackage = dest.skip(max(common - 1, 0)).join('/');
+      relativeImportedPackages.add(packageToResolve);
+      return '$pathToCommon$pathToPackage.dart';
+    }
+
+    while (prefix.isNotEmpty) {
+      final split = cutFromLast(prefix, '.');
+      final left = split[0];
+      right.add(split[1]);
+      // eg: packages[org.apache.pdfbox]/org/apache/pdfbox.dart
+      if (importMap.containsKey(prefix)) {
+        final sub = packageToResolve.replaceAll('.', '/');
+        final pkg = _suffix(importMap[prefix]!, '/');
+        return '$pkg$sub.dart';
+      }
+      prefix = left;
+    }
+    return null;
+  }
+
+  String _suffix(String str, String suffix) {
+    if (str.endsWith(suffix)) {
+      return str;
+    }
+    return str + suffix;
+  }
+
+  @override
+  List<String> getImportStrings() {
+    return importStrings;
+  }
+}
+
+/// Writer which executes custom callback on passed class elements.
+///
+/// This class is provided for debugging purposes.
+class CallbackWriter implements BindingsWriter {
+  CallbackWriter(this.callback);
+  Future<void> Function(Iterable<ClassDecl>) callback;
+  @override
+  Future<void> writeBindings(Iterable<ClassDecl> classes) async {
+    await callback(classes);
+  }
+}
+
+/// Writer which takes writes C and Dart bindings to specified directories.
+///
+/// The structure of dart files is determined by package structure of java.
+/// One dart file corresponds to one java package, and it's path is decided by
+/// fully qualified name of the package.
+///
+/// Example:
+/// `android.os` -> `$dartWrappersRoot`/`android/os.dart`
+class FilesWriter extends BindingsWriter {
+  static const _initFileName = '_init.dart';
+
+  FilesWriter(this.config);
+  Config config;
+
+  @override
+  Future<void> writeBindings(Iterable<ClassDecl> classes) async {
+    final preamble = config.preamble;
+    final Map<String, List<ClassDecl>> packages = {};
+    final Map<String, ClassDecl> classesByName = {};
+    for (var c in classes) {
+      classesByName.putIfAbsent(c.binaryName, () => c);
+      packages.putIfAbsent(c.packageName!, () => <ClassDecl>[]);
+      packages[c.packageName!]!.add(c);
+    }
+    final classNames = classesByName.keys.toSet();
+
+    if (config.bindingsType == BindingsType.packageStructured) {
+      throw UnimplementedError(
+          "Package structured bindings are not yet implemented");
+    }
+
+    final cRoot = config.cRoot!;
+    log.info("Using c root = $cRoot");
+    final dartRoot = config.dartRoot!;
+    log.info("Using dart root = $dartRoot");
+    final libraryName = config.libraryName!;
+
+    log.info('Creating dart init file ...');
+    final initFileUri = dartRoot.resolve(_initFileName);
+    final initFile = await File.fromUri(initFileUri).create(recursive: true);
+    var initCode = DartBindingsGenerator.initFile(libraryName);
+    if (preamble != null) {
+      initCode = '$preamble\n$initCode';
+    }
+    await initFile.writeAsString(initCode, flush: true);
+    final subdir = config.cSubdir ?? '.';
+    final cFileRelativePath = '$subdir/$libraryName.c';
+    final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
+        .create(recursive: true);
+    final cFileStream = cFile.openWrite();
+    if (preamble != null) {
+      cFileStream.writeln(preamble);
+    }
+    cFileStream.write(CPreludes.prelude);
+    ApiPreprocessor.preprocessAll(classesByName, config);
+    for (var packageName in packages.keys) {
+      final relativeFileName = '${packageName.replaceAll('.', '/')}.dart';
+      final dartFileUri = dartRoot.resolve(relativeFileName);
+      log.fine('Writing bindings for $packageName...');
+      final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
+      final resolver = PackagePathResolver(
+          config.importMap ?? const {}, packageName, classNames,
+          predefined: {'java.lang.String': 'jni.JniString'});
+      final cgen = CBindingGenerator(config);
+      final dgen = DartBindingsGenerator(config, resolver);
+
+      final package = packages[packageName]!;
+      final cBindings = package.map(cgen.generateBinding).toList();
+      final dartBindings = package.map(dgen.generateBinding).toList();
+      // write imports from bindings
+      final dartFileStream = dartFile.openWrite();
+      final initImportPath = ('../' *
+              relativeFileName.codeUnits
+                  .where((cu) => '/'.codeUnitAt(0) == cu)
+                  .length) +
+          _initFileName;
+      if (preamble != null) {
+        dartFileStream.writeln(preamble);
+      }
+      dartFileStream
+        ..write(DartBindingsGenerator.bindingFileHeaders)
+        ..write(resolver.getImportStrings().join('\n'))
+        ..write('import "$initImportPath" show jniLookup;\n\n');
+      // write dart bindings only after all imports are figured out
+      dartBindings.forEach(dartFileStream.write);
+      cBindings.forEach(cFileStream.write);
+      await dartFileStream.close();
+    }
+    await cFileStream.close();
+    await BindingsWriter.runDartFormat(dartRoot.toFilePath());
+    log.info('Copying auxiliary files...');
+    await _copyFileFromPackage(
+        'jni', 'src/dartjni.h', cRoot.resolve('$subdir/dartjni.h'));
+    await _copyFileFromPackage(
+        'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'),
+        transform: (s) {
+      return s
+          .replaceAll('{{LIBRARY_NAME}}', libraryName)
+          .replaceAll('{{SUBDIR}}', subdir);
+    });
+    log.info('Completed.');
+  }
+
+  Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
+      {String Function(String)? transform}) async {
+    final packagePath = await findPackageRoot(package);
+    if (packagePath != null) {
+      final sourceFile = File.fromUri(packagePath.resolve(relPath));
+      final targetFile = await File.fromUri(target).create(recursive: true);
+      var source = await sourceFile.readAsString();
+      if (transform != null) {
+        source = transform(source);
+      }
+      await targetFile.writeAsString(source);
+    } else {
+      log.warning('package $package not found! '
+          'skipped copying ${target.toFilePath()}');
+    }
+  }
+}
diff --git a/pkgs/jnigen/lib/src/writers/writers.dart b/pkgs/jnigen/lib/src/writers/writers.dart
deleted file mode 100644
index e6e82fd..0000000
--- a/pkgs/jnigen/lib/src/writers/writers.dart
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright (c) 2022, 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.
-
-import 'dart:io';
-
-import 'package:jnigen/src/bindings/bindings.dart';
-
-import 'package:jnigen/src/elements/elements.dart';
-import 'package:jnigen/src/config/config.dart';
-import 'package:jnigen/src/logging/logging.dart';
-import 'package:jnigen/src/util/find_package.dart';
-
-abstract class BindingsWriter {
-  Future<void> writeBindings(Iterable<ClassDecl> classes);
-}
-
-/// Writer which executes custom callback on passed class elements.
-///
-/// This class is provided for debugging purposes.
-class CallbackWriter implements BindingsWriter {
-  CallbackWriter(this.callback);
-  Future<void> Function(Iterable<ClassDecl>) callback;
-  @override
-  Future<void> writeBindings(Iterable<ClassDecl> classes) async {
-    await callback(classes);
-  }
-}
-
-/// Writer which takes writes C and Dart bindings to specified directories.
-///
-/// The structure of dart files is determined by package structure of java.
-/// One dart file corresponds to one java package, and it's path is decided by
-/// fully qualified name of the package.
-///
-/// Example:
-/// `android.os` -> `$dartWrappersRoot`/`android/os.dart`
-class FilesWriter extends BindingsWriter {
-  static const _initFileName = '_init.dart';
-
-  FilesWriter(this.config);
-  Config config;
-
-  @override
-  Future<void> writeBindings(Iterable<ClassDecl> classes) async {
-    // If the file already exists, show warning.
-    // sort classes so that all classes get written at once.
-    final cRoot = config.cRoot;
-    final dartRoot = config.dartRoot;
-    final libraryName = config.libraryName;
-    final preamble = config.preamble;
-
-    final Map<String, List<ClassDecl>> packages = {};
-    final Map<String, ClassDecl> classesByName = {};
-    for (var c in classes) {
-      classesByName.putIfAbsent(c.binaryName, () => c);
-      packages.putIfAbsent(c.packageName!, () => <ClassDecl>[]);
-      packages[c.packageName!]!.add(c);
-    }
-    final classNames = classesByName.keys.toSet();
-
-    log.info('Creating dart init file ...');
-    final initFileUri = dartRoot.resolve(_initFileName);
-    final initFile = await File.fromUri(initFileUri).create(recursive: true);
-    var initCode = DartPreludes.initFile(config.libraryName);
-    if (preamble != null) {
-      initCode = '$preamble\n$initCode';
-    }
-    await initFile.writeAsString(initCode, flush: true);
-    final subdir = config.cSubdir ?? '.';
-    final cFileRelativePath = '$subdir/$libraryName.c';
-    final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
-        .create(recursive: true);
-    final cFileStream = cFile.openWrite();
-    if (preamble != null) {
-      cFileStream.writeln(preamble);
-    }
-    cFileStream.write(CPreludes.prelude);
-    ApiPreprocessor.preprocessAll(classesByName, config);
-    for (var packageName in packages.keys) {
-      final relativeFileName = '${packageName.replaceAll('.', '/')}.dart';
-      final dartFileUri = dartRoot.resolve(relativeFileName);
-      log.fine('Writing bindings for $packageName...');
-      final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
-      final resolver = PackagePathResolver(
-          config.importMap ?? const {}, packageName, classNames,
-          predefined: {'java.lang.String': 'jni.JniString'});
-      final cgen = CBindingGenerator(config);
-      final dgen = DartBindingsGenerator(config, resolver);
-
-      final package = packages[packageName]!;
-      final cBindings = package.map(cgen.generateBinding).toList();
-      final dartBindings = package.map(dgen.generateBinding).toList();
-      // write imports from bindings
-      final dartFileStream = dartFile.openWrite();
-      final initImportPath = ('../' *
-              relativeFileName.codeUnits
-                  .where((cu) => '/'.codeUnitAt(0) == cu)
-                  .length) +
-          _initFileName;
-      if (preamble != null) {
-        dartFileStream.writeln(preamble);
-      }
-      dartFileStream
-        ..write(DartPreludes.bindingFileHeaders)
-        ..write(resolver.getImportStrings().join('\n'))
-        ..write('import "$initImportPath" show jniLookup;\n\n');
-      // write dart bindings only after all imports are figured out
-      dartBindings.forEach(dartFileStream.write);
-      cBindings.forEach(cFileStream.write);
-      await dartFileStream.close();
-    }
-    await cFileStream.close();
-    log.info('Running dart format...');
-    final formatRes =
-        await Process.run('dart', ['format', dartRoot.toFilePath()]);
-    // if negative exit code, likely due to an interrupt.
-    if (formatRes.exitCode > 0) {
-      log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
-          'This usually means there\'s a syntax error in bindings.\n'
-          'Please look at the generated files and report a bug.');
-    }
-
-    log.info('Copying auxiliary files...');
-    await _copyFileFromPackage(
-        'jni', 'src/dartjni.h', cRoot.resolve('$subdir/dartjni.h'));
-    await _copyFileFromPackage(
-        'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'),
-        transform: (s) {
-      return s
-          .replaceAll('{{LIBRARY_NAME}}', libraryName)
-          .replaceAll('{{SUBDIR}}', subdir);
-    });
-    log.info('Completed.');
-  }
-
-  Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
-      {String Function(String)? transform}) async {
-    final packagePath = await findPackageRoot(package);
-    if (packagePath != null) {
-      final sourceFile = File.fromUri(packagePath.resolve(relPath));
-      final targetFile = await File.fromUri(target).create(recursive: true);
-      var source = await sourceFile.readAsString();
-      if (transform != null) {
-        source = transform(source);
-      }
-      await targetFile.writeAsString(source);
-    } else {
-      log.warning('package $package not found! '
-          'skipped copying ${target.toFilePath()}');
-    }
-  }
-}
diff --git a/pkgs/jnigen/test/README.md b/pkgs/jnigen/test/README.md
new file mode 100644
index 0000000..0911337
--- /dev/null
+++ b/pkgs/jnigen/test/README.md
@@ -0,0 +1,11 @@
+Some notes about tests in this directory:
+
+* [jackson_core_test](jackson_core_test/) is an end-to-end test which generates bindings for `jackson_core` library using the whole jnigen pipeline and compares the generated bindings with expected bindings. [simple_package_test](simple_package_test/) is similar but instead of using a Java library from maven, uses a stub java class.
+* [bindings_test.dart](bindings_test.dart) runs the generated bindings to make sure they work. There are only a few tests here right now.
+* [test_util/](test_util/) directory contains some code common to both `jackson_core_test` and `simple_package_test`. (Especially the functions `generateAndCompareBindings` and `generateAndAnalyzeBindings`).
+* [yaml_config_test.dart](yaml_config_test.dart) runs the same `jackson_core` configuration but through YAML and makes sure it generates the same bindings.
+* The other files contain unit tests for some error-prone components.
+
+Note: Tests fail if summarizer is not previously built and 2 tests try to build it concurrently. We have to address it using a lock file and exponential backoff (#43). Temporarily, run `dart run jnigen:setup` before running tests for the first time.
+
+TODO(#62): Add some unit & integration tests in the java portion.
diff --git a/pkgs/jnigen/test/bindings_test.dart b/pkgs/jnigen/test/bindings_test.dart
index 7d2c3b0..f8d4cd3 100644
--- a/pkgs/jnigen/test/bindings_test.dart
+++ b/pkgs/jnigen/test/bindings_test.dart
@@ -22,37 +22,37 @@
 
 import 'test_util/test_util.dart';
 
-final simplePackagePath = join('test', 'simple_package_test');
-final jacksonCorePath = join('test', 'jackson_core_test');
-final simplePackageJavaPath = join(simplePackagePath, 'java');
+final simplePackageTest = join('test', 'simple_package_test');
+final jacksonCoreTest = join('test', 'jackson_core_test');
+final simplePackageTestJava = join(simplePackageTest, 'java');
 
 Future<void> setupDylibsAndClasses() async {
-  await runCmd('dart', ['run', 'jni:setup']);
-  await runCmd(
-      'dart', ['run', 'jni:setup', '-S', join(simplePackagePath, 'src')]);
-  await runCmd('dart',
-      ['run', 'jni:setup', '-S', join(jacksonCorePath, 'third_party', 'src')]);
+  await runCommand('dart', ['run', 'jni:setup']);
+  await runCommand(
+      'dart', ['run', 'jni:setup', '-s', join(simplePackageTest, 'src')]);
+  await runCommand('dart',
+      ['run', 'jni:setup', '-s', join(jacksonCoreTest, 'third_party', 'src')]);
   final group = join('com', 'github', 'dart_lang', 'jnigen');
-  await runCmd(
+  await runCommand(
       'javac',
       [
         join(group, 'simple_package', 'Example.java'),
         join(group, 'pkg2', 'C2.java')
       ],
-      workingDirectory: simplePackageJavaPath);
-  await runCmd('dart', [
+      workingDirectory: simplePackageTestJava);
+  await runCommand('dart', [
     'run',
     'jnigen:download_maven_jars',
     '--config',
-    join(jacksonCorePath, 'jnigen.yaml')
+    join(jacksonCoreTest, 'jnigen.yaml')
   ]);
 
-  final jacksonJars = await getJarPaths(join(jacksonCorePath, 'third_party'));
+  final jacksonJars = await getJarPaths(join(jacksonCoreTest, 'third_party'));
 
   if (!Platform.isAndroid) {
     Jni.spawn(
-        dylibDir: 'build/jni_libs',
-        classPath: [simplePackageJavaPath, ...jacksonJars]);
+        dylibDir: join('build', 'jni_libs'),
+        classPath: [simplePackageTestJava, ...jacksonJars]);
   }
 }
 
@@ -87,12 +87,10 @@
     aux.delete();
     ex.delete();
   });
-  test('exceptions', () {
-    expect(() => Example.throwException(), throwsA(isA<JniException>()));
-  });
   test('simple json parsing test', () {
     final json = JniString.fromString('[1, true, false, 2, 4]');
-    final factory = JsonFactory();
+    JsonFactory factory;
+    factory = JsonFactory();
     final parser = factory.createParser6(json);
     final values = <bool>[];
     while (!parser.isClosed()) {
@@ -113,4 +111,7 @@
       expect(() => erroneous.nextToken(), throwsA(isA<JniException>()));
     });
   });
+  test('exceptions', () {
+    expect(() => Example.throwException(), throwsException);
+  });
 }
diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart
index f1ed780..352c378 100644
--- a/pkgs/jnigen/test/config_test.dart
+++ b/pkgs/jnigen/test/config_test.dart
@@ -20,44 +20,50 @@
 /// Compares 2 [Config] objects using [expect] to give useful errors when
 /// two fields are not equal.
 void expectConfigsAreEqual(Config a, Config b) {
-  expect(a.classes, equals(b.classes));
-  expect(a.libraryName, equals(b.libraryName));
-  expect(a.cRoot, equals(b.cRoot));
-  expect(a.dartRoot, equals(b.dartRoot));
-  expect(a.sourcePath, equals(b.sourcePath));
-  expect(a.classPath, equals(b.classPath));
-  expect(a.preamble, equals(b.preamble));
-  expect(a.importMap, equals(b.importMap));
+  expect(a.classes, equals(b.classes), reason: "classes");
+  expect(a.libraryName, equals(b.libraryName), reason: "libraryName");
+  expect(a.cRoot, equals(b.cRoot), reason: "cRoot");
+  expect(a.dartRoot, equals(b.dartRoot), reason: "dartRoot");
+  expect(a.sourcePath, equals(b.sourcePath), reason: "sourcePath");
+  expect(a.classPath, equals(b.classPath), reason: "classPath");
+  expect(a.preamble, equals(b.preamble), reason: "preamble");
+  expect(a.importMap, equals(b.importMap), reason: "importMap");
   final am = a.mavenDownloads;
   final bm = b.mavenDownloads;
   if (am != null) {
     expect(bm, isNotNull);
-    expect(am.sourceDeps, bm!.sourceDeps);
-    expect(path.equals(am.sourceDir, bm.sourceDir), isTrue);
-    expect(am.jarOnlyDeps, bm.jarOnlyDeps);
-    expect(path.equals(am.jarDir, bm.jarDir), isTrue);
+    expect(am.sourceDeps, bm!.sourceDeps, reason: "mavenDownloads.sourceDeps");
+    expect(path.equals(am.sourceDir, bm.sourceDir), isTrue,
+        reason: "mavenDownloads.sourceDir");
+    expect(am.jarOnlyDeps, bm.jarOnlyDeps,
+        reason: "mavenDownloads.jarOnlyDeps");
+    expect(path.equals(am.jarDir, bm.jarDir), isTrue,
+        reason: "mavenDownloads.jarDir");
   } else {
-    expect(bm, isNull);
+    expect(bm, isNull, reason: "mavenDownloads");
   }
   final aa = a.androidSdkConfig;
   final ba = b.androidSdkConfig;
   if (aa != null) {
-    expect(ba, isNotNull);
-    expect(aa.versions, ba!.versions);
-    expect(aa.sdkRoot, ba.sdkRoot);
-    expect(aa.includeSources, ba.includeSources);
+    expect(ba, isNotNull, reason: "androidSdkConfig");
+    expect(aa.versions, ba!.versions, reason: "androidSdkConfig.versions");
+    expect(aa.sdkRoot, ba.sdkRoot, reason: "androidSdkConfig.sdkRoot");
+    expect(aa.includeSources, ba.includeSources,
+        reason: "androidSdkConfig.includeSources");
   } else {
-    expect(ba, isNull);
+    expect(ba, isNull, reason: "androidSdkConfig");
   }
   final aso = a.summarizerOptions;
   final bso = b.summarizerOptions;
   if (aso != null) {
-    expect(bso, isNotNull);
-    expect(aso.extraArgs, bso!.extraArgs);
-    expect(aso.workingDirectory, bso.workingDirectory);
-    expect(aso.backend, bso.backend);
+    expect(bso, isNotNull, reason: "summarizerOptions");
+    expect(aso.extraArgs, bso!.extraArgs,
+        reason: "summarizerOptions.extraArgs");
+    expect(aso.workingDirectory, bso.workingDirectory,
+        reason: "summarizerOptions.workingDirectory");
+    expect(aso.backend, bso.backend, reason: "summarizerOptions.backend");
   } else {
-    expect(bso, isNull);
+    expect(bso, isNull, reason: "summarizerOptions");
   }
 }
 
diff --git a/pkgs/jnigen/test/jackson_core_test/generate.dart b/pkgs/jnigen/test/jackson_core_test/generate.dart
index 7335757..7b708c4 100644
--- a/pkgs/jnigen/test/jackson_core_test/generate.dart
+++ b/pkgs/jnigen/test/jackson_core_test/generate.dart
@@ -25,26 +25,25 @@
     '// limitations under the License.\n';
 
 const testName = 'jackson_core_test';
-final thirdParty = join('test', testName, 'third_party');
-const deps = ['com.fasterxml.jackson.core:jackson-core:2.13.3'];
+final thirdPartyDir = join('test', testName, 'third_party');
+const deps = ['com.fasterxml.jackson.core:jackson-core:2.13.4'];
 
 Config getConfig(
-    {required String root,
-    bool generateFullVersion = false,
-    bool useAsm = false}) {
+    {String? root, bool generateFullVersion = false, bool useAsm = false}) {
+  final rootDir = root ?? thirdPartyDir;
   final config = Config(
     mavenDownloads: MavenDownloads(
       sourceDeps: deps,
-      sourceDir: join(thirdParty, 'java'),
-      jarDir: join(thirdParty, 'jar'),
+      sourceDir: join(thirdPartyDir, 'java'),
+      jarDir: join(thirdPartyDir, 'jar'),
     ),
     summarizerOptions: SummarizerOptions(
       backend: useAsm ? 'asm' : null,
     ),
     preamble: jacksonPreamble,
     libraryName: testName,
-    cRoot: Uri.directory(join(root, 'src')),
-    dartRoot: Uri.directory(join(root, 'lib')),
+    cRoot: Uri.directory(join(rootDir, 'src')),
+    dartRoot: Uri.directory(join(rootDir, 'lib')),
     classes: (generateFullVersion)
         ? ['com.fasterxml.jackson.core']
         : [
@@ -52,7 +51,7 @@
             'com.fasterxml.jackson.core.JsonParser',
             'com.fasterxml.jackson.core.JsonToken',
           ],
-    logLevel: Level.WARNING,
+    logLevel: Level.INFO,
     exclude: BindingExclusions(
       fields: excludeAll<Field>([
         ['com.fasterxml.jackson.core.JsonFactory', 'DEFAULT_QUOTE_CHAR'],
@@ -65,13 +64,4 @@
   return config;
 }
 
-Future<void> generate(
-    {required String root,
-    bool generateFullVersion = false,
-    bool useAsm = false}) async {
-  final config = getConfig(
-      root: root, generateFullVersion: generateFullVersion, useAsm: useAsm);
-  await generateJniBindings(config);
-}
-
-void main() => generate(root: thirdParty);
+void main() async => await generateJniBindings(getConfig());
diff --git a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
index c1c9185..992c30b 100644
--- a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
+++ b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
@@ -2,8 +2,6 @@
 // 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.
 
-import 'dart:io';
-
 import 'package:path/path.dart' hide equals;
 
 import 'package:test/test.dart';
@@ -11,38 +9,21 @@
 import '../test_util/test_util.dart';
 import 'generate.dart';
 
-const packageTestsDir = 'test';
-const testName = 'jackson_core_test';
-
 void main() async {
-  final thirdParty = join(packageTestsDir, testName, 'third_party');
   test("compare generated bindings for jackson_core", () async {
-    final tempDir = await Directory(thirdParty).createTemp("test_");
-    await generate(root: tempDir.path);
-
-    compareDirs(join(thirdParty, 'lib'), join(tempDir.path, "lib"));
-    compareDirs(join(thirdParty, 'src'), join(tempDir.path, "src"));
-    tempDir.deleteSync(recursive: true);
+    final lib = join(thirdPartyDir, 'lib');
+    final src = join(thirdPartyDir, 'src');
+    await generateAndCompareBindings(getConfig(), lib, src);
   });
-  Future<void> analyze(String path) async {
-    final analyzeProc = await Process.start('dart', ['analyze', path],
-        mode: ProcessStartMode.inheritStdio);
-    final exitCode = await analyzeProc.exitCode;
-    expect(exitCode, 0);
-  }
 
   test(
       'generate and analyze bindings for complete library, '
       'not just required classes', () async {
-    final tempDir = await Directory(thirdParty).createTemp("test_");
-    await generate(root: tempDir.path, generateFullVersion: true);
-    await analyze(tempDir.path);
-    tempDir.deleteSync(recursive: true);
+    final config = getConfig(generateFullVersion: true);
+    await generateAndAnalyzeBindings(config);
   }, timeout: Timeout(Duration(minutes: 2)));
   test('generate and analyze bindings using ASM', () async {
-    final tempDir = await Directory(thirdParty).createTemp("test_");
-    await generate(root: tempDir.path, generateFullVersion: true, useAsm: true);
-    await analyze(tempDir.path);
-    tempDir.deleteSync(recursive: true);
+    final config = getConfig(generateFullVersion: true, useAsm: true);
+    await generateAndAnalyzeBindings(config);
   }, timeout: Timeout(Duration(minutes: 2)));
 }
diff --git a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
index d5d7d64..5a4f69c 100644
--- a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
+++ b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml
@@ -1,13 +1,12 @@
 maven_downloads:
   source_deps:
-    - 'com.fasterxml.jackson.core:jackson-core:2.13.3'
+    - 'com.fasterxml.jackson.core:jackson-core:2.13.4'
   source_dir: test/jackson_core_test/third_party/java
   jar_dir: test/jackson_core_test/third_party/jar
-
 dart_root: test/jackson_core_test/third_party/lib
 c_root: test/jackson_core_test/third_party/src
 library_name: jackson_core_test
-log_level: warning
+log_level: info
 classes:
   - 'com.fasterxml.jackson.core.JsonFactory'
   - 'com.fasterxml.jackson.core.JsonParser'
@@ -16,6 +15,10 @@
 exclude:
   fields:
     - 'com.fasterxml.jackson.core.JsonFactory#DEFAULT_QUOTE_CHAR'
+    - 'com.fasterxml.jackson.core.Base64Variant#PADDING_CHAR_NONE'
+    - 'com.fasterxml.jackson.core.base.ParserMinimalBase#CHAR_NULL'
+    - 'com.fasterxml.jackson.core.io.UTF32Reader#NC'
+
 
 preamble: |
   // Generated from jackson-core which is licensed under the Apache License 2.0.
diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/src/dartjni.h b/pkgs/jnigen/test/jackson_core_test/third_party/src/dartjni.h
index 0ce5069..bc72baa 100644
--- a/pkgs/jnigen/test/jackson_core_test/third_party/src/dartjni.h
+++ b/pkgs/jnigen/test/jackson_core_test/third_party/src/dartjni.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jnigen/test/package_resolver_test.dart b/pkgs/jnigen/test/package_resolver_test.dart
index f8f1be8..91e32dd 100644
--- a/pkgs/jnigen/test/package_resolver_test.dart
+++ b/pkgs/jnigen/test/package_resolver_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-import 'package:jnigen/src/bindings/symbol_resolver.dart';
+import 'package:jnigen/src/writers/files_writer.dart';
 import 'package:jnigen/src/util/name_utils.dart';
 import 'package:test/test.dart';
 
diff --git a/pkgs/jnigen/test/simple_package_test/generate.dart b/pkgs/jnigen/test/simple_package_test/generate.dart
index 67a5de0..99db612 100644
--- a/pkgs/jnigen/test/simple_package_test/generate.dart
+++ b/pkgs/jnigen/test/simple_package_test/generate.dart
@@ -8,8 +8,6 @@
 import 'package:path/path.dart';
 import 'package:jnigen/jnigen.dart';
 
-import '../test_util/test_util.dart';
-
 const testName = 'simple_package_test';
 final testRoot = join('test', testName);
 final javaPath = join(testRoot, 'java');
@@ -28,21 +26,18 @@
   join(javaPrefix, 'pkg2', 'C2.java'),
 ];
 
-Future<void> compileJavaSources(String workingDir, List<String> files) async {
-  await runCmd('javac', files, workingDirectory: workingDir);
+void compileJavaSources(String workingDir, List<String> files) async {
+  final procRes = Process.runSync('javac', files, workingDirectory: workingDir);
+  if (procRes.exitCode != 0) {
+    throw "javac exited with ${procRes.exitCode}\n"
+        "$procRes.stderr";
+  }
 }
 
-Future<void> generateSources(String lib, String src) async {
-  await compileJavaSources(javaPath, javaFiles);
-  final cWrapperDir = Uri.directory(join(testRoot, src));
-  final dartWrappersRoot = Uri.directory(join(testRoot, lib));
-  final cDir = Directory.fromUri(cWrapperDir);
-  final dartDir = Directory.fromUri(dartWrappersRoot);
-  for (var dir in [cDir, dartDir]) {
-    if (await dir.exists()) {
-      await dir.delete(recursive: true);
-    }
-  }
+Config getConfig() {
+  compileJavaSources(javaPath, javaFiles);
+  final cWrapperDir = Uri.directory(join(testRoot, "src"));
+  final dartWrappersRoot = Uri.directory(join(testRoot, "lib"));
   final config = Config(
     sourcePath: [Uri.directory(javaPath)],
     classPath: [Uri.directory(javaPath)],
@@ -53,10 +48,10 @@
     preamble: preamble,
     cRoot: cWrapperDir,
     dartRoot: dartWrappersRoot,
-    logLevel: Level.WARNING,
+    logLevel: Level.INFO,
     libraryName: 'simple_package',
   );
-  await generateJniBindings(config);
+  return config;
 }
 
-void main() async => await generateSources('lib', 'src');
+void main() async => await generateJniBindings(getConfig());
diff --git a/pkgs/jnigen/test/simple_package_test/generated_files_test.dart b/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
index ae1b4ba..c37de4a 100644
--- a/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
+++ b/pkgs/jnigen/test/simple_package_test/generated_files_test.dart
@@ -2,8 +2,6 @@
 // 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.
 
-import 'dart:io';
-
 import 'package:test/test.dart';
 import 'package:path/path.dart' hide equals;
 
@@ -11,18 +9,11 @@
 import '../test_util/test_util.dart';
 
 void main() async {
-  await generateSources('test_lib', 'test_src');
-  // test if generated file == expected file
-  test('compare generated files', () {
-    compareDirs(join(testRoot, 'lib'), join(testRoot, 'test_lib'));
-    compareDirs(join(testRoot, 'src'), join(testRoot, 'test_src'));
-  });
-  tearDownAll(() async {
-    for (var path in ['test_lib', 'test_src']) {
-      final folder = Directory(join(testRoot, path));
-      if (await folder.exists()) {
-        await folder.delete(recursive: true);
-      }
-    }
-  });
+  test("Generate and compare bindings for simple_package", () async {
+    await generateAndCompareBindings(
+      getConfig(),
+      join(testRoot, "lib"),
+      join(testRoot, "src"),
+    );
+  }); // test if generated file == expected file
 }
diff --git a/pkgs/jnigen/test/simple_package_test/src/dartjni.h b/pkgs/jnigen/test/simple_package_test/src/dartjni.h
index 0ce5069..bc72baa 100644
--- a/pkgs/jnigen/test/simple_package_test/src/dartjni.h
+++ b/pkgs/jnigen/test/simple_package_test/src/dartjni.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+// Note: include appropriate system jni.h as found by CMake, not third_party/jni.h.
 #include <jni.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -32,58 +33,102 @@
 #include <android/log.h>
 #endif
 
-#define JNI_LOG_TAG "Dart-JNI"
-
 #ifdef __ANDROID__
-#define __ENVP_CAST (JNIEnv **)
+#define __ENVP_CAST (JNIEnv**)
 #else
-#define __ENVP_CAST (void **)
+#define __ENVP_CAST (void**)
 #endif
 
+/// Stores the global state of the JNI.
 typedef struct JniContext {
-	JavaVM *jvm;
-	jobject classLoader;
-	jmethodID loadClassMethod;
-	jobject currentActivity;
-	jobject appContext;
+  JavaVM* jvm;
+  jobject classLoader;
+  jmethodID loadClassMethod;
+  jobject currentActivity;
+  jobject appContext;
 } JniContext;
 
-extern thread_local JNIEnv *jniEnv;
+// jniEnv for this thread, used by inline functions in this header,
+// therefore declared as extern.
+extern thread_local JNIEnv* jniEnv;
 
 extern JniContext jni;
 
-enum DartJniLogLevel {
-	JNI_VERBOSE = 2,
-	JNI_DEBUG,
-	JNI_INFO,
-	JNI_WARN,
-	JNI_ERROR
-};
-
+/// Types used by JNI API to distinguish between primitive types.
 enum JniType {
-	boolType = 0,
-	byteType = 1,
-	shortType = 2,
-	charType = 3,
-	intType = 4,
-	longType = 5,
-	floatType = 6,
-	doubleType = 7,
-	objectType = 8,
-	voidType = 9,
+  booleanType = 0,
+  byteType = 1,
+  shortType = 2,
+  charType = 3,
+  intType = 4,
+  longType = 5,
+  floatType = 6,
+  doubleType = 7,
+  objectType = 8,
+  voidType = 9,
 };
 
-FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// Result type for use by JNI.
+///
+/// If [exception] is null, it means the result is valid.
+/// It's assumed that the caller knows the expected type in [result].
+typedef struct JniResult {
+  jvalue result;
+  jthrowable exception;
+} JniResult;
 
-FFI_PLUGIN_EXPORT JavaVM *GetJavaVM(void);
+/// Similar to [JniResult] but for class lookups.
+typedef struct JniClassLookupResult {
+  jclass classRef;
+  jthrowable exception;
+} JniClassLookupResult;
 
-FFI_PLUGIN_EXPORT int DestroyJavaVM();
+/// Similar to [JniResult] but for method/field ID lookups.
+typedef struct JniPointerResult {
+  void* id;
+  jthrowable exception;
+} JniPointerResult;
 
-FFI_PLUGIN_EXPORT JNIEnv *GetJniEnv(void);
+/// This struct contains functions which wrap method call / field access conveniently along with
+/// exception checking.
+///
+/// Flutter embedding checks for pending JNI exceptions before an FFI transition, which requires us
+/// to check for and clear the exception before returning to dart code, which requires these functions
+/// to return result types.
+typedef struct JniAccessors {
+  JniClassLookupResult (*getClass)(char* internalName);
+  JniPointerResult (*getFieldID)(jclass cls, char* fieldName, char* signature);
+  JniPointerResult (*getStaticFieldID)(jclass cls,
+                                       char* fieldName,
+                                       char* signature);
+  JniPointerResult (*getMethodID)(jclass cls,
+                                  char* methodName,
+                                  char* signature);
+  JniPointerResult (*getStaticMethodID)(jclass cls,
+                                        char* methodName,
+                                        char* signature);
+  JniResult (*newObject)(jclass cls, jmethodID ctor, jvalue* args);
+  JniResult (*callMethod)(jobject obj,
+                          jmethodID methodID,
+                          int callType,
+                          jvalue* args);
+  JniResult (*callStaticMethod)(jclass cls,
+                                jmethodID methodID,
+                                int callType,
+                                jvalue* args);
+  JniResult (*getField)(jobject obj, jfieldID fieldID, int callType);
+  JniResult (*getStaticField)(jclass cls, jfieldID fieldID, int callType);
+} JniAccessors;
 
-FFI_PLUGIN_EXPORT JNIEnv *SpawnJvm(JavaVMInitArgs *args);
+FFI_PLUGIN_EXPORT JniAccessors* GetAccessors();
 
-FFI_PLUGIN_EXPORT jclass LoadClass(const char *name);
+FFI_PLUGIN_EXPORT JavaVM* GetJavaVM(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* GetJniEnv(void);
+
+FFI_PLUGIN_EXPORT JNIEnv* SpawnJvm(JavaVMInitArgs* args);
+
+FFI_PLUGIN_EXPORT jclass LoadClass(const char* name);
 
 FFI_PLUGIN_EXPORT jobject GetClassLoader(void);
 
@@ -91,17 +136,8 @@
 
 FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void);
 
-/// For use by jni_gen's generated code
-/// don't use these.
-
-// these 2 fn ptr vars will be defined by generated code library
-extern JniContext (*context_getter)(void);
-extern JNIEnv *(*env_getter)(void);
-
-// this function will be exported by generated code library
-// it will set above 2 variables.
-FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
-		JNIEnv *(*eg)(void));
+// Migration note: Below inline functions are required by C bindings, but can be moved to dartjni.c
+// once migration to pure dart bindings is complete.
 
 // `static inline` because `inline` doesn't work, it may still not
 // inline the function in which case a linker error may be produced.
@@ -109,77 +145,98 @@
 // There has to be a better way to do this. Either to force inlining on target
 // platforms, or just leave it as normal function.
 
-static inline void __load_class_into(jclass *cls, const char *name) {
+static inline void __load_class_into(jclass* cls, const char* name) {
 #ifdef __ANDROID__
-	jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
-	*cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
-	                                   jni.loadClassMethod, className);
-	(*jniEnv)->DeleteLocalRef(jniEnv, className);
+  jstring className = (*jniEnv)->NewStringUTF(jniEnv, name);
+  *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni.classLoader,
+                                     jni.loadClassMethod, className);
+  (*jniEnv)->DeleteLocalRef(jniEnv, className);
 #else
-	*cls = (*jniEnv)->FindClass(jniEnv, name);
+  *cls = (*jniEnv)->FindClass(jniEnv, name);
 #endif
 }
 
-static inline void load_class(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		__load_class_into(cls, name);
-	}
+static inline void load_class(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    __load_class_into(cls, name);
+  }
 }
 
-static inline void load_class_gr(jclass *cls, const char *name) {
-	if (*cls == NULL) {
-		jclass tmp;
-		__load_class_into(&tmp, name);
-		*cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
-		(*jniEnv)->DeleteLocalRef(jniEnv, tmp);
-	}
+static inline void load_class_gr(jclass* cls, const char* name) {
+  if (*cls == NULL) {
+    jclass tmp;
+    __load_class_into(&tmp, name);
+    *cls = (*jniEnv)->NewGlobalRef(jniEnv, tmp);
+    (*jniEnv)->DeleteLocalRef(jniEnv, tmp);
+  }
 }
 
 static inline void attach_thread() {
-	if (jniEnv == NULL) {
-		(*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv,
-		                                NULL);
-	}
+  if (jniEnv == NULL) {
+    (*jni.jvm)->AttachCurrentThread(jni.jvm, __ENVP_CAST & jniEnv, NULL);
+  }
 }
 
-static inline void load_env() {
-	if (jniEnv == NULL) {
-		jni = context_getter();
-		jniEnv = env_getter();
-	}
+static inline void load_method(jclass cls,
+                               jmethodID* res,
+                               const char* name,
+                               const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_method(jclass cls, jmethodID *res, const char *name,
-                               const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_method(jclass cls,
+                                      jmethodID* res,
+                                      const char* name,
+                                      const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_static_method(jclass cls, jmethodID *res,
-                                      const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticMethodID(jniEnv, cls, name, sig);
-	}
+static inline void load_field(jclass cls,
+                              jfieldID* res,
+                              const char* name,
+                              const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
+  }
 }
 
-static inline void load_field(jclass cls, jfieldID *res, const char *name,
-                              const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetFieldID(jniEnv, cls, name, sig);
-	}
-}
-
-static inline void load_static_field(jclass cls, jfieldID *res,
-                                     const char *name, const char *sig) {
-	if (*res == NULL) {
-		*res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
-	}
+static inline void load_static_field(jclass cls,
+                                     jfieldID* res,
+                                     const char* name,
+                                     const char* sig) {
+  if (*res == NULL) {
+    *res = (*jniEnv)->GetStaticFieldID(jniEnv, cls, name, sig);
+  }
 }
 
 static inline jobject to_global_ref(jobject ref) {
-	jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
-	(*jniEnv)->DeleteLocalRef(jniEnv, ref);
-	return g;
+  jobject g = (*jniEnv)->NewGlobalRef(jniEnv, ref);
+  (*jniEnv)->DeleteLocalRef(jniEnv, ref);
+  return g;
 }
 
+// These functions are useful for C+Dart bindings, and not required for pure dart bindings.
+
+FFI_PLUGIN_EXPORT JniContext GetJniContext();
+/// For use by jni_gen's generated code
+/// don't use these.
+
+// these 2 fn ptr vars will be defined by generated code library
+extern JniContext (*context_getter)(void);
+extern JNIEnv* (*env_getter)(void);
+
+// this function will be exported by generated code library
+// it will set above 2 variables.
+FFI_PLUGIN_EXPORT void setJniGetters(struct JniContext (*cg)(void),
+                                     JNIEnv* (*eg)(void));
+
+static inline void load_env() {
+  if (jniEnv == NULL) {
+    jni = context_getter();
+    jniEnv = env_getter();
+  }
+}
diff --git a/pkgs/jnigen/test/test_util/test_util.dart b/pkgs/jnigen/test/test_util/test_util.dart
index ca625de..1a38b16 100644
--- a/pkgs/jnigen/test/test_util/test_util.dart
+++ b/pkgs/jnigen/test/test_util/test_util.dart
@@ -4,24 +4,26 @@
 
 import 'dart:io';
 
+import 'package:jnigen/jnigen.dart';
 import 'package:path/path.dart' hide equals;
 import 'package:test/test.dart';
 
-Future<bool> hasNoFilesInDir(String path) async {
+Future<bool> isEmptyOrNotExistDir(String path) async {
   final dir = Directory(path);
   return (!await dir.exists()) || (await dir.list().length == 0);
 }
 
-Future<int> runCmd(String exec, List<String> args,
+/// Runs "$exec ${args...}" and returns exit code.
+/// Required because only Process.start provides inheritStdio argument
+Future<int> runCommand(String exec, List<String> args,
     {String? workingDirectory}) async {
   stderr.writeln('[exec] $exec ${args.join(" ")}');
   final proc = await Process.start(exec, args,
-      workingDirectory: workingDirectory,
-      runInShell: true,
-      mode: ProcessStartMode.inheritStdio);
-  return proc.exitCode;
+      workingDirectory: workingDirectory, mode: ProcessStartMode.inheritStdio);
+  return await proc.exitCode;
 }
 
+/// List all JAR files in [testRoot]/jar
 Future<List<String>> getJarPaths(String testRoot) async {
   final jarPath = join(testRoot, 'jar');
   if (!await Directory(jarPath).exists()) {
@@ -54,3 +56,38 @@
         equals(b.readAsStringSync().replaceAll("\r\n", "\n")));
   }
 }
+
+Future<void> _generateTempBindings(Config config, Directory tempDir) async {
+  final tempSrc = tempDir.uri.resolve("src/");
+  final tempLib = tempDir.uri.resolve("lib/");
+  config.cRoot = tempSrc;
+  config.dartRoot = tempLib;
+  await generateJniBindings(config);
+}
+
+Future<void> generateAndCompareBindings(
+    Config config, String lib, String src) async {
+  final currentDir = Directory.current;
+  final tempDir = currentDir.createTempSync("jnigen_test_temp");
+  final tempSrc = tempDir.uri.resolve("src/");
+  final tempLib = tempDir.uri.resolve("lib/");
+  try {
+    await _generateTempBindings(config, tempDir);
+    compareDirs(lib, tempLib.toFilePath());
+    compareDirs(src, tempSrc.toFilePath());
+  } finally {
+    tempDir.deleteSync(recursive: true);
+  }
+}
+
+Future<void> generateAndAnalyzeBindings(Config config) async {
+  final tempDir = Directory.current.createTempSync("jnigen_test_temp");
+  try {
+    await _generateTempBindings(config, tempDir);
+    final analyzeResult = Process.runSync("dart", ["analyze", tempDir.path]);
+    expect(analyzeResult.exitCode, equals(0),
+        reason: "Analyzer exited with non-zero status");
+  } finally {
+    tempDir.deleteSync(recursive: true);
+  }
+}
diff --git a/pkgs/jnigen/test/yaml_config_test.dart b/pkgs/jnigen/test/yaml_config_test.dart
index 0885381..30083d1 100644
--- a/pkgs/jnigen/test/yaml_config_test.dart
+++ b/pkgs/jnigen/test/yaml_config_test.dart
@@ -4,8 +4,7 @@
 
 // End-to-end test confirming yaml config works as expected.
 
-import 'dart:io';
-
+import 'package:jnigen/jnigen.dart';
 import 'package:path/path.dart' hide equals;
 import 'package:test/test.dart';
 
@@ -17,30 +16,15 @@
   final testSrc = join(thirdParty, 'test_src_yaml');
   final lib = join(thirdParty, 'lib');
   final src = join(thirdParty, 'src');
-  final config = join('test', 'jackson_core_test', 'jnigen.yaml');
+  final configFile = join('test', 'jackson_core_test', 'jnigen.yaml');
   test('generate and compare bindings using YAML config', () async {
-    final jnigenProc = await Process.start(
-        'dart',
-        [
-          'run',
-          'jnigen',
-          '--config',
-          config,
-          '-Dc_root=$testSrc',
-          '-Ddart_root=$testLib'
-        ],
-        mode: ProcessStartMode.inheritStdio);
-    expect(await jnigenProc.exitCode, equals(0));
-
-    final analyzeProc =
-        Process.runSync('dart', ['analyze', testLib], runInShell: true);
-    expect(analyzeProc.exitCode, equals(0));
-
-    compareDirs(lib, testLib);
-    compareDirs(src, testSrc);
-
-    for (var dir in [testLib, testSrc]) {
-      Directory(dir).deleteSync(recursive: true);
-    }
+    final args = [
+      '--config',
+      configFile,
+      '-Dc_root=$testSrc',
+      '-Ddart_root=$testLib',
+    ];
+    final config = Config.parseArgs(args);
+    await generateAndCompareBindings(config, lib, src);
   }, timeout: Timeout.factor(4));
 }