Skip to content

Commit e46b07f

Browse files
[jni] Store and display stack trace of release point for debugging (#2851)
- Added `Jni.captureStackTraceOnRelease` which defaults to `false`. When this is set, the stack traces of the release points will be stored for `JObject`s to help debug `DoubleReleaseError` and `UseAfterReleaseError`s. This includes the points where `JObject`s have been registered to be released by an `arena` via `JObject.releaseBy`. - Changed the behavior of `JObject.releasedBy`. It now does not throw a `DoubleReleaseError` if the object was manually released before the end of arena.
1 parent 6aaf398 commit e46b07f

File tree

36 files changed

+2065
-3218
lines changed

36 files changed

+2065
-3218
lines changed

pkgs/jni/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 0.15.3-wip
2+
3+
- Added `Jni.captureStackTraceOnRelease` which defaults to `false`. When this is
4+
set, the stack traces of the release points will be stored for `JObject`s to
5+
help debug `DoubleReleaseError` and `UseAfterReleaseError`s. This includes the
6+
points where `JObject`s have been registered to be released by an `arena` via
7+
`JObject.releaseBy`.
8+
- Changed the behavior of `JObject.releasedBy`. It now does not throw a
9+
`DoubleReleaseError` if the object was manually released before the end of
10+
arena.
11+
112
## 0.15.2
213

314
- Do not fail `flutter build` if JDK is not found for desktop.

pkgs/jni/ffigen.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ enums:
2626
'JniType': 'JniCallType'
2727
'jobjectRefType': 'JObjectRefType'
2828
functions:
29+
rename:
30+
'FindClass': 'JniFindClass'
31+
'GetJavaVM': 'JniGetJavaVM'
2932
exclude:
3033
# Exclude init functions supposed to be defined in loaded DLL, not JNI
3134
- 'JNI_.*'

pkgs/jni/lib/src/errors.dart

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,41 @@ import 'third_party/generated_bindings.dart';
1111
// TODO(#567): Add the fact that [JException] is now a [JObject] to the
1212
// CHANGELOG.
1313

14-
final class UseAfterReleaseError extends StateError {
15-
UseAfterReleaseError() : super('Use after release error');
14+
mixin _ExplainsRelease on StateError {
15+
String? get releaseStackTrace;
16+
17+
@override
18+
String toString() {
19+
final sb = StringBuffer(super.toString());
20+
if (releaseStackTrace != null) {
21+
sb.write('\n');
22+
sb.write(releaseStackTrace);
23+
} else {
24+
sb.writeln('\nTo see where the object was released, '
25+
'set `Jni.captureStackTraceOnRelease = true`.');
26+
}
27+
return sb.toString();
28+
}
29+
}
30+
31+
final class UseAfterReleaseError extends StateError with _ExplainsRelease {
32+
@override
33+
final String? releaseStackTrace;
34+
35+
UseAfterReleaseError([this.releaseStackTrace])
36+
: super('Use after release error');
1637
}
1738

1839
// TODO(#567): Use NullPointerError once it's available.
1940
final class JNullError extends StateError {
2041
JNullError() : super('The reference was null');
2142
}
2243

23-
final class DoubleReleaseError extends StateError {
24-
DoubleReleaseError() : super('Double release error');
44+
final class DoubleReleaseError extends StateError with _ExplainsRelease {
45+
@override
46+
final String? releaseStackTrace;
47+
48+
DoubleReleaseError([this.releaseStackTrace]) : super('Double release error');
2549
}
2650

2751
/// Represents JNI errors that might be returned by methods like `CreateJavaVM`.

pkgs/jni/lib/src/jni.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ abstract final class Jni {
7070
}
7171
}
7272

73+
/// Whether to capture the stack trace when an object is released.
74+
///
75+
/// This is useful for debugging [DoubleReleaseError] and
76+
/// [UseAfterReleaseError].
77+
///
78+
/// Defaults to `false`.
79+
static bool get captureStackTraceOnRelease =>
80+
_bindings.getCaptureStackTraceOnRelease() != 0;
81+
82+
static set captureStackTraceOnRelease(bool value) =>
83+
_bindings.setCaptureStackTraceOnRelease(value ? 1 : 0);
84+
7385
/// Spawn an instance of JVM using JNI. This method should be called at the
7486
/// beginning of the program with appropriate options, before other isolates
7587
/// are spawned.
@@ -170,15 +182,15 @@ abstract final class Jni {
170182

171183
/// Returns pointer to current JNI JavaVM instance
172184
Pointer<JavaVM> getJavaVM() {
173-
return _bindings.GetJavaVM();
185+
return _bindings.JniGetJavaVM();
174186
}
175187

176188
/// Finds the class from its [name].
177189
///
178190
/// Uses the correct class loader on Android.
179191
/// Prefer this over `Jni.env.FindClass`.
180192
static JClassPtr findClass(String name) {
181-
return using((arena) => _bindings.FindClass(name.toNativeChars(arena)))
193+
return using((arena) => _bindings.JniFindClass(name.toNativeChars(arena)))
182194
.checkedClassRef;
183195
}
184196

@@ -365,6 +377,14 @@ extension InternalJniExtension on Jni {
365377
return Jni._bindings.newBooleanFinalizableHandle(object, reference);
366378
}
367379

380+
static Dart_FinalizableHandle newStackTraceFinalizableHandle(
381+
Object object,
382+
Pointer<Char> reference,
383+
) {
384+
ProtectedJniExtensions.ensureInitialized();
385+
return Jni._bindings.newStackTraceFinalizableHandle(object, reference);
386+
}
387+
368388
static void deleteFinalizableHandle(
369389
Dart_FinalizableHandle finalizableHandle, Object object) {
370390
ProtectedJniExtensions.ensureInitialized();

pkgs/jni/lib/src/jobject.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,19 @@ class JObject {
210210
bool get isReleased => reference.isReleased;
211211

212212
/// Registers this object to be released at the end of [arena]'s lifetime.
213-
void releasedBy(Arena arena) => arena.onReleaseAll(release);
213+
void releasedBy(Arena arena) {
214+
assert(() {
215+
if (Jni.captureStackTraceOnRelease && reference is JGlobalReference) {
216+
(reference as JGlobalReference).registeredInArena();
217+
}
218+
return true;
219+
}());
220+
arena.onReleaseAll(() {
221+
if (!isReleased) {
222+
release();
223+
}
224+
});
225+
}
214226
}
215227

216228
extension JObjectUseExtension<T extends JObject?> on T {

pkgs/jni/lib/src/jreference.dart

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,33 +83,80 @@ final class JGlobalReference extends JReference {
8383
/// The finalizable handle that deletes [_JFinalizable.pointer].
8484
final Dart_FinalizableHandle _jobjectFinalizableHandle;
8585
final Pointer<Bool> _isReleased;
86+
final Pointer<Pointer<Char>> _releasedStackTracePointer;
8687

87-
JGlobalReference._(
88-
super.finalizable, this._jobjectFinalizableHandle, this._isReleased)
88+
JGlobalReference._(super.finalizable, this._jobjectFinalizableHandle,
89+
this._isReleased, this._releasedStackTracePointer)
8990
: super._();
9091

9192
factory JGlobalReference(Pointer<Void> pointer) {
9293
final finalizable = _JFinalizable(pointer);
9394
final isReleased = calloc<Bool>();
95+
var releasedStackTracePointer = nullptr.cast<Pointer<Char>>();
9496
final jobjectFinalizableHandle =
9597
InternalJniExtension.newJObjectFinalizableHandle(
9698
finalizable, finalizable.pointer, JObjectRefType.JNIGlobalRefType);
9799
InternalJniExtension.newBooleanFinalizableHandle(finalizable, isReleased);
98-
return JGlobalReference._(
99-
finalizable, jobjectFinalizableHandle, isReleased);
100+
assert(() {
101+
releasedStackTracePointer = calloc<Pointer<Char>>();
102+
InternalJniExtension.newStackTraceFinalizableHandle(
103+
finalizable, releasedStackTracePointer.cast());
104+
return true;
105+
}());
106+
return JGlobalReference._(finalizable, jobjectFinalizableHandle, isReleased,
107+
releasedStackTracePointer);
100108
}
101109

102110
@override
103111
bool get isNull => pointer == nullptr;
104112

113+
@override
114+
JObjectPtr get pointer {
115+
if (isReleased) {
116+
throw UseAfterReleaseError(_releasedStackTrace);
117+
}
118+
return _finalizable.pointer;
119+
}
120+
121+
void _appendToStackTrace(String stackTrace) {
122+
final previousStackTrace = _releasedStackTrace ?? '';
123+
final nativeStr = '$previousStackTrace$stackTrace'.toNativeUtf8();
124+
if (_releasedStackTracePointer != nullptr) {
125+
malloc.free(_releasedStackTracePointer.value);
126+
}
127+
_releasedStackTracePointer.value = nativeStr.cast();
128+
}
129+
105130
@override
106131
void _setAsReleased() {
107132
if (isReleased) {
108-
throw DoubleReleaseError();
133+
throw DoubleReleaseError(_releasedStackTrace);
109134
}
110135
_isReleased.value = true;
111136
InternalJniExtension.deleteFinalizableHandle(
112137
_jobjectFinalizableHandle, _finalizable);
138+
assert(() {
139+
if (Jni.captureStackTraceOnRelease) {
140+
_appendToStackTrace('Object was released at:\n${StackTrace.current}\n');
141+
}
142+
return true;
143+
}());
144+
}
145+
146+
@internal
147+
void registeredInArena() {
148+
if (Jni.captureStackTraceOnRelease) {
149+
_appendToStackTrace(
150+
'Object was registered to be released by an arena at:\n'
151+
'${StackTrace.current}\n');
152+
}
153+
}
154+
155+
String? get _releasedStackTrace {
156+
if (_releasedStackTracePointer.value != nullptr) {
157+
return _releasedStackTracePointer.value.cast<Utf8>().toDartString();
158+
}
159+
return null;
113160
}
114161

115162
@override

0 commit comments

Comments
 (0)