wip callback migration
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test.dart b/pkgs/ffigen/test/native_objc_test/isolate_test.dart
index 6e0c013..d6d145b 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test.dart
@@ -102,6 +102,23 @@
       port.close();
     }
 
+    // Runs on other isolate.
+    void case2Helper(SendPort sendPort) async {
+      final port = ReceivePort();
+      sendPort.send(port.sendPort);
+
+      final block = ObjCBlock_ffiVoid_Sendable.listener((Sendable s) {
+        // Do nothing.
+      }, keepIsolateAlive: false);
+
+      sendPort.send(block);
+
+      await for (final _ in port) {
+        break;
+      }
+      port.close();
+    }
+
     test('Sending block through a port', () async {
       final completer = Completer<int>();
       ObjCBlock<Void Function(Int32)>? block = ObjCBlock_ffiVoid_Int32.listener(
@@ -214,5 +231,72 @@
       expect(() => sendable.value, throwsA(isA<UseAfterReleaseError>()));
       expect(() => sendable.ref.release(), throwsA(isA<DoubleReleaseError>()));
     });
+
+    test(
+      'Case 2: Isolate shuts down before handling message (Memory Leak)',
+      () async {
+        await using((arena) async {
+          final sendableTracker = ReferenceTracker(arena);
+          Sendable? sendable = Sendable();
+          sendable.value = 789;
+          sendableTracker.track(sendable);
+
+          final port = ReceivePort();
+          final queue = StreamQueue(port);
+          final isolate = await Isolate.spawn(
+            case2Helper,
+            port.sendPort,
+            onExit: port.sendPort,
+          );
+
+          final helperSendPort = await queue.next as SendPort;
+          final block = await queue.next as ObjCBlock<Void Function(Sendable)>;
+
+          // Invoke the block, passing the sendable object.
+          block(sendable!);
+
+          // IMMEDIATELY kill the helper isolate.
+          isolate.kill(priority: Isolate.immediate);
+
+          // Wait for the helper isolate to completely exit.
+          while (await queue.next != null) {
+            // Wait for onExit signal which sends null.
+          }
+          port.close();
+
+          // Clear our own references to sendable and the block.
+          sendable = null;
+
+          // Force Garbage Collection.
+          doGC();
+
+          // In a buggy implementation, the Sendable object will have leaked
+          // because the message containing it was dropped during isolate shutdown
+          // without releasing the retained ObjC reference.
+          // So sendableTracker.isAlive will still be true.
+          expect(sendableTracker.isAlive, true);
+        });
+      },
+      skip: !canDoGC,
+    );
+
+    test(
+      'Case 3: Isolate is already shut down before message is sent (Crash/UB)',
+      () async {
+        // Run the subprocess script and expect it to crash (exit code != 0).
+        final scriptPath = path.join(
+          packagePathForTests,
+          'test',
+          'native_objc_test',
+          'isolate_test_case3_subprocess.dart',
+        );
+        final result = await Process.run(Platform.resolvedExecutable, [
+          scriptPath,
+        ]);
+
+        // In a buggy implementation, the subprocess crashes (segfaults) so the exit code is non-zero.
+        expect(result.exitCode, isNot(0));
+      },
+    );
   });
 }
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test.h b/pkgs/ffigen/test/native_objc_test/isolate_test.h
index 221f1fe..0c4c9b1 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test.h
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test.h
@@ -4,9 +4,13 @@
 
 #import <Foundation/NSObject.h>
 
+@class Sendable;
+
 typedef void (^Listener)(int32_t);
+typedef void (^ListenerWithSendable)(Sendable *);
 
 @interface Sendable : NSObject {}
 @property int32_t value;
 + (Listener)dummyMethodToForceGenerationOfListener;
++ (ListenerWithSendable)dummyMethodToForceGenerationOfListenerWithSendable;
 @end
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test.m b/pkgs/ffigen/test/native_objc_test/isolate_test.m
index fc205af..e0ce9c6 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test.m
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test.m
@@ -5,4 +5,10 @@
 #include "isolate_test.h"
 
 @implementation Sendable
