Build Status Coverage Status pub package package publisher

Introduction

Bindings generator to call Java APIs from Dart code via dart:ffi and JNI.

JNIgen scans compiled JAR files or Java source code to generate a description of the API, then uses that to generate Dart bindings. The Dart bindings call the C bindings, which in-turn call the Java functions through JNI. Shared functionality and base classes are provided through the support library, package:jni.

Note

The YAML configuration format is deprecated and will be removed in a future version. Please migrate to the programmatic Dart generator API. You can use the migration skill in skills/jnigen-migrate-yaml-to-dart to automate the migration with an agent, but it also serves as good documentation if doing the migration manually.

Getting Started

This guide demonstrates how to call a custom Java API from a Flutter application targeting Android. It assumes that Flutter has been set up to build apps for Android (instructions) and that the Flutter app was created via flutter create in_app_java. If you encounter any issues running the commands below, check the Requirements section below for additional platform-specific instructions.

  1. Run flutter pub get at least once to get the dependencies of your app. This is necessary so that JNIgen can get the classpaths of Android Gradle libraries.

  2. Add the helper package package:jni as a dependency and the bindings generator package:jnigen as a dev_dependency to the pubspec of your app by running: flutter pub add jni dev:jnigen.

  3. Write the Java code and place it in the android/ subproject of your app. For this example, we'll place the following code under android/app/src/main/java/com/example/in_app_java/AndroidUtils.java. It defines a simple Java API to show a native Android toast.

    package com.example.in_app_java;
    
    import android.app.Activity;
    import android.widget.Toast;
    import androidx.annotation.Keep;
    
    @Keep
    public abstract class AndroidUtils { 
        private AndroidUtils() {} // Hide constructor
    
        public static void showToast(Activity mainActivity, CharSequence text, int duration) {
            mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, text, duration).show());
        }
    }
    
  4. To generate the bindings, we will write a script using package:jnigen and place it under tool/jnigen.dart. The script constructs a JniGenerator object and calls generate(). The JniGenerator object configures the bindings that JNIgen will generate for the Java code. Refer to the code comments below and the API docs to learn more about available configuration options.

    import 'dart:io';
      
    import 'package:jnigen/jnigen.dart';
      
    void main(List<String> args) async {
      final packageRoot = Platform.script.resolve('../');
      final generator = JniGenerator(
        input: Input(
          // Required. List of classes or packages for which bindings should be generated.
          classes: [
            'com.example.in_app_java', // Generate the entire package
            'androidx.emoji2.text.EmojiCompat', // From gradle's compile classpath
            'androidx.emoji2.text.DefaultEmojiCompatConfig', // From gradle's compile classpath
            'android.os.Build', // from gradle's compile classpath
          ],
          // Optional. List of directories that contain the source files for which to generate bindings.
          sourcePath: [packageRoot.resolve('android/app/src/main/java')],
          // Optional. Configuration to search for Android SDK libraries.
          androidSdk: AndroidSdk(
            addGradleDeps: true,
            androidExample: packageRoot,
          ),
        ),
        output: Output(
          dart: DartOutput(
            // Required. Output path for generated bindings.
            path: packageRoot.resolve('lib/android_utils.g.dart'),
            // Optional. Write bindings into a single file (instead of one file per class).
            structure: OutputStructure.singleFile,
          ),
        ),
      );
      await generator.generate();
    }
    
  5. Run the script with dart run tool/jnigen.dart to generate the bindings. This will create the output lib/android_utils.g.dart file, which can be imported by Dart code to access the Java APIs. This command must be re-run whenever the JNIgen configuration (in tool/jnigen.dart) or the Java sources for which bindings are generated change.

  6. Import android_utils.g.dart in your Flutter app and call the generated methods to access the native Java API:

    void showToast() {
      final activity =
          androidActivity(PlatformDispatcher.instance.engineId!)?.as(Activity.type);
      final message = 'This is a native toast shown from a Flutter app via JNI.';
      AndroidUtils.showToast(
        activity,
        message.toJString().as(CharSequence.type),
        0,
      );
    }
    

