Skip to content

Commit 19c5fb4

Browse files
Attach Dart finalizer to Store to close on Flutter hot restart.
Then ensure Store is kept alive until methods accessing its pointer have completed.
1 parent 3b819e8 commit 19c5fb4

File tree

5 files changed

+55
-12
lines changed

5 files changed

+55
-12
lines changed

objectbox/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## latest
22

3+
* Resolve "another store is still open" issue after Flutter hot restart (hot reload continues to work). #387
4+
* Add `Store.isClosed()`. #390
35
* Add note to `objectbox.g.dart` on how to re-generate (update) it.
46

57
## 1.4.0 (2022-02-22)

objectbox/lib/src/native/bindings/bindings.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ Object? _dartAPIInitException;
118118
/// Unfortunately, ffigen keeps those private.
119119
typedef _NativeClose = Int32 Function(Pointer<Void> ptr);
120120

121+
late final native_store_close =
122+
_lib!.lookup<NativeFunction<_NativeClose>>('obx_store_close');
121123
late final native_query_close =
122124
_lib!.lookup<NativeFunction<_NativeClose>>('obx_query_close');
123125
late final native_query_prop_close =

objectbox/lib/src/native/observable.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ extension ObservableStore on Store {
8585
observer.cObserver =
8686
C.dartc_observe_single_type(_ptr, entityId, observer.nativePort);
8787
});
88+
reachabilityFence(this);
8889

8990
return observer.stream;
9091
}
@@ -125,6 +126,7 @@ extension ObservableStore on Store {
125126
observer.init(() {
126127
observer.cObserver = C.dartc_observe(_ptr, observer.nativePort);
127128
}, broadcast: broadcast);
129+
reachabilityFence(this);
128130

129131
if (broadcast) {
130132
_onClose[observer] = observer.close;

objectbox/lib/src/native/store.dart

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ class Store {
3535
/// This meant for tests only; do not enable for releases!
3636
static bool debugLogs = false;
3737

38-
late final Pointer<OBX_store> _cStore;
38+
late Pointer<OBX_store> _cStore;
39+
late final Pointer<OBX_dart_finalizer> _cFinalizer;
3940
HashMap<int, Type>? _entityTypeById;
4041
final _boxes = HashMap<Type, Box>();
4142
final ModelDefinition _defs;
42-
bool _closed = false;
4343
Stream<List<Type>>? _entityChanges;
4444
final _reader = ReaderWithCBuffer();
4545
Transaction? _tx;
@@ -154,6 +154,8 @@ class Store {
154154
_reference.setUint64(1 * _int64Size, _ptr.address);
155155

156156
_openStoreDirectories.add(_absoluteDirectoryPath);
157+
158+
_attachFinalizer();
157159
} catch (e) {
158160
_reader.clear();
159161
rethrow;
@@ -261,6 +263,8 @@ class Store {
261263

262264
// Not setting _reference as this is a replacement for obtaining a store
263265
// via reference.
266+
267+
_attachFinalizer();
264268
} catch (e) {
265269
_reader.clear();
266270
rethrow;
@@ -296,6 +300,24 @@ class Store {
296300
}
297301
}
298302

303+
/// Attach a finalizer (using Dart C API) so when garbage collected, most
304+
/// importantly on Flutter's hot restart (not hot reload), the native Store is
305+
/// properly closed.
306+
///
307+
/// During regular use it's still recommended to explicitly call
308+
/// close() and not rely on garbage collection [to avoid out-of-memory
309+
/// errors](https://github.com/dart-lang/language/issues/1847#issuecomment-1002751632).
310+
void _attachFinalizer() {
311+
initializeDartAPI();
312+
// Keep the finalizer so it can be detached when close() is called.
313+
_cFinalizer = C.dartc_attach_finalizer(
314+
this, native_store_close, _cStore.cast(), 1024 * 1024);
315+
if (_cFinalizer == nullptr) {
316+
close();
317+
throwLatestNativeError(context: 'attach store finalizer');
318+
}
319+
}
320+
299321
/// Returns if an open store (i.e. opened before and not yet closed) was found
300322
/// for the given [directoryPath] (or if null the [defaultDirectoryPath]).
301323
static bool isOpen(String? directoryPath) {
@@ -312,12 +334,14 @@ class Store {
312334
/// a single underlying native store. See [Store.fromReference] for more details.
313335
ByteData get reference => _reference;
314336

337+
/// Returns if this store is already closed and can no longer be used.
338+
bool isClosed() => _cStore.address == 0;
339+
315340
/// Closes this store.
316341
///
317342
/// Don't try to call any other ObjectBox methods after the store is closed.
318343
void close() {
319-
if (_closed) return;
320-
_closed = true;
344+
if (isClosed()) return;
321345

322346
_boxes.values.forEach(InternalBoxAccess.close);
323347
_boxes.clear();
@@ -331,8 +355,14 @@ class Store {
331355

332356
if (!_weak) {
333357
_openStoreDirectories.remove(_absoluteDirectoryPath);
334-
checkObx(C.store_close(_cStore));
358+
final errors = List.filled(2, 0);
359+
if (_cFinalizer != nullptr) {
360+
errors[0] = C.dartc_detach_finalizer(_cFinalizer, this);
361+
}
362+
errors[1] = C.store_close(_cStore);
363+
errors.forEach(checkObx);
335364
}
365+
_cStore = nullptr;
336366
}
337367

338368
/// Returns a cached Box instance.
@@ -459,7 +489,11 @@ class Store {
459489
/// not started; false if shutting down (or an internal error occurred).
460490
///
461491
/// Use to wait until all puts by [Box.putQueued] have finished.
462-
bool awaitAsyncCompletion() => C.store_await_async_submitted(_ptr);
492+
bool awaitAsyncCompletion() {
493+
final result = C.store_await_async_submitted(_ptr);
494+
reachabilityFence(this);
495+
return result;
496+
}
463497

464498
/// Await for previously submitted async operations to be completed
465499
/// (the async queue does not have to become idle).
@@ -468,14 +502,16 @@ class Store {
468502
/// not started; false if shutting down (or an internal error occurred).
469503
///
470504
/// Use to wait until all puts by [Box.putQueued] have finished.
471-
bool awaitAsyncSubmitted() => C.store_await_async_submitted(_ptr);
505+
bool awaitAsyncSubmitted() {
506+
final result = C.store_await_async_submitted(_ptr);
507+
reachabilityFence(this);
508+
return result;
509+
}
472510

473511
/// The low-level pointer to this store.
474512
@pragma('vm:prefer-inline')
475-
Pointer<OBX_store> get _ptr {
476-
if (_closed) throw StateError('Cannot access a closed store pointer');
477-
return _cStore;
478-
}
513+
Pointer<OBX_store> get _ptr =>
514+
isClosed() ? throw StateError('Store is closed') : _cStore;
479515
}
480516

481517
/// Internal only.

objectbox/test/basics_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import 'dart:io';
44
import 'dart:isolate';
55

66
import 'package:async/async.dart';
7-
import 'package:meta/meta.dart';
87
import 'package:objectbox/internal.dart';
98
import 'package:objectbox/src/native/bindings/bindings.dart';
109
import 'package:objectbox/src/native/bindings/helpers.dart';
@@ -114,8 +113,10 @@ void main() {
114113
expect(false, Store.isOpen(''));
115114
expect(false, Store.isOpen('testdata-basics'));
116115
final env = TestEnv('basics');
116+
expect(false, env.store.isClosed());
117117
expect(true, Store.isOpen('testdata-basics'));
118118
env.closeAndDelete();
119+
expect(true, env.store.isClosed());
119120
expect(false, Store.isOpen('testdata-basics'));
120121
});
121122

0 commit comments

Comments
 (0)