[ffigen] 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
diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md
index 3de820d..a1aef8c 100644
--- a/pkgs/ffigen/CHANGELOG.md
+++ b/pkgs/ffigen/CHANGELOG.md
@@ -9,6 +9,7 @@
 - When generating typedefs for `Pointer<NativeFunction<Function>>`, 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/pkgs/ffigen/lib/src/code_generator/func.dart b/pkgs/ffigen/lib/src/code_generator/func.dart
index cbdebe7..766cc28 100644
--- a/pkgs/ffigen/lib/src/code_generator/func.dart
+++ b/pkgs/ffigen/lib/src/code_generator/func.dart
@@ -42,6 +42,7 @@
   final bool exposeSymbolAddress;
   final bool exposeFunctionTypedefs;
   final bool isLeaf;
+  final bool objCReturnsRetained;
   final FfiNativeConfig ffiNativeConfig;
   late final String funcPointerName;
 
@@ -61,6 +62,7 @@
     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 @@
   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 @@
         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");
+      final nativeFuncName = needsWrapper ? funcVarName : enclosingFuncName;
+      s.write('''
+@${w.ffiLibraryPrefix}.Native<$cType>(symbol: '$originalName'$assetString$isLeafString)
+external $ffiReturnType $nativeFuncName($ffiArgDeclString);
 
-      s.write(
-          'external ${functionType.returnType.getFfiDartType(w)} $enclosingFuncName(\n');
-      for (final p in functionType.dartTypeParameters) {
-        s.write('  ${p.type.getFfiDartType(w)} ${p.name},\n');
+''');
+      if (needsWrapper) {
+        final libArg = functionType.returnType.sameDartAndFfiDartType
+            ? ''
+            : '${w.className} lib, ';
+        s.write('''
+$dartReturnType $enclosingFuncName($libArg$dartArgDeclString) => $funcImplCall;
+
+''');
       }