++ (Listener)dummyMethodToForceGenerationOfListener {
+  return nil;
+}
++ (ListenerWithSendable)dummyMethodToForceGenerationOfListenerWithSendable {
+  return nil;
+}
 @end
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
index b13b57e..a5dc422 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
@@ -24,12 +24,32 @@
 );
 
 @ffi.Native<
+  ffi.Pointer<objc.ObjCBlockImpl> Function(
+    ffi.Pointer<objc.ObjCBlockImpl>,
+    ffi.Pointer<objc.ObjCBlockImpl>,
+    ffi.Pointer<objc.DOBJC_Context>,
+  )
+>(isLeaf: true)
+external ffi.Pointer<objc.ObjCBlockImpl> _rdx59v_wrapBlockingBlock_xtuoz7(
+  ffi.Pointer<objc.ObjCBlockImpl> block,
+  ffi.Pointer<objc.ObjCBlockImpl> listnerBlock,
+  ffi.Pointer<objc.DOBJC_Context> context,
+);
+
+@ffi.Native<
   ffi.Pointer<objc.ObjCBlockImpl> Function(ffi.Pointer<objc.ObjCBlockImpl>)
 >(isLeaf: true)
 external ffi.Pointer<objc.ObjCBlockImpl> _rdx59v_wrapListenerBlock_1bqef4y(
   ffi.Pointer<objc.ObjCBlockImpl> block,
 );
 
+@ffi.Native<
+  ffi.Pointer<objc.ObjCBlockImpl> Function(ffi.Pointer<objc.ObjCBlockImpl>)
+>(isLeaf: true)
+external ffi.Pointer<objc.ObjCBlockImpl> _rdx59v_wrapListenerBlock_xtuoz7(
+  ffi.Pointer<objc.ObjCBlockImpl> block,
+);
+
 /// Construction methods for `objc.ObjCBlock<ffi.Void Function(ffi.Int32)>`.
 abstract final class ObjCBlock_ffiVoid_Int32 {
   /// Returns a block that wraps the given raw block pointer.
@@ -246,6 +266,252 @@
   }
 }
 
+/// Construction methods for `objc.ObjCBlock<ffi.Void Function(Sendable)>`.
+abstract final class ObjCBlock_ffiVoid_Sendable {
+  /// Returns a block that wraps the given raw block pointer.
+  static objc.ObjCBlock<ffi.Void Function(Sendable)> fromPointer(
+    ffi.Pointer<objc.ObjCBlockImpl> pointer, {
+    bool retain = false,
+    bool release = false,
+  }) => objc.ObjCBlock<ffi.Void Function(Sendable)>(
+    pointer,
+    retain: retain,
+    release: release,
+  );
+
+  /// Creates a block from a C function pointer.
+  ///
+  /// This block must be invoked by native code running on the same thread as
+  /// the isolate that registered it. Invoking the block on the wrong thread
+  /// will result in a crash.
+  static objc.ObjCBlock<ffi.Void Function(Sendable)> fromFunctionPointer(
+    ffi.Pointer<
+      ffi.NativeFunction<
+        ffi.Void Function(ffi.Pointer<objc.ObjCObjectImpl> arg0)
+      >
+    >
+    ptr,
+  ) => objc.ObjCBlock<ffi.Void Function(Sendable)>(
+    objc.newPointerBlock(_fnPtrCallable, ptr.cast()),
+    retain: false,
+    release: true,
+  );
+
+  /// Creates a block from a Dart function.
+  ///
+  /// This block must be invoked by native code running on the same thread as
+  /// the isolate that registered it. Invoking the block on the wrong thread
+  /// will result in a crash.
+  ///
+  /// If `keepIsolateAlive` is true, this block will keep this isolate alive
+  /// until it is garbage collected by both Dart and ObjC.
+  static objc.ObjCBlock<ffi.Void Function(Sendable)> fromFunction(
+    void Function(Sendable) fn, {
+    bool keepIsolateAlive = true,
+  }) => objc.ObjCBlock<ffi.Void Function(Sendable)>(
+    objc.newClosureBlock(_closureCallable, (
+      ffi.Pointer<objc.ObjCObjectImpl> arg0,
+    ) {
+      return fn(Sendable.fromPointer(arg0, retain: true, release: true));
+    }, keepIsolateAlive),
+    retain: false,
+    release: true,
+  );
+
+  /// Creates a listener block from a Dart function.
+  ///
+  /// This is based on FFI's NativeCallable.listener, and has the same
+  /// capabilities and limitations. This block can be invoked from any thread,
+  /// but only supports void functions, and is not run synchronously. See
+  /// NativeCallable.listener for more details.
+  ///
+  /// If `keepIsolateAlive` is true, this block will keep this isolate alive
+  /// until it is garbage collected by both Dart and ObjC.
+  static objc.ObjCBlock<ffi.Void Function(Sendable)> listener(
+    void Function(Sendable) fn, {
+    bool keepIsolateAlive = true,
+  }) {
+    final raw = objc.newClosureBlock(_listenerCallable.nativeFunction.cast(), (
+      ffi.Pointer<objc.ObjCObjectImpl> arg0,
+    ) {
+      return fn(Sendable.fromPointer(arg0, retain: false, release: true));
+    }, keepIsolateAlive);
+    final wrapper = _rdx59v_wrapListenerBlock_xtuoz7(raw);
+    objc.objectRelease(raw.cast());
+    return objc.ObjCBlock<ffi.Void Function(Sendable)>(
+      wrapper,
+      retain: false,
+      release: true,
+    );
+  }
+
+  /// Creates a blocking block from a Dart function.
+  ///
+  /// This callback can be invoked from any native thread, and will block the
+  /// caller until the callback is handled by the Dart isolate that created
+  /// the block. Async functions are not supported.
+  ///
+  /// If `keepIsolateAlive` is true, this block will keep this isolate alive
+  /// until it is garbage collected by both Dart and ObjC. If the owner isolate
+  /// has shut down, and the block is invoked by native code, it may block
+  /// indefinitely, or have other undefined behavior.
+  static objc.ObjCBlock<ffi.Void Function(Sendable)> blocking(
+    void Function(Sendable) fn, {
+    bool keepIsolateAlive = true,
+  }) {
+    final raw = objc.newClosureBlock(_blockingCallable.nativeFunction.cast(), (
+      ffi.Pointer<objc.ObjCObjectImpl> arg0,
+    ) {
+      return fn(Sendable.fromPointer(arg0, retain: false, release: true));
+    }, keepIsolateAlive);
+    final rawListener = objc.newClosureBlock(
+      _blockingListenerCallable.nativeFunction.cast(),
+      (ffi.Pointer<objc.ObjCObjectImpl> arg0) {
+        return fn(Sendable.fromPointer(arg0, retain: false, release: true));
+      },
+      keepIsolateAlive,
+    );
+    final wrapper = _rdx59v_wrapBlockingBlock_xtuoz7(
+      raw,
+      rawListener,
+      objc.objCContext,
+    );
+    objc.objectRelease(raw.cast());
+    objc.objectRelease(rawListener.cast());
+    return objc.ObjCBlock<ffi.Void Function(Sendable)>(
+      wrapper,
+      retain: false,
+      release: true,
+    );
+  }
+
+  static void _listenerTrampoline(
+    ffi.Pointer<objc.ObjCBlockImpl> block,
+    ffi.Pointer<objc.ObjCObjectImpl> arg0,
+  ) {
+    (objc.getBlockClosure(block)
+        as void Function(ffi.Pointer<objc.ObjCObjectImpl>))(arg0);
+    objc.objectRelease(block.cast());
+  }
+
+  static ffi.NativeCallable<
+    ffi.Void Function(
+      ffi.Pointer<objc.ObjCBlockImpl>,
+      ffi.Pointer<objc.ObjCObjectImpl>,
+    )
+  >
+  _listenerCallable =
+      ffi.NativeCallable<
+          ffi.Void Function(
+            ffi.Pointer<objc.ObjCBlockImpl>,
+            ffi.Pointer<objc.ObjCObjectImpl>,
+          )
+        >.listener(_listenerTrampoline)
+        ..keepIsolateAlive = false;
+  static void _blockingTrampoline(
+    ffi.Pointer<objc.ObjCBlockImpl> block,
+    ffi.Pointer<ffi.Void> waiter,
+    ffi.Pointer<objc.ObjCObjectImpl> arg0,
+  ) {
+    try {
+      (objc.getBlockClosure(block)
+          as void Function(ffi.Pointer<objc.ObjCObjectImpl>))(arg0);
+    } catch (e) {
+    } finally {
+      objc.signalWaiter(waiter);
+      objc.objectRelease(block.cast());
+    }
+  }
+
+  static ffi.NativeCallable<
+    ffi.Void Function(
+      ffi.Pointer<objc.ObjCBlockImpl>,
+      ffi.Pointer<ffi.Void>,
+      ffi.Pointer<objc.ObjCObjectImpl>,
+    )
+  >
+  _blockingCallable =
+      ffi.NativeCallable<
+          ffi.Void Function(
+            ffi.Pointer<objc.ObjCBlockImpl>,
+            ffi.Pointer<ffi.Void>,
+            ffi.Pointer<objc.ObjCObjectImpl>,
+          )
+        >.isolateLocal(_blockingTrampoline)
+        ..keepIsolateAlive = false;
+  static ffi.NativeCallable<
+    ffi.Void Function(
+      ffi.Pointer<objc.ObjCBlockImpl>,
+      ffi.Pointer<ffi.Void>,
+      ffi.Pointer<objc.ObjCObjectImpl>,
+    )
+  >
+  _blockingListenerCallable =
+      ffi.NativeCallable<
+          ffi.Void Function(
+            ffi.Pointer<objc.ObjCBlockImpl>,
+            ffi.Pointer<ffi.Void>,
+            ffi.Pointer<objc.ObjCObjectImpl>,
+          )
+        >.listener(_blockingTrampoline)
+        ..keepIsolateAlive = false;
+  static void _fnPtrTrampoline(
+    ffi.Pointer<objc.ObjCBlockImpl> block,
+    ffi.Pointer<objc.ObjCObjectImpl> arg0,
+  ) => block.ref.target
+      .cast<
+        ffi.NativeFunction<
+          ffi.Void Function(ffi.Pointer<objc.ObjCObjectImpl> arg0)
+        >
+      >()
+      .asFunction<void Function(ffi.Pointer<objc.ObjCObjectImpl>)>()(arg0);
+  static ffi.Pointer<ffi.Void> _fnPtrCallable =
+      ffi.Pointer.fromFunction<
+            ffi.Void Function(
+              ffi.Pointer<objc.ObjCBlockImpl>,
+              ffi.Pointer<objc.ObjCObjectImpl>,
+            )
+          >(_fnPtrTrampoline)
+          .cast();
+  static void _closureTrampoline(
+    ffi.Pointer<objc.ObjCBlockImpl> block,
+    ffi.Pointer<objc.ObjCObjectImpl> arg0,
+  ) =>
+      (objc.getBlockClosure(block)
+          as void Function(ffi.Pointer<objc.ObjCObjectImpl>))(arg0);
+  static ffi.Pointer<ffi.Void> _closureCallable =
+      ffi.Pointer.fromFunction<
+            ffi.Void Function(
+              ffi.Pointer<objc.ObjCBlockImpl>,
+              ffi.Pointer<objc.ObjCObjectImpl>,
+            )
+          >(_closureTrampoline)
+          .cast();
+}
+
+/// Call operator for `objc.ObjCBlock<ffi.Void Function(Sendable)>`.
+extension ObjCBlock_ffiVoid_Sendable$CallExtension
+    on objc.ObjCBlock<ffi.Void Function(Sendable)> {
+  void call(Sendable arg0) {
+    final _$$ref = arg0.ref;
+    return ref.pointer.ref.invoke
+        .cast<
+          ffi.NativeFunction<
+            ffi.Void Function(
+              ffi.Pointer<objc.ObjCBlockImpl> block,
+              ffi.Pointer<objc.ObjCObjectImpl> arg0,
+            )
+          >
+        >()
+        .asFunction<
+          void Function(
+            ffi.Pointer<objc.ObjCBlockImpl>,
+            ffi.Pointer<objc.ObjCObjectImpl>,
+          )
+        >()(ref.pointer, _$$ref.pointer);
+  }
+}
+
 /// Sendable
 extension type Sendable._(objc.ObjCObject object$)
     implements objc.ObjCObject, objc.NSObject {
@@ -302,6 +568,20 @@
     );
   }
 
+  /// dummyMethodToForceGenerationOfListenerWithSendable
+  static objc.ObjCBlock<ffi.Void Function(Sendable)>
+  dummyMethodToForceGenerationOfListenerWithSendable() {
+    final $ret = _objc_msgSend_uwvaik(
+      _class_Sendable,
+      _sel_dummyMethodToForceGenerationOfListenerWithSendable,
+    );
+    return ObjCBlock_ffiVoid_Sendable.fromPointer(
+      $ret,
+      retain: true,
+      release: true,
+    );
+  }
+
   /// new
   static Sendable new$() {
     final $ret = _objc_msgSend_151sglz(_class_Sendable, _sel_new);
@@ -450,6 +730,8 @@
 late final _sel_dummyMethodToForceGenerationOfListener = objc.registerName(
   "dummyMethodToForceGenerationOfListener",
 );
+late final _sel_dummyMethodToForceGenerationOfListenerWithSendable = objc
+    .registerName("dummyMethodToForceGenerationOfListenerWithSendable");
 late final _sel_init = objc.registerName("init");
 late final _sel_isKindOfClass_ = objc.registerName("isKindOfClass:");
 late final _sel_new = objc.registerName("new");
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.m b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.m
index e04acaf..fcc267b 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.m
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.m
@@ -71,6 +71,29 @@
     listenerBlock(waiter, arg0);
   });
 }
