sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Native callables: Allow closing natively

Open simolus3 opened this issue 1 month ago • 14 comments

I'm running into an issue with NativeCallable combined with NativeFinalizers. In some C libraries, one can provide a cleanup function when registering a callback. That cleanup function would get called when the native library determines it doesn't need the original callback anymore.

An example for this are user-defined functions in SQLite, where:

  1. I pass a void (*xFunc)(sqlite3_context*,int,sqlite3_value**) for the actual function, and
  2. a void(*xDestroy)(void*) to get called when the SQLite wants to free the callback.

In Dart, I

  1. Create a NativeCallable.isolateLocal to pass as xFunc, and
  2. another NativeCallable.isolateLocal to pass as xDestroy. The second native callable calls close() on the first one and itself.

That worked fine for a long time, but I ran into crashes when adopting native finalizers. In particular, the order of events is:

  1. An isolate shuts down.
  2. A native finalizer for a SQLite isolate runs.
  3. SQLite calls xDestroy, which now crashes the Dart VM with Cannot invoke native callback while unwind error propagates.

This pattern of callback handling is not uncommon in C, but it seems like there is no easy way to safely dispose these callbacks in Dart. I have some ideas for convoluted workarounds, but I'm wondering if there could be away to simplify this. For instance, perhaps a native library could be allowed to close a NativeCallable:

  • On NativeCallable, there could be a Pointer<Void> closeToken.
  • Additionally, there could be an static Pointer<NativeFinalizerFunction> closeNative entrypoint.

Then, I could pass the closeToken as a context to the native library and the closeNative function pointer as xDestroy, allowing SQLite to close the native callable.

simolus3 avatar Oct 31 '25 23:10 simolus3