-      s.write(');\n\n');
     } else {
-      // 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');
+      funcPointerName = w.wrapperLevelUniqueNamer.makeUnique('_${name}Ptr');
 
-      s.write('(\n');
-      for (final p in functionType.dartTypeParameters) {
-        s.write('    ${p.name},\n');
-      }
-      s.write('  );\n');
-      s.write('}\n');
+      // Write enclosing function.
+      s.write('''
+$dartReturnType $enclosingFuncName($dartArgDeclString) {
+  return $funcImplCall;
+}
+
+''');
 
       if (exposeSymbolAddress) {
         // Add to SymbolAddress in writer.
@@ -150,12 +184,14 @@
           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/pkgs/ffigen/lib/src/code_generator/func_type.dart b/pkgs/ffigen/lib/src/code_generator/func_type.dart
index 1e734bc..49dcf60 100644
--- a/pkgs/ffigen/lib/src/code_generator/func_type.dart
+++ b/pkgs/ffigen/lib/src/code_generator/func_type.dart
@@ -79,6 +79,11 @@
       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());
 
   @override
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_block.dart b/pkgs/ffigen/lib/src/code_generator/objc_block.dart
index b61efc7..d16ab11 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_block.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_block.dart
@@ -229,6 +229,9 @@
   bool get sameDartAndCType => false;
 
   @override
+  bool get sameDartAndFfiDartType => false;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index 2ed1f10..495cdf1 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -402,6 +402,9 @@
   bool get sameDartAndCType => false;
 
   @override
+  bool get sameDartAndFfiDartType => false;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart b/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart
index a8d8c52..f3c0b87 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_nullable.dart
@@ -45,6 +45,9 @@
   bool get sameDartAndCType => false;
 
   @override
+  bool get sameDartAndFfiDartType => false;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/code_generator/pointer.dart b/pkgs/ffigen/lib/src/code_generator/pointer.dart
index dcaf597..30e2e6b 100644
--- a/pkgs/ffigen/lib/src/code_generator/pointer.dart
+++ b/pkgs/ffigen/lib/src/code_generator/pointer.dart
@@ -88,6 +88,9 @@
   bool get sameDartAndCType => false;
 
   @override
+  bool get sameDartAndFfiDartType => false;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/code_generator/type.dart b/pkgs/ffigen/lib/src/code_generator/type.dart
index fff7f0f..c1d1e22 100644
--- a/pkgs/ffigen/lib/src/code_generator/type.dart
+++ b/pkgs/ffigen/lib/src/code_generator/type.dart
@@ -54,6 +54,9 @@
   /// 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.
   ///
@@ -139,6 +142,9 @@
   bool get sameDartAndCType => sameFfiDartAndCType;
 
   @override
+  bool get sameDartAndFfiDartType => true;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/code_generator/typealias.dart b/pkgs/ffigen/lib/src/code_generator/typealias.dart
index fe971a5..e31d148 100644
--- a/pkgs/ffigen/lib/src/code_generator/typealias.dart
+++ b/pkgs/ffigen/lib/src/code_generator/typealias.dart
@@ -159,6 +159,9 @@
   bool get sameDartAndCType => type.sameDartAndCType;
 
   @override
+  bool get sameDartAndFfiDartType => type.sameDartAndFfiDartType;
+
+  @override
   String convertDartTypeToFfiDartType(
     Writer w,
     String value, {
diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/functiondecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/functiondecl_parser.dart
index f9d1121..c2291b4 100644
--- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/functiondecl_parser.dart
+++ b/pkgs/ffigen/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 @@
   List<Func> funcs = [];
   bool incompleteStructParameter = false;
   bool unimplementedParameterType = false;
+  bool objCReturnsRetained = false;
   _ParserFunc();
 }
 
@@ -69,6 +72,13 @@
       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 @@
         exposeFunctionTypedefs:
             config.exposeFunctionTypedefs.shouldInclude(funcName),
         isLeaf: config.leafFunctions.shouldInclude(funcName),
+        objCReturnsRetained: _stack.top.objCReturnsRetained,
         ffiNativeConfig: config.ffiNativeConfig,
       ));
     }
@@ -147,3 +158,19 @@
 Type _getParameterType(clang_types.CXCursor cursor) {
   return cursor.toCodeGenType();
 }
+
+Pointer<
+        NativeFunction<
+            Int32 Function(
+                clang_types.CXCursor, clang_types.CXCursor, Pointer<Void>)>>?
+    _parseAnnotationVisitorPtr;
+int _parseAnnotationVisitor(clang_types.CXCursor cursor,
+    clang_types.CXCursor parent, Pointer<Void> 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/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
index d249389..cf8ab3a 100644
--- a/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
+++ b/pkgs/ffigen/test/native_objc_test/automated_ref_count_test.m
@@ -89,12 +89,14 @@
 
 @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/pkgs/ffigen/test/native_objc_test/block_config.yaml b/pkgs/ffigen/test/native_objc_test/block_config.yaml
index 841439e..9f12686 100644
--- a/pkgs/ffigen/test/native_objc_test/block_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/block_config.yaml
@@ -6,6 +6,9 @@
 objc-interfaces:
   include:
     - BlockTester
+functions:
+  include:
+    - getBlockRetainCount
 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 7ca61d6..d2068ce 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_test.dart
@@ -238,26 +238,26 @@
     Pointer<Void> 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<Void> 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<Void>, Pointer<Void>, Pointer<Void>)
@@ -276,13 +276,10 @@
 
       // 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 @@
 
       // 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<Void>, Pointer<Void>, Pointer<Void>)
@@ -316,11 +313,9 @@
       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 @@
 
       // 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<Void>, Pointer<Void>, Pointer<Void>)