+
+typedef void  (^_ListenerTrampoline_1)(id arg0);
+__attribute__((visibility("default"))) __attribute__((used))
+_ListenerTrampoline_1 _rdx59v_wrapListenerBlock_xtuoz7(_ListenerTrampoline_1 block) NS_RETURNS_RETAINED {
+  return ^void(id arg0) {
+    objc_retainBlock(block);
+    block((__bridge id)(__bridge_retained void*)(arg0));
+  };
+}
+
+typedef void  (^_BlockingTrampoline_1)(void * waiter, id arg0);
+__attribute__((visibility("default"))) __attribute__((used))
+_ListenerTrampoline_1 _rdx59v_wrapBlockingBlock_xtuoz7(
+    _BlockingTrampoline_1 block, _BlockingTrampoline_1 listenerBlock,
+    DOBJC_Context* ctx) NS_RETURNS_RETAINED {
+  BLOCKING_BLOCK_IMPL(ctx, ^void(id arg0), {
+    objc_retainBlock(block);
+    block(nil, (__bridge id)(__bridge_retained void*)(arg0));
+  }, {
+    objc_retainBlock(listenerBlock);
+    listenerBlock(waiter, (__bridge id)(__bridge_retained void*)(arg0));
+  });
+}
 #undef BLOCKING_BLOCK_IMPL
 
 #pragma clang diagnostic pop
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test_case3_subprocess.dart b/pkgs/ffigen/test/native_objc_test/isolate_test_case3_subprocess.dart
new file mode 100644
index 0000000..220668f
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test_case3_subprocess.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:ffi';
+import 'dart:isolate';
+import 'isolate_test_bindings.dart';
+
+void case3Helper(SendPort sendPort) async {
+  final block = ObjCBlock_ffiVoid_Int32.listener((int x) {
+    // Do nothing.
+  }, keepIsolateAlive: false);
+  sendPort.send(block);
+}
+
+void main() async {
+  final port = ReceivePort();
+  final exitPort = ReceivePort();
+  await Isolate.spawn(case3Helper, port.sendPort, onExit: exitPort.sendPort);
+
+  final block = await port.first as ObjCBlock<Void Function(Int32)>;
+
+  // Wait for the helper isolate to exit.
+  await exitPort.first;
+
+  // The helper isolate is now completely shut down.
+  // Invoking the block now will call the deleted native function pointer
+  // under the buggy implementation, resulting in a crash.
+  block(123);
+}