dispose method

void dispose()

Release the native handle. Must be called when done.

Marks disposed FIRST (every Isolate.run FFI method early-returns on _disposed, and the check + _runFfi spawn happen with no await between them, so no new background call can start once this runs), then defers the actual coreFree until any IN-FLIGHT background-isolate calls finish — otherwise coreFree would free the handle while a background isolate is still dereferencing it (use-after-free).

Implementation

void dispose() {
  if (_disposed) return;
  _disposed = true;
  clearStreamCallback();
  clearHsiCallback();
  final pending = _inflightFfi.toList();
  Future<void> freeWhenIdle() async {
    if (pending.isNotEmpty) {
      try {
        await Future.wait<Object?>(pending);
      } catch (_) {
        // A failed in-flight call still completed (no longer touching the
        // handle) — proceed to free.
      }
    }
    _ffi.coreFree(_handle);

    // Only now is it safe to free the trampolines. `coreFree` drops the
    // handle, which drops the embedded tokio runtime and joins its workers,
    // so no task can be part-way through a callback dispatch any more. Doing
    // this before `coreFree` is what crashed — see [_retiredCallables].
    for (final callable in _retiredCallables) {
      callable.close();
    }
    _retiredCallables.clear();

    if (_sdkCryptoTable != null) {
      calloc.free(_sdkCryptoTable!);
      _sdkCryptoTable = null;
    }
  }

  unawaited(freeWhenIdle());
}