That's it! Run your app with flutter run on an Android device to see it in action.

The complete example can be found in example/in_app_java, which adds a few more classes to demonstrate using classes from Gradle JAR and source dependencies.

More Examples

Additional examples showcasing how JNIgen can be used in different scenarios (e.g. to generate bindings for Kotlin) can be found in the example directory.

Supported platforms

Platform Dart Standalone Flutter
Android n/a Supported
Linux Supported Supported
Windows Supported Supported
macOS Supported Not Yet

On Android, the Flutter application runs embedded in the Android JVM. On other platforms, a JVM needs to be explicitly spawned using Jni.spawn. The helper package package:jni provides the infrastructure for initializing and managing the JNI on both Android and non-Android platforms.

Dart (standalone) target

package:jni is an FFI plugin containing native code, and any bindings generated from JNIgen contain native code too.

On Flutter targets, native libraries are built automatically and bundled. On standalone platforms, no such infrastructure exists yet. As a stopgap solution, running dart run jni:setup in a target directory builds all JNI native dependencies of the package into build/jni_libs.

To start a JVM, call Jni.spawn. It's assumed that all dependencies are built into the same target directory, so that once JNI is initialized, generated bindings can load their respective C libraries automatically.

If a custom build path has been set for the dynamic libraries built by dart run jni:setup --build-path path/to/dylib, the same path must be passed to Jni.spawn. Also, anytime a new Dart isolate is spawned, the directory must be set again using Jni.setDylibDir.

Requirements

SDK

Flutter SDK is required.

Dart standalone target is supported, but due to some problems with pubspec format, the dart command must be from the Flutter SDK and not Dart SDK. See dart-lang/pub#3563.

Java tooling

JNIgen requires a JDK to be installed. JNIgen officially supports Java versions 17 to 21.

By default, JNIgen uses the JDK configured in Flutter (which is typically Android Studio's bundled JDK). If that fails (eg because Flutter is not installed, or isn't set up for Android development) it falls back to relying on the JAVA_HOME and PATH environment variables.

Windows

On windows, append the path of jvm.dll in your JDK installation to PATH. For example, on PowerShell:

$env:Path += ";${env:JAVA_HOME}\bin\server"

The above will only add jvm.dll to PATH for the current PowerShell session, use the Control Panel to add it to the path permanently.

If JAVA_HOME not set, find the java.exe executable and set the environment variable in Control Panel. If java is installed through a package manager, there may be a more automatic way to do this. (e.g. scoop reset).

C tooling

CMake and a standard C toolchain are required to build package:jni.

Configuration

JNIgen is configured using a Dart script, typically placed under tool/jnigen.dart and executed via dart run tool/jnigen.dart. The script should instantiate a JniGenerator with your desired configuration and call await generator.generate().

Example

import 'dart:io';

import 'package:jnigen/jnigen.dart';

Future<void> main() async {
  final packageRoot = Platform.script.resolve('../');
  final generator = JniGenerator(
    // Required. Output path and structure for the generated bindings.
    output: Output(
      dart: DartOutput(
        path: packageRoot.resolve('lib/src/generated_bindings.dart'),
        structure: OutputStructure.singleFile,
      ),
    ),
    // Where to find Java classes and source files.
    input: Input(
      // Required. Fully-qualified names of classes or packages to generate bindings for.
      classes: ['com.example.MyClass'],
      // Optional. Directories to search for Java source files.
      sourcePath: [packageRoot.resolve('android/app/src/main/java')],
    ),
  );
  await generator.generate();
}

Run the script to generate bindings:

dart run tool/jnigen.dart

See the examples and API documentation for more information.

FAQs

I am getting ClassNotFoundError at runtime.

JNIgen does not handle getting the classes into application. It has to be done by target-specific mechanism. Such as adding a Gradle dependency on Android, or manually providing classpath to Jni.spawn on desktop / standalone targets.

