diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 2f88c44e6..913c1420d 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -3,6 +3,8 @@ This is a 1.0 release candidate - we encourage everyone to try it out and provide any last-minute feedback, especially to new/changed APIs. +* Query now supports auto-closing. You can still call `close()` manually if you want to free native resources sooner + than they would be by Dart's garbage collector, but it's not mandatory anymore. * Change the "meta-model" fields to provide completely type-safe query building. Conditions you specify are now checked at compile time to match the queried entity. * Make property queries fully typed, `PropertyQuery.find()` now returns the appropriate `List<...>` type without casts. diff --git a/objectbox/lib/src/native/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart index a4a6b2afe..f2b7ffa47 100644 --- a/objectbox/lib/src/native/bindings/bindings.dart +++ b/objectbox/lib/src/native/bindings/bindings.dart @@ -8,6 +8,7 @@ import 'objectbox-c.dart'; export 'objectbox-c.dart'; // ignore_for_file: public_member_api_docs +// ignore_for_file: non_constant_identifier_names /// Tries to use an already loaded objectbox dynamic library. This is the only /// option for macOS and iOS and is ~5 times faster than loading from file so @@ -18,7 +19,8 @@ ObjectBoxC? _tryObjectBoxLibProcess() { ObjectBoxC? obxc; try { - obxc = ObjectBoxC(DynamicLibrary.process()); + _lib = DynamicLibrary.process(); + obxc = ObjectBoxC(_lib!); _isSupportedVersion(obxc); // may throw in case symbols are not found } catch (_) { // ignore errors (i.e. symbol not found) @@ -27,19 +29,19 @@ ObjectBoxC? _tryObjectBoxLibProcess() { } ObjectBoxC? _tryObjectBoxLibFile() { - DynamicLibrary? lib; + _lib = null; var libName = 'objectbox'; if (Platform.isWindows) { libName += '.dll'; try { - lib = DynamicLibrary.open(libName); + _lib = DynamicLibrary.open(libName); } on ArgumentError { libName = 'lib/' + libName; } } else if (Platform.isMacOS) { libName = 'lib' + libName + '.dylib'; try { - lib = DynamicLibrary.open(libName); + _lib = DynamicLibrary.open(libName); } on ArgumentError { libName = '/usr/local/lib/' + libName; } @@ -50,8 +52,8 @@ ObjectBoxC? _tryObjectBoxLibFile() { } else { return null; } - lib ??= DynamicLibrary.open(libName); - return ObjectBoxC(lib); + _lib ??= DynamicLibrary.open(libName); + return ObjectBoxC(_lib!); } bool _isSupportedVersion(ObjectBoxC obxc) => obxc.version_is_at_least( @@ -77,9 +79,8 @@ ObjectBoxC loadObjectBoxLib() { return obxc; } -ObjectBoxC? _cachedBindings; - -ObjectBoxC get C => _cachedBindings ??= loadObjectBoxLib(); +DynamicLibrary? _lib; +late final ObjectBoxC C = loadObjectBoxLib(); /// Init DartAPI in C for async callbacks. /// @@ -111,3 +112,12 @@ void initializeDartAPI() { // -1 => failed to initialize - incompatible Dart version int _dartAPIInitialized = 0; Object? _dartAPIInitException; + +/// A couple of native functions we need as callbacks to pass back to native. +/// Unfortunately, ffigen keeps those private. +typedef _native_close = Int32 Function(Pointer ptr); + +late final native_query_close = + _lib!.lookup>('obx_query_close'); +late final native_query_prop_close = + _lib!.lookup>('obx_query_prop_close'); diff --git a/objectbox/lib/src/native/query/builder.dart b/objectbox/lib/src/native/query/builder.dart index 63ca47b76..5742d62a8 100644 --- a/objectbox/lib/src/native/query/builder.dart +++ b/objectbox/lib/src/native/query/builder.dart @@ -47,10 +47,7 @@ class QueryBuilder extends _QueryBuilder { onListen: subscribe, onResume: subscribe, onPause: () => subscription.pause(), - onCancel: () { - subscription.cancel(); - query.close(); - }); + onCancel: () => subscription.cancel()); if (triggerImmediately) controller.add(query); return controller.stream; } diff --git a/objectbox/lib/src/native/query/property.dart b/objectbox/lib/src/native/query/property.dart index efdb9a05c..5a6147b22 100644 --- a/objectbox/lib/src/native/query/property.dart +++ b/objectbox/lib/src/native/query/property.dart @@ -3,17 +3,36 @@ part of query; /// Property query base. class PropertyQuery { final Pointer _cProp; + late final Pointer _cFinalizer; + bool _closed = false; final int _type; bool _distinct = false; bool _caseSensitive = false; PropertyQuery._(Pointer cQuery, ModelProperty property) : _type = property.type, - _cProp = - checkObxPtr(C.query_prop(cQuery, property.id.id), 'property query'); + _cProp = checkObxPtr( + C.query_prop(cQuery, property.id.id), 'property query') { + _cFinalizer = C.dartc_attach_finalizer( + this, native_query_prop_close, _cProp.cast(), 64); + if (_cFinalizer == nullptr) { + close(); + throwLatestNativeError(); + } + } /// Close the property query, freeing its resources - void close() => checkObx(C.query_prop_close(_cProp)); + void close() { + if (!_closed) { + _closed = true; + var err = 0; + if (_cFinalizer != nullptr) { + err = C.dartc_detach_finalizer(_cFinalizer, this); + } + checkObx(C.query_prop_close(_cProp)); + checkObx(err); + } + } int _count() { final ptr = malloc(); diff --git a/objectbox/lib/src/native/query/query.dart b/objectbox/lib/src/native/query/query.dart index a92d5c57b..5ec92a170 100644 --- a/objectbox/lib/src/native/query/query.dart +++ b/objectbox/lib/src/native/query/query.dart @@ -618,6 +618,7 @@ class _ConditionGroupAll extends _ConditionGroup { class Query { bool _closed = false; final Pointer _cQuery; + late final Pointer _cFinalizer; final Store _store; final EntityDefinition _entity; @@ -631,7 +632,17 @@ class Query { } Query._(this._store, Pointer cBuilder, this._entity) - : _cQuery = checkObxPtr(C.query(cBuilder), 'create query'); + : _cQuery = checkObxPtr(C.query(cBuilder), 'create query') { + initializeDartAPI(); + + // Keep the finalizer so we can detach it when close() is called manually. + _cFinalizer = + C.dartc_attach_finalizer(this, native_query_close, _cQuery.cast(), 256); + if (_cFinalizer == nullptr) { + close(); + throwLatestNativeError(); + } + } /// Configure an [offset] for this query. /// @@ -675,9 +686,15 @@ class Query { /// Close the query and free resources. void close() { - if (_closed) return; - _closed = true; - checkObx(C.query_close(_cQuery)); + if (!_closed) { + _closed = true; + var err = 0; + if (_cFinalizer != nullptr) { + err = C.dartc_detach_finalizer(_cFinalizer, this); + } + checkObx(C.query_close(_cQuery)); + checkObx(err); + } } /// Finds Objects matching the query and returns the first result or null diff --git a/objectbox/pubspec.yaml b/objectbox/pubspec.yaml index d02437592..6008515d6 100644 --- a/objectbox/pubspec.yaml +++ b/objectbox/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: objectbox_generator: any pedantic: ^1.11.0 test: ^1.16.5 - ffigen: ^2.2.2 + ffigen: ^2.4.2 # No null-safety compatible version yet and we only need it in tests. # flat_buffers: 1.12.0 diff --git a/objectbox/test/query_property_test.dart b/objectbox/test/query_property_test.dart index 9c372072e..6c30f6b37 100644 --- a/objectbox/test/query_property_test.dart +++ b/objectbox/test/query_property_test.dart @@ -63,6 +63,14 @@ void main() { final tString = TestEntity_.tString; + test('property query auto-close', () { + // Finalizer is executed after the query object goes out of scope. + // Note: only caught by valgrind - I've tested that it actually catches + // when the finalizer assignment was disabled. Now, this will only fail in + // CI when running valgrind.sh - if finalizer won't work properly. + box.query().build().property(TestEntity_.tString).find(); + }); + test('.count (basic query)', () { box.putMany(integerList()); box.putMany(stringList()); diff --git a/objectbox/test/query_test.dart b/objectbox/test/query_test.dart index c354a8325..5d4980544 100644 --- a/objectbox/test/query_test.dart +++ b/objectbox/test/query_test.dart @@ -17,6 +17,14 @@ void main() { }); tearDown(() => env.close()); + test('Query auto-close', () { + // Finalizer is executed after the query object goes out of scope. + // Note: only caught by valgrind - I've tested that it actually catches + // when the finalizer assignment was disabled. Now, this will only fail in + // CI when running valgrind.sh - if finalizer won't work properly. + box.query().build().find(); + }); + test('Query with no conditions, and order as desc ints', () { box.putMany([ TestEntity(tInt: 0), diff --git a/objectbox/test/stream_test.dart b/objectbox/test/stream_test.dart index e373613e7..931a573a0 100644 --- a/objectbox/test/stream_test.dart +++ b/objectbox/test/stream_test.dart @@ -91,17 +91,17 @@ void main() { await sub2.cancel(); }); - test('can only use query during listen()', () async { + test('can use query after subscription is canceled', () async { + // This subscribes, gets the first element and cancels immediately. + // We're testing that if user keeps the query instance, they can use it + // later. This is only possible because of query auto-close with finalizers. final query = await box .query() .watch(triggerImmediately: true) .first .timeout(defaultTimeout); - expect( - query.count, - throwsA(predicate( - (StateError e) => e.toString().contains('Query already closed')))); + expect(query.count(), 0); }); test(