From 14f0e40c35b0530c180def9c551bcb3e4405899f Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Wed, 1 Nov 2023 19:02:51 -0700 Subject: [PATCH] ObjC static functions (#633) * WIP static functions * Revert old code gen changes * Refactor function code gen * Arg and return conversions * fix bugs * Fix analysis, add more tests * fmt * Fix autorelease pool test * Handle NS_RETURNS_RETAINED * Daco's comments --- CHANGELOG.md | 1 + lib/src/code_generator/func.dart | 94 +++++++---- lib/src/code_generator/func_type.dart | 5 + lib/src/code_generator/objc_block.dart | 3 + lib/src/code_generator/objc_interface.dart | 3 + lib/src/code_generator/objc_nullable.dart | 3 + lib/src/code_generator/pointer.dart | 3 + lib/src/code_generator/type.dart | 6 + lib/src/code_generator/typealias.dart | 3 + .../sub_parsers/functiondecl_parser.dart | 27 +++ .../automated_ref_count_test.m | 10 +- test/native_objc_test/block_config.yaml | 3 + test/native_objc_test/block_test.dart | 62 +++---- test/native_objc_test/block_test.m | 30 +--- test/native_objc_test/static_func_config.yaml | 18 ++ .../static_func_native_config.yaml | 19 +++ .../static_func_native_test.dart | 156 ++++++++++++++++++ test/native_objc_test/static_func_test.dart | 156 ++++++++++++++++++ test/native_objc_test/static_func_test.m | 63 +++++++ test/native_objc_test/util.h | 35 ++++ 20 files changed, 603 insertions(+), 97 deletions(-) create mode 100644 test/native_objc_test/static_func_config.yaml create mode 100644 test/native_objc_test/static_func_native_config.yaml create mode 100644 test/native_objc_test/static_func_native_test.dart create mode 100644 test/native_objc_test/static_func_test.dart create mode 100644 test/native_objc_test/static_func_test.m create mode 100644 test/native_objc_test/util.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de820db..a1aef8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - When generating typedefs for `Pointer>`, also generate a typedef for the `Function`. - Use Dart wrapper types in args and returns of ObjCBlocks. +- Use Dart wrapper types in args and returns of static functions. - Bump min SDK version to 3.2.0-114.0.dev. - Renamed `asset` to `assetId` for `ffi-native`. diff --git a/lib/src/code_generator/func.dart b/lib/src/code_generator/func.dart index cbdebe7e..766cc289 100644 --- a/lib/src/code_generator/func.dart +++ b/lib/src/code_generator/func.dart @@ -42,6 +42,7 @@ class Func extends LookUpBinding { final bool exposeSymbolAddress; final bool exposeFunctionTypedefs; final bool isLeaf; + final bool objCReturnsRetained; final FfiNativeConfig ffiNativeConfig; late final String funcPointerName; @@ -61,6 +62,7 @@ class Func extends LookUpBinding { this.exposeSymbolAddress = false, this.exposeFunctionTypedefs = false, this.isLeaf = false, + this.objCReturnsRetained = false, super.isInternal, this.ffiNativeConfig = const FfiNativeConfig(enabled: false), }) : functionType = FunctionType( @@ -93,8 +95,6 @@ class Func extends LookUpBinding { BindingString toBindingString(Writer w) { final s = StringBuffer(); final enclosingFuncName = name; - final funcVarName = w.wrapperLevelUniqueNamer.makeUnique('_$name'); - funcPointerName = w.wrapperLevelUniqueNamer.makeUnique('_${name}Ptr'); if (dartDoc != null) { s.write(makeDartDoc(dartDoc!)); @@ -109,37 +109,71 @@ class Func extends LookUpBinding { functionType.getCType(w, writeArgumentNames: false); final dartType = _exposedFunctionTypealias?.getFfiDartType(w) ?? functionType.getFfiDartType(w, writeArgumentNames: false); + final needsWrapper = !functionType.sameDartAndFfiDartType && !isInternal; + + final isLeafString = isLeaf ? 'isLeaf:true' : ''; + final funcVarName = w.wrapperLevelUniqueNamer.makeUnique('_$name'); + final ffiReturnType = functionType.returnType.getFfiDartType(w); + final ffiArgDeclString = functionType.dartTypeParameters + .map((p) => '${p.type.getFfiDartType(w)} ${p.name},\n') + .join(''); + + late final String dartReturnType; + late final String dartArgDeclString; + late final String funcImplCall; + if (needsWrapper) { + dartReturnType = functionType.returnType.getDartType(w); + dartArgDeclString = functionType.dartTypeParameters + .map((p) => '${p.type.getDartType(w)} ${p.name},\n') + .join(''); + + final argString = functionType.dartTypeParameters + .map((p) => + '${p.type.convertDartTypeToFfiDartType(w, p.name, objCRetain: false)},\n') + .join(''); + funcImplCall = functionType.returnType.convertFfiDartTypeToDartType( + w, + '$funcVarName($argString)', + ffiNativeConfig.enabled ? 'lib' : 'this', + objCRetain: !objCReturnsRetained, + ); + } else { + dartReturnType = ffiReturnType; + dartArgDeclString = ffiArgDeclString; + final argString = + functionType.dartTypeParameters.map((p) => '${p.name},\n').join(''); + funcImplCall = '$funcVarName($argString)'; + } if (ffiNativeConfig.enabled) { final assetString = ffiNativeConfig.assetId != null ? ", assetId: '${ffiNativeConfig.assetId}'" : ''; - final isLeafString = isLeaf ? ', isLeaf: true' : ''; - s.write( - "@${w.ffiLibraryPrefix}.Native<$cType>(symbol: '$originalName'$assetString$isLeafString)\n"); - - s.write( - 'external ${functionType.returnType.getFfiDartType(w)} $enclosingFuncName(\n'); - for (final p in functionType.dartTypeParameters) { - s.write(' ${p.type.getFfiDartType(w)} ${p.name},\n'); + final nativeFuncName = needsWrapper ? funcVarName : enclosingFuncName; + s.write(''' +@${w.ffiLibraryPrefix}.Native<$cType>(symbol: '$originalName'$assetString$isLeafString) +external $ffiReturnType $nativeFuncName($ffiArgDeclString); + +'''); + if (needsWrapper) { + final libArg = functionType.returnType.sameDartAndFfiDartType + ? '' + : '${w.className} lib, '; + s.write(''' +$dartReturnType $enclosingFuncName($libArg$dartArgDeclString) => $funcImplCall; + +'''); } - s.write(');\n\n'); } else { + funcPointerName = w.wrapperLevelUniqueNamer.makeUnique('_${name}Ptr'); + // Write enclosing function. - s.write( - '${functionType.returnType.getFfiDartType(w)} $enclosingFuncName(\n'); - for (final p in functionType.dartTypeParameters) { - s.write(' ${p.type.getFfiDartType(w)} ${p.name},\n'); - } - s.write(') {\n'); - s.write('return $funcVarName'); + s.write(''' +$dartReturnType $enclosingFuncName($dartArgDeclString) { + return $funcImplCall; +} - s.write('(\n'); - for (final p in functionType.dartTypeParameters) { - s.write(' ${p.name},\n'); - } - s.write(' );\n'); - s.write('}\n'); +'''); if (exposeSymbolAddress) { // Add to SymbolAddress in writer. @@ -150,12 +184,14 @@ class Func extends LookUpBinding { ptrName: funcPointerName, ); } + // Write function pointer. - s.write( - "late final $funcPointerName = ${w.lookupFuncIdentifier}<${w.ffiLibraryPrefix}.NativeFunction<$cType>>('$originalName');\n"); - final isLeafString = isLeaf ? 'isLeaf:true' : ''; - s.write( - 'late final $funcVarName = $funcPointerName.asFunction<$dartType>($isLeafString);\n\n'); + s.write(''' +late final $funcPointerName = ${w.lookupFuncIdentifier}< + ${w.ffiLibraryPrefix}.NativeFunction<$cType>>('$originalName'); +late final $funcVarName = $funcPointerName.asFunction<$dartType>($isLeafString); + +'''); } return BindingString(type: BindingStringType.func, string: s.toString()); diff --git a/lib/src/code_generator/func_type.dart b/lib/src/code_generator/func_type.dart index 1e734bc4..49dcf605 100644 --- a/lib/src/code_generator/func_type.dart +++ b/lib/src/code_generator/func_type.dart @@ -78,6 +78,11 @@ class FunctionType extends Type { returnType.sameDartAndCType && dartTypeParameters.every((p) => p.type.sameDartAndCType); + @override + bool get sameDartAndFfiDartType => + returnType.sameDartAndFfiDartType && + dartTypeParameters.every((p) => p.type.sameDartAndFfiDartType); + @override String toString() => _getTypeImpl(false, (Type t) => t.toString()); diff --git a/lib/src/code_generator/objc_block.dart b/lib/src/code_generator/objc_block.dart index b61efc7c..d16ab112 100644 --- a/lib/src/code_generator/objc_block.dart +++ b/lib/src/code_generator/objc_block.dart @@ -228,6 +228,9 @@ _id.ref.invoke.cast<$natTrampFnType>().asFunction<$trampFuncFfiDartType>()( @override bool get sameDartAndCType => false; + @override + bool get sameDartAndFfiDartType => false; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/code_generator/objc_interface.dart b/lib/src/code_generator/objc_interface.dart index 2ed1f10e..495cdf18 100644 --- a/lib/src/code_generator/objc_interface.dart +++ b/lib/src/code_generator/objc_interface.dart @@ -401,6 +401,9 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} { @override bool get sameDartAndCType => false; + @override + bool get sameDartAndFfiDartType => false; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/code_generator/objc_nullable.dart b/lib/src/code_generator/objc_nullable.dart index a8d8c522..f3c0b877 100644 --- a/lib/src/code_generator/objc_nullable.dart +++ b/lib/src/code_generator/objc_nullable.dart @@ -44,6 +44,9 @@ class ObjCNullable extends Type { @override bool get sameDartAndCType => false; + @override + bool get sameDartAndFfiDartType => false; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/code_generator/pointer.dart b/lib/src/code_generator/pointer.dart index dcaf5973..30e2e6ba 100644 --- a/lib/src/code_generator/pointer.dart +++ b/lib/src/code_generator/pointer.dart @@ -87,6 +87,9 @@ class ObjCObjectPointer extends PointerType { @override bool get sameDartAndCType => false; + @override + bool get sameDartAndFfiDartType => false; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/code_generator/type.dart b/lib/src/code_generator/type.dart index fff7f0f0..c1d1e227 100644 --- a/lib/src/code_generator/type.dart +++ b/lib/src/code_generator/type.dart @@ -54,6 +54,9 @@ abstract class Type { /// Returns whether the dart type and C type string are same. bool get sameDartAndCType => sameFfiDartAndCType; + /// Returns whether the dart type and FFI dart type string are same. + bool get sameDartAndFfiDartType => true; + /// Returns generated Dart code that converts the given value from its /// DartType to its FfiDartType. /// @@ -138,6 +141,9 @@ abstract class BindingType extends NoLookUpBinding implements Type { @override bool get sameDartAndCType => sameFfiDartAndCType; + @override + bool get sameDartAndFfiDartType => true; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/code_generator/typealias.dart b/lib/src/code_generator/typealias.dart index fe971a54..e31d1488 100644 --- a/lib/src/code_generator/typealias.dart +++ b/lib/src/code_generator/typealias.dart @@ -158,6 +158,9 @@ class Typealias extends BindingType { @override bool get sameDartAndCType => type.sameDartAndCType; + @override + bool get sameDartAndFfiDartType => type.sameDartAndFfiDartType; + @override String convertDartTypeToFfiDartType( Writer w, diff --git a/lib/src/header_parser/sub_parsers/functiondecl_parser.dart b/lib/src/header_parser/sub_parsers/functiondecl_parser.dart index f9d1121d..c2291b4a 100644 --- a/lib/src/header_parser/sub_parsers/functiondecl_parser.dart +++ b/lib/src/header_parser/sub_parsers/functiondecl_parser.dart @@ -2,6 +2,8 @@ // 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 'dart:ffi'; + import 'package:ffigen/src/code_generator.dart'; import 'package:ffigen/src/config_provider/config_types.dart'; import 'package:ffigen/src/header_parser/data.dart'; @@ -20,6 +22,7 @@ class _ParserFunc { List funcs = []; bool incompleteStructParameter = false; bool unimplementedParameterType = false; + bool objCReturnsRetained = false; _ParserFunc(); } @@ -69,6 +72,13 @@ List? parseFunctionDeclaration(clang_types.CXCursor cursor) { return _stack.pop().funcs; } + // Look for any annotations on the function. + clang.clang_visitChildren( + cursor, + _parseAnnotationVisitorPtr ??= Pointer.fromFunction( + _parseAnnotationVisitor, exceptional_visitor_return), + nullptr); + // Initialized with a single value with no prefix and empty var args. var varArgFunctions = [VarArgFunction('', [])]; if (config.varArgFunctions.containsKey(funcName)) { @@ -97,6 +107,7 @@ List? parseFunctionDeclaration(clang_types.CXCursor cursor) { exposeFunctionTypedefs: config.exposeFunctionTypedefs.shouldInclude(funcName), isLeaf: config.leafFunctions.shouldInclude(funcName), + objCReturnsRetained: _stack.top.objCReturnsRetained, ffiNativeConfig: config.ffiNativeConfig, )); } @@ -147,3 +158,19 @@ List _getParameters(clang_types.CXCursor cursor, String funcName) { Type _getParameterType(clang_types.CXCursor cursor) { return cursor.toCodeGenType(); } + +Pointer< + NativeFunction< + Int32 Function( + clang_types.CXCursor, clang_types.CXCursor, Pointer)>>? + _parseAnnotationVisitorPtr; +int _parseAnnotationVisitor(clang_types.CXCursor cursor, + clang_types.CXCursor parent, Pointer clientData) { + switch (cursor.kind) { + case clang_types.CXCursorKind.CXCursor_NSReturnsRetained: + _stack.top.objCReturnsRetained = true; + break; + default: + } + return clang_types.CXChildVisitResult.CXChildVisit_Continue; +} diff --git a/test/native_objc_test/automated_ref_count_test.m b/test/native_objc_test/automated_ref_count_test.m index d2493896..cf8ab3a1 100644 --- a/test/native_objc_test/automated_ref_count_test.m +++ b/test/native_objc_test/automated_ref_count_test.m @@ -89,12 +89,14 @@ - (ArcTestObject*)returnsRetained NS_RETURNS_RETAINED { @end -id createAutoreleasePool() { - return [NSAutoreleasePool new]; +// Pass around the NSAutoreleasePool as a void* to bypass the Dart wrappers so +// that we can precisely control the life cycle. +void* createAutoreleasePool() { + return (void*)[NSAutoreleasePool new]; } -void destroyAutoreleasePool(id pool) { - [pool release]; +void destroyAutoreleasePool(void* pool) { + [((NSAutoreleasePool*)pool) release]; } @implementation RefCounted diff --git a/test/native_objc_test/block_config.yaml b/test/native_objc_test/block_config.yaml index 841439e0..9f12686e 100644 --- a/test/native_objc_test/block_config.yaml +++ b/test/native_objc_test/block_config.yaml @@ -6,6 +6,9 @@ exclude-all-by-default: true objc-interfaces: include: - BlockTester +functions: + include: + - getBlockRetainCount headers: entry-points: - 'block_test.m' diff --git a/test/native_objc_test/block_test.dart b/test/native_objc_test/block_test.dart index 7ca61d6c..d2068ced 100644 --- a/test/native_objc_test/block_test.dart +++ b/test/native_objc_test/block_test.dart @@ -238,26 +238,26 @@ void main() { Pointer funcPointerBlockRefCountTest() { final block = IntBlock.fromFunctionPointer(lib, Pointer.fromFunction(_add100, 999)); - expect(BlockTester.getBlockRetainCount_(lib, block.pointer.cast()), 1); + expect(lib.getBlockRetainCount(block.pointer.cast()), 1); return block.pointer.cast(); } test('Function pointer block ref counting', () { final rawBlock = funcPointerBlockRefCountTest(); doGC(); - expect(BlockTester.getBlockRetainCount_(lib, rawBlock), 0); + expect(lib.getBlockRetainCount(rawBlock), 0); }); Pointer funcBlockRefCountTest() { final block = IntBlock.fromFunction(lib, makeAdder(4000)); - expect(BlockTester.getBlockRetainCount_(lib, block.pointer.cast()), 1); + expect(lib.getBlockRetainCount(block.pointer.cast()), 1); return block.pointer.cast(); } test('Function block ref counting', () { final rawBlock = funcBlockRefCountTest(); doGC(); - expect(BlockTester.getBlockRetainCount_(lib, rawBlock), 0); + expect(lib.getBlockRetainCount(rawBlock), 0); }); (Pointer, Pointer, Pointer) @@ -276,13 +276,10 @@ void main() { // One reference held by inputBlock object, another bound to the // outputBlock lambda. - expect( - BlockTester.getBlockRetainCount_(lib, inputBlock.pointer.cast()), 2); + expect(lib.getBlockRetainCount(inputBlock.pointer.cast()), 2); - expect( - BlockTester.getBlockRetainCount_(lib, blockBlock.pointer.cast()), 1); - expect( - BlockTester.getBlockRetainCount_(lib, outputBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(blockBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(outputBlock.pointer.cast()), 1); return ( inputBlock.pointer.cast(), blockBlock.pointer.cast(), @@ -297,10 +294,10 @@ void main() { // 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(BlockTester.getBlockRetainCount_(lib, inputBlock), 1); + expect(lib.getBlockRetainCount(inputBlock), 1); - expect(BlockTester.getBlockRetainCount_(lib, blockBlock), 0); - expect(BlockTester.getBlockRetainCount_(lib, outputBlock), 0); + expect(lib.getBlockRetainCount(blockBlock), 0); + expect(lib.getBlockRetainCount(outputBlock), 0); }); (Pointer, Pointer, Pointer) @@ -316,11 +313,9 @@ void main() { expect(outputBlock(1), 6); doGC(); - expect(BlockTester.getBlockRetainCount_(lib, inputBlock), 2); - expect( - BlockTester.getBlockRetainCount_(lib, blockBlock.pointer.cast()), 1); - expect( - BlockTester.getBlockRetainCount_(lib, outputBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(inputBlock), 2); + expect(lib.getBlockRetainCount(blockBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(outputBlock.pointer.cast()), 1); return ( inputBlock, blockBlock.pointer.cast(), @@ -335,10 +330,10 @@ void main() { // 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(BlockTester.getBlockRetainCount_(lib, inputBlock), 2); + expect(lib.getBlockRetainCount(inputBlock), 2); - expect(BlockTester.getBlockRetainCount_(lib, blockBlock), 0); - expect(BlockTester.getBlockRetainCount_(lib, outputBlock), 0); + expect(lib.getBlockRetainCount(blockBlock), 0); + expect(lib.getBlockRetainCount(outputBlock), 0); }); (Pointer, Pointer, Pointer) @@ -353,13 +348,10 @@ void main() { // One reference held by inputBlock object, another held internally by the // ObjC implementation of the blockBlock. - expect( - BlockTester.getBlockRetainCount_(lib, inputBlock.pointer.cast()), 2); + expect(lib.getBlockRetainCount(inputBlock.pointer.cast()), 2); - expect( - BlockTester.getBlockRetainCount_(lib, blockBlock.pointer.cast()), 1); - expect( - BlockTester.getBlockRetainCount_(lib, outputBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(blockBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(outputBlock.pointer.cast()), 1); return ( inputBlock.pointer.cast(), blockBlock.pointer.cast(), @@ -371,9 +363,9 @@ void main() { final (inputBlock, blockBlock, outputBlock) = nativeBlockBlockDartCallRefCountTest(); doGC(); - expect(BlockTester.getBlockRetainCount_(lib, inputBlock), 0); - expect(BlockTester.getBlockRetainCount_(lib, blockBlock), 0); - expect(BlockTester.getBlockRetainCount_(lib, outputBlock), 0); + expect(lib.getBlockRetainCount(inputBlock), 0); + expect(lib.getBlockRetainCount(blockBlock), 0); + expect(lib.getBlockRetainCount(outputBlock), 0); }); (Pointer, Pointer) nativeBlockBlockObjCCallRefCountTest() { @@ -382,18 +374,16 @@ void main() { expect(outputBlock(1), 14); doGC(); - expect( - BlockTester.getBlockRetainCount_(lib, blockBlock.pointer.cast()), 1); - expect( - BlockTester.getBlockRetainCount_(lib, outputBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(blockBlock.pointer.cast()), 1); + expect(lib.getBlockRetainCount(outputBlock.pointer.cast()), 1); return (blockBlock.pointer.cast(), outputBlock.pointer.cast()); } test('Calling a native block block from ObjC has correct ref counting', () { final (blockBlock, outputBlock) = nativeBlockBlockObjCCallRefCountTest(); doGC(); - expect(BlockTester.getBlockRetainCount_(lib, blockBlock), 0); - expect(BlockTester.getBlockRetainCount_(lib, outputBlock), 0); + expect(lib.getBlockRetainCount(blockBlock), 0); + expect(lib.getBlockRetainCount(outputBlock), 0); }); (Pointer, Pointer) objectBlockRefCountTest(Allocator alloc) { diff --git a/test/native_objc_test/block_test.m b/test/native_objc_test/block_test.m index 3dd4289d..d27a97c8 100644 --- a/test/native_objc_test/block_test.m +++ b/test/native_objc_test/block_test.m @@ -5,6 +5,8 @@ #import #import +#include "util.h" + typedef struct { double x; double y; @@ -61,7 +63,6 @@ @interface BlockTester : NSObject { } + (BlockTester*)makeFromBlock:(IntBlock)block; + (BlockTester*)makeFromMultiplier:(int32_t)mult; -+ (uint64_t)getBlockRetainCount:(void*)block; - (int32_t)call:(int32_t)x; - (IntBlock)getBlock; - (void)pokeBlock; @@ -91,33 +92,6 @@ + (BlockTester*)makeFromMultiplier:(int32_t)mult { 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:(void*)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/test/native_objc_test/static_func_config.yaml b/test/native_objc_test/static_func_config.yaml new file mode 100644 index 00000000..bbafeaa7 --- /dev/null +++ b/test/native_objc_test/static_func_config.yaml @@ -0,0 +1,18 @@ +name: StaticFuncTestObjCLibrary +description: 'Test ObjC static functions' +language: objc +output: 'static_func_bindings.dart' +exclude-all-by-default: true +functions: + include: + - getBlockRetainCount + - staticFuncOfObject + - staticFuncOfNullableObject + - staticFuncOfBlock + - staticFuncReturnsRetained + - staticFuncReturnsRetainedArg +headers: + entry-points: + - 'static_func_test.m' +preamble: | + // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field diff --git a/test/native_objc_test/static_func_native_config.yaml b/test/native_objc_test/static_func_native_config.yaml new file mode 100644 index 00000000..2a591c9f --- /dev/null +++ b/test/native_objc_test/static_func_native_config.yaml @@ -0,0 +1,19 @@ +name: StaticFuncTestObjCLibrary +description: 'Test ObjC static functions using @Native' +language: objc +output: 'static_func_native_bindings.dart' +exclude-all-by-default: true +ffi-native: +functions: + include: + - getBlockRetainCount + - staticFuncOfObject + - staticFuncOfNullableObject + - staticFuncOfBlock + - staticFuncReturnsRetained + - staticFuncReturnsRetainedArg +headers: + entry-points: + - 'static_func_test.m' +preamble: | + // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field diff --git a/test/native_objc_test/static_func_native_test.dart b/test/native_objc_test/static_func_native_test.dart new file mode 100644 index 00000000..93cbcad7 --- /dev/null +++ b/test/native_objc_test/static_func_native_test.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2023, 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') + +// Keep in sync with static_func_test.dart. These are the same tests, but using +// @Native. + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:ffi/ffi.dart'; +import '../test_utils.dart'; +import 'static_func_native_bindings.dart'; +import 'util.dart'; + +typedef IntBlock = ObjCBlock_Int32_Int32; + +void main() { + late StaticFuncTestObjCLibrary lib; + late void Function(Pointer, Pointer) executeInternalCommand; + + group('static functions', () { + setUpAll(() { + logWarnings(); + final dylib = File('test/native_objc_test/static_func_test.dylib'); + verifySetupFile(dylib); + lib = StaticFuncTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path)); + + executeInternalCommand = DynamicLibrary.process().lookupFunction< + Void Function(Pointer, Pointer), + void Function( + Pointer, Pointer)>('Dart_ExecuteInternalCommand'); + + generateBindingsForCoverage('static_func'); + }); + + doGC() { + final gcNow = "gc-now".toNativeUtf8(); + executeInternalCommand(gcNow.cast(), nullptr); + calloc.free(gcNow); + } + + Pointer staticFuncOfObjectRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = staticFuncOfObject(lib, obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test('Objects passed through static functions have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncOfObjectRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + + Pointer staticFuncOfNullableObjectRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = staticFuncOfNullableObject(lib, obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test('Nullables passed through static functions have correct ref counts', + () { + using((Arena arena) { + final (counter) = staticFuncOfNullableObjectRefCountTest(arena); + doGC(); + expect(counter.value, 0); + + expect(staticFuncOfNullableObject(lib, null), isNull); + }); + }); + + Pointer staticFuncOfBlockRefCountTest() { + final block = IntBlock.fromFunction(lib, (int x) => 2 * x); + expect(getBlockRetainCount(block.pointer.cast()), 1); + + final outputBlock = staticFuncOfBlock(lib, block); + expect(block, outputBlock); + expect(getBlockRetainCount(block.pointer.cast()), 2); + + return block.pointer.cast(); + } + + test('Blocks passed through static functions have correct ref counts', () { + final (rawBlock) = staticFuncOfBlockRefCountTest(); + doGC(); + expect(getBlockRetainCount(rawBlock), 0); + }); + + Pointer staticFuncReturnsRetainedRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final outputObj = staticFuncReturnsRetained(lib, counter); + expect(counter.value, 1); + + return counter; + } + + test( + 'Objects returned from static functions with NS_RETURNS_RETAINED ' + 'have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncReturnsRetainedRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + + Pointer staticFuncOfObjectReturnsRetainedRefCountTest( + Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = staticFuncReturnsRetainedArg(lib, obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test( + 'Objects passed through static functions with NS_RETURNS_RETAINED ' + 'have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncOfObjectReturnsRetainedRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + }); +} diff --git a/test/native_objc_test/static_func_test.dart b/test/native_objc_test/static_func_test.dart new file mode 100644 index 00000000..f3d9c01b --- /dev/null +++ b/test/native_objc_test/static_func_test.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2023, 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') + +// Keep in sync with static_func_test.dart. These are the same tests, but +// without using @Native. + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:ffi/ffi.dart'; +import '../test_utils.dart'; +import 'static_func_bindings.dart'; +import 'util.dart'; + +typedef IntBlock = ObjCBlock_Int32_Int32; + +void main() { + late StaticFuncTestObjCLibrary lib; + late void Function(Pointer, Pointer) executeInternalCommand; + + group('static functions', () { + setUpAll(() { + logWarnings(); + final dylib = File('test/native_objc_test/static_func_test.dylib'); + verifySetupFile(dylib); + lib = StaticFuncTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path)); + + executeInternalCommand = DynamicLibrary.process().lookupFunction< + Void Function(Pointer, Pointer), + void Function( + Pointer, Pointer)>('Dart_ExecuteInternalCommand'); + + generateBindingsForCoverage('static_func'); + }); + + doGC() { + final gcNow = "gc-now".toNativeUtf8(); + executeInternalCommand(gcNow.cast(), nullptr); + calloc.free(gcNow); + } + + Pointer staticFuncOfObjectRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = lib.staticFuncOfObject(obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test('Objects passed through static functions have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncOfObjectRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + + Pointer staticFuncOfNullableObjectRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = lib.staticFuncOfNullableObject(obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test('Nullables passed through static functions have correct ref counts', + () { + using((Arena arena) { + final (counter) = staticFuncOfNullableObjectRefCountTest(arena); + doGC(); + expect(counter.value, 0); + + expect(lib.staticFuncOfNullableObject(null), isNull); + }); + }); + + Pointer staticFuncOfBlockRefCountTest() { + final block = IntBlock.fromFunction(lib, (int x) => 2 * x); + expect(lib.getBlockRetainCount(block.pointer.cast()), 1); + + final outputBlock = lib.staticFuncOfBlock(block); + expect(block, outputBlock); + expect(lib.getBlockRetainCount(block.pointer.cast()), 2); + + return block.pointer.cast(); + } + + test('Blocks passed through static functions have correct ref counts', () { + final (rawBlock) = staticFuncOfBlockRefCountTest(); + doGC(); + expect(lib.getBlockRetainCount(rawBlock), 0); + }); + + Pointer staticFuncReturnsRetainedRefCountTest(Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final outputObj = lib.staticFuncReturnsRetained(counter); + expect(counter.value, 1); + + return counter; + } + + test( + 'Objects returned from static functions with NS_RETURNS_RETAINED ' + 'have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncReturnsRetainedRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + + Pointer staticFuncOfObjectReturnsRetainedRefCountTest( + Allocator alloc) { + final counter = alloc(); + counter.value = 0; + + final obj = StaticFuncTestObj.newWithCounter_(lib, counter); + expect(counter.value, 1); + + final outputObj = lib.staticFuncReturnsRetainedArg(obj); + expect(obj, outputObj); + expect(counter.value, 1); + + return counter; + } + + test( + 'Objects passed through static functions with NS_RETURNS_RETAINED ' + 'have correct ref counts', () { + using((Arena arena) { + final (counter) = staticFuncOfObjectReturnsRetainedRefCountTest(arena); + doGC(); + expect(counter.value, 0); + }); + }); + }); +} diff --git a/test/native_objc_test/static_func_test.m b/test/native_objc_test/static_func_test.m new file mode 100644 index 00000000..5fc3a14d --- /dev/null +++ b/test/native_objc_test/static_func_test.m @@ -0,0 +1,63 @@ +// Copyright (c) 2023, 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 + +#include "util.h" + +@interface StaticFuncTestObj : NSObject { + int32_t* counter; +} ++ (instancetype)newWithCounter:(int32_t*) _counter; +- (instancetype)initWithCounter:(int32_t*) _counter; +- (void)setCounter:(int32_t*) _counter; +- (void)dealloc; +@end + +StaticFuncTestObj* staticFuncOfObject(StaticFuncTestObj* a) { + return a; +} + +StaticFuncTestObj* _Nullable staticFuncOfNullableObject( + StaticFuncTestObj* _Nullable a) { + return a; +} + +typedef int32_t (^IntBlock)(int32_t); +IntBlock staticFuncOfBlock(IntBlock a) { + return a; +} + +NS_RETURNS_RETAINED StaticFuncTestObj* staticFuncReturnsRetained( + int32_t* counter) { + return [StaticFuncTestObj newWithCounter: counter]; +} + +NS_RETURNS_RETAINED StaticFuncTestObj* staticFuncReturnsRetainedArg( + StaticFuncTestObj* a) { + return [a retain]; +} + + +@implementation StaticFuncTestObj ++ (instancetype)newWithCounter:(int32_t*) _counter { + return [[StaticFuncTestObj alloc] initWithCounter: _counter]; +} + +- (instancetype)initWithCounter:(int32_t*) _counter { + counter = _counter; + ++*counter; + return [super init]; +} + +- (void)setCounter:(int32_t*) _counter { + counter = _counter; + ++*counter; +} + +- (void)dealloc { + if (counter != nil) --*counter; + [super dealloc]; +} +@end diff --git a/test/native_objc_test/util.h b/test/native_objc_test/util.h new file mode 100644 index 00000000..0694aea5 --- /dev/null +++ b/test/native_objc_test/util.h @@ -0,0 +1,35 @@ +// Copyright (c) 2023, 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. + +#ifndef _TEST_UTIL_H_ +#define _TEST_UTIL_H_ + +typedef struct { + void* isa; + int flags; + // There are other fields, but we just need the flags and isa. +} BlockRefCountExtractor; + +static void* valid_block_isa = NULL; +uint64_t getBlockRetainCount(void* 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; +} + +#endif // _TEST_UTIL_H_