Skip to content

Enable sharing JObjects across isolates #1060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions pkgs/jni/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
`fill` object and not its Java runtime type.
- `JObject`s now check the types using `instanceof` in debug mode when using
`castTo`.
- **Breaking Change**: `Jni.initDLApi()` is removed.
- Added the ability to share `JObject`s across isolates.
```dart
// This now works.
final foo = 'foo'.toJString();
Isolate.run(() {
// `foo` is usable from another isolate.
print(foo);
});
```

## 0.7.3

Expand Down
8 changes: 6 additions & 2 deletions pkgs/jni/example/integration_test/on_device_jni_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import '../../test/jarray_test.dart' as jarray_test;
import '../../test/boxed_test.dart' as boxed_test;
import '../../test/type_test.dart' as type_test;
import '../../test/load_test.dart' as load_test;
import '../../test/isolate_test.dart' as isolate_test;

void integrationTestRunner(String description, void Function() testCallback) {
testWidgets(description, (widgetTester) async => testCallback());
void integrationTestRunner(String description, void Function() testCallback,
{Object? skip}) {
testWidgets(description, (_) async => testCallback(),
skip: skip != null && skip != false);
}

void main() {
Expand All @@ -33,6 +36,7 @@ void main() {
boxed_test.run,
type_test.run,
load_test.run,
isolate_test.run,
];
for (var testSuite in testSuites) {
testSuite(testRunner: integrationTestRunner);
Expand Down
2 changes: 2 additions & 0 deletions pkgs/jni/ffigen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ output: 'lib/src/third_party/jni_bindings_generated.dart'
headers:
entry-points:
- 'src/dartjni.h' # Exports majority of JNI functions
- 'src/internal.h'
- 'src/third_party/global_jni_env.h' # Exports GlobalJniEnv type
- 'src/jni_constants.h'
include-directives:
- 'src/dartjni.h'
- 'src/internal.h'
- 'src/third_party/global_jni_env.h'
- 'third_party/jni.h' # jni.h from Android NDK
- 'src/jni_constants.h'
Expand Down
19 changes: 6 additions & 13 deletions pkgs/jni/lib/src/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,17 @@ import 'package:jni/src/third_party/generated_bindings.dart';
// TODO(#567): Add the fact that [JException] is now a [JObject] to the
// CHANGELOG.

final class UseAfterReleaseError extends Error {
@override
String toString() {
return 'Use after release error';
}
final class UseAfterReleaseError extends StateError {
UseAfterReleaseError() : super('Use after release error');
}

// TODO(#567): Use NullPointerError once it's available.
final class JNullError extends Error {
@override
String toString() => 'The reference was null';
final class JNullError extends StateError {
JNullError() : super('The reference was null');
}

final class DoubleReleaseError extends Error {
@override
String toString() {
return 'Double release error';
}
final class DoubleReleaseError extends StateError {
DoubleReleaseError() : super('Double release error');
}

/// Represents JNI errors that might be returned by methods like
Expand Down
45 changes: 39 additions & 6 deletions pkgs/jni/lib/src/jni.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,22 @@ abstract final class Jni {
/// On flutter it's done by library. On dart standalone we don't
/// know the library path.)
static void setDylibDir({required String dylibDir}) {
_dylibDir = dylibDir;
if (!Platform.isAndroid) {
_dylibDir = dylibDir;
}
}

static bool _initialized = false;

/// Initializes DartApiDL used for Continuations and interface implementation.
static void initDLApi() {
assert(NativeApi.majorVersion == 2);
assert(NativeApi.minorVersion >= 3);
final result = _bindings.InitDartApiDL(NativeApi.initializeApiDLData);
assert(result == 0);
static void _ensureInitialized() {
if (!_initialized) {
assert(NativeApi.majorVersion == 2);
assert(NativeApi.minorVersion >= 3);
final result = _bindings.InitDartApiDL(NativeApi.initializeApiDLData);
_initialized = result == 0;
assert(_initialized);
}
}

/// Spawn an instance of JVM using JNI. This method should be called at the
Expand Down Expand Up @@ -238,6 +245,7 @@ extension ProtectedJniExtensions on Jni {

/// Returns a new PortContinuation.
static JReference newPortContinuation(ReceivePort port) {
Jni._ensureInitialized();
return JGlobalReference(
Jni._bindings
.PortContinuation__ctor(port.sendPort.nativePort)
Expand All @@ -253,6 +261,7 @@ extension ProtectedJniExtensions on Jni {
NativeFunction<
Pointer<Void> Function(Uint64, Pointer<Void>, Pointer<Void>)>>
functionPtr) {
Jni._ensureInitialized();
return JGlobalReference(Jni._bindings
.PortProxy__newInstance(
Jni.env.toJStringPtr(binaryName),
Expand All @@ -267,6 +276,30 @@ extension ProtectedJniExtensions on Jni {
Pointer<CallbackResult> result, JObjectPtr object) async {
Jni._bindings.resultFor(result, object);
}

static Dart_FinalizableHandle newJObjectFinalizableHandle(
Object object,
Pointer<Void> reference,
int refType,
) {
Jni._ensureInitialized();
return Jni._bindings
.newJObjectFinalizableHandle(object, reference, refType);
}

static Dart_FinalizableHandle newBooleanFinalizableHandle(
Object object,
Pointer<Bool> reference,
) {
Jni._ensureInitialized();
return Jni._bindings.newBooleanFinalizableHandle(object, reference);
}

static void deleteFinalizableHandle(
Dart_FinalizableHandle finalizableHandle, Object object) {
Jni._ensureInitialized();
Jni._bindings.deleteFinalizableHandle(finalizableHandle, object);
}
}

extension AdditionalEnvMethods on GlobalJniEnv {
Expand Down
7 changes: 6 additions & 1 deletion pkgs/jni/lib/src/jobject.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:jni/internal_helpers_for_jnigen.dart';

import 'accessors.dart';
import 'jni.dart';
import 'jreference.dart';
import 'lang/jstring.dart';
import 'types.dart';

Expand Down Expand Up @@ -66,6 +67,10 @@ class JObject {

bool get isNull => reference.isNull;

/// Releases the underlying [reference].
///
/// Releasing in one isolate while using or releasing in another isolate might
/// crash in the JNI layer.
void release() {
reference.release();
}
Expand Down
87 changes: 63 additions & 24 deletions pkgs/jni/lib/src/jreference.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@

import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:jni/src/third_party/generated_bindings.dart';

import 'errors.dart';
import 'jni.dart';

extension ProtectedJReference on JReference {
void setAsReleased() {
if (_released) {
throw DoubleReleaseError();
}
_released = true;
JGlobalReference._finalizer.detach(this);
_setAsReleased();
}

void ensureNotNull() {
Expand All @@ -28,16 +25,24 @@ extension ProtectedJReference on JReference {
///
/// Detaches the finalizer so the underlying pointer will not be deleted.
JObjectPtr toPointer() {
setAsReleased();
return _pointer;
_setAsReleased();
return _finalizable.pointer;
}
}

/// A thin wrapper around a pointer that makes it [Finalizable].
@pragma('vm:deeply-immutable')
final class _JFinalizable implements Finalizable {
final Pointer<Void> pointer;

_JFinalizable(this.pointer);
}

@pragma('vm:deeply-immutable')
abstract final class JReference {
final JObjectPtr _pointer;
bool _released = false;
final _JFinalizable _finalizable;

JReference(this._pointer);
JReference(this._finalizable);

/// The underlying JNI reference.
///
Expand All @@ -46,61 +51,95 @@ abstract final class JReference {
/// Be careful when storing this in a variable since it might have gotten
/// released upon use.
JObjectPtr get pointer {
if (_released) throw UseAfterReleaseError();
return _pointer;
if (isReleased) throw UseAfterReleaseError();
return _finalizable.pointer;
}

/// Whether the underlying JNI reference is deleted or not.
bool get isReleased => _released;
bool get isReleased;

/// Whether the underlying JNI reference is `null` or not.
bool get isNull;

/// Deletes the underlying JNI reference and marks this as released.
///
/// Releasing in one isolate while using or releasing in another isolate might
/// crash in the JNI layer.
///
/// Throws [DoubleReleaseError] if this is already released.
///
/// Further uses of this object will throw [UseAfterReleaseError].
void release() {
setAsReleased();
_setAsReleased();
_deleteReference();
}

void _deleteReference();

void _setAsReleased();
}

/// A managed JNI global reference.
///
/// Uses a [NativeFinalizer] to delete the JNI global reference when finalized.
final class JGlobalReference extends JReference implements Finalizable {
static final _finalizer =
NativeFinalizer(Jni.env.ptr.ref.DeleteGlobalRef.cast());

JGlobalReference(super._reference) {
_finalizer.attach(this, _pointer, detach: this);
@pragma('vm:deeply-immutable')
final class JGlobalReference extends JReference {
/// The finalizable handle that deletes [_JFinalizable.pointer].
final Dart_FinalizableHandle _jobjectFinalizableHandle;
final Pointer<Bool> _isReleased;

JGlobalReference._(
super._finalizable, this._jobjectFinalizableHandle, this._isReleased);

factory JGlobalReference(Pointer<Void> pointer) {
final finalizable = _JFinalizable(pointer);
final isReleased = calloc<Bool>();
final jobjectFinalizableHandle =
ProtectedJniExtensions.newJObjectFinalizableHandle(
finalizable, finalizable.pointer, JObjectRefType.JNIGlobalRefType);
ProtectedJniExtensions.newBooleanFinalizableHandle(finalizable, isReleased);
return JGlobalReference._(
finalizable, jobjectFinalizableHandle, isReleased);
}

@override
bool get isNull => pointer == nullptr;

@override
void _setAsReleased() {
if (isReleased) {
throw DoubleReleaseError();
}
_isReleased.value = true;
ProtectedJniExtensions.deleteFinalizableHandle(
_jobjectFinalizableHandle, _finalizable);
}

@override
void _deleteReference() {
Jni.env.DeleteGlobalRef(_pointer);
Jni.env.DeleteGlobalRef(_finalizable.pointer);
}

@override
bool get isReleased => _isReleased.value;
}

final jNullReference = _JNullReference();
final JReference jNullReference = _JNullReference();

@pragma('vm:deeply-immutable')
final class _JNullReference extends JReference {
_JNullReference() : super(nullptr);
_JNullReference() : super(_JFinalizable(nullptr));

@override
bool get isReleased => false;

@override
void _deleteReference() {
// No need to delete `null`.
}

@override
void release() {
void _setAsReleased() {
// No need to release `null`.
}

Expand Down
Loading