[ffigen] Automatic ref counting for ObjC objects (#372)

* Infra for automatic memory management

* Add (failing) auto ref count test

* Fix the test

* Fix analysis, and remove init ownership

* Rename arc_test

* Add a manual release function

* More tests

* Format

* Give each test its own counter

* Brian's comments
diff --git a/pkgs/ffigen/lib/src/code_generator/func.dart b/pkgs/ffigen/lib/src/code_generator/func.dart
index 2f64de1..e0eb0cb 100644
--- a/pkgs/ffigen/lib/src/code_generator/func.dart
+++ b/pkgs/ffigen/lib/src/code_generator/func.dart
@@ -31,6 +31,7 @@
   final bool exposeSymbolAddress;
   final bool exposeFunctionTypedefs;
   final bool isLeaf;
+  late final String funcPointerName;
 
   /// Contains typealias for function type if [exposeFunctionTypedefs] is true.
   Typealias? _exposedCFunctionTypealias;
@@ -86,7 +87,7 @@
     final s = StringBuffer();
     final enclosingFuncName = name;
     final funcVarName = w.wrapperLevelUniqueNamer.makeUnique('_$name');
-    final funcPointerName = w.wrapperLevelUniqueNamer.makeUnique('_${name}Ptr');
+    funcPointerName = w.wrapperLevelUniqueNamer.makeUnique('_${name}Ptr');
 
     if (dartDoc != null) {
       s.write(makeDartDoc(dartDoc!));
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 23cbb7a..2c2644e 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
@@ -49,6 +49,27 @@
     return s.toString();
   });
 
+  late final _retainFunc = Func(
+    name: '_objc_retain',
+    originalName: 'objc_retain',
+    returnType: PointerType(objCObjectType),
+    parameters: [Parameter(name: 'value', type: PointerType(objCObjectType))],
+    isInternal: true,
+  );
+  late final _releaseFunc = Func(
+    name: '_objc_release',
+    originalName: 'objc_release',
+    returnType: voidType,
+    parameters: [Parameter(name: 'value', type: PointerType(objCObjectType))],
+    isInternal: true,
+  );
+  late final _releaseFinalizer = ObjCInternalGlobal(
+    '_objc_releaseFinalizer',
+    (Writer w) => '${w.ffiLibraryPrefix}.NativeFinalizer('
+        '${_releaseFunc.funcPointerName}.cast())',
+    _releaseFunc,
+  );
+
   // We need to load a separate instance of objc_msgSend for each signature.
   final _msgSendFuncs = <String, Func>{};
   Func getMsgSendFunc(Type returnType, List<ObjCMethodParam> params) {
@@ -72,9 +93,10 @@
   final _selObjects = <String, ObjCInternalGlobal>{};
   ObjCInternalGlobal getSelObject(String methodName) {
     return _selObjects[methodName] ??= ObjCInternalGlobal(
-        PointerType(objCSelType),
-        '_sel_${methodName.replaceAll(":", "_")}',
-        () => '${registerName.name}("$methodName")');
+      '_sel_${methodName.replaceAll(":", "_")}',
+      (Writer w) => '${registerName.name}("$methodName")',
+      registerName,
+    );
   }
 
   // See https://clang.llvm.org/docs/Block-ABI-Apple.html
