[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',