[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());
}