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

Commit

Permalink
Interface implementation (#326)
Browse files Browse the repository at this point in the history
* attach finalizable to close the port for interfaces 
* add missing JCharacter boxed type
* add .implement method completed for interfaces
* fix descriptor issues
* temporarily do not check the ffigen bindings until ffigen#555 is solved
* remove duplicated java source in jni + sort ffigen.yaml inline functions
* add the interface implementation under an experiment flag + docs
  • Loading branch information
HosseinYousefi authored Jul 25, 2023
1 parent c7fbb15 commit c951f23
Show file tree
Hide file tree
Showing 81 changed files with 2,774 additions and 340 deletions.
14 changes: 9 additions & 5 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,15 @@ jobs:
flag-name: jni_tests
parallel: true
path-to-lcov: ./jni/coverage/lcov.info
- name: regenerate & compare ffigen bindings
## Use git to verify no source files have changed
run: |
dart run tool/generate_ffi_bindings.dart
git diff --exit-code -- lib/src/third_party src/third_party
# TODO(https://github.com/dart-lang/ffigen/issues/555): Ffigen generated
# on my machine has macOS specific stuff and CI does not.
# We should just generate the struct as opaque, but we currently can't.
#
# - name: regenerate & compare ffigen bindings
# ## Use git to verify no source files have changed
# run: |
# dart run tool/generate_ffi_bindings.dart
# git diff --exit-code -- lib/src/third_party src/third_party

## Run tests for package:jni on windows, just to confirm it works.
## Do not, for example, collect coverage or check formatting.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ A `*` denotes required configuration.
| `source_path` | List of directory paths | Directories to search for source files. Note: source_path for dependencies downloaded using `maven_downloads` configuration is added automatically without the need to specify here. |
| `class_path` | List of directory / JAR paths | Classpath for API summary generation. This should include any JAR dependencies of the source files in `source_path`. |
| `classes` * | List of qualified class / package names | List of qualified class / package names. `source_path` will be scanned assuming the sources follow standard java-ish hierarchy. That is a.b.c either maps to a directory `a/b/c` or a class file `a/b/c.java`. |
| `enable_experiment` | List of experiment names:<br><ul><li>`interface_implementation`</li></ul> | List of enabled experiments. These features are still in development and their API might break. |
| `output:` | (Subsection) | This subsection will contain configuration related to output files. |
| `output:` >> `bindings_type` | `c_based` (default) or `dart_only` | Binding generation strategy. [Trade-offs](#pure-dart-bindings) are explained at the end of this document. |
| `output:` >> `c:` | (Subsection) | This subsection specified C output configuration. Required if `bindings_type` is `c_based`. |
Expand Down
4 changes: 4 additions & 0 deletions jni/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.0-wip-2
* Added `PortProxy` and related methods used for interface implementation.
* Added the missing binding for `java.lang.Character`.

## 0.5.0
* **Breaking Change** ([#137](https://github.com/dart-lang/jnigen/issues/137)): Java primitive types are now all lowercase like `jint`, `jshort`, ...
* The bindings for `java.util.Set`, `java.util.Map`, `java.util.List` and the numeric types like `java.lang.Integer`, `java.lang.Boolean`, ... are now included in `package:jni`.
Expand Down
20 changes: 20 additions & 0 deletions jni/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,31 @@ rootProject.allprojects {
apply plugin: 'com.android.library'

android {
// Keeping the classes from being removed by proguard.
defaultConfig {
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
}
}

// Condition for namespace compatibility in AGP 8
if (project.android.hasProperty("namespace")) {
namespace 'com.github.dart_lang.jni'
}

// Adding [PortContinuation] and [PortProxy] classes shared between Flutter and
// Dart-standalone versions of package:jni.
sourceSets {
main {
java {
srcDirs '../java/src/main/java'
}
}
}

// Bumping the plugin compileSdkVersion requires all clients of this plugin
// to bump the version in their app.
compileSdkVersion 31
Expand Down
1 change: 1 addition & 0 deletions jni/android/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-keep class com.github.dart_lang.jni.** { *; }
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@

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

@Keep
public class JniPlugin implements FlutterPlugin, ActivityAware {

@Override
Expand Down

This file was deleted.

45 changes: 31 additions & 14 deletions jni/bin/setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ void verboseLog(String msg) {
}
}

/// Find path to C sources in pub cache for package specified by [packageName].
/// Find path to C or Java 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 {
Future<String> findSources(String packageName, String subDirectory) async {
final packageConfig = await findPackageConfig(Directory.current);
if (packageConfig == null) {
throw UnsupportedError("Please run from project root.");
Expand All @@ -106,7 +106,7 @@ Future<String> findSources(String packageName) async {
if (package == null) {
throw UnsupportedError("Cannot find package: $packageName");
}
return package.root.resolve("src").toFilePath();
return package.root.resolve(subDirectory).toFilePath();
}

/// Return '/src' directories of all dependencies which has a CMakeLists.txt
Expand Down Expand Up @@ -170,7 +170,8 @@ void main(List<String> arguments) async {

final sources = options.sources;
for (var packageName in options.packages) {
sources.add(await findSources(packageName));
// It's assumed C FFI sources are in "src/" relative to package root.
sources.add(await findSources(packageName, 'src'));
}
if (sources.isEmpty) {
final dependencySources = await findDependencySources();
Expand All @@ -185,6 +186,28 @@ void main(List<String> arguments) async {
exitCode = 1;
return;
}

final currentDirUri = Uri.directory(".");
final buildPath = options.buildPath ??
currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
final buildDir = Directory(buildPath);
await buildDir.create(recursive: true);

final javaSrc = await findSources('jni', 'java');
final targetJar = File.fromUri(buildDir.uri.resolve('jni.jar'));
if (!needsBuild(targetJar, Directory.fromUri(Uri.directory(javaSrc)))) {
verboseLog('Last modified of ${targetJar.path}: '
'${targetJar.lastModifiedSync()}.');
stderr.writeln('Target newer than source, skipping build.');
} else {
verboseLog('Running mvn package for jni java sources to $buildPath.');
await runCommand(
'mvn',
['package', '-Dtarget=${buildDir.absolute.path}'],
await findSources('jni', 'java'),
);
}

for (var srcPath in sources) {
final srcDir = Directory(srcPath);
if (!srcDir.existsSync()) {
Expand All @@ -194,20 +217,14 @@ void main(List<String> arguments) async {
}

verboseLog("srcPath: $srcPath");

final currentDirUri = Uri.directory(".");
final buildPath = options.buildPath ??
currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
final buildDir = Directory(buildPath);
await buildDir.create(recursive: true);
verboseLog("buildPath: $buildPath");

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");
verboseLog("Last modified of ${targetFile.path}: "
"${targetFile.lastModifiedSync()}.");
stderr.writeln('Target newer than source, skipping build.');
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion jni/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.6.0-dev.1"
version: "0.6.0-dev.2"
js:
dependency: transitive
description:
Expand Down
28 changes: 20 additions & 8 deletions jni/ffigen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,33 @@ functions:
- 'setJniGetters'
- 'jni_log'
# Inline functions
# keep-sorted start
- 'acquire_lock'
- 'release_lock'
- 'init_lock'
- 'attach_thread'
- 'check_exception'
- 'destroy_cond'
- 'destroy_lock'
- 'init_cond'
- 'init_lock'
- 'load_class'
- 'load_class_global_ref'
- 'attach_thread'
- 'load_method'
- 'load_static_method'
- 'load_class_local_ref'
- 'load_class_platform'
- 'load_env'
- 'load_field'
- 'load_method'
- 'load_static_field'
- 'load_static_method'
- 'release_lock'
- 'signal_cond'
- 'thread_id'
- 'to_global_ref'
- 'check_exception'
- 'load_env'
# This is a native function in Java. No need to call it from Dart.
- 'to_global_ref_result'
- 'wait_for'
# keep-sorted end
# Native functions in Java. No need to call them from Dart.
- 'Java_com_github_dart_1lang_jni_PortContinuation__1resumeWith'
- 'Java_com_github_dart_1lang_jni_PortProxy__1invoke'
structs:
exclude:
- 'JniContext'
Expand All @@ -57,6 +68,7 @@ structs:
- '_JNIEnv'
- 'JNIInvokeInterface'
- '__va_list_tag'
- 'CallbackResult'
rename:
## opaque struct definitions, base types of jfieldID and jmethodID
'_jfieldID': 'jfieldID_'
Expand Down
1 change: 1 addition & 0 deletions jni/java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<build>
<finalName>jni</finalName>
<directory>${target}</directory>
</build>

<dependencies>
Expand Down
82 changes: 82 additions & 0 deletions jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

package com.github.dart_lang.jni;

import java.lang.reflect.*;

public class PortProxy implements InvocationHandler {
static {
System.loadLibrary("dartjni");
}

private final long port;
private final long threadId;
private final long functionPtr;

private PortProxy(long port, long threadId, long functionPtr) {
this.port = port;
this.threadId = threadId;
this.functionPtr = functionPtr;
}

private static String getDescriptor(Method method) {
StringBuilder descriptor = new StringBuilder();
descriptor.append(method.getName()).append("(");
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class<?> paramType : parameterTypes) {
appendType(descriptor, paramType);
}
descriptor.append(")");
appendType(descriptor, method.getReturnType());
return descriptor.toString();
}

private static void appendType(StringBuilder descriptor, Class<?> type) {
if (type == void.class) {
descriptor.append("V");
} else if (type == boolean.class) {
descriptor.append("Z");
} else if (type == byte.class) {
descriptor.append("B");
} else if (type == char.class) {
descriptor.append("C");
} else if (type == short.class) {
descriptor.append("S");
} else if (type == int.class) {
descriptor.append("I");
} else if (type == long.class) {
descriptor.append("J");
} else if (type == float.class) {
descriptor.append("F");
} else if (type == double.class) {
descriptor.append("D");
} else if (type.isArray()) {
descriptor.append('[');
appendType(descriptor, type.getComponentType());
} else {
descriptor.append("L").append(type.getName().replace('.', '/')).append(";");
}
}

public static Object newInstance(String binaryName, long port, long threadId, long functionPtr)
throws ClassNotFoundException {
Class clazz = Class.forName(binaryName);
return Proxy.newProxyInstance(
clazz.getClassLoader(), new Class[] {clazz}, new PortProxy(port, threadId, functionPtr));
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return _invoke(port, threadId, functionPtr, proxy, getDescriptor(method), args);
}

private native Object _invoke(
long port,
long threadId,
long functionPtr,
Object proxy,
String methodDescriptor,
Object[] args);
}
3 changes: 2 additions & 1 deletion jni/lib/internal_helpers_for_jnigen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/// not to be used directly.
library internal_helpers_for_jnigen;

export 'src/accessors.dart';
export 'src/jni.dart' show ProtectedJniExtensions;
export 'src/jreference.dart';
export 'src/accessors.dart';
export 'src/method_invocation.dart';
4 changes: 4 additions & 0 deletions jni/lib/jni_symbols.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ files:
name: JBoolean
type_class: JBooleanType
super_count: 1
'java.lang.Character':
name: JCharacter
type_class: JCharacterType
super_count: 1
'java.util.Set':
name: JSet
type_class: JSetType
Expand Down
Loading

0 comments on commit c951f23

Please sign in to comment.