Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Initial JNI support #11

Merged
merged 24 commits into from
Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7551ff3
Add JNI support
mahesh-hegde Jul 7, 2022
8bbeda4
Some general cleanup:
mahesh-hegde Jul 8, 2022
c0b1a49
add integration tests, remove macos target in CI
mahesh-hegde Jul 8, 2022
686ae25
specify package versions in pubspec
mahesh-hegde Jul 8, 2022
852691f
README
mahesh-hegde Jul 8, 2022
2a1e7e0
try to run test coverage in both packages
mahesh-hegde Jul 8, 2022
f09ddda
workflow: fix coverage file path
mahesh-hegde Jul 8, 2022
ab793ae
add coveralls finish step
mahesh-hegde Jul 8, 2022
01aef56
Fix typo in coveralls flag
mahesh-hegde Jul 8, 2022
e4e3fd0
Windows support
mahesh-hegde Jul 10, 2022
2fa0c92
temporarily skip testing on windows
mahesh-hegde Jul 10, 2022
7e443a2
Implement suggestions in PR Review 1
mahesh-hegde Jul 11, 2022
9b85ac5
check use-after-free and double-free
mahesh-hegde Jul 12, 2022
f820996
Some suggestions from PR Review 1
mahesh-hegde Jul 12, 2022
dd7dfef
bin/setup.dart
mahesh-hegde Jul 14, 2022
ea680d5
Most changes suggested in Review 1
mahesh-hegde Jul 14, 2022
5b0ed0c
update workflow: try bin/setup.dart
mahesh-hegde Jul 14, 2022
f92678a
update workflow: try bin/setup.dart (attempt 2)
mahesh-hegde Jul 14, 2022
a72f63b
Add issue references to TODOs
mahesh-hegde Jul 14, 2022
87f60e2
Fix C warning on desktop targets
mahesh-hegde Jul 14, 2022
87f0f41
Some minor changes
mahesh-hegde Jul 17, 2022
849901e
add coverage:ignore-file and exclude some ffigen variables
mahesh-hegde Jul 17, 2022
36fee46
use package_config to find sources during setup
mahesh-hegde Jul 17, 2022
6a61e92
coverage ignore - fix possible typo
mahesh-hegde Jul 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 74 additions & 13 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,61 @@ jobs:
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: jni_gen_tests
parallel: true
path-to-lcov: ./jni_gen/coverage/lcov.info

build_jni_example_linux:
## TODO: More minimal test on windows after fixing dev dependency.
## i.e do not rerun analyze and format steps, and do not require flutter.

