diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 29cd32b09..6936f6811 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md @@ -3,17 +3,26 @@ - __Breaking change__: Code-gen the ObjC `id` type to `ObjCObjectBase` rather than `NSObject`, since not all ObjC classes inherit from `NSObject`. Eg `NSProxy`. +- __Breaking change__: Generate a native trampoline for each listener block, to + fix a ref counting bug: https://github.com/dart-lang/native/issues/835. + - If you have listener blocks affected by this ref count bug, a .m file will + be generated containing the trampoline. You must compile this .m file into + your package. If you already have a flutter plugin or build.dart, you can + simply add this generated file to that build. + - If you don't use listener blocks, you can ignore the .m file. + - You can choose where the generated .m file is placed with the + `output.objc-bindings` config option. - __Breaking change__: Native enums are now generated as real Dart enums, instead of abstract classes with integer constants. Native enum members with the same integer values are handled properly on the Dart side, and native functions that use enums in their signatures now accept the generated enums on the Dart - side, instead of integer values. -- Rename ObjC interface methods that clash with type names. Fixes - https://github.com/dart-lang/native/issues/1007. + side, instead of integer values. - __Breaking change__: Enum integer types are implementation-defined and not part of the ABI. Therefore FFIgen does a best-effort approach trying to mimic the most common compilers for the various OS and architecture combinations. To silence the warning set config `silence-enum-warning` to `true`. +- Rename ObjC interface methods that clash with type names. Fixes + https://github.com/dart-lang/native/issues/1007. ## 12.0.0 @@ -26,7 +35,7 @@ - __Breaking change__: Use `package:objective_c` in ObjC bindings. - ObjC packages will have a flutter dependency (until https://github.com/dart-lang/native/issues/1068 is fixed). - - Core classes such as `NSString` have been moved intpu `package:objective_c`. + - Core classes such as `NSString` have been moved into `package:objective_c`. - ObjC class methods don't need the ubiquitous `lib` argument anymore. In fact, ffigen won't even generate the native library class (unless it needs to bind top level functions without using `@Native`). It is still necessary diff --git a/pkgs/ffigen/README.md b/pkgs/ffigen/README.md index ee6b2bdac..178e99fe0 100644 --- a/pkgs/ffigen/README.md +++ b/pkgs/ffigen/README.md @@ -593,6 +593,28 @@ ffi-native: language: 'objc' ``` + + + output -> objc-bindings + + Choose where the generated ObjC code (if any) is placed. The default path + is `'${output.bindings}.m'`, so if your Dart bindings are in + `generated_bindings.dart`, your ObjC code will be in + `generated_bindings.dart.m`. +

