[ffigen] Memory management for Blocks (#429)

* Parse args in objc test setup.dart to allow building specific tests

* Load block copy/release functions

* Copy created block, and delete original

* Add finalizer to block

* Basic test

* Another test

* Small refactor

* Fix tests
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_block.dart b/pkgs/ffigen/lib/src/code_generator/objc_block.dart
index 711e469..73f5a31 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_block.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_block.dart
@@ -28,6 +28,8 @@
   BindingString toBindingString(Writer w) {
     final s = StringBuffer();
 
+    builtInFunctions.ensureBlockUtilsExist(w, s);
+
     final params = <Parameter>[];
     for (int i = 0; i < argTypes.length; ++i) {
       params.add(Parameter(name: 'arg$i', type: argTypes[i]));
@@ -97,25 +99,26 @@
     s.write('}\n');
 
     // Write the wrapper class.
-    s.write('class $name {\n');
-    s.write('  final ${blockPtr.getCType(w)} _impl;\n');
-    s.write('  final ${w.className} _lib;\n');
-    s.write('  $name._(this._impl, this._lib);\n');
-
-    // Constructor from a function pointer.
     final defaultValue = returnType.getDefaultValue(w, '_lib');
     final exceptionalReturn = defaultValue == null ? '' : ', $defaultValue';
     s.write('''
-  $name.fromFunctionPointer(this._lib, $natFnPtr ptr)
-      : _impl =  _lib.${builtInFunctions.newBlock.name}(
+class $name extends _ObjCBlockBase {
+  $name._(${blockPtr.getCType(w)} id, ${w.className} lib) :
+      super._(id, lib, retain: false, release: true);
+
+  /// Creates a block from a C function pointer.
+  $name.fromFunctionPointer(${w.className} lib, $natFnPtr ptr) :
+      this._(lib.${builtInFunctions.newBlock.name}(
           ${w.ffiLibraryPrefix}.Pointer.fromFunction<
               ${trampFuncType.getCType(w)}>($funcPtrTrampoline
-                  $exceptionalReturn).cast(), ptr.cast());
-  $name.fromFunction(this._lib, ${funcType.getDartType(w)} fn)
-      : _impl =  _lib.${builtInFunctions.newBlock.name}(
+                  $exceptionalReturn).cast(), ptr.cast()), lib);
+
+  /// Creates a block from a Dart function.
+  $name.fromFunction(${w.className} lib, ${funcType.getDartType(w)} fn) :
+      this._(lib.${builtInFunctions.newBlock.name}(
           ${w.ffiLibraryPrefix}.Pointer.fromFunction<
               ${trampFuncType.getCType(w)}>($closureTrampoline
-                  $exceptionalReturn).cast(), $registerClosure(fn));
+                  $exceptionalReturn).cast(), $registerClosure(fn)), lib);
 ''');
 
     // Call method.
@@ -125,9 +128,9 @@
       s.write(' ${params[i].name}');
     }
     s.write(''') {
-    ${isVoid ? '' : 'return '}_impl.ref.invoke.cast<
+    ${isVoid ? '' : 'return '}_id.ref.invoke.cast<
         ${natTrampFnType.getCType(w)}>().asFunction<
-            ${trampFuncType.getDartType(w)}>()(_impl''');
+            ${trampFuncType.getDartType(w)}>()(_id''');
     for (int i = 0; i < params.length; ++i) {
       s.write(', ${params[i].name}');
     }
@@ -135,7 +138,7 @@
   }''');
 
     // Get the pointer to the underlying block.
-    s.write('  ${blockPtr.getCType(w)} get pointer => _impl;\n');
+    s.write('  ${blockPtr.getCType(w)} get pointer => _id;\n');
 
     s.write('}\n');
     return BindingString(
@@ -151,12 +154,7 @@
     for (final t in argTypes) {
       t.addDependencies(dependencies);
     }
-
-    builtInFunctions.newBlockDesc.addDependencies(dependencies);
-    builtInFunctions.blockDescSingleton.addDependencies(dependencies);
-    builtInFunctions.blockStruct.addDependencies(dependencies);
-    builtInFunctions.concreteGlobalBlock.addDependencies(dependencies);
-    builtInFunctions.newBlock.addDependencies(dependencies);
+    builtInFunctions.addBlockDependencies(dependencies);
   }
 
   @override
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart b/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
index c7208a9..ff96125 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
@@ -18,15 +18,15 @@
   );
   late final registerName = ObjCInternalFunction(
       '_registerName', _registerNameFunc, (Writer w, String name) {
-    final s = StringBuffer();
     final selType = _registerNameFunc.functionType.returnType.getCType(w);
-    s.write('\n$selType $name(String name) {\n');
-    s.write('  final cstr = name.toNativeUtf8();\n');
-    s.write('  final sel = ${_registerNameFunc.name}(cstr.cast());\n');
-    s.write('  ${w.ffiPkgLibraryPrefix}.calloc.free(cstr);\n');
-    s.write('  return sel;\n');
-    s.write('}\n');
-    return s.toString();
+    return '''
+$selType $name(String name) {
+  final cstr = name.toNativeUtf8();
+  final sel = ${_registerNameFunc.name}(cstr.cast());
+  ${w.ffiPkgLibraryPrefix}.calloc.free(cstr);
+  return sel;
+}
+''';
   });
 
   late final _getClassFunc = Func(
@@ -38,9 +38,8 @@
   );
   late final getClass =
       ObjCInternalFunction('_getClass', _getClassFunc, (Writer w, String name) {
-    final s = StringBuffer();
     final objType = _getClassFunc.functionType.returnType.getCType(w);
-    s.write('''
+    return '''
 $objType $name(String name) {
   final cstr = name.toNativeUtf8();
   final clazz = ${_getClassFunc.name}(cstr.cast());
@@ -50,8 +49,7 @@
   }
   return clazz;
 }
-''');
-    return s.toString();
+''';
   });
 
   late final _retainFunc = Func(
@@ -75,6 +73,27 @@
     _releaseFunc,
   );
 
+  late final _blockCopyFunc = Func(
+    name: '_Block_copy',
+    originalName: '_Block_copy',
+    returnType: PointerType(voidType),
+    parameters: [Parameter(name: 'value', type: PointerType(voidType))],
+    isInternal: true,
+  );
+  late final _blockReleaseFunc = Func(
+    name: '_Block_release',
+    originalName: '_Block_release',
+    returnType: voidType,
+    parameters: [Parameter(name: 'value', type: PointerType(voidType))],
+    isInternal: true,
+  );
+  late final _blockReleaseFinalizer = ObjCInternalGlobal(
+    '_objc_releaseFinalizer',
+    (Writer w) => '${w.ffiLibraryPrefix}.NativeFinalizer('
+        '${_blockReleaseFunc.funcPointerName}.cast())',
+    _blockReleaseFunc,
+  );
+
   // We need to load a separate instance of objc_msgSend for each signature.
   final _msgSendFuncs = <String, Func>{};
   Func getMsgSendFunc(Type returnType, List<ObjCMethodParam> params) {
@@ -130,17 +149,17 @@
   );
   late final newBlockDesc =
       ObjCInternalFunction('_newBlockDesc', null, (Writer w, String name) {
-    final s = StringBuffer();
     final blockType = blockStruct.getCType(w);
     final descType = blockDescStruct.getCType(w);
     final descPtr = PointerType(blockDescStruct).getCType(w);
-    s.write('\n$descPtr $name() {\n');
-    s.write('  final d = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$descType>('
-        '${w.ffiLibraryPrefix}.sizeOf<$descType>());\n');
-    s.write('  d.ref.size = ${w.ffiLibraryPrefix}.sizeOf<$blockType>();\n');
-    s.write('  return d;\n');
-    s.write('}\n');
-    return s.toString();
+    return '''
+$descPtr $name() {
+  final d = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$descType>(
+      ${w.ffiLibraryPrefix}.sizeOf<$descType>());
+  d.ref.size = ${w.ffiLibraryPrefix}.sizeOf<$blockType>();
+  return d;
+}
+''';
   });
   late final blockDescSingleton = ObjCInternalGlobal(
     '_objc_block_desc',
@@ -152,62 +171,67 @@
     (Writer w) => '${w.lookupFuncIdentifier}<${voidType.getCType(w)}>('
         "'_NSConcreteGlobalBlock')",
   );
-  late final newBlock =
-      ObjCInternalFunction('_newBlock', null, (Writer w, String name) {
-    final s = StringBuffer();
+  late final newBlock = ObjCInternalFunction('_newBlock', _blockCopyFunc,
+      (Writer w, String name) {
     final blockType = blockStruct.getCType(w);
     final blockPtr = PointerType(blockStruct).getCType(w);
     final voidPtr = PointerType(voidType).getCType(w);
-    s.write('\n$blockPtr $name($voidPtr invoke, $voidPtr target) {\n');
-    s.write('  final b = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$blockType>('
-        '${w.ffiLibraryPrefix}.sizeOf<$blockType>());\n');
-    s.write('  b.ref.isa = ${concreteGlobalBlock.name};\n');
-    s.write('  b.ref.invoke = invoke;\n');
-    s.write('  b.ref.target = target;\n');
-    s.write('  b.ref.descriptor = ${blockDescSingleton.name};\n');
-    s.write('  return b;\n');
-    s.write('}\n');
-    return s.toString();
+    return '''
+$blockPtr $name($voidPtr invoke, $voidPtr target) {
+  final b = ${w.ffiPkgLibraryPrefix}.calloc.allocate<$blockType>(
+      ${w.ffiLibraryPrefix}.sizeOf<$blockType>());
+  b.ref.isa = ${concreteGlobalBlock.name};
+  b.ref.invoke = invoke;
+  b.ref.target = target;
+  b.ref.descriptor = ${blockDescSingleton.name};
+  final copy = ${_blockCopyFunc.name}(b.cast()).cast<$blockType>();
+  ${w.ffiPkgLibraryPrefix}.calloc.free(b);
+  return copy;
+}
+''';
   });
 
-  bool utilsExist = false;
-  void ensureUtilsExist(Writer w, StringBuffer s) {
-    if (utilsExist) return;
-    utilsExist = true;
-
-    final objType = PointerType(objCObjectType).getCType(w);
+  void _writeFinalizableClass(
+      Writer w,
+      StringBuffer s,
+      String name,
+      String kind,
+      String idType,
+      String retain,
+      String release,
+      String finalizer) {
     s.write('''
-class _ObjCWrapper implements ${w.ffiLibraryPrefix}.Finalizable {
-  final $objType _id;
+class $name implements ${w.ffiLibraryPrefix}.Finalizable {
+  final $idType _id;
   final ${w.className} _lib;
   bool _pendingRelease;
 
-  _ObjCWrapper._(this._id, this._lib,
+  $name._(this._id, this._lib,
       {bool retain = false, bool release = false}) : _pendingRelease = release {
     if (retain) {
-      _lib.${_retainFunc.name}(_id);
+      _lib.$retain(_id.cast());
     }
     if (release) {
-      _lib.${_releaseFinalizer.name}.attach(this, _id.cast(), detach: this);
+      _lib.$finalizer.attach(this, _id.cast(), detach: this);
     }
   }
 
-  /// Releases the reference to the underlying ObjC object held by this wrapper.
+  /// Releases the reference to the underlying ObjC $kind held by this wrapper.
   /// Throws a StateError if this wrapper doesn't currently hold a reference.
   void release() {
     if (_pendingRelease) {
       _pendingRelease = false;
-      _lib.${_releaseFunc.name}(_id);
-      _lib.${_releaseFinalizer.name}.detach(this);
+      _lib.$release(_id.cast());
+      _lib.$finalizer.detach(this);
     } else {
       throw StateError(
-          'Released an ObjC object that was unowned or already released.');
+          'Released an ObjC $kind that was unowned or already released.');
     }
   }
 
   @override
   bool operator ==(Object other) {
-    return other is _ObjCWrapper && _id == other._id;
+    return other is $name && _id == other._id;
   }
 
   @override
@@ -216,6 +240,36 @@
 ''');
   }
 
+  bool utilsExist = false;
+  void ensureUtilsExist(Writer w, StringBuffer s) {
+    if (utilsExist) return;
+    utilsExist = true;
+    _writeFinalizableClass(
+        w,
+        s,
+        '_ObjCWrapper',
+        'object',
+        PointerType(objCObjectType).getCType(w),
+        _retainFunc.name,
+        _releaseFunc.name,
+        _releaseFinalizer.name);
+  }
+
+  bool blockUtilsExist = false;
+  void ensureBlockUtilsExist(Writer w, StringBuffer s) {
+    if (blockUtilsExist) return;
+    blockUtilsExist = true;
+    _writeFinalizableClass(
+        w,
+        s,
+        '_ObjCBlockBase',
+        'block',
+        PointerType(blockStruct).getCType(w),
+        _blockCopyFunc.name,
+        _blockReleaseFunc.name,
+        _blockReleaseFinalizer.name);
+  }
+
   void addDependencies(Set<Binding> dependencies) {
     registerName.addDependencies(dependencies);
     getClass.addDependencies(dependencies);
@@ -230,33 +284,46 @@
     }
   }
 
+  void addBlockDependencies(Set<Binding> dependencies) {
+    newBlockDesc.addDependencies(dependencies);
+    blockDescSingleton.addDependencies(dependencies);
+    blockStruct.addDependencies(dependencies);
+    concreteGlobalBlock.addDependencies(dependencies);
+    newBlock.addDependencies(dependencies);
+    _blockCopyFunc.addDependencies(dependencies);
+    _blockReleaseFunc.addDependencies(dependencies);
+    _blockReleaseFinalizer.addDependencies(dependencies);
+  }
+
   final _interfaceRegistry = <String, ObjCInterface>{};
   void registerInterface(ObjCInterface interface) {
     _interfaceRegistry[interface.originalName] = interface;
   }
 
   void generateNSStringUtils(Writer w, StringBuffer s) {
-    // Generate a constructor that wraps stringWithCString.
-    s.write('  factory NSString(${w.className} _lib, String str) {\n');
-    s.write('    final cstr = str.toNativeUtf8();\n');
-    s.write('    final nsstr = stringWithCString_encoding_('
-        '_lib, cstr.cast(), 4 /* UTF8 */);\n');
-    s.write('    ${w.ffiPkgLibraryPrefix}.calloc.free(cstr);\n');
-    s.write('    return nsstr;\n');
-    s.write('  }\n\n');
+    // Generate a constructor that wraps stringWithCString, and a toString
+    // method that wraps UTF8String.
+    s.write('''
+  factory NSString(${w.className} _lib, String str) {
+    final cstr = str.toNativeUtf8();
+    final nsstr = stringWithCString_encoding_(_lib, cstr.cast(), 4 /* UTF8 */);
+    ${w.ffiPkgLibraryPrefix}.calloc.free(cstr);
+    return nsstr;
+  }
 
-    // Generate a toString method that wraps UTF8String.
-    s.write('  @override\n');
-    s.write('  String toString() => (UTF8String).cast<'
-        '${w.ffiPkgLibraryPrefix}.Utf8>().toDartString();\n\n');
+  @override
+  String toString() =>
+      (UTF8String).cast<${w.ffiPkgLibraryPrefix}.Utf8>().toDartString();
+''');
   }
 
   void generateStringUtils(Writer w, StringBuffer s) {
     // Generate an extension on String to convert to NSString
-    s.write('extension StringToNSString on String {\n');
-    s.write('  NSString toNSString(${w.className} lib) => '
-        'NSString(lib, this);\n');
-    s.write('}\n\n');
+    s.write('''
+extension StringToNSString on String {
+  NSString toNSString(${w.className} lib) => NSString(lib, this);
+}
+''');
   }
 }
 
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index b33e97b..61e88ef 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -103,7 +103,7 @@
   }
 
   /// Returns a [$name] that wraps the given raw object pointer.
-  static $name castFromPointer($natLib lib, ffi.Pointer<ObjCObject> other,
+  static $name castFromPointer($natLib lib, $objType other,
       {bool retain = false, bool release = false}) {
     return $name._(other, lib, retain: retain, release: release);
   }
@@ -350,11 +350,10 @@
         _isObject(arg.type) ||
         _isInstanceType(arg.type) ||
         arg.type is ObjCBlock) {
-      final field = arg.type is ObjCBlock ? '_impl' : '_id';
       if (arg.isNullable) {
-        return '${arg.name}?.$field ?? ffi.nullptr';
+        return '${arg.name}?._id ?? ffi.nullptr';
       } else {
-        return '${arg.name}.$field';
+        return '${arg.name}._id';
       }
     }
     return arg.name;
diff --git a/pkgs/ffigen/test/native_objc_test/block_test.dart b/pkgs/ffigen/test/native_objc_test/block_test.dart
index 87f882d..99d1460 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_test.dart
@@ -9,12 +9,14 @@
 import 'dart:io';
 
 import 'package:test/test.dart';
+import 'package:ffi/ffi.dart';
 import '../test_utils.dart';
 import 'block_bindings.dart';
 import 'util.dart';
 
 void main() {
   late BlockTestObjCLibrary lib;
+  late void Function(Pointer<Char>, Pointer<Void>) executeInternalCommand;
 
   group('Blocks', () {
     setUpAll(() {
@@ -22,9 +24,21 @@
       final dylib = File('test/native_objc_test/block_test.dylib');
       verifySetupFile(dylib);
       lib = BlockTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+
+      executeInternalCommand = DynamicLibrary.process().lookupFunction<
+          Void Function(Pointer<Char>, Pointer<Void>),
+          void Function(
+              Pointer<Char>, Pointer<Void>)>('Dart_ExecuteInternalCommand');
+
       generateBindingsForCoverage('block');
     });
 
+    doGC() {
+      final gcNow = "gc-now".toNativeUtf8();
+      executeInternalCommand(gcNow.cast(), nullptr);
+      calloc.free(gcNow);
+    }
+
     test('BlockTester is working', () {
       // This doesn't test any Block functionality, just that the BlockTester
       // itself is working correctly.
@@ -56,6 +70,31 @@
       expect(blockTester.call_(123), 4123);
       expect(block(123), 4123);
     });
+
+    Pointer<Void> funcPointerBlockRefCountTest() {
+      final block = ObjCBlock.fromFunctionPointer(
+          lib, Pointer.fromFunction(_add100, 999));
+      expect(BlockTester.getBlockRetainCount_(lib, block.pointer), 1);
+      return block.pointer.cast();
+    }
+
+    test('Function pointer block ref counting', () {
+      final rawBlock = funcPointerBlockRefCountTest();
+      doGC();
+      expect(BlockTester.getBlockRetainCount_(lib, rawBlock.cast()), 0);
+    });
+
+    Pointer<Void> funcBlockRefCountTest() {
+      final block = ObjCBlock.fromFunction(lib, makeAdder(4000));
+      expect(BlockTester.getBlockRetainCount_(lib, block.pointer), 1);
+      return block.pointer.cast();
+    }
+
+    test('Function pointer block ref counting', () {
+      final rawBlock = funcBlockRefCountTest();
+      doGC();
+      expect(BlockTester.getBlockRetainCount_(lib, rawBlock.cast()), 0);
+    });
   });
 }
 
diff --git a/pkgs/ffigen/test/native_objc_test/block_test.m b/pkgs/ffigen/test/native_objc_test/block_test.m
index 7d17080..c0052e3 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test.m
+++ b/pkgs/ffigen/test/native_objc_test/block_test.m
@@ -13,6 +13,7 @@
 }
 + (BlockTester*)makeFromBlock:(IntBlock)block;
 + (BlockTester*)makeFromMultiplier:(int32_t)mult;