@@ -116,9 +138,9 @@
     return s.toString();
   });
   late final blockDescSingleton = ObjCInternalGlobal(
-    PointerType(blockDescStruct),
     '_objc_block_desc',
-    () => '${newBlockDesc.name}()',
+    (Writer w) => '${newBlockDesc.name}()',
+    blockDescStruct,
   );
   late final newBlock =
       ObjCInternalFunction('_newBlock', null, (Writer w, String name) {
@@ -144,11 +166,33 @@
 
     final objType = PointerType(objCObjectType).getCType(w);
     s.write('''
-class _ObjCWrapper {
+class _ObjCWrapper implements ${w.ffiLibraryPrefix}.Finalizable {
   final $objType _id;
   final ${w.className} _lib;
+  bool _pendingRelease;
 
-  _ObjCWrapper._(this._id, this._lib);
+  _ObjCWrapper._(this._id, this._lib,
+      {bool retain = false, bool release = false}) : _pendingRelease = release {
+    if (retain) {
+      _lib.${_retainFunc.name}(_id);
+    }
+    if (release) {
+      _lib.${_releaseFinalizer.name}.attach(this, _id.cast(), detach: this);
+    }
+  }
+
+  /// Releases the reference to the underlying ObjC object 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);
+    } else {
+      throw StateError(
+          'Released an ObjC object that was unowned or already released.');
+    }
+  }
 
   @override
   bool operator ==(Object other) {
@@ -164,6 +208,9 @@
   void addDependencies(Set<Binding> dependencies) {
     registerName.addDependencies(dependencies);
     getClass.addDependencies(dependencies);
+    _retainFunc.addDependencies(dependencies);
+    _releaseFunc.addDependencies(dependencies);
+    _releaseFinalizer.addDependencies(dependencies);
     for (final func in _msgSendFuncs.values) {
       func.addDependencies(dependencies);
     }
@@ -229,17 +276,17 @@
 
 /// Globals only used internally by ObjC bindings, such as classes and SELs.
 class ObjCInternalGlobal extends LookUpBinding {
-  final Type type;
-  final String Function() makeValue;
+  final String Function(Writer) makeValue;
+  Binding? binding;
 
-  ObjCInternalGlobal(this.type, String name, this.makeValue)
+  ObjCInternalGlobal(String name, this.makeValue, [this.binding])
       : super(originalName: name, name: name, isInternal: true);
 
   @override
   BindingString toBindingString(Writer w) {
     final s = StringBuffer();
     name = w.wrapperLevelUniqueNamer.makeUnique(name);
-    s.write('late final ${type.getCType(w)} $name = ${makeValue()};');
+    s.write('late final $name = ${makeValue(w)};');
     return BindingString(type: BindingStringType.global, string: s.toString());
   }
 
@@ -247,6 +294,6 @@
   void addDependencies(Set<Binding> dependencies) {
     if (dependencies.contains(this)) return;
     dependencies.add(this);
-    type.addDependencies(dependencies);
+    binding?.addDependencies(dependencies);
   }
 }
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index e92b4e3..d8216e9 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -82,27 +82,28 @@
       s.write(makeDartDoc(dartDoc!));
     }
 
-    final uniqueNamer = UniqueNamer({name});
+    final uniqueNamer = UniqueNamer({name, '_id', '_lib'});
     final natLib = w.className;
 
     builtInFunctions.ensureUtilsExist(w, s);
     final objType = PointerType(objCObjectType).getCType(w);
 
     // Class declaration.
-    s.write('class $name ');
-    uniqueNamer.markUsed('_id');
-    s.write('extends ${superType?.name ?? '_ObjCWrapper'} {\n');
-    s.write('  $name._($objType id, $natLib lib) : super._(id, lib);\n\n');
+    s.write('''
+class $name extends ${superType?.name ?? '_ObjCWrapper'} {
+  $name._($objType id, $natLib lib,
+      {bool retain = false, bool release = false}) :
+          super._(id, lib, retain: retain, release: release);
 
-    // Cast method.
-    s.write('  static $name castFrom<T extends _ObjCWrapper>(T other) {\n');
-    s.write('    return $name._(other._id, other._lib);\n');
-    s.write('  }\n\n');
+  static $name castFrom<T extends _ObjCWrapper>(T other) {
+    return $name._(other._id, other._lib, retain: true, release: true);
+  }
 
-    s.write(
-        '  static $name castFromPointer($natLib lib, ffi.Pointer<ObjCObject> other) {\n');
-    s.write('    return $name._(other, lib);\n');
-    s.write('  }\n\n');
+  static $name castFromPointer($natLib lib, ffi.Pointer<ObjCObject> other) {
+    return $name._(other, lib, retain: true, release: true);
+  }
+
+''');
 
     if (isNSString) {
       builtInFunctions.generateNSStringUtils(w, s);
@@ -185,8 +186,8 @@
       }
       s.write(');\n');
       if (convertReturn) {
-        final result = _doReturnConversion(
-            returnType, '_ret', name, '_lib', m.isNullableReturn);
+        final result = _doReturnConversion(returnType, '_ret', name, '_lib',
+            m.isNullableReturn, m.isOwnedReturn);
         s.write('    return $result;');
       }
 
@@ -214,9 +215,9 @@
     }
 
     _classObject = ObjCInternalGlobal(
-        PointerType(objCObjectType),
         '_class_$originalName',
-        () => '${builtInFunctions.getClass.name}("$originalName")')
+        (Writer w) => '${builtInFunctions.getClass.name}("$originalName")',
+        builtInFunctions.getClass)
       ..addDependencies(dependencies);
 
     if (isNSString) {
@@ -345,22 +346,20 @@
   }
 
   String _doReturnConversion(Type type, String value, String enclosingClass,
-      String library, bool isNullable) {
-    String prefix = "";
-    if (isNullable) {
-      prefix += "$value.address == 0 ? null : ";
-    }
+      String library, bool isNullable, bool isOwnedReturn) {
+    final prefix = isNullable ? '$value.address == 0 ? null : ' : '';
+    final ownerFlags = 'retain: ${!isOwnedReturn}, release: true';
     if (type is ObjCInterface) {
-      return prefix + '${type.name}._($value, $library)';
+      return '$prefix${type.name}._($value, $library, $ownerFlags)';
     }
     if (type is ObjCBlock) {
-      return prefix + '${type.name}._($value, $library)';
+      return '$prefix${type.name}._($value, $library)';
     }
     if (_isObject(type)) {
-      return prefix + 'NSObject._($value, $library)';
+      return '${prefix}NSObject._($value, $library, $ownerFlags)';
     }
     if (_isInstanceType(type)) {
-      return prefix + '$enclosingClass._($value, $library)';
+      return '$prefix$enclosingClass._($value, $library, $ownerFlags)';
     }
     return prefix + value;
   }
@@ -445,6 +444,8 @@
     // msgSend is deduped by signature, so this check covers the signature.
     return msgSend == other.msgSend;
   }
+
+  bool get isOwnedReturn => originalName == 'new' || originalName == 'alloc';
 }
 
 class ObjCMethodParam {
diff --git a/pkgs/ffigen/test/native_objc_test/automated_ref_count_config.yaml b/pkgs/ffigen/test/native_objc_test/automated_ref_count_config.yaml
new file mode 100644
index 0000000..b925491
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_config.yaml
@@ -0,0 +1,12 @@
+name: AutomatedRefCountTestObjCLibrary
+description: 'Tests automatic reference counting of Objective-C objects'
+language: objc
+output: 'test/native_objc_test/automated_ref_count_bindings.dart'
+functions:
+  exclude:
+    - '.*'
+headers:
+  entry-points:
+    - 'test/native_objc_test/automated_ref_count_test.m'
+preamble: |
+  // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field
diff --git a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart
new file mode 100644
index 0000000..59261fe
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.dart
@@ -0,0 +1,123 @@
+// Copyright (c) 2022, 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.
+
+// Objective C support is only available on mac.
+@TestOn('mac-os')
+
+import 'dart:ffi';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:ffi/ffi.dart';
+import '../test_utils.dart';
+import 'automated_ref_count_bindings.dart';
+import 'util.dart';
+
+void main() {
+  late AutomatedRefCountTestObjCLibrary lib;
+  late void Function(Pointer<Char>, Pointer<Void>) executeInternalCommand;
+
+  group('Automatic reference counting', () {
+    setUpAll(() {
+      logWarnings();
+      final dylib =
+          File('test/native_objc_test/automated_ref_count_test.dylib');
+      verifySetupFile(dylib);
+      lib = AutomatedRefCountTestObjCLibrary(
+          DynamicLibrary.open(dylib.absolute.path));
+
+      executeInternalCommand = DynamicLibrary.process().lookupFunction<
+          Void Function(Pointer<Char>, Pointer<Void>),
+          void Function(
+              Pointer<Char>, Pointer<Void>)>('Dart_ExecuteInternalCommand');
+
+      generateBindingsForCoverage('automated_ref_count');
+    });
+
+    doGC() {
+      final gcNow = "gc-now".toNativeUtf8();
+      executeInternalCommand(gcNow.cast(), nullptr);
+      calloc.free(gcNow);
+    }
+
+    verifyRefCountsInner(Pointer<Int32> counter) {
+      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 1);
+      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 2);
+      final obj3 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 3);
+    }
+
+    test('Verify ref counts', () {
+      // To get the GC to work correctly, the references to the objects all have
+      // to be in a separate function.
+      final counter = calloc<Int32>();
+      verifyRefCountsInner(counter);
+      doGC();
+      expect(counter.value, 0);
+      calloc.free(counter);
+    });
+
+    test('Manual release', () {
+      final counter = calloc<Int32>();
+      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 1);
+      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 2);
+      final obj3 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 3);
+
+      // GC to clean up temporaries created between alloc and initWithCounter_.
+      doGC();
+      expect(counter.value, 3);
+
+      obj1.release();
+      expect(counter.value, 2);
+      obj2.release();
+      expect(counter.value, 1);
+      obj3.release();
+      expect(counter.value, 0);
+
+      expect(() => obj1.release(), throwsStateError);
+      calloc.free(counter);
+    });
+
+    ArcTestObject unownedReferenceInner2(Pointer<Int32> counter) {
+      final obj1 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 1);
+      final obj1b = obj1.unownedReference();
+      expect(counter.value, 1);
+
+      // Make a second object so that the counter check in unownedReferenceInner
+      // sees some sort of change. Otherwise this test could pass just by the GC
+      // not working correctly.
+      final obj2 = ArcTestObject.alloc(lib).initWithCounter_(counter);
+      expect(counter.value, 2);
+
+      return obj1b;
+    }
+
+    unownedReferenceInner(Pointer<Int32> counter) {
+      final obj1b = unownedReferenceInner2(counter);
+      doGC(); // Collect obj1 and obj2.
+      // The underlying object obj1 and obj1b points to still exists, because
+      // obj1b took a reference to it. So we still have 1 object.
+      expect(counter.value, 1);
+    }
+
+    test("Method that returns a reference we don't own", () {
+      // Most ObjC API methods return us a reference without incrementing the
+      // ref count (ie, returns us a reference we don't own). So the wrapper
+      // object has to take ownership by calling retain. This test verifies that
+      // is working correctly by holding a reference to an object returned by a
+      // method, after the original wrapper object is gone.
+      final counter = calloc<Int32>();
+      unownedReferenceInner(counter);
+      doGC();
+      expect(counter.value, 0);
+      calloc.free(counter);
+    });
+  });
+}
diff --git a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
new file mode 100644
index 0000000..6c3186d
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
@@ -0,0 +1,34 @@
+// Copyright (c) 2022, 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 <Foundation/NSObject.h>
+
+@interface ArcTestObject : NSObject {
+  int32_t* counter;
+}
+
+- (instancetype)initWithCounter:(int32_t*) _counter;
+- (void)dealloc;
+- (ArcTestObject*)unownedReference;
+
+@end
+
+@implementation ArcTestObject
+
+- (instancetype)initWithCounter:(int32_t*) _counter {
+  counter = _counter;
+  ++*counter;
+  return [super init];
+}
+
+- (void)dealloc {
+  --*counter;
+  [super dealloc];
+}
+
+- (ArcTestObject*)unownedReference {
+  return self;
+}
+
+@end
diff --git a/pkgs/ffigen/test/native_objc_test/cast_test.dart b/pkgs/ffigen/test/native_objc_test/cast_test.dart
index 86ec063..55b787b 100644
--- a/pkgs/ffigen/test/native_objc_test/cast_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/cast_test.dart
@@ -15,7 +15,7 @@
 import 'util.dart';
 
 void main() {
-  late Castaway testInstance;
+  Castaway? testInstance;
   late CastTestObjCLibrary lib;
 
   group('cast', () {
@@ -29,26 +29,26 @@
     });
 
     test('castFrom', () {
-      final fromCast = Castaway.castFrom(testInstance.meAsNSObject());
-      expect(fromCast, testInstance);
+      final fromCast = Castaway.castFrom(testInstance!.meAsNSObject());
+      expect(fromCast, testInstance!);
     });
 
     test('castFromPointer', () {
-      final meAsInt = testInstance.meAsInt();
+      final meAsInt = testInstance!.meAsInt();
       final fromCast = Castaway.castFromPointer(
           lib, Pointer<ObjCObject>.fromAddress(meAsInt));
-      expect(fromCast, testInstance);
+      expect(fromCast, testInstance!);
     });
 
     test('equality equals', () {
-      final meAsInt = testInstance.meAsInt();
+      final meAsInt = testInstance!.meAsInt();
       final fromCast = Castaway.castFromPointer(
           lib, Pointer<ObjCObject>.fromAddress(meAsInt));
-      expect(fromCast, testInstance);
+      expect(fromCast, testInstance!);
     });
 
     test('equality not equals', () {
-      final meAsInt = testInstance.meAsInt();
+      final meAsInt = testInstance!.meAsInt();
       final fromCast = Castaway.castFromPointer(
           lib, Pointer<ObjCObject>.fromAddress(meAsInt));
       expect(fromCast, isNot(equals(NSObject.new1(lib))));
diff --git a/pkgs/ffigen/test/native_objc_test/category_test.dart b/pkgs/ffigen/test/native_objc_test/category_test.dart
index 2dda7cf..8a8df0b 100644
--- a/pkgs/ffigen/test/native_objc_test/category_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/category_test.dart
@@ -14,7 +14,7 @@
 import 'util.dart';
 
 void main() {
-  late Thing testInstance;
+  Thing? testInstance;
   late CategoryTestObjCLibrary lib;
 
   group('categories', () {
@@ -28,8 +28,8 @@
     });
 
     test('Category method', () {
-      expect(testInstance.add_Y_(1000, 234), 1234);
-      expect(testInstance.sub_Y_(1234, 1000), 234);
+      expect(testInstance!.add_Y_(1000, 234), 1234);
+      expect(testInstance!.sub_Y_(1234, 1000), 234);
     });
   });
 }
diff --git a/pkgs/ffigen/test/native_objc_test/method_test.dart b/pkgs/ffigen/test/native_objc_test/method_test.dart
index 6ab2679..ebb1fba 100644
--- a/pkgs/ffigen/test/native_objc_test/method_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/method_test.dart
@@ -14,7 +14,7 @@
 import 'util.dart';
 
 void main() {
-  late MethodInterface testInstance;
+  MethodInterface? testInstance;
   late MethodTestObjCLibrary lib;
 
   group('method calls', () {
@@ -29,19 +29,19 @@
 
     group('Instance methods', () {
       test('No arguments', () {
-        expect(testInstance.add(), 5);
+        expect(testInstance!.add(), 5);
       });
 
       test('One argument', () {
-        expect(testInstance.add_(23), 23);
+        expect(testInstance!.add_(23), 23);
       });
 
       test('Two arguments', () {
-        expect(testInstance.add_Y_(23, 17), 40);
+        expect(testInstance!.add_Y_(23, 17), 40);
       });
 
       test('Three arguments', () {
-        expect(testInstance.add_Y_Z_(23, 17, 60), 100);
+        expect(testInstance!.add_Y_Z_(23, 17, 60), 100);
       });
     });
 
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_test.dart b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
index f2fbf74..8814dc5 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
@@ -15,8 +15,8 @@
 
 void main() {
   late NullableTestObjCLibrary lib;
-  late NullableInterface nullableInterface;
-  late NSObject obj;
+  NullableInterface? nullableInterface;
+  NSObject? obj;
   group('method calls', () {
     setUpAll(() {
       logWarnings();
@@ -30,12 +30,12 @@
 
     group('Nullable property', () {
       test('Not null', () {
-        nullableInterface.nullableObjectProperty = obj;
-        expect(nullableInterface.nullableObjectProperty, obj);
+        nullableInterface!.nullableObjectProperty = obj!;
+        expect(nullableInterface!.nullableObjectProperty, obj!);
       });
       test('Null', () {
-        nullableInterface.nullableObjectProperty = null;
-        expect(nullableInterface.nullableObjectProperty, null);
+        nullableInterface!.nullableObjectProperty = null;
+        expect(nullableInterface!.nullableObjectProperty, null);
       });
     });
 
@@ -51,7 +51,7 @@
     group('Nullable arguments', () {
       test('Not null', () {
         expect(
-            NullableInterface.isNullWithNullableNSObjectArg_(lib, obj), false);
+            NullableInterface.isNullWithNullableNSObjectArg_(lib, obj!), false);
       });
       test('Null', () {
         expect(
@@ -61,7 +61,8 @@
 
     group('Not-nullable arguments', () {
       test('Not null', () {
-        expect(NullableInterface.isNullWithNotNullableNSObjectPtrArg_(lib, obj),
+        expect(
+            NullableInterface.isNullWithNotNullableNSObjectPtrArg_(lib, obj!),
             false);
       });
     });
diff --git a/pkgs/ffigen/test/native_objc_test/property_test.dart b/pkgs/ffigen/test/native_objc_test/property_test.dart
index f38b04c..72c11e0 100644
--- a/pkgs/ffigen/test/native_objc_test/property_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/property_test.dart
@@ -14,7 +14,7 @@
 import 'util.dart';
 
 void main() {
-  late PropertyInterface testInstance;
+  PropertyInterface? testInstance;
   late PropertyTestObjCLibrary lib;
 
   group('properties', () {
@@ -29,12 +29,12 @@
 
     group('instance properties', () {
       test('read-only property', () {
-        expect(testInstance.readOnlyProperty, 7);
+        expect(testInstance!.readOnlyProperty, 7);
       });
 
       test('read-write property', () {
-        testInstance.readWriteProperty = 23;
-        expect(testInstance.readWriteProperty, 23);
+        testInstance!.readWriteProperty = 23;
+        expect(testInstance!.readWriteProperty, 23);
       });
     });
 
diff --git a/pkgs/ffigen/test/native_objc_test/setup.dart b/pkgs/ffigen/test/native_objc_test/setup.dart
index 5b170cc..3e9b5e0 100644
--- a/pkgs/ffigen/test/native_objc_test/setup.dart
+++ b/pkgs/ffigen/test/native_objc_test/setup.dart
@@ -46,6 +46,7 @@
 }
 
 const testNames = [
+  'automated_ref_count',
   'block',
   'cast',
   'category',