@@ -812,18 +812,65 @@ final class FfiChangesetIterator implements RawChangesetIterator, Finalizable {
812812 }
813813}
814814
815- final class FfiDatabase implements RawSqliteDatabase {
815+ /// For user-defined functions, SQLite hooks, or virtual file systems, we
816+ /// register function pointers as [NativeCallable] s.
817+ ///
818+ /// To avoid leaking resources, we should [NativeCallable.close] those once
819+ /// they're no longer used. SQLite provides the `xDestroy` callback for this.
820+ /// For a long time, this package used an additional callable for `xDestroy`
821+ /// that closed the original callables and itself.
822+ ///
823+ /// After migrating to native finalizers however, this approach stopped working.
824+ /// Because native finalizers can run as the Dart isolate is shutting down, we
825+ /// can't invoke Dart code anymore. There is no good way to close callables from
826+ /// C (https://dartbug.com/61887), so we can't use `xDestroy` callbacks from
827+ /// SQLite.
828+ ///
829+ /// Instead, we:
830+ ///
831+ /// - Manually close callables when the database is closed in Dart.
832+ /// - Use (regular, non-native) finalizers to asynchronously close callbacks
833+ /// for databases that haven't been closed manually.
834+ final class _FunctionFinalizers {
835+ final List <NativeCallable > _callables = [];
836+
837+ void closeAll () {
838+ for (final callable in _callables) {
839+ callable.close ();
840+ }
841+ }
842+
843+ static final Finalizer <_FunctionFinalizers > finalizer = Finalizer (
844+ (f) => f.closeAll (),
845+ );
846+ }
847+
848+ final class FfiDatabase implements RawSqliteDatabase , Finalizable {
816849 final Pointer <sqlite3> db;
850+ final _FunctionFinalizers _functions = _FunctionFinalizers ();
851+ final Object _detachToken = Object ();
817852
818853 NativeCallable <_UpdateHook >? _installedUpdateHook;
819854 NativeCallable <_CommitHook >? _installedCommitHook;
820855 NativeCallable <_RollbackHook >? _installedRollbackHook;
821856
822- FfiDatabase (this .db);
857+ FfiDatabase (this .db) {
858+ databaseFinalizer.attach (this , db.cast (), detach: _detachToken);
859+ _FunctionFinalizers .finalizer.attach (
860+ this ,
861+ _functions,
862+ detach: _detachToken,
863+ );
864+ }
823865
824866 @override
825867 int sqlite3_close_v2 () {
826- return libsqlite3.sqlite3_close_v2 (db);
868+ final rc = libsqlite3.sqlite3_close_v2 (db);
869+
870+ _functions.closeAll ();
871+ _FunctionFinalizers .finalizer.detach (_detachToken);
872+ databaseFinalizer.detach (_detachToken);
873+ return rc;
827874 }
828875
829876 @override
@@ -883,15 +930,15 @@ final class FfiDatabase implements RawSqliteDatabase {
883930 required RawCollation collation,
884931 }) {
885932 final name = allocateBytes (collationName, additionalLength: 1 );
886- final compare = collation.toNative ();
933+ final compare = collation.toNative (_functions );
887934
888935 final result = libsqlite3.sqlite3_create_collation_v2 (
889936 db,
890937 name.cast (),
891938 eTextRep,
892939 nullPtr (),
893940 compare.nativeFunction,
894- _xDestroy ([compare] ),
941+ nullPtr ( ),
895942 );
896943 name.free ();
897944
@@ -910,10 +957,10 @@ final class FfiDatabase implements RawSqliteDatabase {
910957 }) {
911958 final functionNamePtr = allocateBytes (functionName, additionalLength: 1 );
912959
913- final step = xStep.toNative ();
914- final $final = xFinal.toNative (true );
915- final value = xValue.toNative (false );
916- final inverse = xInverse.toNative ();
960+ final step = xStep.toNative (_functions );
961+ final $final = xFinal.toNative (clean : true , finalizers : _functions );
962+ final value = xValue.toNative (clean : false , finalizers : _functions );
963+ final inverse = xInverse.toNative (_functions );
917964
918965 final result = libsqlite3.sqlite3_create_window_function (
919966 db,
@@ -925,7 +972,7 @@ final class FfiDatabase implements RawSqliteDatabase {
925972 $final.nativeFunction,
926973 value.nativeFunction,
927974 inverse.nativeFunction,
928- _xDestroy ([step, $final, value, inverse] ),
975+ nullPtr ( ),
929976 );
930977 functionNamePtr.free ();
931978 return result;
@@ -942,9 +989,9 @@ final class FfiDatabase implements RawSqliteDatabase {
942989 }) {
943990 final functionNamePtr = allocateBytes (functionName, additionalLength: 1 );
944991
945- final func = xFunc? .toNative ();
946- final step = xStep? .toNative ();
947- final $final = xFinal? .toNative (true );
992+ final func = xFunc? .toNative (_functions );
993+ final step = xStep? .toNative (_functions );
994+ final $final = xFinal? .toNative (clean : true , finalizers : _functions );
948995
949996 final result = libsqlite3.sqlite3_create_function_v2 (
950997 db,
@@ -955,11 +1002,7 @@ final class FfiDatabase implements RawSqliteDatabase {
9551002 func? .nativeFunction ?? nullPtr (),
9561003 step? .nativeFunction ?? nullPtr (),
9571004 $final? .nativeFunction ?? nullPtr (),
958- _xDestroy ([
959- if (func != null ) func,
960- if (step != null ) step,
961- if ($final != null ) $final,
962- ]),
1005+ nullPtr (),
9631006 );
9641007 functionNamePtr.free ();
9651008 return result;
@@ -973,7 +1016,7 @@ final class FfiDatabase implements RawSqliteDatabase {
9731016 _installedUpdateHook = null ;
9741017 libsqlite3.sqlite3_update_hook (db, nullPtr (), nullPtr ());
9751018 } else {
976- final native = _installedUpdateHook = hook.toNative ();
1019+ final native = _installedUpdateHook = hook.toNative (_functions );
9771020 libsqlite3.sqlite3_update_hook (db, native .nativeFunction, nullPtr ());
9781021 }
9791022
@@ -988,7 +1031,7 @@ final class FfiDatabase implements RawSqliteDatabase {
9881031 _installedCommitHook = null ;
9891032 libsqlite3.sqlite3_commit_hook (db, nullPtr (), nullPtr ());
9901033 } else {
991- final native = _installedCommitHook = hook.toNative ();
1034+ final native = _installedCommitHook = hook.toNative (_functions );
9921035 libsqlite3.sqlite3_commit_hook (db, native .nativeFunction, nullPtr ());
9931036 }
9941037
@@ -1002,7 +1045,7 @@ final class FfiDatabase implements RawSqliteDatabase {
10021045 if (hook == null ) {
10031046 libsqlite3.sqlite3_rollback_hook (db, nullPtr (), nullPtr ());
10041047 } else {
1005- final native = _installedRollbackHook = hook.toNative ();
1048+ final native = _installedRollbackHook = hook.toNative (_functions );
10061049 libsqlite3.sqlite3_rollback_hook (db, native .nativeFunction, nullPtr ());
10071050 }
10081051
@@ -1024,23 +1067,6 @@ final class FfiDatabase implements RawSqliteDatabase {
10241067 RawStatementCompiler newCompiler (List <int > utf8EncodedSql) {
10251068 return FfiStatementCompiler (this , allocateBytes (utf8EncodedSql));
10261069 }
1027-
1028- static Pointer <NativeFunction <Void Function (Pointer <Void >)>> _xDestroy (
1029- List <NativeCallable > callables,
1030- ) {
1031- void destroy (Pointer <Void > _) {
1032- for (final callable in callables) {
1033- callable.close ();
1034- }
1035- }
1036-
1037- final callable = NativeCallable <Void Function (Pointer <Void >)>.isolateLocal (
1038- destroy,
1039- )..keepIsolateAlive = false ;
1040- callables.add (callable);
1041-
1042- return callable.nativeFunction;
1043- }
10441070}
10451071
10461072final class FfiStatementCompiler implements RawStatementCompiler {
@@ -1101,12 +1127,15 @@ final class FfiStatementCompiler implements RawStatementCompiler {
11011127 }
11021128}
11031129
1104- final class FfiStatement implements RawSqliteStatement {
1130+ final class FfiStatement implements RawSqliteStatement , Finalizable {
11051131 final Pointer <sqlite3_stmt> stmt;
1132+ final Object _detachToken = Object ();
11061133
11071134 final List <Pointer > _allocatedArguments = [];
11081135
1109- FfiStatement (this .stmt);
1136+ FfiStatement (this .stmt) {
1137+ statementFinalizer.attach (this , stmt.cast (), detach: _detachToken);
1138+ }
11101139
11111140 @visibleForTesting
11121141 List <Pointer > get allocatedArguments => _allocatedArguments;
@@ -1251,6 +1280,7 @@ final class FfiStatement implements RawSqliteStatement {
12511280 @override
12521281 void sqlite3_finalize () {
12531282 libsqlite3.sqlite3_finalize (stmt);
1283+ statementFinalizer.detach (_detachToken);
12541284 }
12551285
12561286 @override
@@ -1455,72 +1485,93 @@ typedef _UpdateHook =
14551485typedef _CommitHook = Int Function (Pointer <Void >);
14561486typedef _RollbackHook = Void Function (Pointer <Void >);
14571487
1488+ extension on NativeCallable {
1489+ void closeIn (_FunctionFinalizers finalizers) {
1490+ finalizers._callables.add (this );
1491+ }
1492+ }
1493+
14581494extension on RawXFunc {
1459- NativeCallable <_XFunc > toNative () {
1495+ NativeCallable <_XFunc > toNative (_FunctionFinalizers finalizers ) {
14601496 return NativeCallable .isolateLocal ((
1461- Pointer <sqlite3_context> ctx,
1462- int nArgs,
1463- Pointer <Pointer <sqlite3_value>> args,
1464- ) {
1465- this (FfiContext (ctx), _ValueList (nArgs, args));
1466- })..keepIsolateAlive = false ;
1497+ Pointer <sqlite3_context> ctx,
1498+ int nArgs,
1499+ Pointer <Pointer <sqlite3_value>> args,
1500+ ) {
1501+ this (FfiContext (ctx), _ValueList (nArgs, args));
1502+ })
1503+ ..closeIn (finalizers)
1504+ ..keepIsolateAlive = false ;
14671505 }
14681506}
14691507
14701508extension on RawXFinal {
1471- NativeCallable <_XFinal > toNative (bool clean) {
1509+ NativeCallable <_XFinal > toNative ({
1510+ required bool clean,
1511+ required _FunctionFinalizers finalizers,
1512+ }) {
14721513 return NativeCallable .isolateLocal ((Pointer <sqlite3_context> ctx) {
1473- final context = FfiContext (ctx);
1474- this (context);
1475- if (clean) context.freeContext ();
1476- })..keepIsolateAlive = false ;
1514+ final context = FfiContext (ctx);
1515+ this (context);
1516+ if (clean) context.freeContext ();
1517+ })
1518+ ..closeIn (finalizers)
1519+ ..keepIsolateAlive = false ;
14771520 }
14781521}
14791522
14801523extension on RawCollation {
1481- NativeCallable <_XCompare > toNative () {
1524+ NativeCallable <_XCompare > toNative (_FunctionFinalizers finalizers ) {
14821525 return NativeCallable .isolateLocal ((
1483- Pointer <Void > _,
1484- int lengthA,
1485- Pointer <Void > a,
1486- int lengthB,
1487- Pointer <Void > b,
1488- ) {
1489- final dartA = a.cast <sqlite3_char>().readNullableString (lengthA);
1490- final dartB = b.cast <sqlite3_char>().readNullableString (lengthB);
1491-
1492- return this (dartA, dartB);
1493- }, exceptionalReturn: 0 )..keepIsolateAlive = false ;
1526+ Pointer <Void > _,
1527+ int lengthA,
1528+ Pointer <Void > a,
1529+ int lengthB,
1530+ Pointer <Void > b,
1531+ ) {
1532+ final dartA = a.cast <sqlite3_char>().readNullableString (lengthA);
1533+ final dartB = b.cast <sqlite3_char>().readNullableString (lengthB);
1534+
1535+ return this (dartA, dartB);
1536+ }, exceptionalReturn: 0 )
1537+ ..closeIn (finalizers)
1538+ ..keepIsolateAlive = false ;
14941539 }
14951540}
14961541
14971542extension on RawUpdateHook {
1498- NativeCallable <_UpdateHook > toNative () {
1543+ NativeCallable <_UpdateHook > toNative (_FunctionFinalizers finalizers ) {
14991544 return NativeCallable .isolateLocal ((
1500- Pointer <Void > _,
1501- int kind,
1502- Pointer <sqlite3_char> db,
1503- Pointer <sqlite3_char> table,
1504- int rowid,
1505- ) {
1506- final tableName = table.readString ();
1507- this (kind, tableName, rowid);
1508- })..keepIsolateAlive = false ;
1545+ Pointer <Void > _,
1546+ int kind,
1547+ Pointer <sqlite3_char> db,
1548+ Pointer <sqlite3_char> table,
1549+ int rowid,
1550+ ) {
1551+ final tableName = table.readString ();
1552+ this (kind, tableName, rowid);
1553+ })
1554+ ..closeIn (finalizers)
1555+ ..keepIsolateAlive = false ;
15091556 }
15101557}
15111558
15121559extension on RawCommitHook {
1513- NativeCallable <_CommitHook > toNative () {
1560+ NativeCallable <_CommitHook > toNative (_FunctionFinalizers finalizers ) {
15141561 return NativeCallable .isolateLocal ((Pointer <Void > _) {
1515- return this ();
1516- }, exceptionalReturn: 1 )..keepIsolateAlive = false ;
1562+ return this ();
1563+ }, exceptionalReturn: 1 )
1564+ ..closeIn (finalizers)
1565+ ..keepIsolateAlive = false ;
15171566 }
15181567}
15191568
15201569extension on RawRollbackHook {
1521- NativeCallable <_RollbackHook > toNative () {
1570+ NativeCallable <_RollbackHook > toNative (_FunctionFinalizers finalizers ) {
15221571 return NativeCallable .isolateLocal ((Pointer <Void > _) {
1523- this ();
1524- })..keepIsolateAlive = false ;
1572+ this ();
1573+ })
1574+ ..closeIn (finalizers)
1575+ ..keepIsolateAlive = false ;
15251576 }
15261577}
0 commit comments