test_jni:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./jni/example
working-directory: ./jni
steps:
- uses: actions/checkout@v3
## Requires flutter to analyze example.
## Using dart alone doesn't work.
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
- run: flutter config --enable-linux-desktop
- run: |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this kind of logic into build/setup.dart that way we can also use it locally instead of only on the CI. (Just invoke the dart script here.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed 18 comments because github folded them.

This install and configure step is specific to linux on CI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      - run: |
          cd ../src/
          mkdir build
          cd build
          cmake .. && cmake --build .

I see this twice in the file (both Linux).

cd src/
mkdir build
cd build
cmake .. && cmake --build .
cd ../..
- run: flutter pub get
- run: flutter build linux
- name: Check formatting
run: flutter format --output=none --set-exit-if-changed .
- name: Run lints
run: flutter analyze --fatal-infos
- name: Get dependencies
run: dart pub get
- name: Run tests
run: dart test
- name: Install coverage
run: dart pub global activate coverage
- name: Collect coverage
run: dart pub global run coverage:test_with_coverage
- name: Upload coverage
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: jni_tests
parallel: true
path-to-lcov: ./jni/coverage/lcov.info

build_jni_example_windows:
runs-on: windows-latest
build_jni_example_linux:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./jni/example
Expand All @@ -101,12 +135,26 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- run: flutter config --enable-windows-desktop
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
- run: |
cd ../src/
mkdir build
cd build
cmake .. && cmake --build .
cd ../../example
- run: flutter config --enable-linux-desktop
- run: flutter pub get
- run: flutter build windows
- run: flutter test
- run: flutter build linux

build_jni_example_macos:
runs-on: macos-latest
build_jni_example_windows:
runs-on: windows-latest
defaults:
run:
working-directory: ./jni/example
Expand All @@ -115,10 +163,13 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
architecture: x64
- run: flutter config --enable-macos-desktop
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
- run: flutter config --enable-windows-desktop
- run: flutter pub get
- run: flutter build macos
- run: flutter build windows

build_jni_example_android:
runs-on: ubuntu-latest
Expand All @@ -138,3 +189,13 @@ jobs:
- run: flutter build apk
- run: flutter build appbundle

coveralls_finish:
needs: [test_jni_gen, test_jni]
runs-on: ubuntu-latest
steps:
- name: Coveralls finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


94 changes: 20 additions & 74 deletions jni/README.md
Original file line number Diff line number Diff line change
@@ -1,92 +1,38 @@
# jni
# jni (experimental module)
mahesh-hegde marked this conversation as resolved.
Show resolved Hide resolved

A new Flutter FFI plugin project.
This is a utility library to access JNI from Dart / Flutter code, intended as a supplement for `jnigen` code generator, as well as provide the common base components (such as managing the JVM instance) to the code generated by `jni_gen`.

## Getting Started
This library contains:

This project is a starting point for a Flutter
[FFI plugin](https://docs.flutter.dev/development/platform-integration/c-interop),
a specialized package that includes native code directly invoked with Dart FFI.
* functions to access the JNIEnv and JavaVM variables from JNI, and wrapper functions to those provided by JNI. (`Jni.getEnv`, `Jni.getJavaVM`).

## Project stucture
* Functions to spawn a JVM on desktop platforms (`Jni.spawn`).

This template uses the following structure:
* Some utility functions to make it easier to work with JNI in Dart; eg: To convert a java string object to Dart string (mostly as extension methods on `Pointer<JniEnv>`).

* `src`: Contains the native source code, and a CmakeFile.txt file for building
that source code into a dynamic library.
* Some Android-specific helpers (get application context and current activity references).

* `lib`: Contains the Dart code that defines the API of the plugin, and which
calls into the native code using `dart:ffi`.
* Some helper classes and functions to simplify one-off uses (`JniObject` and `JniClass` intended for calling functions by specifying the name and arguments. It will reduce some boilerplate when you're debugging. Note: this API is slightly incomplete).

* platform folders (`android`, `ios`, `windows`, etc.): Contains the build files
for building and bundling the native code library with the platform application.
This is intended for one-off / debugging uses of JNI, as well as providing a base library for code generated by jni_gen.

## Buidling and bundling native code
__To interface a complete java library, look forward for `jni_gen`.__

The `pubspec.yaml` specifies FFI plugins as follows:
## Platform support
The focus of this project is Android, since it already has a JVM and JNI allows interop with existing code and Platform APIs. Experimental desktop support exists for Linux using the JVM invocation API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The focus of this project is Flutter Android, since Flutter Android apps already have a JVM, and JNI enables interop with existing Java code and Android Platform APIs. This project also (partially) supports Linux desktop by spawning a JVM through JNI.


```yaml
plugin:
platforms:
some_platform:
ffiPlugin: true
```
## Version note
This library is at an early stage and the public API may rapidly change based on requirements of `jni_gen`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library is at an early stage of development and we do not provide backwards compatibility of the API at this point.


This configuration invokes the native build for the various target platforms
and bundles the binaries in Flutter applications using these FFI plugins.
## Documentation

This can be combined with dartPluginClass, such as when FFI is used for the
implementation of one platform in a federated plugin:
There are well-commented test files under `test/` directory and a flutter based example under `example/`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"There are" and "exists" are usually signs a sentence can be rewritten. (Inverting the sentence usually helps.)

The test/ directory contains files with comments explaining <...>, and the example/ directory contains a flutter example.


```yaml
plugin:
implements: some_other_plugin
platforms:
some_platform:
dartPluginClass: SomeClass
ffiPlugin: true
```
Using this library assumes some familiarity with JNI - it's threading model and object references, among other things.

A plugin can have both FFI and method channels:
## jni_gen

```yaml
plugin:
platforms:
some_platform:
pluginClass: SomeName
ffiPlugin: true
```
This library is a part of `jni_gen` - a 2022 GSoC project.

The native build systems that are invoked by FFI (and method channel) plugins are:

* For Android: Gradle, which invokes the Android NDK for native builds.
* See the documentation in android/build.gradle.
* For iOS and MacOS: Xcode, via CocoaPods.
* See the documentation in ios/jni.podspec.
* See the documentation in macos/jni.podspec.
* For Linux and Windows: CMake.
* See the documentation in linux/CMakeLists.txt.
* See the documentation in windows/CMakeLists.txt.

## Binding to native code

To use the native code, bindings in Dart are needed.
To avoid writing these by hand, they are generated from the header file
(`src/jni.h`) by `package:ffigen`.
Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`.

## Invoking native code

Very short-running native functions can be directly invoked from any isolate.
For example, see `sum` in `lib/jni.dart`.

Longer-running functions should be invoked on a helper isolate to avoid
dropping frames in Flutter applications.
For example, see `sumAsync` in `lib/jni.dart`.

## Flutter help

For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
The broader aim of jni_gen is making Java APIs accessible from dart in an idiomatic way. The current focus is on the Android platform.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current focus is on the Android platform.

This was already mentioned earlier in the readme.


61 changes: 61 additions & 0 deletions jni/android/src/main/java/dev/dart/jni/JniPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dev.dart.jni;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import android.util.Log;
import android.app.Activity;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;

import android.content.Context;

@Keep
public class JniPlugin implements FlutterPlugin, ActivityAware {

@Override
public void
onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
setup(binding.getApplicationContext());
}

public static void registerWith(Registrar registrar) {
JniPlugin plugin = new JniPlugin();
plugin.setup(registrar.activeContext());
}

private void setup(Context context) {
initializeJni(context, getClass().getClassLoader());
}

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}

// Activity handling methods
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
Activity activity = binding.getActivity();
setJniActivity(activity, activity.getApplicationContext());
}

@Override
public void onDetachedFromActivityForConfigChanges() {}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
Activity activity = binding.getActivity();
setJniActivity(activity, activity.getApplicationContext());
}

@Override
public void onDetachedFromActivity() {}

native void initializeJni(Context context, ClassLoader classLoader);
native void setJniActivity(Activity activity, Context context);

static {
System.loadLibrary("dartjni");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.dart.jni_example;

import android.app.Activity;
import android.os.Handler;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.Keep;

@Keep
class AnyToast {
static AnyToast makeText(Activity mainActivity, Context context, CharSequence text, int duration) {
AnyToast toast = new AnyToast();
toast.mainActivity = mainActivity;
toast.context = context;
toast.text = text;
toast.duration = duration;
return toast;
}

void show() {
mainActivity.runOnUiThread(() -> Toast.makeText(context, text, duration).show());
}

Activity mainActivity;
Context context;
CharSequence text;
int duration;
}
Loading