+ This ObjC file will only be generated if it's needed. If it is generated, + it must be compiled into your package, as part of a flutter plugin or + build.dart script. If your package already has some sort of native build, + you can simply add this generated ObjC file to that build. + + + +```yaml +output: + ... + objc-bindings: 'generated_bindings.m' +``` + output -> symbol-file diff --git a/pkgs/ffigen/ffigen.schema.json b/pkgs/ffigen/ffigen.schema.json index 766090272..71311a85c 100644 --- a/pkgs/ffigen/ffigen.schema.json +++ b/pkgs/ffigen/ffigen.schema.json @@ -23,6 +23,9 @@ "bindings": { "$ref": "#/$defs/filePath" }, + "objc-bindings": { + "$ref": "#/$defs/filePath" + }, "symbol-file": { "type": "object", "additionalProperties": false, diff --git a/pkgs/ffigen/lib/src/code_generator/binding.dart b/pkgs/ffigen/lib/src/code_generator/binding.dart index 6ad26ba79..8c4028e0b 100644 --- a/pkgs/ffigen/lib/src/code_generator/binding.dart +++ b/pkgs/ffigen/lib/src/code_generator/binding.dart @@ -37,6 +37,9 @@ abstract class Binding { /// Note: This does not print the typedef dependencies. /// Must call [getTypedefDependencies] first. BindingString toBindingString(Writer w); + + /// Returns the Objective C bindings, if any. + BindingString? toObjCBindingString(Writer w) => null; } /// Base class for bindings which look up symbols in dynamic library. diff --git a/pkgs/ffigen/lib/src/code_generator/compound.dart b/pkgs/ffigen/lib/src/code_generator/compound.dart index 1cc369787..d7c81e350 100644 --- a/pkgs/ffigen/lib/src/code_generator/compound.dart +++ b/pkgs/ffigen/lib/src/code_generator/compound.dart @@ -156,6 +156,9 @@ abstract class Compound extends BindingType { @override String getCType(Writer w) => name; + @override + String getNativeType({String varName = ''}) => '$originalName $varName'; + @override bool get sameFfiDartAndCType => true; } diff --git a/pkgs/ffigen/lib/src/code_generator/enum_class.dart b/pkgs/ffigen/lib/src/code_generator/enum_class.dart index ed544c404..37d93c385 100644 --- a/pkgs/ffigen/lib/src/code_generator/enum_class.dart +++ b/pkgs/ffigen/lib/src/code_generator/enum_class.dart @@ -255,6 +255,9 @@ class EnumClass extends BindingType { @override String getDartType(Writer w) => name; + @override + String getNativeType({String varName = ''}) => '$originalName $varName'; + @override bool get sameFfiDartAndCType => nativeType.sameFfiDartAndCType; diff --git a/pkgs/ffigen/lib/src/code_generator/func_type.dart b/pkgs/ffigen/lib/src/code_generator/func_type.dart index 49dcf605b..a103c3e4a 100644 --- a/pkgs/ffigen/lib/src/code_generator/func_type.dart +++ b/pkgs/ffigen/lib/src/code_generator/func_type.dart @@ -68,6 +68,12 @@ class FunctionType extends Type { String getDartType(Writer w, {bool writeArgumentNames = true}) => _getTypeImpl(writeArgumentNames, (Type t) => t.getDartType(w)); + @override + String getNativeType({String varName = ''}) { + final arg = dartTypeParameters.map((p) => p.type.getNativeType()); + return '${returnType.getNativeType()} (*$varName)(${arg.join(', ')})'; + } + @override bool get sameFfiDartAndCType => returnType.sameFfiDartAndCType && @@ -141,6 +147,10 @@ class NativeFunc extends Type { @override String getFfiDartType(Writer w) => getCType(w); + @override + String getNativeType({String varName = ''}) => + _type.getNativeType(varName: varName); + @override bool get sameFfiDartAndCType => true; diff --git a/pkgs/ffigen/lib/src/code_generator/handle.dart b/pkgs/ffigen/lib/src/code_generator/handle.dart index aaa0649e7..4c785078f 100644 --- a/pkgs/ffigen/lib/src/code_generator/handle.dart +++ b/pkgs/ffigen/lib/src/code_generator/handle.dart @@ -18,6 +18,11 @@ class HandleType extends Type { @override String getFfiDartType(Writer w) => 'Object'; + // The real native type is Dart_Handle, but that would mean importing + // dart_api.h into the generated native code. + @override + String getNativeType({String varName = ''}) => 'void* $varName'; + @override bool get sameFfiDartAndCType => false; diff --git a/pkgs/ffigen/lib/src/code_generator/imports.dart b/pkgs/ffigen/lib/src/code_generator/imports.dart index d0b5352ac..a9390f654 100644 --- a/pkgs/ffigen/lib/src/code_generator/imports.dart +++ b/pkgs/ffigen/lib/src/code_generator/imports.dart @@ -40,9 +40,10 @@ class ImportedType extends Type { final LibraryImport libraryImport; final String cType; final String dartType; + final String nativeType; final String? defaultValue; - ImportedType(this.libraryImport, this.cType, this.dartType, + ImportedType(this.libraryImport, this.cType, this.dartType, this.nativeType, [this.defaultValue]); @override @@ -54,6 +55,9 @@ class ImportedType extends Type { @override String getFfiDartType(Writer w) => cType == dartType ? getCType(w) : dartType; + @override + String getNativeType({String varName = ''}) => '$nativeType $varName'; + @override bool get sameFfiDartAndCType => cType == dartType; @@ -93,27 +97,36 @@ final objcPkgImport = LibraryImport( importPathWhenImportedByPackageObjC: '../objective_c.dart'); final self = LibraryImport('self', ''); -final voidType = ImportedType(ffiImport, 'Void', 'void'); - -final unsignedCharType = ImportedType(ffiImport, 'UnsignedChar', 'int', '0'); -final signedCharType = ImportedType(ffiImport, 'SignedChar', 'int', '0'); -final charType = ImportedType(ffiImport, 'Char', 'int', '0'); -final unsignedShortType = ImportedType(ffiImport, 'UnsignedShort', 'int', '0'); -final shortType = ImportedType(ffiImport, 'Short', 'int', '0'); -final unsignedIntType = ImportedType(ffiImport, 'UnsignedInt', 'int', '0'); -final intType = ImportedType(ffiImport, 'Int', 'int', '0'); -final unsignedLongType = ImportedType(ffiImport, 'UnsignedLong', 'int', '0'); -final longType = ImportedType(ffiImport, 'Long', 'int', '0'); -final unsignedLongLongType = - ImportedType(ffiImport, 'UnsignedLongLong', 'int', '0'); -final longLongType = ImportedType(ffiImport, 'LongLong', 'int', '0'); - -final floatType = ImportedType(ffiImport, 'Float', 'double', '0.0'); -final doubleType = ImportedType(ffiImport, 'Double', 'double', '0.0'); - -final sizeType = ImportedType(ffiImport, 'Size', 'int', '0'); -final wCharType = ImportedType(ffiImport, 'WChar', 'int', '0'); - -final objCObjectType = ImportedType(objcPkgImport, 'ObjCObject', 'ObjCObject'); -final objCSelType = ImportedType(objcPkgImport, 'ObjCSelector', 'ObjCSelector'); -final objCBlockType = ImportedType(objcPkgImport, 'ObjCBlock', 'ObjCBlock'); +final voidType = ImportedType(ffiImport, 'Void', 'void', 'void'); + +final unsignedCharType = + ImportedType(ffiImport, 'UnsignedChar', 'int', 'unsigned char', '0'); +final signedCharType = + ImportedType(ffiImport, 'SignedChar', 'int', 'char', '0'); +final charType = ImportedType(ffiImport, 'Char', 'int', 'char', '0'); +final unsignedShortType = + ImportedType(ffiImport, 'UnsignedShort', 'int', 'unsigned short', '0'); +final shortType = ImportedType(ffiImport, 'Short', 'int', 'short', '0'); +final unsignedIntType = + ImportedType(ffiImport, 'UnsignedInt', 'int', 'unsigned', '0'); +final intType = ImportedType(ffiImport, 'Int', 'int', 'int', '0'); +final unsignedLongType = + ImportedType(ffiImport, 'UnsignedLong', 'int', 'unsigned long', '0'); +final longType = ImportedType(ffiImport, 'Long', 'int', 'long', '0'); +final unsignedLongLongType = ImportedType( + ffiImport, 'UnsignedLongLong', 'int', 'unsigned long long', '0'); +final longLongType = + ImportedType(ffiImport, 'LongLong', 'int', 'long long', '0'); + +final floatType = ImportedType(ffiImport, 'Float', 'double', 'float', '0.0'); +final doubleType = ImportedType(ffiImport, 'Double', 'double', 'double', '0.0'); + +final sizeType = ImportedType(ffiImport, 'Size', 'int', 'size_t', '0'); +final wCharType = ImportedType(ffiImport, 'WChar', 'int', 'wchar_t', '0'); + +final objCObjectType = + ImportedType(objcPkgImport, 'ObjCObject', 'ObjCObject', 'void'); +final objCSelType = + ImportedType(objcPkgImport, 'ObjCSelector', 'ObjCSelector', 'void'); +final objCBlockType = + ImportedType(objcPkgImport, 'ObjCBlock', 'ObjCBlock', 'id'); diff --git a/pkgs/ffigen/lib/src/code_generator/library.dart b/pkgs/ffigen/lib/src/code_generator/library.dart index fdce3f1ae..d648e4017 100644 --- a/pkgs/ffigen/lib/src/code_generator/library.dart +++ b/pkgs/ffigen/lib/src/code_generator/library.dart @@ -136,6 +136,25 @@ class Library { } } + /// Generates [file] with the Objective C code needed for the bindings, if + /// any. + /// + /// Returns whether bindings were generated. + bool generateObjCFile(File file) { + final bindings = writer.generateObjC(); + + if (bindings == null) { + // No ObjC code needed. If there's already a file (eg from an earlier + // run), delete it so it's not accidentally included in the build. + if (file.existsSync()) file.deleteSync(); + return false; + } + + if (!file.existsSync()) file.createSync(recursive: true); + file.writeAsStringSync(bindings); + return true; + } + /// Generates [file] with symbol output yaml. void generateSymbolOutputFile(File file, String importPath) { if (!file.existsSync()) file.createSync(recursive: true); diff --git a/pkgs/ffigen/lib/src/code_generator/native_type.dart b/pkgs/ffigen/lib/src/code_generator/native_type.dart index 77afa7863..a959f830e 100644 --- a/pkgs/ffigen/lib/src/code_generator/native_type.dart +++ b/pkgs/ffigen/lib/src/code_generator/native_type.dart @@ -26,27 +26,31 @@ enum SupportedNativeType { /// Represents a primitive native type, such as float. class NativeType extends Type { static const _primitives = { - SupportedNativeType.Void: NativeType._('Void', 'void', null), - SupportedNativeType.Char: NativeType._('Uint8', 'int', '0'), - SupportedNativeType.Int8: NativeType._('Int8', 'int', '0'), - SupportedNativeType.Int16: NativeType._('Int16', 'int', '0'), - SupportedNativeType.Int32: NativeType._('Int32', 'int', '0'), - SupportedNativeType.Int64: NativeType._('Int64', 'int', '0'), - SupportedNativeType.Uint8: NativeType._('Uint8', 'int', '0'), - SupportedNativeType.Uint16: NativeType._('Uint16', 'int', '0'), - SupportedNativeType.Uint32: NativeType._('Uint32', 'int', '0'), - SupportedNativeType.Uint64: NativeType._('Uint64', 'int', '0'), - SupportedNativeType.Float: NativeType._('Float', 'double', '0.0'), - SupportedNativeType.Double: NativeType._('Double', 'double', '0.0'), - SupportedNativeType.IntPtr: NativeType._('IntPtr', 'int', '0'), - SupportedNativeType.UintPtr: NativeType._('UintPtr', 'int', '0'), + SupportedNativeType.Void: NativeType._('Void', 'void', 'void', null), + SupportedNativeType.Char: NativeType._('Uint8', 'int', 'char', '0'), + SupportedNativeType.Int8: NativeType._('Int8', 'int', 'int8_t', '0'), + SupportedNativeType.Int16: NativeType._('Int16', 'int', 'int16_t', '0'), + SupportedNativeType.Int32: NativeType._('Int32', 'int', 'int32_t', '0'), + SupportedNativeType.Int64: NativeType._('Int64', 'int', 'int64_t', '0'), + SupportedNativeType.Uint8: NativeType._('Uint8', 'int', 'uint8_t', '0'), + SupportedNativeType.Uint16: NativeType._('Uint16', 'int', 'uint16_t', '0'), + SupportedNativeType.Uint32: NativeType._('Uint32', 'int', 'uint32_t', '0'), + SupportedNativeType.Uint64: NativeType._('Uint64', 'int', 'uint64_t', '0'), + SupportedNativeType.Float: NativeType._('Float', 'double', 'float', '0.0'), + SupportedNativeType.Double: + NativeType._('Double', 'double', 'double', '0.0'), + SupportedNativeType.IntPtr: NativeType._('IntPtr', 'int', 'intptr_t', '0'), + SupportedNativeType.UintPtr: + NativeType._('UintPtr', 'int', 'uintptr_t', '0'), }; final String _cType; final String _dartType; + final String _nativeType; final String? _defaultValue; - const NativeType._(this._cType, this._dartType, this._defaultValue); + const NativeType._( + this._cType, this._dartType, this._nativeType, this._defaultValue); factory NativeType(SupportedNativeType type) => _primitives[type]!; @@ -56,6 +60,9 @@ class NativeType extends Type { @override String getFfiDartType(Writer w) => _dartType; + @override + String getNativeType({String varName = ''}) => '$_nativeType $varName'; + @override bool get sameFfiDartAndCType => _cType == _dartType; @@ -70,7 +77,7 @@ class NativeType extends Type { } class BooleanType extends NativeType { - const BooleanType._() : super._('Bool', 'bool', 'false'); + const BooleanType._() : super._('Bool', 'bool', 'BOOL', 'false'); static const _boolean = BooleanType._(); factory BooleanType() => _boolean; diff --git a/pkgs/ffigen/lib/src/code_generator/objc_block.dart b/pkgs/ffigen/lib/src/code_generator/objc_block.dart index 9422e87c8..42651b5db 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_block.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_block.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:ffigen/src/code_generator.dart'; +import 'package:ffigen/src/config_provider/config_types.dart'; import 'binding_string.dart'; import 'writer.dart'; @@ -11,6 +12,8 @@ class ObjCBlock extends BindingType { final Type returnType; final List argTypes; + Func? _wrapListenerBlock; + ObjCBlock({ required String usr, required Type returnType, @@ -39,6 +42,8 @@ class ObjCBlock extends BindingType { type.toString().replaceAll(_illegalNameChar, ''); static final _illegalNameChar = RegExp(r'[^0-9a-zA-Z]'); + bool get hasListener => returnType == voidType; + @override BindingString toBindingString(Writer w) { final s = StringBuffer(); @@ -48,7 +53,6 @@ class ObjCBlock extends BindingType { params.add(Parameter(name: 'arg$i', type: argTypes[i])); } - final isVoid = returnType == voidType; final voidPtr = PointerType(voidType).getCType(w); final blockPtr = PointerType(objCBlockType); final funcType = FunctionType(returnType: returnType, parameters: params); @@ -148,7 +152,20 @@ class $name extends ${ObjCBuiltInFunctions.blockBase.gen(w)} { '''); // Listener block constructor is only available for void blocks. - if (isVoid) { + if (hasListener) { + // This snippet is the same as the convFn above, except that the args + // don't need to be retained because they've already been retained by + // _wrapListenerBlock. + final listenerConvertedFnArgs = params + .map((p) => + p.type.convertFfiDartTypeToDartType(w, p.name, objCRetain: false)) + .join(', '); + final listenerConvFnInvocation = returnType.convertDartTypeToFfiDartType( + w, 'fn($listenerConvertedFnArgs)', + objCRetain: true); + final listenerConvFn = + '($paramsFfiDartType) => $listenerConvFnInvocation'; + s.write(''' /// Creates a listener block from a Dart function. /// @@ -160,10 +177,10 @@ class $name extends ${ObjCBuiltInFunctions.blockBase.gen(w)} { /// Note that unlike the default behavior of NativeCallable.listener, listener /// blocks do not keep the isolate alive. $name.listener($funcDartType fn) : - this._($newClosureBlock( + this._(${_wrapListenerBlock?.name ?? ''}($newClosureBlock( (_dartFuncListenerTrampoline ??= $nativeCallableType.listener( $closureTrampoline $exceptionalReturn)..keepIsolateAlive = - false).nativeFunction.cast(), $convFn)); + false).nativeFunction.cast(), $listenerConvFn))); static $nativeCallableType? _dartFuncListenerTrampoline; '''); @@ -187,6 +204,36 @@ pointer.ref.invoke.cast<$natTrampFnType>().asFunction<$trampFuncFfiDartType>()( type: BindingStringType.objcBlock, string: s.toString()); } + @override + BindingString? toObjCBindingString(Writer w) { + if (_wrapListenerBlock == null) return null; + + final argsReceived = []; + final retains = []; + for (int i = 0; i < argTypes.length; ++i) { + final t = argTypes[i]; + final argName = 'arg$i'; + argsReceived.add(t.getNativeType(varName: argName)); + retains.add(t.generateRetain(argName) ?? argName); + } + final fnName = _wrapListenerBlock!.originalName; + final blockTypedef = w.objCLevelUniqueNamer.makeUnique('ListenerBlock'); + + final s = StringBuffer(); + s.write(''' +typedef ${getNativeType(varName: blockTypedef)}; +$blockTypedef $fnName($blockTypedef block) { + $blockTypedef wrapper = [^void(${argsReceived.join(', ')}) { + block(${retains.join(', ')}); + } copy]; + [block release]; + return wrapper; +} +'''); + return BindingString( + type: BindingStringType.objcBlock, string: s.toString()); + } + @override void addDependencies(Set dependencies) { if (dependencies.contains(this)) return; @@ -196,6 +243,18 @@ pointer.ref.invoke.cast<$natTrampFnType>().asFunction<$trampFuncFfiDartType>()( for (final t in argTypes) { t.addDependencies(dependencies); } + + if (hasListener && argTypes.any((t) => t.generateRetain('') != null)) { + _wrapListenerBlock = Func( + name: 'wrapListenerBlock_$name', + returnType: this, + parameters: [Parameter(name: 'block', type: this)], + objCReturnsRetained: true, + isLeaf: true, + isInternal: true, + ffiNativeConfig: const FfiNativeConfig(enabled: true), + )..addDependencies(dependencies); + } } @override @@ -204,6 +263,12 @@ pointer.ref.invoke.cast<$natTrampFnType>().asFunction<$trampFuncFfiDartType>()( @override String getDartType(Writer w) => name; + @override + String getNativeType({String varName = ''}) { + final args = argTypes.map((t) => t.getNativeType()); + return '${returnType.getNativeType()} (^$varName)(${args.join(', ')})'; + } + @override bool get sameFfiDartAndCType => true; @@ -230,6 +295,9 @@ pointer.ref.invoke.cast<$natTrampFnType>().asFunction<$trampFuncFfiDartType>()( }) => ObjCInterface.generateConstructor(name, value, objCRetain); + @override + String? generateRetain(String value) => '[$value copy]'; + @override String toString() => '($returnType (^)(${argTypes.join(', ')}))'; } diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart index cabf9ccaf..39ac429be 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart @@ -356,6 +356,9 @@ class $name extends ${superType?.getDartType(w) ?? wrapObjType} { String getDartType(Writer w) => _isBuiltIn ? '${w.objcPkgPrefix}.$name' : name; + @override + String getNativeType({String varName = ''}) => '$originalName* $varName'; + @override bool get sameFfiDartAndCType => true; @@ -394,6 +397,9 @@ class $name extends ${superType?.getDartType(w) ?? wrapObjType} { return '$className.castFromPointer($value, $ownershipFlags)'; } + @override + String? generateRetain(String value) => '[$value retain]'; + String _getConvertedType(Type type, Writer w, String enclosingClass) { if (type is ObjCInstanceType) return enclosingClass; final baseType = type.typealiasType; diff --git a/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart b/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart index 5889e06b6..327fdf163 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart @@ -38,6 +38,10 @@ class ObjCNullable extends Type { @override String getDartType(Writer w) => '${child.getDartType(w)}?'; + @override + String getNativeType({String varName = ''}) => + '${child.getNativeType(varName: varName)} _Nullable'; + @override bool get sameFfiDartAndCType => child.sameFfiDartAndCType; @@ -78,6 +82,9 @@ class ObjCNullable extends Type { return '$value.address == 0 ? null : $convertedValue'; } + @override + String? generateRetain(String value) => child.generateRetain(value); + @override String toString() => '$child?'; diff --git a/pkgs/ffigen/lib/src/code_generator/pointer.dart b/pkgs/ffigen/lib/src/code_generator/pointer.dart index 8d8f4ca4b..75ce13e84 100644 --- a/pkgs/ffigen/lib/src/code_generator/pointer.dart +++ b/pkgs/ffigen/lib/src/code_generator/pointer.dart @@ -31,6 +31,10 @@ class PointerType extends Type { String getCType(Writer w) => '${w.ffiLibraryPrefix}.Pointer<${child.getCType(w)}>'; + @override + String getNativeType({String varName = ''}) => + '${child.getNativeType()}* $varName'; + // Both the C type and the FFI Dart type are 'Pointer<$cType>'. @override bool get sameFfiDartAndCType => true; @@ -56,6 +60,10 @@ class ConstantArray extends PointerType { @override bool get isIncompleteCompound => baseArrayType.isIncompleteCompound; + @override + String getNativeType({String varName = ''}) => + '${child.getNativeType()} $varName[$length]'; + @override String toString() => '$child[$length]'; @@ -79,6 +87,10 @@ class IncompleteArray extends PointerType { @override Type get baseArrayType => child.baseArrayType; + @override + String getNativeType({String varName = ''}) => + '${child.getNativeType()} $varName[]'; + @override String toString() => '$child[]'; @@ -96,6 +108,9 @@ class ObjCObjectPointer extends PointerType { @override String getDartType(Writer w) => '${w.objcPkgPrefix}.ObjCObjectBase'; + @override + String getNativeType({String varName = ''}) => 'id $varName'; + @override bool get sameDartAndCType => false; @@ -118,4 +133,7 @@ class ObjCObjectPointer extends PointerType { String? objCEnclosingClass, }) => '${getDartType(w)}($value, retain: $objCRetain, release: true)'; + + @override + String? generateRetain(String value) => '[$value retain]'; } diff --git a/pkgs/ffigen/lib/src/code_generator/type.dart b/pkgs/ffigen/lib/src/code_generator/type.dart index 111d7eef8..8bebcec0c 100644 --- a/pkgs/ffigen/lib/src/code_generator/type.dart +++ b/pkgs/ffigen/lib/src/code_generator/type.dart @@ -48,6 +48,15 @@ abstract class Type { /// as getFfiDartType. For ObjC bindings this refers to the wrapper object. String getDartType(Writer w) => getFfiDartType(w); + /// Returns the C/ObjC type of the Type. This is the type as it appears in + /// C/ObjC source code. It should not be used in Dart source code. + /// + /// This method takes a [varName] arg because some C/ObjC types embed the + /// variable name inside the type. Eg, to pass an ObjC block as a function + /// argument, the syntax is `int (^arg)(int)`, where arg is the [varName]. + String getNativeType({String varName = ''}) => + throw 'No native mapping for type: $this'; + /// Returns whether the FFI dart type and C type string are same. bool get sameFfiDartAndCType; @@ -85,6 +94,10 @@ abstract class Type { }) => value; + /// Returns generated ObjC code that retains a reference to the given value. + /// Returns null if the Type does not need to be retained. + String? generateRetain(String value) => null; + /// Returns a human readable string representation of the Type. This is mostly /// just for debugging, but it may also be used for non-functional code (eg to /// name a variable or type in generated code). @@ -136,6 +149,10 @@ abstract class BindingType extends NoLookUpBinding implements Type { @override String getDartType(Writer w) => getFfiDartType(w); + @override + String getNativeType({String varName = ''}) => + throw 'No native mapping for type: $this'; + @override bool get sameDartAndCType => sameFfiDartAndCType; @@ -159,6 +176,9 @@ abstract class BindingType extends NoLookUpBinding implements Type { }) => value; + @override + String? generateRetain(String value) => null; + @override String toString() => originalName; diff --git a/pkgs/ffigen/lib/src/code_generator/typealias.dart b/pkgs/ffigen/lib/src/code_generator/typealias.dart index 0112d3463..6adf5c42d 100644 --- a/pkgs/ffigen/lib/src/code_generator/typealias.dart +++ b/pkgs/ffigen/lib/src/code_generator/typealias.dart @@ -130,6 +130,10 @@ class Typealias extends BindingType { @override String getCType(Writer w) => name; + @override + String getNativeType({String varName = ''}) => + type.getNativeType(varName: varName); + @override String getFfiDartType(Writer w) { if (_ffiDartAliasName != null) { @@ -183,6 +187,9 @@ class Typealias extends BindingType { objCEnclosingClass: objCEnclosingClass, ); + @override + String? generateRetain(String value) => type.generateRetain(value); + @override String cacheKey() => type.cacheKey(); @@ -224,4 +231,7 @@ class ObjCInstanceType extends Typealias { // objCEnclosingClass must be present, because instancetype can only // occur inside a class. ObjCInterface.generateConstructor(objCEnclosingClass!, value, objCRetain); + + @override + String getNativeType({String varName = ''}) => 'id $varName'; } diff --git a/pkgs/ffigen/lib/src/code_generator/writer.dart b/pkgs/ffigen/lib/src/code_generator/writer.dart index 0381f299e..a93ce4866 100644 --- a/pkgs/ffigen/lib/src/code_generator/writer.dart +++ b/pkgs/ffigen/lib/src/code_generator/writer.dart @@ -100,9 +100,12 @@ class Writer { late UniqueNamer _initialTopLevelUniqueNamer, _initialWrapperLevelUniqueNamer; /// Used by [Binding]s for generating required code. - late UniqueNamer _topLevelUniqueNamer, _wrapperLevelUniqueNamer; + late UniqueNamer _topLevelUniqueNamer; UniqueNamer get topLevelUniqueNamer => _topLevelUniqueNamer; + late UniqueNamer _wrapperLevelUniqueNamer; UniqueNamer get wrapperLevelUniqueNamer => _wrapperLevelUniqueNamer; + late UniqueNamer _objCLevelUniqueNamer; + UniqueNamer get objCLevelUniqueNamer => _objCLevelUniqueNamer; late String _arrayHelperClassPrefix; @@ -220,6 +223,7 @@ class Writer { void _resetUniqueNamersNamers() { _topLevelUniqueNamer = _initialTopLevelUniqueNamer.clone(); _wrapperLevelUniqueNamer = _initialWrapperLevelUniqueNamer.clone(); + _objCLevelUniqueNamer = UniqueNamer({}); } void markImportUsed(LibraryImport import) { @@ -334,12 +338,14 @@ class Writer { return result.toString(); } + List get _allBindings => [ + ...noLookUpBindings, + ...ffiNativeBindings, + ...lookUpBindings, + ]; + Map generateSymbolOutputYamlMap(String importFilePath) { - final bindings = [ - ...noLookUpBindings, - ...ffiNativeBindings, - ...lookUpBindings - ]; + final bindings = _allBindings; if (!canGenerateSymbolOutput) { throw Exception( "Invalid state: generateSymbolOutputYamlMap() called before generate()"); @@ -379,6 +385,25 @@ class Writer { } return result; } + + /// Writes the Objective C code needed for the bindings, if any. Returns null + /// if there are no bindings that need generated ObjC code. + String? generateObjC() { + final s = StringBuffer(); + s.write(''' +#include + +'''); + bool empty = true; + for (final binding in _allBindings) { + final bindingString = binding.toObjCBindingString(this); + if (bindingString != null) { + empty = false; + s.write(bindingString.string); + } + } + return empty ? null : s.toString(); + } } /// Manages the generated `_SymbolAddress` class. diff --git a/pkgs/ffigen/lib/src/config_provider/config.dart b/pkgs/ffigen/lib/src/config_provider/config.dart index aabf54266..26ec5587d 100644 --- a/pkgs/ffigen/lib/src/config_provider/config.dart +++ b/pkgs/ffigen/lib/src/config_provider/config.dart @@ -37,6 +37,10 @@ class Config { String get output => _output; late String _output; + /// Output ObjC file name. + String get outputObjC => _outputObjC ?? '$output.m'; + String? _outputObjC; + /// Symbol file config. SymbolFile? get symbolFile => _symbolFile; late SymbolFile? _symbolFile; @@ -259,6 +263,7 @@ class Config { outputExtractor(node.value, filename, packageConfig), result: (node) { _output = (node.value as OutputConfig).output; + _outputObjC = (node.value as OutputConfig).outputObjC; _symbolFile = (node.value as OutputConfig).symbolFile; }, )), @@ -794,6 +799,10 @@ class Config { valueConfigSpec: _filePathStringConfigSpec(), required: true, ), + HeterogeneousMapEntry( + key: strings.objCBindings, + valueConfigSpec: _filePathStringConfigSpec(), + ), HeterogeneousMapEntry( key: strings.symbolFile, valueConfigSpec: HeterogeneousMapConfigSpec( diff --git a/pkgs/ffigen/lib/src/config_provider/config_types.dart b/pkgs/ffigen/lib/src/config_provider/config_types.dart index 7bec4928e..c5c66f63f 100644 --- a/pkgs/ffigen/lib/src/config_provider/config_types.dart +++ b/pkgs/ffigen/lib/src/config_provider/config_types.dart @@ -400,9 +400,10 @@ class SymbolFile { class OutputConfig { final String output; + final String? outputObjC; final SymbolFile? symbolFile; - OutputConfig(this.output, this.symbolFile); + OutputConfig(this.output, this.outputObjC, this.symbolFile); } class RawVarArgFunction { diff --git a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart index 41860e89c..bc18a3a1c 100644 --- a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart +++ b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart @@ -57,8 +57,8 @@ void loadImportedTypes(YamlMap fileConfig, for (final key in symbols.keys) { final usr = key as String; final value = symbols[usr]! as YamlMap; - usrTypeMappings[usr] = ImportedType( - libraryImport, value['name'] as String, value['name'] as String); + final name = value['name'] as String; + usrTypeMappings[usr] = ImportedType(libraryImport, name, name, name); } } @@ -134,12 +134,13 @@ Map makeImportTypeMapping( final lib = rawTypeMappings[key]![0]; final cType = rawTypeMappings[key]![1]; final dartType = rawTypeMappings[key]![2]; + final nativeType = key; if (strings.predefinedLibraryImports.containsKey(lib)) { - typeMappings[key] = - ImportedType(strings.predefinedLibraryImports[lib]!, cType, dartType); + typeMappings[key] = ImportedType( + strings.predefinedLibraryImports[lib]!, cType, dartType, nativeType); } else if (libraryImportsMap.containsKey(lib)) { typeMappings[key] = - ImportedType(libraryImportsMap[lib]!, cType, dartType); + ImportedType(libraryImportsMap[lib]!, cType, dartType, nativeType); } else { throw Exception("Please declare $lib under library-imports."); } @@ -201,7 +202,7 @@ Type makeTypeFromRawVarArgType( throw Exception('Please declare $lib in library-imports.'); } final typeName = rawVarArgTypeSplit[1].replaceAll(' ', ''); - baseType = ImportedType(libraryImport, typeName, typeName); + baseType = ImportedType(libraryImport, typeName, typeName, typeName); } else { throw Exception( 'Invalid type $rawVarArgType : Expected 0 or 1 .(dot) separators.'); @@ -431,11 +432,15 @@ String llvmPathExtractor(List value) { OutputConfig outputExtractor( dynamic value, String? configFilename, PackageConfig? packageConfig) { if (value is String) { - return OutputConfig(_normalizePath(value, configFilename), null); + return OutputConfig(_normalizePath(value, configFilename), null, null); } value = value as Map; return OutputConfig( _normalizePath((value)[strings.bindings] as String, configFilename), + value.containsKey(strings.objCBindings) + ? _normalizePath( + (value)[strings.objCBindings] as String, configFilename) + : null, value.containsKey(strings.symbolFile) ? symbolFileOutputExtractor( value[strings.symbolFile], configFilename, packageConfig) diff --git a/pkgs/ffigen/lib/src/executables/ffigen.dart b/pkgs/ffigen/lib/src/executables/ffigen.dart index dde472381..8c1eab8f7 100644 --- a/pkgs/ffigen/lib/src/executables/ffigen.dart +++ b/pkgs/ffigen/lib/src/executables/ffigen.dart @@ -62,6 +62,12 @@ Future main(List args) async { _logger .info(successPen('Finished, Bindings generated in ${gen.absolute.path}')); + final objCGen = File(config.outputObjC); + if (library.generateObjCFile(objCGen)) { + _logger.info(successPen('Finished, Objective C bindings generated ' + 'in ${objCGen.absolute.path}')); + } + if (config.symbolFile != null) { final symbolFileGen = File(config.symbolFile!.output); library.generateSymbolOutputFile( diff --git a/pkgs/ffigen/lib/src/strings.dart b/pkgs/ffigen/lib/src/strings.dart index 1255e27f3..17f8add43 100644 --- a/pkgs/ffigen/lib/src/strings.dart +++ b/pkgs/ffigen/lib/src/strings.dart @@ -30,7 +30,8 @@ String get dynamicLibParentName => Platform.isWindows ? 'bin' : 'lib'; const output = 'output'; // Sub-keys of output. -const bindings = "bindings"; +const bindings = 'bindings'; +const objCBindings = 'objc-bindings'; const symbolFile = 'symbol-file'; const language = 'language'; diff --git a/pkgs/ffigen/test/native_objc_test/.gitignore b/pkgs/ffigen/test/native_objc_test/.gitignore index ad4daf662..24a8c4487 100644 --- a/pkgs/ffigen/test/native_objc_test/.gitignore +++ b/pkgs/ffigen/test/native_objc_test/.gitignore @@ -1,2 +1,5 @@ *_bindings.dart +*_bindings.m +*_bindings.dart.m *-Swift.h +*.o diff --git a/pkgs/ffigen/test/native_objc_test/block_config.yaml b/pkgs/ffigen/test/native_objc_test/block_config.yaml index 62be7a2ce..c52f0320b 100644 --- a/pkgs/ffigen/test/native_objc_test/block_config.yaml +++ b/pkgs/ffigen/test/native_objc_test/block_config.yaml @@ -1,11 +1,24 @@ name: BlockTestObjCLibrary description: 'Tests calling Objective-C blocks.' language: objc -output: 'block_bindings.dart' +output: + bindings: 'block_bindings.dart' + objc-bindings: 'block_bindings.m' exclude-all-by-default: true objc-interfaces: include: - BlockTester +typedefs: + include: + - IntBlock + - FloatBlock + - DoubleBlock + - Vec4Block + - VoidBlock + - ObjectBlock + - NullableObjectBlock + - BlockBlock + - ListenerBlock headers: entry-points: - 'block_test.m' diff --git a/pkgs/ffigen/test/native_objc_test/block_test.dart b/pkgs/ffigen/test/native_objc_test/block_test.dart index 710d4e174..892648a01 100644 --- a/pkgs/ffigen/test/native_objc_test/block_test.dart +++ b/pkgs/ffigen/test/native_objc_test/block_test.dart @@ -21,16 +21,6 @@ import '../test_utils.dart'; import 'block_bindings.dart'; import 'util.dart'; -// The generated block names are stable but verbose, so typedef them. -typedef IntBlock = ObjCBlock_Int32_Int32; -typedef FloatBlock = ObjCBlock_ffiFloat_ffiFloat; -typedef DoubleBlock = ObjCBlock_ffiDouble_ffiDouble; -typedef Vec4Block = ObjCBlock_Vec4_Vec4; -typedef VoidBlock = ObjCBlock_ffiVoid; -typedef ObjectBlock = ObjCBlock_DummyObject_DummyObject; -typedef NullableObjectBlock = ObjCBlock_DummyObject_DummyObject1; -typedef BlockBlock = ObjCBlock_Int32Int32_Int32Int32; - void main() { group('Blocks', () { setUpAll(() { @@ -57,7 +47,7 @@ void main() { test('Block from function pointer', () { final block = - IntBlock.fromFunctionPointer(Pointer.fromFunction(_add100, 999)); + DartIntBlock.fromFunctionPointer(Pointer.fromFunction(_add100, 999)); final blockTester = BlockTester.makeFromBlock_(block); blockTester.pokeBlock(); expect(blockTester.call_(123), 223); @@ -69,7 +59,7 @@ void main() { } test('Block from function', () { - final block = IntBlock.fromFunction(makeAdder(4000)); + final block = DartIntBlock.fromFunction(makeAdder(4000)); final blockTester = BlockTester.makeFromBlock_(block); blockTester.pokeBlock(); expect(blockTester.call_(123), 4123); @@ -79,7 +69,7 @@ void main() { test('Listener block same thread', () async { final hasRun = Completer(); int value = 0; - final block = VoidBlock.listener(() { + final block = DartVoidBlock.listener(() { value = 123; hasRun.complete(); }); @@ -93,7 +83,7 @@ void main() { test('Listener block new thread', () async { final hasRun = Completer(); int value = 0; - final block = VoidBlock.listener(() { + final block = DartVoidBlock.listener(() { value = 123; hasRun.complete(); }); @@ -106,7 +96,7 @@ void main() { }); test('Float block', () { - final block = FloatBlock.fromFunction((double x) { + final block = DartFloatBlock.fromFunction((double x) { return x + 4.56; }); expect(block(1.23), closeTo(5.79, 1e-6)); @@ -114,7 +104,7 @@ void main() { }); test('Double block', () { - final block = DoubleBlock.fromFunction((double x) { + final block = DartDoubleBlock.fromFunction((double x) { return x + 4.56; }); expect(block(1.23), closeTo(5.79, 1e-6)); @@ -132,7 +122,7 @@ void main() { final tempPtr = arena(); final temp = tempPtr.ref; - final block = Vec4Block.fromFunction((Vec4 v) { + final block = DartVec4Block.fromFunction((Vec4 v) { // Twiddle the Vec4 components. temp.x = v.y; temp.y = v.z; @@ -159,7 +149,7 @@ void main() { test('Object block', () { bool isCalled = false; - final block = ObjectBlock.fromFunction((DummyObject x) { + final block = DartObjectBlock.fromFunction((DummyObject x) { isCalled = true; return x; }); @@ -178,7 +168,7 @@ void main() { test('Nullable object block', () { bool isCalled = false; - final block = NullableObjectBlock.fromFunction((DummyObject? x) { + final block = DartNullableObjectBlock.fromFunction((DummyObject? x) { isCalled = true; return x; }); @@ -200,13 +190,13 @@ void main() { }); test('Block block', () { - final blockBlock = BlockBlock.fromFunction((IntBlock intBlock) { - return IntBlock.fromFunction((int x) { + final blockBlock = DartBlockBlock.fromFunction((DartIntBlock intBlock) { + return DartIntBlock.fromFunction((int x) { return 3 * intBlock(x); }); }); - final intBlock = IntBlock.fromFunction((int x) { + final intBlock = DartIntBlock.fromFunction((int x) { return 5 * x; }); final result1 = blockBlock(intBlock); @@ -219,7 +209,7 @@ void main() { test('Native block block', () { final blockBlock = BlockTester.newBlockBlock_(7); - final intBlock = IntBlock.fromFunction((int x) { + final intBlock = DartIntBlock.fromFunction((int x) { return 5 * x; }); final result1 = blockBlock(intBlock); @@ -231,7 +221,7 @@ void main() { Pointer funcPointerBlockRefCountTest() { final block = - IntBlock.fromFunctionPointer(Pointer.fromFunction(_add100, 999)); + DartIntBlock.fromFunctionPointer(Pointer.fromFunction(_add100, 999)); expect( internal_for_testing.blockHasRegisteredClosure(block.pointer), false); expect(blockRetainCount(block.pointer), 1); @@ -245,7 +235,7 @@ void main() { }); Pointer funcBlockRefCountTest() { - final block = IntBlock.fromFunction(makeAdder(4000)); + final block = DartIntBlock.fromFunction(makeAdder(4000)); expect( internal_for_testing.blockHasRegisteredClosure(block.pointer), true); expect(blockRetainCount(block.pointer), 1); @@ -262,7 +252,7 @@ void main() { }); Pointer blockManualRetainRefCountTest() { - final block = IntBlock.fromFunction(makeAdder(4000)); + final block = DartIntBlock.fromFunction(makeAdder(4000)); expect( internal_for_testing.blockHasRegisteredClosure(block.pointer), true); expect(blockRetainCount(block.pointer), 1); @@ -272,7 +262,7 @@ void main() { } int blockManualRetainRefCountTest2(Pointer rawBlock) { - final block = IntBlock.castFromPointer(rawBlock.cast(), + final block = DartIntBlock.castFromPointer(rawBlock.cast(), retain: false, release: true); return blockRetainCount(block.pointer); } @@ -291,11 +281,11 @@ void main() { (Pointer, Pointer, Pointer) blockBlockDartCallRefCountTest() { - final inputBlock = IntBlock.fromFunction((int x) { + final inputBlock = DartIntBlock.fromFunction((int x) { return 5 * x; }); - final blockBlock = BlockBlock.fromFunction((IntBlock intBlock) { - return IntBlock.fromFunction((int x) { + final blockBlock = DartBlockBlock.fromFunction((DartIntBlock intBlock) { + return DartIntBlock.fromFunction((int x) { return 3 * intBlock(x); }); }); @@ -306,31 +296,49 @@ void main() { // One reference held by inputBlock object, another bound to the // outputBlock lambda. expect(blockRetainCount(inputBlock.pointer), 2); + expect( + internal_for_testing + .blockHasRegisteredClosure(inputBlock.pointer.cast()), + true); expect(blockRetainCount(blockBlock.pointer), 1); + expect( + internal_for_testing + .blockHasRegisteredClosure(blockBlock.pointer.cast()), + true); expect(blockRetainCount(outputBlock.pointer), 1); + expect( + internal_for_testing + .blockHasRegisteredClosure(outputBlock.pointer.cast()), + true); return (inputBlock.pointer, blockBlock.pointer, outputBlock.pointer); } - test('Calling a block block from Dart has correct ref counting', () { + test('Calling a block block from Dart has correct ref counting', () async { final (inputBlock, blockBlock, outputBlock) = blockBlockDartCallRefCountTest(); doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive. + doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive. - // This leaks because block functions aren't cleaned up at the moment. - // TODO(https://github.com/dart-lang/ffigen/issues/428): Fix this leak. - expect(blockRetainCount(inputBlock), 1); - + expect(blockRetainCount(inputBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), + false); expect(blockRetainCount(blockBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(blockBlock.cast()), + false); expect(blockRetainCount(outputBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(outputBlock.cast()), + false); }); (Pointer, Pointer, Pointer) blockBlockObjCCallRefCountTest() { late Pointer inputBlock; - final blockBlock = BlockBlock.fromFunction((IntBlock intBlock) { + final blockBlock = DartBlockBlock.fromFunction((DartIntBlock intBlock) { inputBlock = intBlock.pointer; - return IntBlock.fromFunction((int x) { + return DartIntBlock.fromFunction((int x) { return 3 * intBlock(x); }); }); @@ -338,28 +346,44 @@ void main() { expect(outputBlock(1), 6); doGC(); - expect(blockRetainCount(inputBlock), 2); + expect(blockRetainCount(inputBlock), 1); + expect(internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), + false); expect(blockRetainCount(blockBlock.pointer), 1); + expect( + internal_for_testing + .blockHasRegisteredClosure(blockBlock.pointer.cast()), + true); expect(blockRetainCount(outputBlock.pointer), 1); + expect( + internal_for_testing + .blockHasRegisteredClosure(outputBlock.pointer.cast()), + true); return (inputBlock, blockBlock.pointer, outputBlock.pointer); } - test('Calling a block block from ObjC has correct ref counting', () { + test('Calling a block block from ObjC has correct ref counting', () async { final (inputBlock, blockBlock, outputBlock) = blockBlockObjCCallRefCountTest(); doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive. + doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive. - // This leaks because block functions aren't cleaned up at the moment. - // TODO(https://github.com/dart-lang/ffigen/issues/428): Fix this leak. - expect(blockRetainCount(inputBlock), 2); - + expect(blockRetainCount(inputBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(inputBlock.cast()), + false); expect(blockRetainCount(blockBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(blockBlock.cast()), + false); expect(blockRetainCount(outputBlock), 0); + expect(internal_for_testing.blockHasRegisteredClosure(outputBlock.cast()), + false); }); (Pointer, Pointer, Pointer) nativeBlockBlockDartCallRefCountTest() { - final inputBlock = IntBlock.fromFunction((int x) { + final inputBlock = DartIntBlock.fromFunction((int x) { return 5 * x; }); final blockBlock = BlockTester.newBlockBlock_(7); @@ -410,7 +434,7 @@ void main() { inputCounter.value = 0; outputCounter.value = 0; - final block = ObjectBlock.fromFunction((DummyObject x) { + final block = DartObjectBlock.fromFunction((DummyObject x) { return DummyObject.newWithCounter_(outputCounter); }); @@ -438,7 +462,7 @@ void main() { inputCounter.value = 0; outputCounter.value = 0; - final block = ObjectBlock.fromFunction((DummyObject x) { + final block = DartObjectBlock.fromFunction((DummyObject x) { x.setCounter_(inputCounter); return DummyObject.newWithCounter_(outputCounter); }); @@ -453,21 +477,54 @@ void main() { test( 'Objects received and returned by native blocks have correct ref counts', () { - using((Arena arena) { + using((Arena arena) async { final (inputCounter, outputCounter) = objectNativeBlockRefCountTest(arena); doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive + doGC(); - // This leaks because block functions aren't cleaned up at the moment. - // TODO(https://github.com/dart-lang/ffigen/issues/428): Fix this leak. - expect(inputCounter.value, 1); - + expect(inputCounter.value, 0); expect(outputCounter.value, 0); }); }); + Future<(Pointer, Pointer)> + listenerBlockArgumentRetentionTest() async { + final hasRun = Completer(); + late DartIntBlock inputBlock; + final blockBlock = DartListenerBlock.listener((DartIntBlock intBlock) { + expect(blockRetainCount(intBlock.pointer), 1); + inputBlock = intBlock; + hasRun.complete(); + }); + + final thread = BlockTester.callWithBlockOnNewThread_(blockBlock); + thread.start(); + + await hasRun.future; + expect(inputBlock(123), 12300); + doGC(); + + expect(blockRetainCount(inputBlock.pointer), 1); + expect(blockRetainCount(blockBlock.pointer), 1); + return (inputBlock.pointer, blockBlock.pointer); + } + + test('Listener block arguments are not prematurely destroyed', () async { + // https://github.com/dart-lang/native/issues/835 + final (inputBlock, blockBlock) = + await listenerBlockArgumentRetentionTest(); + doGC(); + await Future.delayed(Duration.zero); // Let dispose message arrive. + doGC(); + + expect(blockRetainCount(inputBlock), 0); + expect(blockRetainCount(blockBlock), 0); + }); + test('Block fields have sensible values', () { - final block = IntBlock.fromFunction(makeAdder(4000)); + final block = DartIntBlock.fromFunction(makeAdder(4000)); final blockPtr = block.pointer; expect(blockPtr.ref.isa, isNot(0)); expect(blockPtr.ref.flags, isNot(0)); // Set by Block_copy. diff --git a/pkgs/ffigen/test/native_objc_test/block_test.m b/pkgs/ffigen/test/native_objc_test/block_test.m index d27a97c82..a5b61bde6 100644 --- a/pkgs/ffigen/test/native_objc_test/block_test.m +++ b/pkgs/ffigen/test/native_objc_test/block_test.m @@ -55,6 +55,7 @@ - (void)dealloc { typedef DummyObject* (^ObjectBlock)(DummyObject*); typedef DummyObject* _Nullable (^NullableObjectBlock)(DummyObject* _Nullable); typedef IntBlock (^BlockBlock)(IntBlock); +typedef void (^ListenerBlock)(IntBlock); // Wrapper around a block, so that our Dart code can test creating and invoking // blocks in Objective C code. @@ -68,11 +69,13 @@ - (IntBlock)getBlock; - (void)pokeBlock; + (void)callOnSameThread:(VoidBlock)block; + (NSThread*)callOnNewThread:(VoidBlock)block; ++ (NSThread*)callWithBlockOnNewThread:(ListenerBlock)block; + (float)callFloatBlock:(FloatBlock)block; + (double)callDoubleBlock:(DoubleBlock)block; + (Vec4)callVec4Block:(Vec4Block)block; + (DummyObject*)callObjectBlock:(ObjectBlock)block NS_RETURNS_RETAINED; + (nullable DummyObject*)callNullableObjectBlock:(NullableObjectBlock)block; ++ (void)callListener:(ListenerBlock)block; + (IntBlock)newBlock:(BlockBlock)block withMult:(int)mult; + (BlockBlock)newBlockBlock:(int)mult; @end @@ -113,6 +116,29 @@ + (NSThread*)callOnNewThread:(VoidBlock)block { return [[NSThread alloc] initWithBlock: block]; } ++ (void)callListener:(ListenerBlock)block { + // Note: This method is invoked on a background thread. + + // This multiplier is defined in a bound variable rather than inside the block + // to force the compiler to make a real lambda style block. Without this, we + // get a _NSConcreteGlobalBlock (essentially a static function pointer), which + // always has a ref count of 0, so we can't test the ref counting. + int mult = 100; + + IntBlock inputBlock = [^int(int x) { + return mult * x; + } copy]; + // ^ copy this stack allocated block to the heap. + block(inputBlock); + [inputBlock release]; // Release the reference held by this scope. +} + ++ (NSThread*)callWithBlockOnNewThread:(ListenerBlock)block { + return [[NSThread alloc] initWithTarget:[BlockTester class] + selector:@selector(callListener:) + object:block]; +} + + (float)callFloatBlock:(FloatBlock)block { return block(1.23); } @@ -131,7 +157,10 @@ + (Vec4)callVec4Block:(Vec4Block)block { } + (DummyObject*)callObjectBlock:(ObjectBlock)block NS_RETURNS_RETAINED { - return block([DummyObject new]); + DummyObject* inputObject = [DummyObject new]; + DummyObject* outputObject = block(inputObject); + [inputObject release]; // Release the reference held by this scope. + return outputObject; } + (nullable DummyObject*)callNullableObjectBlock:(NullableObjectBlock)block { @@ -139,10 +168,13 @@ + (nullable DummyObject*)callNullableObjectBlock:(NullableObjectBlock)block { } + (IntBlock)newBlock:(BlockBlock)block withMult:(int)mult { - return block([^int(int x) { + IntBlock inputBlock = [^int(int x) { return mult * x; - } copy]); + } copy]; // ^ copy this stack allocated block to the heap. + IntBlock outputBlock = block(inputBlock); + [inputBlock release]; // Release the reference held by this scope. + return outputBlock; } + (BlockBlock)newBlockBlock:(int)mult { diff --git a/pkgs/ffigen/test/native_objc_test/setup.dart b/pkgs/ffigen/test/native_objc_test/setup.dart index faa2d4329..c08961ec8 100644 --- a/pkgs/ffigen/test/native_objc_test/setup.dart +++ b/pkgs/ffigen/test/native_objc_test/setup.dart @@ -5,18 +5,8 @@ import 'dart:async'; import 'dart:io'; -Future _buildLib(String input, String output) async { - final args = [ - '-shared', - '-fpic', - '-x', - 'objective-c', - input, - '-framework', - 'Foundation', - '-o', - output, - ]; +Future _runClang(List flags, String output) async { + final args = [...flags, '-o', output]; final process = await Process.start('clang', args); unawaited(stdout.addStream(process.stdout)); unawaited(stderr.addStream(process.stderr)); @@ -27,6 +17,23 @@ Future _buildLib(String input, String output) async { print('Generated file: $output'); } +Future _buildObject(String input) async { + final output = '$input.o'; + await _runClang(['-x', 'objective-c', '-c', input, '-fpic'], output); + return output; +} + +Future _linkLib(List inputs, String output) => + _runClang(['-shared', '-framework', 'Foundation', ...inputs], output); + +Future _buildLib(List inputs, String output) async { + final objFiles = []; + for (final input in inputs) { + objFiles.add(await _buildObject(input)); + } + await _linkLib(objFiles, output); +} + Future _buildSwift( String input, String outputHeader, String outputLib) async { final args = [ @@ -82,27 +89,35 @@ List _getTestNames() { } Future build(List testNames) async { - print('Building Dynamic Library for Objective C Native Tests...'); - for (final name in testNames) { - final mFile = '${name}_test.m'; - if (await File(mFile).exists()) { - await _buildLib(mFile, '${name}_test.dylib'); - } - } - + // Swift build comes first because the generated header is consumed by ffigen. print('Building Dynamic Library and Header for Swift Tests...'); for (final name in testNames) { final swiftFile = '${name}_test.swift'; - if (await File(swiftFile).exists()) { + if (File(swiftFile).existsSync()) { await _buildSwift( swiftFile, '${name}_test-Swift.h', '${name}_test.dylib'); } } + // Ffigen comes next because it may generate an ObjC file that is compiled + // into the dylib. print('Generating Bindings for Objective C Native Tests...'); for (final name in testNames) { await _generateBindings('${name}_config.yaml'); } + + // Finally we build the dylib. + print('Building Dynamic Library for Objective C Native Tests...'); + for (final name in testNames) { + final mFile = '${name}_test.m'; + if (File(mFile).existsSync()) { + final bindingMFile = '${name}_bindings.m'; + await _buildLib([ + mFile, + if (File(bindingMFile).existsSync()) bindingMFile, + ], '${name}_test.dylib'); + } + } } Future clean(List testNames) async { @@ -110,15 +125,17 @@ Future clean(List testNames) async { final filenames = [ for (final name in testNames) ...[ '${name}_bindings.dart', + '${name}_bindings.m', + '${name}_bindings.o', '${name}_test_bindings.dart', + '${name}_test.o', '${name}_test.dylib' ], ]; - Future.wait(filenames.map((fileName) async { - final file = File(fileName); - final exists = await file.exists(); - if (exists) await file.delete(); - })); + for (final filename in filenames) { + final file = File(filename); + if (file.existsSync()) file.deleteSync(); + } } Future main(List arguments) async { diff --git a/pkgs/objective_c/example/pubspec.lock b/pkgs/objective_c/example/pubspec.lock index f0ad922c3..8a8169699 100644 --- a/pkgs/objective_c/example/pubspec.lock +++ b/pkgs/objective_c/example/pubspec.lock @@ -79,18 +79,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -119,25 +119,25 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" objective_c: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.0.0" + version: "1.1.0-wip" path: dependency: transitive description: @@ -203,10 +203,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" vector_math: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: vm_service - sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + sha256: "360c4271613beb44db559547d02f8b0dc044741d0eeb9aa6ccdb47e8ec54c63a" url: "https://pub.dev" source: hosted - version: "14.0.0" + version: "14.2.3" yaml: dependency: transitive description: