Skip to content

Commit ec38b3b

Browse files
committed
implement native finalizers and use them to auto-close query and property-query
1 parent 860ae33 commit ec38b3b

File tree

9 files changed

+87
-26
lines changed

9 files changed

+87
-26
lines changed

objectbox/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
This is a 1.0 release candidate - we encourage everyone to try it out and provide any last-minute feedback,
44
especially to new/changed APIs.
55

6+
* Query now supports auto-closing. You can still call `close()` manually if you want to free native resources sooner
7+
than they would be by Dart's garbage collector, but it's not mandatory anymore.
68
* Change the "meta-model" fields to provide completely type-safe query building.
79
Conditions you specify are now checked at compile time to match the queried entity.
810
* Make property queries fully typed, `PropertyQuery.find()` now returns the appropriate `List<...>` type without casts.

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'objectbox-c.dart';
88
export 'objectbox-c.dart';
99

1010
// ignore_for_file: public_member_api_docs
11+
// ignore_for_file: non_constant_identifier_names
1112

1213
/// Tries to use an already loaded objectbox dynamic library. This is the only
1314
/// option for macOS and iOS and is ~5 times faster than loading from file so
@@ -18,7 +19,8 @@ ObjectBoxC? _tryObjectBoxLibProcess() {
1819

1920
ObjectBoxC? obxc;
2021
try {
21-
obxc = ObjectBoxC(DynamicLibrary.process());
22+
_lib = DynamicLibrary.process();
23+
obxc = ObjectBoxC(_lib!);
2224
_isSupportedVersion(obxc); // may throw in case symbols are not found
2325
} catch (_) {
2426
// ignore errors (i.e. symbol not found)
@@ -27,19 +29,19 @@ ObjectBoxC? _tryObjectBoxLibProcess() {
2729
}
2830

2931
ObjectBoxC? _tryObjectBoxLibFile() {
30-
DynamicLibrary? lib;
32+
_lib = null;
3133
var libName = 'objectbox';
3234
if (Platform.isWindows) {
3335
libName += '.dll';
3436
try {
35-
lib = DynamicLibrary.open(libName);
37+
_lib = DynamicLibrary.open(libName);
3638
} on ArgumentError {
3739
libName = 'lib/' + libName;
3840
}
3941
} else if (Platform.isMacOS) {
4042
libName = 'lib' + libName + '.dylib';
4143
try {
42-
lib = DynamicLibrary.open(libName);
44+
_lib = DynamicLibrary.open(libName);
4345
} on ArgumentError {
4446
libName = '/usr/local/lib/' + libName;
4547
}
@@ -50,8 +52,8 @@ ObjectBoxC? _tryObjectBoxLibFile() {
5052
} else {
5153
return null;
5254
}
53-
lib ??= DynamicLibrary.open(libName);
54-
return ObjectBoxC(lib);
55+
_lib ??= DynamicLibrary.open(libName);
56+
return ObjectBoxC(_lib!);
5557
}
5658

5759
bool _isSupportedVersion(ObjectBoxC obxc) => obxc.version_is_at_least(
@@ -77,9 +79,8 @@ ObjectBoxC loadObjectBoxLib() {
7779
return obxc;
7880
}
7981

80-
ObjectBoxC? _cachedBindings;
81-
82-
ObjectBoxC get C => _cachedBindings ??= loadObjectBoxLib();
82+
DynamicLibrary? _lib;
83+
late final ObjectBoxC C = loadObjectBoxLib();
8384

8485
/// Init DartAPI in C for async callbacks.
8586
///
@@ -111,3 +112,12 @@ void initializeDartAPI() {
111112
// -1 => failed to initialize - incompatible Dart version
112113
int _dartAPIInitialized = 0;
113114
Object? _dartAPIInitException;
115+
116+
/// A couple of native functions we need as callbacks to pass back to native.
117+
/// Unfortunately, ffigen keeps those private.
118+
typedef _native_close = Int32 Function(Pointer<Void> ptr);
119+
120+
late final native_query_close =
121+
_lib!.lookup<NativeFunction<_native_close>>('obx_query_close');
122+
late final native_query_prop_close =
123+
_lib!.lookup<NativeFunction<_native_close>>('obx_query_prop_close');

objectbox/lib/src/native/query/builder.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ class QueryBuilder<T> extends _QueryBuilder<T> {
4747
onListen: subscribe,
4848
onResume: subscribe,
4949
onPause: () => subscription.pause(),
50-
onCancel: () {
51-
subscription.cancel();
52-
query.close();
53-
});
50+
onCancel: () => subscription.cancel());
5451
if (triggerImmediately) controller.add(query);
5552
return controller.stream;
5653
}

objectbox/lib/src/native/query/property.dart

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,36 @@ part of query;
33
/// Property query base.
44
class PropertyQuery<T> {
55
final Pointer<OBX_query_prop> _cProp;
6+
late final Pointer<OBX_dart_finalizer> _cFinalizer;
7+
bool _closed = false;
68
final int _type;
79
bool _distinct = false;
810
bool _caseSensitive = false;
911

1012
PropertyQuery._(Pointer<OBX_query> cQuery, ModelProperty property)
1113
: _type = property.type,
12-
_cProp =
13-
checkObxPtr(C.query_prop(cQuery, property.id.id), 'property query');
14+
_cProp = checkObxPtr(
15+
C.query_prop(cQuery, property.id.id), 'property query') {
16+
_cFinalizer = C.dartc_attach_finalizer(
17+
this, native_query_prop_close, _cProp.cast(), 64);
18+
if (_cFinalizer == nullptr) {
19+
close();
20+
throwLatestNativeError();
21+
}
22+
}
1423

1524
/// Close the property query, freeing its resources
16-
void close() => checkObx(C.query_prop_close(_cProp));
25+
void close() {
26+
if (!_closed) {
27+
_closed = true;
28+
var err = 0;
29+
if (_cFinalizer != nullptr) {
30+
err = C.dartc_detach_finalizer(_cFinalizer, this);
31+
}
32+
checkObx(C.query_prop_close(_cProp));
33+
checkObx(err);
34+
}
35+
}
1736

1837
int _count() {
1938
final ptr = malloc<Uint64>();

objectbox/lib/src/native/query/query.dart

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ class _ConditionGroupAll<EntityT> extends _ConditionGroup<EntityT> {
618618
class Query<T> {
619619
bool _closed = false;
620620
final Pointer<OBX_query> _cQuery;
621+
late final Pointer<OBX_dart_finalizer> _cFinalizer;
621622
final Store _store;
622623
final EntityDefinition<T> _entity;
623624

@@ -631,7 +632,17 @@ class Query<T> {
631632
}
632633

633634
Query._(this._store, Pointer<OBX_query_builder> cBuilder, this._entity)
634-
: _cQuery = checkObxPtr(C.query(cBuilder), 'create query');
635+
: _cQuery = checkObxPtr(C.query(cBuilder), 'create query') {
636+
initializeDartAPI();
637+
638+
// Keep the finalizer so we can detach it when close() is called manually.
639+
_cFinalizer =
640+
C.dartc_attach_finalizer(this, native_query_close, _cQuery.cast(), 256);
641+
if (_cFinalizer == nullptr) {
642+
close();
643+
throwLatestNativeError();
644+
}
645+
}
635646

636647
/// Configure an [offset] for this query.
637648
///
@@ -675,9 +686,15 @@ class Query<T> {
675686

676687
/// Close the query and free resources.
677688
void close() {
678-
if (_closed) return;
679-
_closed = true;
680-
checkObx(C.query_close(_cQuery));
689+
if (!_closed) {
690+
_closed = true;
691+
var err = 0;
692+
if (_cFinalizer != nullptr) {
693+
err = C.dartc_detach_finalizer(_cFinalizer, this);
694+
}
695+
checkObx(C.query_close(_cQuery));
696+
checkObx(err);
697+
}
681698
}
682699

683700
/// Finds Objects matching the query and returns the first result or null

objectbox/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dev_dependencies:
2121
objectbox_generator: any
2222
pedantic: ^1.11.0
2323
test: ^1.16.5
24-
ffigen: ^2.2.2
24+
ffigen: ^2.4.2
2525
# No null-safety compatible version yet and we only need it in tests.
2626
# flat_buffers: 1.12.0
2727

objectbox/test/query_property_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ void main() {
6363

6464
final tString = TestEntity_.tString;
6565

66+
test('property query auto-close', () {
67+
// Finalizer is executed after the query object goes out of scope.
68+
// Note: only caught by valgrind - I've tested that it actually catches
69+
// when the finalizer assignment was disabled. Now, this will only fail in
70+
// CI when running valgrind.sh - if finalizer won't work properly.
71+
box.query().build().property(TestEntity_.tString).find();
72+
});
73+
6674
test('.count (basic query)', () {
6775
box.putMany(integerList());
6876
box.putMany(stringList());

objectbox/test/query_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ void main() {
1717
});
1818
tearDown(() => env.close());
1919

20+
test('Query auto-close', () {
21+
// Finalizer is executed after the query object goes out of scope.
22+
// Note: only caught by valgrind - I've tested that it actually catches
23+
// when the finalizer assignment was disabled. Now, this will only fail in
24+
// CI when running valgrind.sh - if finalizer won't work properly.
25+
box.query().build().find();
26+
});
27+
2028
test('Query with no conditions, and order as desc ints', () {
2129
box.putMany(<TestEntity>[
2230
TestEntity(tInt: 0),

objectbox/test/stream_test.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,17 @@ void main() {
9191
await sub2.cancel();
9292
});
9393

94-
test('can only use query during listen()', () async {
94+
test('can use query after subscription is canceled', () async {
95+
// This subscribes, gets the first element and cancels immediately.
96+
// We're testing that if user keeps the query instance, they can use it
97+
// later. This is only possible because of query auto-close with finalizers.
9598
final query = await box
9699
.query()
97100
.watch(triggerImmediately: true)
98101
.first
99102
.timeout(defaultTimeout);
100103

101-
expect(
102-
query.count,
103-
throwsA(predicate(
104-
(StateError e) => e.toString().contains('Query already closed'))));
104+
expect(query.count(), 0);
105105
});
106106

107107
test(

0 commit comments

Comments
 (0)