++ (uint64_t)getBlockRetainCount:(IntBlock)block;
 - (int32_t)call:(int32_t)x;
 - (IntBlock)getBlock;
 - (void)pokeBlock;
@@ -33,6 +34,33 @@
   return bt;
 }
 
+typedef struct {
+  void* isa;
+  int flags;
+  // There are other fields, but we just need the flags and isa.
+} BlockRefCountExtractor;
+
+void* valid_block_isa = NULL;
++ (uint64_t)getBlockRetainCount:(IntBlock)block {
+  BlockRefCountExtractor* b = (BlockRefCountExtractor*)block;
+  // HACK: The only way I can find to reliably figure out that a block has been
+  // deleted is to check the isa field (the lower bits of the flags field seem
+  // to be randomized, not just set to 0). But we also don't know the value this
+  // field has when it's constructed (copying the block changes it from
+  // _NSConcreteGlobalBlock to an internal value). So we assume that the first
+  // time this function is called, we have a valid block, and on subsequent
+  // calls we check to see if the isa field changed.
+  if (valid_block_isa == NULL) {
+    valid_block_isa = b->isa;
+  }
+  if (b->isa != valid_block_isa) {
+    return 0;
+  }
+  // The ref count is stored in the lower bits of the flags field, but skips the
+  // 0x1 bit.
+  return (b->flags & 0xFFFF) >> 1;
+}
+
 - (int32_t)call:(int32_t)x {
   return myBlock(x);
 }
diff --git a/pkgs/ffigen/test/native_objc_test/setup.dart b/pkgs/ffigen/test/native_objc_test/setup.dart
index 9194c45..2e6701b 100644
--- a/pkgs/ffigen/test/native_objc_test/setup.dart
+++ b/pkgs/ffigen/test/native_objc_test/setup.dart
@@ -57,9 +57,7 @@
   return names;
 }
 
-final testNames = _getTestNames();
-
-Future<void> build() async {
+Future<void> build(List<String> testNames) async {
   print('Building Dynamic Library for Objective C Native Tests...');
   for (final name in testNames) {
     await _buildLib('${name}_test.m', '${name}_test.dylib');
@@ -71,7 +69,7 @@
   }
 }
 
-Future<void> clean() async {
+Future<void> clean(List<String> testNames) async {
   print('Deleting generated and built files...');
   final filenames = [
     for (final name in testNames) ...[
@@ -93,8 +91,8 @@
   }
 
   if (arguments.isNotEmpty && arguments[0] == 'clean') {
-    return await clean();
+    return await clean(_getTestNames());
   }
 
-  return await build();
+  return await build(arguments.isNotEmpty ? arguments : _getTestNames());
 }