On Android, proguard prunes classes which it deems inaccessible. Since JNI class lookup happens in runtime, this leads to ClassNotFound errors in release mode even if the dependency is included in Gradle. in_app_java example discusses two mechanisms to prevent this: using Keep annotation (androidx.annotation.Keep) for the code written in the application itself, and proguard-rules file for external libraries.

Lastly, some libraries such as java.awt do not exist in Android. Attempting to use libraries which depend on them can also lead to ClassNotFound errors.

JNIgen is not finding classes.

Ensure you are providing the correct source and class paths, and they follow the standard directory structure. If your class name is com.abc.MyClass, MyClass must be in com/abc/MyClass.java relative to one of the source paths, or com/abc/MyClass.class relative to one of the class paths specified in the configuration.

If the classes are in JAR file, make sure to provide the path to the JAR file itself, and not to the containing directory.

JNIgen is unable to parse sources.

If the errors are similar to symbol not found, ensure all dependencies of the source are available. If such dependency is compiled, it can be included in class_path.

Generate bindings for built-in types

For your convenience, a number of built-in Java types, for example, many of those in java.lang and java.util, are provided to your build by jni by default.

Those don't need to be included in the classes block, and will error if you do so, with the following error: Fatal: Trying to re-import the generated classes.

For any other types that are in core Java, you can add them in your classes block and bindings will be generated when you run the generate bindings task.

Below is an example showing how you might generate bindings for several classes in java.time.* and a java.lang class that is not included by default:

final generator = JniGenerator(
  input: Input(
    classes: [
      'java.time.Instant',
      'java.time.ZoneOffset',
      'java.time.ZonedDateTime',
      'java.lang.Math',
      // 'java.lang.Integer', // Will error, already included in binary
    ],
  ),
  output: Output(
    dart: DartOutput(
      path: packageRoot.resolve('lib/gen/'),
    ),
  ),
);
await generator.generate();

How are classes mapped into bindings?

Each Java class generates a subclass of JObject class, which wraps a jobject reference in JNI. Nested classes use _ as separator, Example.NestedClass will be mapped to Example_NestedClass.

Does JObject hold a local or global reference? Does it need to be manually released?

Each Java object returned into Dart creates a JNI global reference. Reference deletion is taken care of by NativeFinalizer and that's usually sufficient.

It's a good practice to keep the interface between languages sparse. However, if there's a need to create several references (e.g. in a loop), you can use the FFI Arena mechanism (using function) and releasedBy method, or manually release the object using release method.

Android core libraries

These days, Android projects depend heavily on AndroidX and other libraries downloaded via Gradle. We have a tracking issue to improve detection of Android SDK and dependencies (#793). Currently, we can fetch the JAR dependencies of an Android project by running a Gradle stub, if AndroidSdk(addGradleDeps: true) is specified. However, core libraries (the android.** namespace) are not downloaded through Gradle. The core libraries are shipped as stub JARs with the Android SDK. ($SDK_ROOT/platforms/android-$VERSION/android-stubs-src.jar). Currently, we don't have an automatic mechanism for using these. You can unpack this JAR manually into some directory and provide it as a source path.

Having said that, there are two caveats to this caveat:

  • SDK stubs after version 28 are incomplete. OpenJDK Doclet API we use to generate API summaries will error on incomplete sources.
  • The API can't process the java.** namespaces in the Android SDK stubs, because it expects a module layout. So if you want to generate bindings for, say, java.lang.Math, you cannot use the Android SDK stubs. OpenJDK sources can be used instead. See Generate bindings for built-in types above for instructions on how to use OpenJDK sources.

The JAR files ($SDK_ROOT/platforms/android-$VERSION/android.jar) can be used instead. But compiled JARs do not include JavaDoc and method parameter names. This JAR is automatically included by Gradle when AndroidSdk(addGradleDeps: true) is specified.

Contributing

See CONTRIBUTING.md in the root of the repository for information on how to contribute.

Libraries

jnigen
This library exports a high level programmatic API to JNIgen, the entry point of which is the JniGenGenerator.generate method on a JniGenerator object.
tools
Tooling utilities for Android SDK resolution, Gradle dependencies, and summarizer building.