@@ -353,13 +348,10 @@
 
       // 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 @@
       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<Void>, Pointer<Void>) nativeBlockBlockObjCCallRefCountTest() {
@@ -382,18 +374,16 @@
       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<Int32>, Pointer<Int32>) objectBlockRefCountTest(Allocator alloc) {
diff --git a/pkgs/ffigen/test/native_objc_test/block_test.m b/pkgs/ffigen/test/native_objc_test/block_test.m
index 3dd4289..d27a97c 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test.m
+++ b/pkgs/ffigen/test/native_objc_test/block_test.m
@@ -5,6 +5,8 @@
 #import <Foundation/NSObject.h>
 #import <Foundation/NSThread.h>
 
+#include "util.h"
+
 typedef struct {
   double x;
   double y;
@@ -61,7 +63,6 @@
 }
 + (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 @@
   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/pkgs/ffigen/test/native_objc_test/static_func_config.yaml b/pkgs/ffigen/test/native_objc_test/static_func_config.yaml
new file mode 100644
index 0000000..bbafeaa
--- /dev/null
+++ b/pkgs/ffigen/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/pkgs/ffigen/test/native_objc_test/static_func_native_config.yaml b/pkgs/ffigen/test/native_objc_test/static_func_native_config.yaml
new file mode 100644
index 0000000..2a591c9
--- /dev/null
+++ b/pkgs/ffigen/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/pkgs/ffigen/test/native_objc_test/static_func_native_test.dart b/pkgs/ffigen/test/native_objc_test/static_func_native_test.dart
new file mode 100644
index 0000000..93cbcad
--- /dev/null
+++ b/pkgs/ffigen/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<Char>, Pointer<Void>) 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<Char>, Pointer<Void>),
+          void Function(
+              Pointer<Char>, Pointer<Void>)>('Dart_ExecuteInternalCommand');
+
+      generateBindingsForCoverage('static_func');
+    });
+
+    doGC() {
+      final gcNow = "gc-now".toNativeUtf8();
+      executeInternalCommand(gcNow.cast(), nullptr);
+      calloc.free(gcNow);
+    }
+
+    Pointer<Int32> staticFuncOfObjectRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Int32> staticFuncOfNullableObjectRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Void> 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<Int32> staticFuncReturnsRetainedRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Int32> staticFuncOfObjectReturnsRetainedRefCountTest(
+        Allocator alloc) {
+      final counter = alloc<Int32>();
+      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/pkgs/ffigen/test/native_objc_test/static_func_test.dart b/pkgs/ffigen/test/native_objc_test/static_func_test.dart
new file mode 100644
index 0000000..f3d9c01
--- /dev/null
+++ b/pkgs/ffigen/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<Char>, Pointer<Void>) 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<Char>, Pointer<Void>),
+          void Function(
+              Pointer<Char>, Pointer<Void>)>('Dart_ExecuteInternalCommand');
+
+      generateBindingsForCoverage('static_func');
+    });
+
+    doGC() {
+      final gcNow = "gc-now".toNativeUtf8();
+      executeInternalCommand(gcNow.cast(), nullptr);
+      calloc.free(gcNow);
+    }
+
+    Pointer<Int32> staticFuncOfObjectRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Int32> staticFuncOfNullableObjectRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Void> 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<Int32> staticFuncReturnsRetainedRefCountTest(Allocator alloc) {
+      final counter = alloc<Int32>();
+      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<Int32> staticFuncOfObjectReturnsRetainedRefCountTest(
+        Allocator alloc) {
+      final counter = alloc<Int32>();
+      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/pkgs/ffigen/test/native_objc_test/static_func_test.m b/pkgs/ffigen/test/native_objc_test/static_func_test.m
new file mode 100644
index 0000000..5fc3a14
--- /dev/null
+++ b/pkgs/ffigen/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 <Foundation/NSString.h>
+
+#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/pkgs/ffigen/test/native_objc_test/util.h b/pkgs/ffigen/test/native_objc_test/util.h
new file mode 100644
index 0000000..0694aea
--- /dev/null
+++ b/pkgs/ffigen/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_