[ffigen] Migrate test/setup.dart to a build hook (#3334)
diff --git a/.github/workflows/ffigen.yml b/.github/workflows/ffigen.yml
index c06836a..7bb4748 100644
--- a/.github/workflows/ffigen.yml
+++ b/.github/workflows/ffigen.yml
@@ -41,8 +41,6 @@
       - name: Check formatting
         run: dart format --output=none --set-exit-if-changed .
         if: always() && steps.install.outcome == 'success'
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Analyze code
         run: dart analyze --fatal-infos
 
@@ -61,8 +59,6 @@
         run: dart pub get && flutter pub get --directory="../jni"
       - name: Install libclang-14-dev
         run: sudo apt-get install libclang-14-dev
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run VM tests
         run: dart test
       - name: Generate package:jni bindings
@@ -87,8 +83,6 @@
         uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30
         with:
           brew: clang-format
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Install coverage
         run: dart pub global activate coverage
       - name: Run VM tests and collect coverage
@@ -127,8 +121,6 @@
         uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30
         with:
           brew: clang-format
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run VM tests
         run: dart test
 
@@ -145,8 +137,6 @@
           channel: stable
       - name: Install dependencies
         run: dart pub get && dart pub get --directory="../objective_c"
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run Flutter tests
         run: dart test
 
@@ -163,8 +153,6 @@
           channel: stable
       - name: Install dependencies
         run: dart pub get && flutter pub get --directory="../jni"
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run VM tests
         run: dart test
       - name: Generate package:jni bindings
@@ -194,7 +182,5 @@
           channel: stable
       - name: Install dependencies
         run: flutter pub get
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run VM tests
         run: dart test test_flutter/
diff --git a/.github/workflows/ffigen_weekly.yml b/.github/workflows/ffigen_weekly.yml
index 8ebdb0c..74671fd 100644
--- a/.github/workflows/ffigen_weekly.yml
+++ b/.github/workflows/ffigen_weekly.yml
@@ -37,8 +37,6 @@
         uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30
         with:
           brew: clang-format
-      - name: Build test dylib and bindings
-        run: dart --enable-asserts test/setup.dart
       - name: Run VM tests
         run: dart test
       - name: Generate package:jni bindings
diff --git a/pkgs/ffigen/hook/build.dart b/pkgs/ffigen/hook/build.dart
new file mode 100644
index 0000000..e115124
--- /dev/null
+++ b/pkgs/ffigen/hook/build.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2026, 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 'dart:io';
+
+import 'package:code_assets/code_assets.dart';
+import 'package:hooks/hooks.dart';
+import 'package:logging/logging.dart';
+import 'package:native_toolchain_c/native_toolchain_c.dart';
+import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart';
+import 'package:path/path.dart' as p;
+
+// All ObjC source files are compiled with ARC enabled except these.
+const arcDisabledFiles = <String>{'ref_count_test.m'};
+
+final logger = Logger('')
+  ..level = Level.INFO
+  ..onRecord.listen((record) {
+    print('${record.level.name}: ${record.time}: ${record.message}');
+  });
+
+void main(List<String> args) async {
+  await build(args, (input, output) async {
+    if (input.userDefines['include_test_utils'] != true) {
+      return;
+    }
+
+    if (!input.config.buildCodeAssets) {
+      return;
+    }
+
+    final packageName = input.packageName;
+
+    // Build native_test.c. This is an ordinary build, so we can use CBuilder.
+    await CBuilder.library(
+      name: 'native_test',
+      assetName: 'native_test',
+      sources: [
+        'test/native_test/native_test.c',
+        if (input.config.code.targetOS == OS.windows)
+          'test/native_test/native_test.def',
+      ],
+    ).run(input: input, output: output, logger: logger);
+
+    if (input.config.code.targetOS == OS.macOS) {
+      final builder = await CustomBuilder.create(
+        input,
+        input.packageRoot.toFilePath(),
+      );
+
+      // Build swift_class_test.swift. There's no swift compilation package, so
+      // we have to use a CustomBuilder.
+      final objcTestDir = input.packageRoot.resolve('test/native_objc_test/');
+      const swiftModule = 'swift_class_test';
+      final swiftFile = objcTestDir.resolve('swift_class_test.swift');
+      final swiftHeader = objcTestDir.resolve('swift_class_test-Swift.h');
+      final swiftLib = input.outputDirectory.resolve('$swiftModule.dylib');
+
+      await builder.buildSwift(swiftFile, swiftModule, swiftHeader, swiftLib);
+      output.assets.code.add(
+        CodeAsset(
+          package: packageName,
+          name: swiftModule,
+          file: swiftLib,
+          linkMode: DynamicLoadingBundled(),
+        ),
+      );
+
+      // Build all the ObjC files. Some of the files have different compile
+      // flags, so we need to use the CustomBuilder again.
+      final mFiles = _findFiles(objcTestDir, '.m');
+      final hFiles = _findFiles(objcTestDir, '.h');
+
+      final objFiles = <String>[];
+      for (final mFile in mFiles) {
+        final useArc = !arcDisabledFiles.contains(mFile.pathSegments.last);
+        objFiles.add(
+          await builder.buildObject(mFile, [
+            '-x',
+            'objective-c',
+            if (useArc) '-fobjc-arc',
+            '-Wno-nullability-completeness',
+            '-DDISABLE_METHOD',
+          ]),
+        );
+      }
+
+      // Add dart_api_dl.c from objective_c package.
+      final dartApiDl = input.packageRoot.resolve(
+        '../objective_c/src/include/dart_api_dl.c',
+      );
+      objFiles.add(await builder.buildObject(dartApiDl, []));
+
+      const objcAsset = 'objc_test';
+      final objcLib = input.outputDirectory.resolve('$objcAsset.dylib');
+      await builder.linkLib(objFiles, objcLib, ['-framework', 'Foundation']);
+
+      output.dependencies.addAll([...mFiles, ...hFiles, swiftFile, dartApiDl]);
+
+      output.assets.code.add(
+        CodeAsset(
+          package: packageName,
+          name: objcAsset,
+          file: objcLib,
+          linkMode: DynamicLoadingBundled(),
+        ),
+      );
+    }
+  });
+}
+
+List<Uri> _findFiles(Uri dir, String suffix) => Directory.fromUri(dir)
+    .listSync()
+    .whereType<File>()
+    .map((f) => f.uri)
+    .where((uri) => uri.pathSegments.last.endsWith(suffix))
+    .toList();
+
+class CustomBuilder {
+  final String _comp;
+  final String _rootDir;
+  final Uri _tempOutDir;
+  CustomBuilder._(this._comp, this._rootDir, this._tempOutDir);
+
+  static Future<CustomBuilder> create(BuildInput input, String rootDir) async {
+    final resolver = CompilerResolver(
+      codeConfig: input.config.code,
+      logger: logger,
+    );
+    return CustomBuilder._(
+      (await resolver.resolveCompiler()).uri.toFilePath(),
+      rootDir,
+      input.outputDirectory.resolve('obj/'),
+    );
+  }
+
+  Future<String> buildObject(Uri input, List<String> flags) async {
+    assert(input.toFilePath().startsWith(_rootDir));
+    final relativeInput = p.relative(input.toFilePath(), from: _rootDir);
+    final output = '${_tempOutDir.resolve(relativeInput).toFilePath()}.o';
+    File(output).parent.createSync(recursive: true);
+    await _runClang([
+      ...flags,
+      '-c',
+      input.toFilePath(),
+      '-fpic',
+      '-gline-tables-only',
+    ], output);
+    return output;
+  }
+
+  Future<void> linkLib(List<String> objects, Uri output, List<String> flags) =>
+      _runClang(['-shared', ...flags, ...objects], output.toFilePath());
+
+  Future<void> _runClang(List<String> flags, String output) =>
+      _run(_comp, [...flags, '-o', output]);
+
+  Future<void> buildSwift(
+    Uri input,
+    String moduleName,
+    Uri outputHeader,
+    Uri outputLib,
+  ) async {
+    final args = [
+      '-c',
+      input.toFilePath(),
+      '-module-name',
+      moduleName,
+      '-emit-library',
+      '-emit-objc-header-path',
+      outputHeader.toFilePath(),
+      '-o',
+      outputLib.toFilePath(),
+    ];
+    await _run('swiftc', args);
+  }
+
+  Future<void> _run(String cmd, List<String> args) async {
+    final proc = await Process.run(cmd, args);
+    if (proc.exitCode != 0) {
+      throw Exception(
+        'Command failed: $cmd ${args.join(" ")}\n'
+        '${proc.stdout}\n${proc.stderr}',
+      );
+    }
+  }
+}
diff --git a/pkgs/ffigen/pubspec.yaml b/pkgs/ffigen/pubspec.yaml
index 5d383a8..ac08248 100644
--- a/pkgs/ffigen/pubspec.yaml
+++ b/pkgs/ffigen/pubspec.yaml
@@ -21,13 +21,16 @@
 dependencies:
   args: ^2.6.0
   cli_util: ^0.4.2
+  code_assets: ^1.0.0
   collection: ^1.18.0
   dart_style: ^3.0.0
   ffi: ^2.0.1
   file: ^7.0.0
   glob: ^2.0.0
+  hooks: ^1.0.0
   logging: ^1.0.0
   meta: ^1.11.0
+  native_toolchain_c: ^0.17.4
   package_config: ^2.1.0
   path: ^1.8.0
   pub_semver: ^2.1.4
@@ -55,5 +58,7 @@
 
 hooks:
   user_defines:
+    ffigen:
+      include_test_utils: true
     objective_c:
       include_test_utils: true
diff --git a/pkgs/ffigen/test/native_objc_test/arc_config.yaml b/pkgs/ffigen/test/native_objc_test/arc_config.yaml
index 3bfcb20..81d5cc3 100644
--- a/pkgs/ffigen/test/native_objc_test/arc_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/arc_config.yaml
@@ -11,6 +11,8 @@
   include:
     - ArcTestObject
     - ArcDtorTestObject
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'arc_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/arc_test.dart b/pkgs/ffigen/test/native_objc_test/arc_test.dart
index d333462..f6cd3be 100644
--- a/pkgs/ffigen/test/native_objc_test/arc_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/arc_test.dart
@@ -16,20 +16,9 @@
 import 'util.dart';
 
 void main() {
-  late ArcTestObjCLibrary lib;
-
   group('ARC', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = ArcTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      loadLibrary();
     });
 
     test('objectRetainCount edge cases', () {
@@ -131,7 +120,7 @@
       Pointer<ObjCObjectImpl>,
     )
     copyMethodsInner(Pointer<Int32> counter) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final obj1 = ArcTestObject.newWithCounter(counter);
       expect(counter.value, 1);
       final obj2 = obj1.copyMe();
@@ -171,7 +160,7 @@
       expect(objectRetainCount(obj8raw), greaterThan(0));
       expect(objectRetainCount(obj9raw), greaterThan(0));
 
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(objectRetainCount(obj1raw), greaterThan(0));
       expect(objectRetainCount(obj2raw), greaterThan(0));
       expect(objectRetainCount(obj3raw), greaterThan(0));
@@ -238,17 +227,17 @@
       final counter = calloc<Int32>();
       counter.value = 0;
 
-      final pool1 = lib.objc_autoreleasePoolPush();
+      final pool1 = objc_autoreleasePoolPush();
       final obj1raw = autoreleaseMethodsInner(counter);
       doGC();
       // The autorelease pool is still holding a reference to the object.
       expect(counter.value, 1);
       expect(objectRetainCount(obj1raw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool1);
+      objc_autoreleasePoolPop(pool1);
       expect(counter.value, 0);
       expect(objectRetainCount(obj1raw), 0);
 
-      final pool2 = lib.objc_autoreleasePoolPush();
+      final pool2 = objc_autoreleasePoolPush();
       final obj2 = ArcTestObject.makeAndAutorelease(counter);
       final obj2raw = obj2.ref.pointer;
       expect(counter.value, 1);
@@ -256,7 +245,7 @@
       doGC();
       expect(counter.value, 1);
       expect(objectRetainCount(obj2raw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool2);
+      objc_autoreleasePoolPop(pool2);
       // The obj2 variable still holds a reference to the object.
       expect(counter.value, 1);
       expect(objectRetainCount(obj2raw), greaterThan(0));
@@ -351,13 +340,13 @@
       counter.value = 0;
       // The getters of retain properties retain+autorelease the value. So we
       // need an autorelease pool.
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final (outerObjRaw, retainObjRaw) = retainPropertiesInner(counter);
       doGC();
       expect(objectRetainCount(retainObjRaw), greaterThan(0));
       expect(objectRetainCount(outerObjRaw), 0);
       expect(counter.value, 1);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(objectRetainCount(retainObjRaw), 0);
       expect(objectRetainCount(outerObjRaw), 0);
       expect(counter.value, 0);
@@ -397,7 +386,7 @@
       counter.value = 0;
       // The getters of copy properties retain+autorelease the value. So we need
       // an autorelease pool.
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final (outerObjRaw, copyObjRaw, anotherCopyRaw) = copyPropertiesInner(
         counter,
       );
@@ -406,7 +395,7 @@
       expect(objectRetainCount(outerObjRaw), 0);
       expect(objectRetainCount(copyObjRaw), 0);
       expect(objectRetainCount(anotherCopyRaw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(counter.value, 0);
       expect(objectRetainCount(outerObjRaw), 0);
       expect(objectRetainCount(copyObjRaw), 0);
diff --git a/pkgs/ffigen/test/native_objc_test/arc_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/arc_test_bindings.dart
index bd0f84b..ff8280e 100644
--- a/pkgs/ffigen/test/native_objc_test/arc_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/arc_test_bindings.dart
@@ -2,47 +2,18 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
 
-/// Tests ARC
-class ArcTestObjCLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
+@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
+external void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
 
-  /// The symbols are looked up in [dynamicLibrary].
-  ArcTestObjCLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
-
-  /// The symbols are looked up with [lookup].
-  ArcTestObjCLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
-
-  void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool) {
-    return _objc_autoreleasePoolPop(pool);
-  }
-
-  late final _objc_autoreleasePoolPopPtr =
-      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
-        'objc_autoreleasePoolPop',
-      );
-  late final _objc_autoreleasePoolPop = _objc_autoreleasePoolPopPtr
-      .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
-
-  ffi.Pointer<ffi.Void> objc_autoreleasePoolPush() {
-    return _objc_autoreleasePoolPush();
-  }
-
-  late final _objc_autoreleasePoolPushPtr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Void> Function()>>(
-        'objc_autoreleasePoolPush',
-      );
-  late final _objc_autoreleasePoolPush = _objc_autoreleasePoolPushPtr
-      .asFunction<ffi.Pointer<ffi.Void> Function()>();
-}
+@ffi.Native<ffi.Pointer<ffi.Void> Function()>()
+external ffi.Pointer<ffi.Void> objc_autoreleasePoolPush();
 
 /// ArcDtorTestObject
 extension type ArcDtorTestObject._(objc.ObjCObject object$)
diff --git a/pkgs/ffigen/test/native_objc_test/bad_method_config.yaml b/pkgs/ffigen/test/native_objc_test/bad_method_config.yaml
index 7adfb15..4a0634b 100644
--- a/pkgs/ffigen/test/native_objc_test/bad_method_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/bad_method_config.yaml
@@ -14,6 +14,8 @@
 objc-interfaces:
   include:
     - 'BadMethodTestObject'
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'bad_method_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/bad_method_test.dart b/pkgs/ffigen/test/native_objc_test/bad_method_test.dart
index 8227081..619a7cb 100644
--- a/pkgs/ffigen/test/native_objc_test/bad_method_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/bad_method_test.dart
@@ -16,16 +16,7 @@
 void main() {
   group('bad_method_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test("Test incomplete struct methods that weren't skipped", () {
diff --git a/pkgs/ffigen/test/native_objc_test/bad_override_config.yaml b/pkgs/ffigen/test/native_objc_test/bad_override_config.yaml
index 0aa4718..ee92488 100644
--- a/pkgs/ffigen/test/native_objc_test/bad_override_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/bad_override_config.yaml
@@ -16,6 +16,8 @@
     - BadOverrideChild
     - BadOverrideSibbling
     - BadOverrideGrandchild
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'bad_override_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/bad_override_test.dart b/pkgs/ffigen/test/native_objc_test/bad_override_test.dart
index 82cd044..5257a7a 100644
--- a/pkgs/ffigen/test/native_objc_test/bad_override_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/bad_override_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('bad overrides', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Method vs getter', () {
diff --git a/pkgs/ffigen/test/native_objc_test/block_annotation_config.yaml b/pkgs/ffigen/test/native_objc_test/block_annotation_config.yaml
index 76e0aab..b27f904 100644
--- a/pkgs/ffigen/test/native_objc_test/block_annotation_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/block_annotation_config.yaml
@@ -20,6 +20,8 @@
 typedefs:
   include:
     - EmptyBlock
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'block_annotation_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/block_annotation_test.dart b/pkgs/ffigen/test/native_objc_test/block_annotation_test.dart
index ffe5dd0..fc3c691 100644
--- a/pkgs/ffigen/test/native_objc_test/block_annotation_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_annotation_test.dart
@@ -21,8 +21,6 @@
 import 'util.dart';
 
 void main() {
-  late final BlockAnnotationTestLibrary lib;
-
   group('Block annotations', () {
     // Due to https://github.com/dart-lang/native/issues/1490 we can't directly
     // codegen blocks that return retain or consume args. Instead we create them
@@ -35,25 +33,14 @@
     // correct block type.
 
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = BlockAnnotationTestLibrary(
-        DynamicLibrary.open(dylib.absolute.path),
-      );
+      loadLibrary();
     });
 
     void objectProducerTest(EmptyObject producer()) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       EmptyObject? obj = producer();
       final ptr = obj.ref.pointer;
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
       expect(objectRetainCount(ptr), 1);
       expect(obj, isNotNull);
@@ -200,12 +187,12 @@
     Future<void> objectListenerTest(
       void Function(Completer<EmptyObject>) producer,
     ) async {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       Completer<EmptyObject>? completer = Completer<EmptyObject>();
       producer(completer);
       EmptyObject? obj = await completer.future;
       final ptr = obj.ref.pointer;
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
       expect(objectRetainCount(ptr), 1);
       expect(obj, isNotNull);
@@ -292,10 +279,10 @@
     }, skip: !canDoGC);
 
     void blockProducerTest(DartEmptyBlock producer()) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       DartEmptyBlock? obj = producer();
       final ptr = obj.ref.pointer;
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
       expect(blockRetainCount(ptr), 1);
       expect(obj, isNotNull);
diff --git a/pkgs/ffigen/test/native_objc_test/block_annotation_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/block_annotation_test_bindings.dart
index 80e46f5..fb43680 100644
--- a/pkgs/ffigen/test/native_objc_test/block_annotation_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_annotation_test_bindings.dart
@@ -2,48 +2,13 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
 
-/// Tests block annotations
-class BlockAnnotationTestLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
-
-  /// The symbols are looked up in [dynamicLibrary].
-  BlockAnnotationTestLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
-
-  /// The symbols are looked up with [lookup].
-  BlockAnnotationTestLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
-
-  void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool) {
-    return _objc_autoreleasePoolPop(pool);
-  }
-
-  late final _objc_autoreleasePoolPopPtr =
-      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
-        'objc_autoreleasePoolPop',
-      );
-  late final _objc_autoreleasePoolPop = _objc_autoreleasePoolPopPtr
-      .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
-
-  ffi.Pointer<ffi.Void> objc_autoreleasePoolPush() {
-    return _objc_autoreleasePoolPush();
-  }
-
-  late final _objc_autoreleasePoolPushPtr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Void> Function()>>(
-        'objc_autoreleasePoolPush',
-      );
-  late final _objc_autoreleasePoolPush = _objc_autoreleasePoolPushPtr
-      .asFunction<ffi.Pointer<ffi.Void> Function()>();
-}
-
 @ffi.Native<
   ffi.Void Function(
     ffi.Pointer<objc.ObjCObjectImpl>,
@@ -175,6 +140,12 @@
   ffi.Pointer<objc.ObjCBlockImpl> block,
 );
 
+@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
+external void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
+
+@ffi.Native<ffi.Pointer<ffi.Void> Function()>()
+external ffi.Pointer<ffi.Void> objc_autoreleasePoolPush();
+
 /// BlockAnnotationTest
 extension type BlockAnnotationTest._(objc.ObjCObject object$)
     implements objc.ObjCObject, objc.NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/block_config.yaml b/pkgs/ffigen/test/native_objc_test/block_config.yaml
index 49bc89d..8a3feaa 100644
--- a/pkgs/ffigen/test/native_objc_test/block_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/block_config.yaml
@@ -31,6 +31,8 @@
   include:
     - objc_autoreleasePoolPop
     - objc_autoreleasePoolPush
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'block_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/block_inherit_config.yaml b/pkgs/ffigen/test/native_objc_test/block_inherit_config.yaml
index 912fd9a..a6630f4 100644
--- a/pkgs/ffigen/test/native_objc_test/block_inherit_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/block_inherit_config.yaml
@@ -15,6 +15,8 @@
     - ReturnPlatypus
     - AcceptMammal
     - AcceptPlatypus
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'block_inherit_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/block_inherit_test.dart b/pkgs/ffigen/test/native_objc_test/block_inherit_test.dart
index 93b0782..6981d3f 100644
--- a/pkgs/ffigen/test/native_objc_test/block_inherit_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_inherit_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('Block inheritance', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('BlockInheritTestBase', () {
diff --git a/pkgs/ffigen/test/native_objc_test/block_test.dart b/pkgs/ffigen/test/native_objc_test/block_test.dart
index 9221f2b..257b2cb 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_test.dart
@@ -48,21 +48,9 @@
     )();
 
 void main() {
-  late final BlockTestObjCLibrary lib;
-
   group('Blocks', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = BlockTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
-
+      loadLibrary();
       BlockTester.setup(NativeApi.initializeApiDLData);
     });
 
@@ -501,7 +489,7 @@
 
     (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>)
     blockBlockDartCallRefCountTest() {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final inputBlock = IntBlock.fromFunction((int x) {
         return 5 * x;
       });
@@ -514,7 +502,7 @@
       });
       final outputBlock = blockBlock(inputBlock);
       expect(outputBlock(1), 15);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
 
       // One reference held by inputBlock object, another bound to the
@@ -575,7 +563,7 @@
 
     (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>)
     blockBlockObjCCallRefCountTest() {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       late Pointer<ObjCBlockImpl> inputBlock;
       final blockBlock = BlockBlock.fromFunction((
         ObjCBlock<Int32 Function(Int32)> intBlock,
@@ -587,7 +575,7 @@
       });
       final outputBlock = BlockTester.newBlock(blockBlock, withMult: 2);
       expect(outputBlock(1), 6);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
 
       expect(blockRetainCount(inputBlock), 1);
@@ -639,14 +627,14 @@
 
     (Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>, Pointer<ObjCBlockImpl>)
     nativeBlockBlockDartCallRefCountTest() {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final inputBlock = IntBlock.fromFunction((int x) {
         return 5 * x;
       });
       final blockBlock = BlockTester.newBlockBlock(7);
       final outputBlock = blockBlock(inputBlock);
       expect(outputBlock(1), 35);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       doGC();
 
       // One reference held by inputBlock object, another held internally by the
@@ -700,7 +688,7 @@
     );
 
     (Pointer<Int32>, Pointer<Int32>) objectBlockRefCountTest(Allocator alloc) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final inputCounter = alloc<Int32>();
       final outputCounter = alloc<Int32>();
       inputCounter.value = 0;
@@ -715,7 +703,7 @@
       expect(inputCounter.value, 1);
       expect(outputCounter.value, 1);
 
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       return (inputCounter, outputCounter);
     }
 
@@ -735,7 +723,7 @@
     (Pointer<Int32>, Pointer<Int32>) objectNativeBlockRefCountTest(
       Allocator alloc,
     ) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final inputCounter = alloc<Int32>();
       final outputCounter = alloc<Int32>();
       inputCounter.value = 0;
@@ -750,7 +738,7 @@
       expect(inputCounter.value, 1);
       expect(outputCounter.value, 1);
 
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       return (inputCounter, outputCounter);
     }
 
diff --git a/pkgs/ffigen/test/native_objc_test/block_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/block_test_bindings.dart
index dd59565..7a98f94 100644
--- a/pkgs/ffigen/test/native_objc_test/block_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/block_test_bindings.dart
@@ -2,48 +2,13 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
 
-/// Tests calling Objective-C blocks.
-class BlockTestObjCLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
-
-  /// The symbols are looked up in [dynamicLibrary].
-  BlockTestObjCLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
-
-  /// The symbols are looked up with [lookup].
-  BlockTestObjCLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
-
-  void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool) {
-    return _objc_autoreleasePoolPop(pool);
-  }
-
-  late final _objc_autoreleasePoolPopPtr =
-      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
-        'objc_autoreleasePoolPop',
-      );
-  late final _objc_autoreleasePoolPop = _objc_autoreleasePoolPopPtr
-      .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
-
-  ffi.Pointer<ffi.Void> objc_autoreleasePoolPush() {
-    return _objc_autoreleasePoolPush();
-  }
-
-  late final _objc_autoreleasePoolPushPtr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Void> Function()>>(
-        'objc_autoreleasePoolPush',
-      );
-  late final _objc_autoreleasePoolPush = _objc_autoreleasePoolPushPtr
-      .asFunction<ffi.Pointer<ffi.Void> Function()>();
-}
-
 @ffi.Native<
   ffi.Pointer<objc.ObjCBlockImpl> Function(
     ffi.Pointer<objc.ObjCBlockImpl>,
@@ -204,6 +169,12 @@
   ffi.Pointer<objc.ObjCBlockImpl> block,
 );
 
+@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
+external void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
+
+@ffi.Native<ffi.Pointer<ffi.Void> Function()>()
+external ffi.Pointer<ffi.Void> objc_autoreleasePoolPush();
+
 typedef BlockBlock = ffi.Pointer<objc.ObjCBlockImpl>;
 typedef DartBlockBlock =
     objc.ObjCBlock<
diff --git a/pkgs/ffigen/test/native_objc_test/cast_config.yaml b/pkgs/ffigen/test/native_objc_test/cast_config.yaml
index 3750f08..6409523 100644
--- a/pkgs/ffigen/test/native_objc_test/cast_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/cast_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - Castaway
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'cast_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/cast_test.dart b/pkgs/ffigen/test/native_objc_test/cast_test.dart
index ccd6080..ca4acde 100644
--- a/pkgs/ffigen/test/native_objc_test/cast_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/cast_test.dart
@@ -20,16 +20,7 @@
 
   group('cast', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
       testInstance = Castaway();
     });
 
diff --git a/pkgs/ffigen/test/native_objc_test/category_config.yaml b/pkgs/ffigen/test/native_objc_test/category_config.yaml
index 87cb8d0..ee80696 100644
--- a/pkgs/ffigen/test/native_objc_test/category_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/category_config.yaml
@@ -20,6 +20,8 @@
     - InterfaceOnBuiltInType
     - StaticAndInstanceMethodsWithSameNameCategory
     - NSString
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'category_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/category_test.dart b/pkgs/ffigen/test/native_objc_test/category_test.dart
index 85601d6..8cb7321 100644
--- a/pkgs/ffigen/test/native_objc_test/category_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/category_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('categories', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Category methods', () {
diff --git a/pkgs/ffigen/test/native_objc_test/category_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/category_test_bindings.dart
index 1cc525d..b992b19 100644
--- a/pkgs/ffigen/test/native_objc_test/category_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/category_test_bindings.dart
@@ -2,6 +2,9 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
diff --git a/pkgs/ffigen/test/native_objc_test/enum_config.yaml b/pkgs/ffigen/test/native_objc_test/enum_config.yaml
index 9659a58..0ec968f 100644
--- a/pkgs/ffigen/test/native_objc_test/enum_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/enum_config.yaml
@@ -16,6 +16,8 @@
 objc-interfaces:
   include:
     - EnumTestInterface
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     # Multiple includes to repro https://github.com/dart-lang/native/issues/2782
diff --git a/pkgs/ffigen/test/native_objc_test/enum_test.dart b/pkgs/ffigen/test/native_objc_test/enum_test.dart
index d1b183c..cfe1d99 100644
--- a/pkgs/ffigen/test/native_objc_test/enum_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/enum_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('enum', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('NS_ENUM generates a Dart enum', () {
diff --git a/pkgs/ffigen/test/native_objc_test/error_method_config.yaml b/pkgs/ffigen/test/native_objc_test/error_method_config.yaml
index d21c86b..273ee61 100644
--- a/pkgs/ffigen/test/native_objc_test/error_method_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/error_method_config.yaml
@@ -14,6 +14,8 @@
 objc-interfaces:
   include:
     - 'ErrorMethodTestObject'
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'error_method_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/error_method_test.dart b/pkgs/ffigen/test/native_objc_test/error_method_test.dart
index 3e509b9..a6aa2fd 100644
--- a/pkgs/ffigen/test/native_objc_test/error_method_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/error_method_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('error_method_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test("Error method that returns bool", () {
diff --git a/pkgs/ffigen/test/native_objc_test/failed_to_load_config.yaml b/pkgs/ffigen/test/native_objc_test/failed_to_load_config.yaml
index cfdca56..619bbae 100644
--- a/pkgs/ffigen/test/native_objc_test/failed_to_load_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/failed_to_load_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - ClassThatWillFailToLoad
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'failed_to_load_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/forward_decl_config.yaml b/pkgs/ffigen/test/native_objc_test/forward_decl_config.yaml
index b6fb3a9..710486e 100644
--- a/pkgs/ffigen/test/native_objc_test/forward_decl_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/forward_decl_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - ForwardDeclaredClass
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'forward_decl_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/forward_decl_test.dart b/pkgs/ffigen/test/native_objc_test/forward_decl_test.dart
index 94c40ed..8e8d9ba 100644
--- a/pkgs/ffigen/test/native_objc_test/forward_decl_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/forward_decl_test.dart
@@ -16,16 +16,7 @@
 void main() {
   group('forward decl', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Forward declared class', () {
diff --git a/pkgs/ffigen/test/native_objc_test/global_native_config.yaml b/pkgs/ffigen/test/native_objc_test/global_native_config.yaml
index 88c2a3b..04a9074 100644
--- a/pkgs/ffigen/test/native_objc_test/global_native_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/global_native_config.yaml
@@ -4,6 +4,7 @@
 output: 'global_native_test_bindings.dart'
 exclude-all-by-default: true
 ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 globals:
   include:
     - globalString
diff --git a/pkgs/ffigen/test/native_objc_test/global_native_test.dart b/pkgs/ffigen/test/native_objc_test/global_native_test.dart
index ba62a6b..6060314 100644
--- a/pkgs/ffigen/test/native_objc_test/global_native_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/global_native_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('global using @Native', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Global string', () {
diff --git a/pkgs/ffigen/test/native_objc_test/global_native_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/global_native_test_bindings.dart
index 338fbe2..3b8c71b 100644
--- a/pkgs/ffigen/test/native_objc_test/global_native_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/global_native_test_bindings.dart
@@ -2,6 +2,9 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
diff --git a/pkgs/ffigen/test/native_objc_test/global_test.dart b/pkgs/ffigen/test/native_objc_test/global_test.dart
index d6c6c8b..3dc5dad 100644
--- a/pkgs/ffigen/test/native_objc_test/global_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/global_test.dart
@@ -16,20 +16,10 @@
 import 'util.dart';
 
 void main() {
+  late GlobalTestObjCLibrary lib;
   group('global', () {
-    late GlobalTestObjCLibrary lib;
-
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = GlobalTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      lib = GlobalTestObjCLibrary(DynamicLibrary.open(findDylib("objc_test")));
     });
 
     test('Global string', () {
diff --git a/pkgs/ffigen/test/native_objc_test/inherited_instancetype_config.yaml b/pkgs/ffigen/test/native_objc_test/inherited_instancetype_config.yaml
index 9abafce..23d1499 100644
--- a/pkgs/ffigen/test/native_objc_test/inherited_instancetype_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/inherited_instancetype_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - ChildClass
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'inherited_instancetype_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/inherited_instancetype_test.dart b/pkgs/ffigen/test/native_objc_test/inherited_instancetype_test.dart
index fa576ce..e0317d7 100644
--- a/pkgs/ffigen/test/native_objc_test/inherited_instancetype_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/inherited_instancetype_test.dart
@@ -17,16 +17,7 @@
 void main() {
   group('inheritedInstancetype', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Ordinary init method', () {
diff --git a/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml b/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml
index 463512b..01c9640 100644
--- a/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml
@@ -7,6 +7,8 @@
   include:
     - IsInstanceChildClass
     - IsInstanceUnrelatedClass
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'is_instance_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/is_instance_test.dart b/pkgs/ffigen/test/native_objc_test/is_instance_test.dart
index dbd0554..d1a5b83 100644
--- a/pkgs/ffigen/test/native_objc_test/is_instance_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/is_instance_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('isInstance', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Unrelated classes', () {
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_config.yaml b/pkgs/ffigen/test/native_objc_test/isolate_config.yaml
index 5618162..2b75137 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/isolate_config.yaml
@@ -8,6 +8,8 @@
 objc-interfaces:
   include:
     - Sendable
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'isolate_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test.dart b/pkgs/ffigen/test/native_objc_test/isolate_test.dart
index cb2c456..aa56fa8 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test.dart
@@ -20,20 +20,12 @@
 void main() {
   group('isolate', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     // Runs on other isolate (can't use expect function).
     void sendingObjectTest(SendPort sendPort) async {
+      loadLibrary();
       final port = ReceivePort();
       final queue = StreamQueue(port);
       sendPort.send(port.sendPort);
@@ -81,6 +73,7 @@
       sendable.value = 123;
 
       final oldValue = await Isolate.run(() {
+        loadLibrary();
         final oldValue = sendable!.value;
         sendable!.value = 456;
         return oldValue;
@@ -99,6 +92,7 @@
 
     // Runs on other isolate (can't use expect function).
     void sendingBlockTest(SendPort sendPort) async {
+      loadLibrary();
       final port = ReceivePort();
       final queue = StreamQueue(port);
       sendPort.send(port.sendPort);
@@ -154,6 +148,7 @@
       ObjCBlock<Void Function(Int32)>? block = makeBlock(completer);
 
       await Isolate.run(() {
+        loadLibrary();
         block!(123);
       });
       final value = await completer.future;
@@ -175,6 +170,7 @@
       expect(sendable.ref.isReleased, isFalse);
 
       final (oldIsReleased, newIsReleased) = await Isolate.run(() {
+        loadLibrary();
         final oldIsReleased = sendable.ref.isReleased;
         sendable!.ref.release();
         return (oldIsReleased, sendable.ref.isReleased);
@@ -195,6 +191,7 @@
       expect(sendable.ref.isReleased, isFalse);
 
       await Isolate.run(() {
+        loadLibrary();
         sendable!.ref.release();
       });
 
diff --git a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
index caca893..6c698c4 100644
--- a/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/isolate_test_bindings.dart
@@ -2,6 +2,9 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
diff --git a/pkgs/ffigen/test/native_objc_test/log_config.yaml b/pkgs/ffigen/test/native_objc_test/log_config.yaml
index 4f368e2..350e8b2 100644
--- a/pkgs/ffigen/test/native_objc_test/log_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/log_config.yaml
@@ -7,6 +7,8 @@
   include:
     - LogSpamBaseClass
     - LogSpamChildClass
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'log_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/log_test.dart b/pkgs/ffigen/test/native_objc_test/log_test.dart
index 6241783..313de14 100644
--- a/pkgs/ffigen/test/native_objc_test/log_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/log_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('log_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Duplicate method log spam', () {
@@ -36,7 +27,7 @@
         capturedMessages: logs,
         level: Level.SEVERE,
       );
-      verifyBindings('log', logger);
+      verifyBindings('log', logger: logger);
       expect(logs, isNot(contains(contains('matchingMethod'))));
       expect(logs, isNot(contains(contains('instancetypeMethod'))));
     });
diff --git a/pkgs/ffigen/test/native_objc_test/method_config.yaml b/pkgs/ffigen/test/native_objc_test/method_config.yaml
index ac16866..3be584f 100644
--- a/pkgs/ffigen/test/native_objc_test/method_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/method_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - MethodInterface
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'method_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/method_filtering_config.yaml b/pkgs/ffigen/test/native_objc_test/method_filtering_config.yaml
index b071e09..61dad14 100644
--- a/pkgs/ffigen/test/native_objc_test/method_filtering_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/method_filtering_config.yaml
@@ -28,6 +28,8 @@
       include:
         - includedProtocolMethod
       # If include is non-empty, everything else is excluded.
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'method_filtering_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/method_filtering_test.dart b/pkgs/ffigen/test/native_objc_test/method_filtering_test.dart
index 1f6d645..ccfabdc 100644
--- a/pkgs/ffigen/test/native_objc_test/method_filtering_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/method_filtering_test.dart
@@ -11,6 +11,7 @@
 import 'package:ffigen/ffigen.dart';
 import 'package:ffigen/src/config_provider/config.dart';
 import 'package:ffigen/src/config_provider/config_types.dart';
+import 'package:ffigen/src/header_parser.dart' show parse;
 import 'package:logging/logging.dart';
 import 'package:path/path.dart' as path;
 import 'package:pub_semver/pub_semver.dart';
@@ -20,18 +21,18 @@
 
 void main() {
   group('method filtering', () {
-    late final String bindings;
     group('no version info', () {
       late final String bindings;
       setUpAll(() {
-        bindings = File(
+        final config = testConfigFromPath(
           path.join(
             packagePathForTests,
             'test',
             'native_objc_test',
-            'method_filtering_test_bindings.dart',
+            'method_filtering_config.yaml',
           ),
-        ).readAsStringSync();
+        );
+        bindings = parse(testContext(config)).generate();
       });
 
       test('interfaces', () {
diff --git a/pkgs/ffigen/test/native_objc_test/method_filtering_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/method_filtering_test_bindings.dart
index bf6a623..0db2dca 100644
--- a/pkgs/ffigen/test/native_objc_test/method_filtering_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/method_filtering_test_bindings.dart
@@ -2,6 +2,9 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
diff --git a/pkgs/ffigen/test/native_objc_test/method_test.dart b/pkgs/ffigen/test/native_objc_test/method_test.dart
index e0414e4..7b7e333 100644
--- a/pkgs/ffigen/test/native_objc_test/method_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/method_test.dart
@@ -15,38 +15,28 @@
 import 'util.dart';
 
 void main() {
-  late MethodInterface testInstance;
-
   group('method calls', () {
+    late MethodInterface testInterface;
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
-      testInstance = MethodInterface();
+      loadLibrary();
+      testInterface = MethodInterface.alloc().init();
     });
 
     group('Instance methods', () {
       test('No arguments', () {
-        expect(testInstance.add(), 5);
+        expect(testInterface.add(), 5);
       });
 
       test('One argument', () {
-        expect(testInstance.add$1(23), 23);
+        expect(testInterface.add$1(23), 23);
       });
 
       test('Two arguments', () {
-        expect(testInstance.add$2(23, Y: 17), 40);
+        expect(testInterface.add$2(23, Y: 17), 40);
       });
 
       test('Three arguments', () {
-        expect(testInstance.add$3(23, Y: 17, Z: 60), 100);
+        expect(testInterface.add$3(23, Y: 17, Z: 60), 100);
       });
     });
 
@@ -77,7 +67,7 @@
         input.z = 5.6;
         input.w = 7.8;
 
-        final result = testInstance.twiddleVec4Components(input);
+        final result = testInterface.twiddleVec4Components(input);
         expect(result.x, 3.4);
         expect(result.y, 5.6);
         expect(result.z, 7.8);
@@ -87,16 +77,16 @@
       });
 
       test('Floats', () {
-        expect(testInstance.addFloats(1.23, Y: 4.56), closeTo(5.79, 1e-6));
+        expect(testInterface.addFloats(1.23, Y: 4.56), closeTo(5.79, 1e-6));
       });
 
       test('Doubles', () {
-        expect(testInstance.addDoubles(1.23, Y: 4.56), closeTo(5.79, 1e-6));
+        expect(testInterface.addDoubles(1.23, Y: 4.56), closeTo(5.79, 1e-6));
       });
 
       test('Method with same name as a type', () {
         // Test for https://github.com/dart-lang/native/issues/1007
-        final result = testInstance.Vec4$1();
+        final result = testInterface.Vec4$1();
         expect(result.x, 1);
         expect(result.y, 2);
         expect(result.z, 3);
@@ -106,7 +96,7 @@
 
     test('Instance and static methods with same name', () {
       // Test for https://github.com/dart-lang/native/issues/1136
-      expect(testInstance.instStaticSameName(), 123);
+      expect(testInterface.instStaticSameName(), 123);
       expect(MethodInterface.instStaticSameName$1(), 456);
     });
   });
diff --git a/pkgs/ffigen/test/native_objc_test/native_objc_config.yaml b/pkgs/ffigen/test/native_objc_test/native_objc_config.yaml
index d8ffdec..81d18ec 100644
--- a/pkgs/ffigen/test/native_objc_test/native_objc_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/native_objc_config.yaml
@@ -14,6 +14,8 @@
 objc-interfaces:
   include:
     - Foo
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'native_objc_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/native_objc_test.dart b/pkgs/ffigen/test/native_objc_test/native_objc_test.dart
index fd03658..5b6851c 100644
--- a/pkgs/ffigen/test/native_objc_test/native_objc_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/native_objc_test.dart
@@ -17,16 +17,7 @@
 void main() {
   group('native_objc_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Basic types', () {
diff --git a/pkgs/ffigen/test/native_objc_test/native_objc_test.m b/pkgs/ffigen/test/native_objc_test/native_objc_test.m
index e510df8..1c02df3 100644
--- a/pkgs/ffigen/test/native_objc_test/native_objc_test.m
+++ b/pkgs/ffigen/test/native_objc_test/native_objc_test.m
@@ -5,6 +5,8 @@
 #import <Foundation/NSObject.h>
 #import <Foundation/NSString.h>
 
+void ffigen_load_objc_test() {}
+
 @interface Foo : NSObject {
   double doubleVal;
 }
diff --git a/pkgs/ffigen/test/native_objc_test/ns_range_test.dart b/pkgs/ffigen/test/native_objc_test/ns_range_test.dart
index 79c32f2..48fb2de 100644
--- a/pkgs/ffigen/test/native_objc_test/ns_range_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/ns_range_test.dart
@@ -10,6 +10,7 @@
 
 import 'package:ffigen/ffigen.dart';
 import 'package:logging/logging.dart';
+import 'package:ffigen/src/header_parser.dart' show parse;
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 import '../test_utils.dart';
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_config.yaml b/pkgs/ffigen/test/native_objc_test/nullable_config.yaml
index 1c87afc..9697721 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/nullable_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - NullableInterface
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'nullable_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_inheritance_config.yaml b/pkgs/ffigen/test/native_objc_test/nullable_inheritance_config.yaml
index dc43aab..b2e2485 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_inheritance_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/nullable_inheritance_config.yaml
@@ -7,6 +7,8 @@
   include:
     - NullableBase
     - NullableChild
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'nullable_inheritance_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_inheritance_test.dart b/pkgs/ffigen/test/native_objc_test/nullable_inheritance_test.dart
index 26b861b..39b9ded 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_inheritance_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/nullable_inheritance_test.dart
@@ -7,7 +7,7 @@
 import 'dart:ffi';
 import 'dart:io';
 
-import 'package:objective_c/objective_c.dart';
+import 'package:objective_c/objective_c.dart' as objc;
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
@@ -16,24 +16,15 @@
 import 'util.dart';
 
 void main() {
-  late NullableBase nullableBase;
-  late NullableChild nullableChild;
-  late NSObject obj;
   group('Nullable inheritance', () {
+    late NullableChild nullableChild;
+    late NullableBase nullableBase;
+    late objc.NSObject obj;
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
-      nullableBase = NullableBase();
-      nullableChild = NullableChild();
-      obj = NSObject();
+      loadLibrary();
+      nullableChild = NullableChild.alloc().init();
+      nullableBase = NullableBase.alloc().init();
+      obj = objc.NSObject.alloc().init();
     });
 
     group('Base', () {
@@ -41,18 +32,15 @@
         expect(nullableBase.nullableArg(obj), false);
         expect(nullableBase.nullableArg(null), true);
       });
-
       test('Non-null arguments', () {
         expect(nullableBase.nonNullArg(obj), false);
       });
-
       test('Nullable return', () {
-        expect(nullableBase.nullableReturn(false), isA<NSObject>());
+        expect(nullableBase.nullableReturn(false), isA<objc.NSObject>());
         expect(nullableBase.nullableReturn(true), null);
       });
-
       test('Non-null return', () {
-        expect(nullableBase.nonNullReturn(), isA<NSObject>());
+        expect(nullableBase.nonNullReturn(), isA<objc.NSObject>());
       });
     });
 
@@ -67,7 +55,7 @@
       });
 
       test('Nullable return, changed to non-null', () {
-        expect(nullableChild.nullableReturn(false), isA<NSObject>());
+        expect(nullableChild.nullableReturn(false), isA<objc.NSObject>());
       });
 
       test('Non-null return, changed to nullable', () {
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_test.dart b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
index ca75988..2f8bb43 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
@@ -16,32 +16,23 @@
 import 'util.dart';
 
 void main() {
-  late NullableInterface nullableInterface;
   late NSObject obj;
+  late NullableInterface testInterface;
   group('Nullability', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
-      nullableInterface = NullableInterface();
-      obj = NSObject();
+      loadLibrary();
+      obj = NSObject.alloc().init();
+      testInterface = NullableInterface.alloc().init();
     });
 
     group('Nullable property', () {
       test('Not null', () {
-        nullableInterface.nullableObjectProperty = obj;
-        expect(nullableInterface.nullableObjectProperty, obj);
+        testInterface.nullableObjectProperty = obj;
+        expect(testInterface.nullableObjectProperty, obj);
       });
       test('Null', () {
-        nullableInterface.nullableObjectProperty = null;
-        expect(nullableInterface.nullableObjectProperty, null);
+        testInterface.nullableObjectProperty = null;
+        expect(testInterface.nullableObjectProperty, null);
       });
     });
 
diff --git a/pkgs/ffigen/test/native_objc_test/property_config.yaml b/pkgs/ffigen/test/native_objc_test/property_config.yaml
index 4afcc3d..e737491 100644
--- a/pkgs/ffigen/test/native_objc_test/property_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/property_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - PropertyInterface
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'property_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/property_test.dart b/pkgs/ffigen/test/native_objc_test/property_test.dart
index a897dde..6fa711d 100644
--- a/pkgs/ffigen/test/native_objc_test/property_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/property_test.dart
@@ -16,31 +16,21 @@
 import 'util.dart';
 
 void main() {
-  late PropertyInterface testInstance;
-
   group('properties', () {
+    late PropertyInterface testInterface;
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
-      testInstance = PropertyInterface();
+      loadLibrary();
+      testInterface = PropertyInterface.alloc().init();
     });
 
     group('instance properties', () {
       test('read-only property', () {
-        expect(testInstance.readOnlyProperty, 7);
+        expect(testInterface.readOnlyProperty, 7);
       });
 
       test('read-write property', () {
-        testInstance.readWriteProperty = 23;
-        expect(testInstance.readWriteProperty, 23);
+        testInterface.readWriteProperty = 23;
+        expect(testInterface.readWriteProperty, 23);
       });
     });
 
@@ -65,8 +55,8 @@
         input.z = 5.6;
         input.w = 7.8;
 
-        testInstance.structProperty = input;
-        final result = testInstance.structProperty;
+        testInterface.structProperty = input;
+        final result = testInterface.structProperty;
         expect(result.x, 1.2);
         expect(result.y, 3.4);
         expect(result.z, 5.6);
@@ -76,19 +66,19 @@
       });
 
       test('Floats', () {
-        testInstance.floatProperty = 1.23;
-        expect(testInstance.floatProperty, closeTo(1.23, 1e-6));
+        testInterface.floatProperty = 1.23;
+        expect(testInterface.floatProperty, closeTo(1.23, 1e-6));
       });
 
       test('Doubles', () {
-        testInstance.doubleProperty = 1.23;
-        expect(testInstance.doubleProperty, 1.23);
+        testInterface.doubleProperty = 1.23;
+        expect(testInterface.doubleProperty, 1.23);
       });
     });
 
     test('Instance and static properties with same name', () {
       // Test for https://github.com/dart-lang/native/issues/1136
-      expect(testInstance.instStaticSameName, 123);
+      expect(testInterface.instStaticSameName, 123);
       expect(PropertyInterface.getInstStaticSameName$1(), 456);
     });
 
diff --git a/pkgs/ffigen/test/native_objc_test/protocol_config.yaml b/pkgs/ffigen/test/native_objc_test/protocol_config.yaml
index 418f2f2..ebbd294 100644
--- a/pkgs/ffigen/test/native_objc_test/protocol_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/protocol_config.yaml
@@ -22,6 +22,8 @@
     - MyProtocol
     - SecondaryProtocol
     - UnusedProtocol
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'protocol_test.h'
diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.dart b/pkgs/ffigen/test/native_objc_test/protocol_test.dart
index fd5b9e9..5bb68b7 100644
--- a/pkgs/ffigen/test/native_objc_test/protocol_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/protocol_test.dart
@@ -10,7 +10,7 @@
 import 'dart:isolate';
 
 import 'package:ffi/ffi.dart';
-import 'package:objective_c/objective_c.dart' hide ObjCProtocolImpl;
+import 'package:objective_c/objective_c.dart' hide ObjCProtocolImpl, getClass;
 import 'package:objective_c/src/internal.dart' show getProtocol;
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
@@ -25,20 +25,9 @@
 typedef OtherMethodBlock = ObjCBlock_Int32_ffiVoid_Int32_Int32_Int32_Int32;
 
 void main() {
-  late ProtocolTestObjCLibrary lib;
-
   group('protocol', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = ProtocolTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      loadLibrary();
     });
 
     group('ObjC implementation', () {
@@ -484,7 +473,7 @@
     });
 
     (NSObject, Pointer<ObjCBlockImpl>) blockRefCountTestInner() {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final protocolBuilder = ObjCProtocolBuilder();
 
       final block = InstanceMethodBlock.fromFunction(
@@ -495,7 +484,7 @@
         block,
       );
       final protocol = protocolBuilder.build();
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
 
       final blockPtr = block.ref.pointer;
 
@@ -552,6 +541,7 @@
 
       final isolate = Isolate.spawn(
         (sendPort) {
+          loadLibrary();
           final protoKeepAlive = ObjCProtocolBuilder().build(
             keepIsolateAlive: true,
           );
@@ -598,19 +588,19 @@
     }, skip: !canDoGC);
 
     test('class disposal, builder first', () {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       ObjCProtocolBuilder? protocolBuilder = ObjCProtocolBuilder(
         debugName: 'Foo',
       );
 
       NSObject? protocol = protocolBuilder.build();
-      final clazz = lib.getClass(protocol);
+      final clazz = getClass(protocol);
       expect(
-        lib.getClassName(clazz).cast<Utf8>().toDartString(),
+        getClassName(clazz).cast<Utf8>().toDartString(),
         startsWith('Foo'),
       );
       expect(isValidClass(clazz), isTrue);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
 
       protocolBuilder = null;
       doGC();
@@ -622,19 +612,19 @@
     }, skip: !canDoGC);
 
     test('class disposal, instance first', () {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       ObjCProtocolBuilder? protocolBuilder = ObjCProtocolBuilder(
         debugName: 'Foo',
       );
 
       NSObject? protocol = protocolBuilder.build();
-      final clazz = lib.getClass(protocol);
+      final clazz = getClass(protocol);
       expect(
-        lib.getClassName(clazz).cast<Utf8>().toDartString(),
+        getClassName(clazz).cast<Utf8>().toDartString(),
         startsWith('Foo'),
       );
       expect(isValidClass(clazz), isTrue);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
 
       protocolBuilder = null;
       doGC();
diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.m b/pkgs/ffigen/test/native_objc_test/protocol_test.m
index 7a313bf..05a3b83 100644
--- a/pkgs/ffigen/test/native_objc_test/protocol_test.m
+++ b/pkgs/ffigen/test/native_objc_test/protocol_test.m
@@ -4,8 +4,6 @@
 
 #import <dispatch/dispatch.h>
 
-#define DISABLE_METHOD 1
-
 #include "protocol_test.h"
 
 const char* class_getName(Class cls);
diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/protocol_test_bindings.dart
index eb2e33e..fbcf77d 100644
--- a/pkgs/ffigen/test/native_objc_test/protocol_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/protocol_test_bindings.dart
@@ -2,76 +2,13 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
 
-/// Tests implementing protocols
-class ProtocolTestObjCLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
-
-  /// The symbols are looked up in [dynamicLibrary].
-  ProtocolTestObjCLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
-
-  /// The symbols are looked up with [lookup].
-  ProtocolTestObjCLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
-
-  ffi.Pointer<ffi.Void> getClass(objc.ObjCObject object) {
-    return _getClass(object.ref.pointer);
-  }
-
-  late final _getClassPtr =
-      _lookup<
-        ffi.NativeFunction<
-          ffi.Pointer<ffi.Void> Function(ffi.Pointer<objc.ObjCObjectImpl>)
-        >
-      >('getClass');
-  late final _getClass = _getClassPtr
-      .asFunction<
-        ffi.Pointer<ffi.Void> Function(ffi.Pointer<objc.ObjCObjectImpl>)
-      >();
-
-  ffi.Pointer<ffi.Char> getClassName(ffi.Pointer<ffi.Void> cls) {
-    return _getClassName(cls);
-  }
-
-  late final _getClassNamePtr =
-      _lookup<
-        ffi.NativeFunction<
-          ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Void>)
-        >
-      >('getClassName');
-  late final _getClassName = _getClassNamePtr
-      .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Void>)>();
-
-  void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool) {
-    return _objc_autoreleasePoolPop(pool);
-  }
-
-  late final _objc_autoreleasePoolPopPtr =
-      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
-        'objc_autoreleasePoolPop',
-      );
-  late final _objc_autoreleasePoolPop = _objc_autoreleasePoolPopPtr
-      .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
-
-  ffi.Pointer<ffi.Void> objc_autoreleasePoolPush() {
-    return _objc_autoreleasePoolPush();
-  }
-
-  late final _objc_autoreleasePoolPushPtr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Void> Function()>>(
-        'objc_autoreleasePoolPush',
-      );
-  late final _objc_autoreleasePoolPush = _objc_autoreleasePoolPushPtr
-      .asFunction<ffi.Pointer<ffi.Void> Function()>();
-}
-
 @ffi.Native<
   ffi.Int32 Function(ffi.Pointer<objc.ObjCObjectImpl>, ffi.Pointer<ffi.Void>)
 >()
@@ -201,6 +138,25 @@
   ffi.Pointer<objc.ObjCBlockImpl> block,
 );
 
+@ffi.Native<ffi.Pointer<ffi.Void> Function(ffi.Pointer<objc.ObjCObjectImpl>)>(
+  symbol: 'getClass',
+)
+external ffi.Pointer<ffi.Void> _getClass(
+  ffi.Pointer<objc.ObjCObjectImpl> object,
+);
+
+ffi.Pointer<ffi.Void> getClass(objc.ObjCObject object) =>
+    _getClass(object.ref.pointer);
+
+@ffi.Native<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Void>)>()
+external ffi.Pointer<ffi.Char> getClassName(ffi.Pointer<ffi.Void> cls);
+
+@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
+external void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
+
+@ffi.Native<ffi.Pointer<ffi.Void> Function()>()
+external ffi.Pointer<ffi.Void> objc_autoreleasePoolPush();
+
 /// EmptyProtocol
 extension type EmptyProtocol._(objc.ObjCProtocol object$)
     implements objc.ObjCProtocol {
diff --git a/pkgs/ffigen/test/native_objc_test/ref_count_config.yaml b/pkgs/ffigen/test/native_objc_test/ref_count_config.yaml
index 9e198ae..1fe0602 100644
--- a/pkgs/ffigen/test/native_objc_test/ref_count_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/ref_count_config.yaml
@@ -11,6 +11,8 @@
   include:
     - RefCountTestObject
     - RefCounted
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'ref_count_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/ref_count_test.dart b/pkgs/ffigen/test/native_objc_test/ref_count_test.dart
index 3016fbb..7d74df6 100644
--- a/pkgs/ffigen/test/native_objc_test/ref_count_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/ref_count_test.dart
@@ -16,20 +16,9 @@
 import 'util.dart';
 
 void main() {
-  late RefCountTestObjCLibrary lib;
-
   group('Reference counting', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      lib = RefCountTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      loadLibrary();
     });
 
     test('objectRetainCount edge cases', () {
@@ -131,7 +120,7 @@
       Pointer<ObjCObjectImpl>,
     )
     copyMethodsInner(Pointer<Int32> counter) {
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final obj1 = RefCountTestObject.newWithCounter(counter);
       expect(counter.value, 1);
       final obj2 = obj1.copyMe();
@@ -171,7 +160,7 @@
       expect(objectRetainCount(obj8raw), greaterThan(0));
       expect(objectRetainCount(obj9raw), greaterThan(0));
 
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(objectRetainCount(obj1raw), greaterThan(0));
       expect(objectRetainCount(obj2raw), greaterThan(0));
       expect(objectRetainCount(obj3raw), greaterThan(0));
@@ -238,17 +227,17 @@
       final counter = calloc<Int32>();
       counter.value = 0;
 
-      final pool1 = lib.objc_autoreleasePoolPush();
+      final pool1 = objc_autoreleasePoolPush();
       final obj1raw = autoreleaseMethodsInner(counter);
       doGC();
       // The autorelease pool is still holding a reference to the object.
       expect(counter.value, 1);
       expect(objectRetainCount(obj1raw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool1);
+      objc_autoreleasePoolPop(pool1);
       expect(counter.value, 0);
       expect(objectRetainCount(obj1raw), 0);
 
-      final pool2 = lib.objc_autoreleasePoolPush();
+      final pool2 = objc_autoreleasePoolPush();
       final obj2 = RefCountTestObject.makeAndAutorelease(counter);
       final obj2raw = obj2.ref.pointer;
       expect(counter.value, 1);
@@ -256,7 +245,7 @@
       doGC();
       expect(counter.value, 1);
       expect(objectRetainCount(obj2raw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool2);
+      objc_autoreleasePoolPop(pool2);
       // The obj2 variable still holds a reference to the object.
       expect(counter.value, 1);
       expect(objectRetainCount(obj2raw), greaterThan(0));
@@ -351,13 +340,13 @@
       counter.value = 0;
       // The getters of retain properties retain+autorelease the value. So we
       // need an autorelease pool.
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final (outerObjRaw, retainObjRaw) = retainPropertiesInner(counter);
       doGC();
       expect(objectRetainCount(retainObjRaw), greaterThan(0));
       expect(objectRetainCount(outerObjRaw), 0);
       expect(counter.value, 1);
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(objectRetainCount(retainObjRaw), 0);
       expect(objectRetainCount(outerObjRaw), 0);
       expect(counter.value, 0);
@@ -397,7 +386,7 @@
       counter.value = 0;
       // The getters of copy properties retain+autorelease the value. So we need
       // an autorelease pool.
-      final pool = lib.objc_autoreleasePoolPush();
+      final pool = objc_autoreleasePoolPush();
       final (outerObjRaw, copyObjRaw, anotherCopyRaw) = copyPropertiesInner(
         counter,
       );
@@ -406,7 +395,7 @@
       expect(objectRetainCount(outerObjRaw), 0);
       expect(objectRetainCount(copyObjRaw), 0);
       expect(objectRetainCount(anotherCopyRaw), greaterThan(0));
-      lib.objc_autoreleasePoolPop(pool);
+      objc_autoreleasePoolPop(pool);
       expect(counter.value, 0);
       expect(objectRetainCount(outerObjRaw), 0);
       expect(objectRetainCount(copyObjRaw), 0);
diff --git a/pkgs/ffigen/test/native_objc_test/ref_count_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/ref_count_test_bindings.dart
index 39210fe..55cd5ef 100644
--- a/pkgs/ffigen/test/native_objc_test/ref_count_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/ref_count_test_bindings.dart
@@ -2,47 +2,18 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
 
-/// Tests reference counting of Objective-C objects
-class RefCountTestObjCLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
+@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
+external void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
 
-  /// The symbols are looked up in [dynamicLibrary].
-  RefCountTestObjCLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
-
-  /// The symbols are looked up with [lookup].
-  RefCountTestObjCLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
-
-  void objc_autoreleasePoolPop(ffi.Pointer<ffi.Void> pool) {
-    return _objc_autoreleasePoolPop(pool);
-  }
-
-  late final _objc_autoreleasePoolPopPtr =
-      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
-        'objc_autoreleasePoolPop',
-      );
-  late final _objc_autoreleasePoolPop = _objc_autoreleasePoolPopPtr
-      .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
-
-  ffi.Pointer<ffi.Void> objc_autoreleasePoolPush() {
-    return _objc_autoreleasePoolPush();
-  }
-
-  late final _objc_autoreleasePoolPushPtr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Void> Function()>>(
-        'objc_autoreleasePoolPush',
-      );
-  late final _objc_autoreleasePoolPush = _objc_autoreleasePoolPushPtr
-      .asFunction<ffi.Pointer<ffi.Void> Function()>();
-}
+@ffi.Native<ffi.Pointer<ffi.Void> Function()>()
+external ffi.Pointer<ffi.Void> objc_autoreleasePoolPush();
 
 /// RefCountTestObject
 extension type RefCountTestObject._(objc.ObjCObject object$)
diff --git a/pkgs/ffigen/test/native_objc_test/rename_config.yaml b/pkgs/ffigen/test/native_objc_test/rename_config.yaml
index 2113620..7af6f09 100644
--- a/pkgs/ffigen/test/native_objc_test/rename_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/rename_config.yaml
@@ -23,6 +23,8 @@
 structs:
   include:
     - CollidingStructName
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'rename_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/rename_test.dart b/pkgs/ffigen/test/native_objc_test/rename_test.dart
index bed447c..ebbedb1 100644
--- a/pkgs/ffigen/test/native_objc_test/rename_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/rename_test.dart
@@ -17,16 +17,7 @@
 void main() {
   group('rename_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Renamed class', () {
diff --git a/pkgs/ffigen/test/native_objc_test/runtime_version_config.yaml b/pkgs/ffigen/test/native_objc_test/runtime_version_config.yaml
index df6920b..96918f6 100644
--- a/pkgs/ffigen/test/native_objc_test/runtime_version_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/runtime_version_config.yaml
@@ -10,6 +10,8 @@
 objc-categories:
   include:
     - FutureAPICategoryMethods
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'runtime_version_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/runtime_version_test.dart b/pkgs/ffigen/test/native_objc_test/runtime_version_test.dart
index 296e7e4..2112916 100644
--- a/pkgs/ffigen/test/native_objc_test/runtime_version_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/runtime_version_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('runtime version check', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Interface', () {
diff --git a/pkgs/ffigen/test/native_objc_test/sdk_variable_config.yaml b/pkgs/ffigen/test/native_objc_test/sdk_variable_config.yaml
index 94b2c88..d08cdc0 100644
--- a/pkgs/ffigen/test/native_objc_test/sdk_variable_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/sdk_variable_config.yaml
@@ -13,6 +13,8 @@
       exclude:
         # This method is missing on some machines.
         - includesTextListMarkers
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - '$XCODE/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/AppKit.framework/Headers/NSColorPicker.h'
diff --git a/pkgs/ffigen/test/native_objc_test/sdk_variable_test.dart b/pkgs/ffigen/test/native_objc_test/sdk_variable_test.dart
index d072b42..678f173 100644
--- a/pkgs/ffigen/test/native_objc_test/sdk_variable_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/sdk_variable_test.dart
@@ -7,6 +7,8 @@
 import 'dart:ffi';
 import 'dart:io';
 
+import 'package:ffigen/ffigen.dart';
+import 'package:ffigen/src/header_parser.dart' show parse;
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 import '../test_utils.dart';
@@ -14,27 +16,17 @@
 
 void main() {
   group('SDK variable', () {
-    late String bindings;
-
+    late final String bindings;
     setUpAll(() {
-      final dylib = File(
+      final config = testConfigFromPath(
         path.join(
           packagePathForTests,
           'test',
           'native_objc_test',
-          'objc_test.dylib',
+          'sdk_variable_config.yaml',
         ),
       );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
-      bindings = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'sdk_variable_test_bindings.dart',
-        ),
-      ).readAsStringSync();
+      bindings = parse(testContext(config)).generate();
     });
 
     test('XCODE', () {
diff --git a/pkgs/ffigen/test/native_objc_test/setup.dart b/pkgs/ffigen/test/native_objc_test/setup.dart
deleted file mode 100644
index b06a82d..0000000
--- a/pkgs/ffigen/test/native_objc_test/setup.dart
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:args/args.dart';
-
-// All ObjC source files are compiled with ARC enabled except these.
-const arcDisabledFiles = <String>{'ref_count_test.m'};
-
-Future<void> _runClang(List<String> 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));
-  final result = await process.exitCode;
-  if (result != 0) {
-    throw ProcessException('clang', args, 'Build failed', result);
-  }
-  print('Generated file: $output');
-}
-
-Future<String> _buildObject(String input, {bool objc = false}) async {
-  final output = '$input.o';
-  await _runClang([
-    if (objc) ...['-x', 'objective-c'],
-    if (objc && !arcDisabledFiles.contains(input)) '-fobjc-arc',
-    '-Wno-nullability-completeness',
-    '-c',
-    input,
-    '-fpic',
-  ], output);
-  return output;
-}
-
-Future<void> _linkLib(List<String> inputs, String output) =>
-    _runClang(['-shared', '-framework', 'Foundation', ...inputs], output);
-
-Future<void> _buildLib(List<String> inputs, String output) async {
-  final objFiles = <String>[];
-  for (final input in inputs) {
-    objFiles.add(await _buildObject(input, objc: true));
-  }
-  objFiles.add(
-    await _buildObject('../../../objective_c/src/include/dart_api_dl.c'),
-  );
-  await _linkLib(objFiles, output);
-}
-
-Future<void> _buildSwift(
-  String input,
-  String outputHeader,
-  String outputLib,
-) async {
-  final args = [
-    '-c',
-    input,
-    '-emit-objc-header-path',
-    outputHeader,
-    '-emit-library',
-    '-o',
-    outputLib,
-  ];
-  final process = await Process.start('swiftc', args);
-  unawaited(stdout.addStream(process.stdout));
-  unawaited(stderr.addStream(process.stderr));
-  final result = await process.exitCode;
-  if (result != 0) {
-    throw ProcessException('swiftc', args, 'Build failed', result);
-  }
-  print('Generated files: $outputHeader and $outputLib');
-}
-
-List<String> _getTestNames() {
-  const configSuffix = '_config.yaml';
-  final names = <String>[];
-  for (final entity in Directory.current.listSync()) {
-    final filename = entity.uri.pathSegments.last;
-    if (filename.endsWith(configSuffix)) {
-      names.add(filename.substring(0, filename.length - configSuffix.length));
-    }
-  }
-  return names;
-}
-
-Future<void> build(List<String> testNames) async {
-  // 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 (File(swiftFile).existsSync()) {
-      await _buildSwift(
-        swiftFile,
-        '${name}_test-Swift.h',
-        '${name}_test.dylib',
-      );
-    }
-  }
-
-  // Finally we build the dylib containing all the ObjC test code.
-  print('Building Dynamic Library for Objective C Native Tests...');
-  final mFiles = <String>[];
-  for (final name in _getTestNames()) {
-    final mFile = '${name}_test.m';
-    if (File(mFile).existsSync()) mFiles.add(mFile);
-    final bindingMFile = '${name}_test_bindings.m';
-    if (File(bindingMFile).existsSync()) mFiles.add(bindingMFile);
-  }
-  if (mFiles.isNotEmpty) {
-    await _buildLib(mFiles, 'objc_test.dylib');
-  }
-}
-
-Future<void> clean(List<String> testNames) async {
-  print('Deleting built files...');
-  final filenames = [
-    for (final name in testNames) ...[
-      '${name}_test_bindings.o',
-      '${name}_test.o',
-    ],
-    'objc_test.dylib',
-  ];
-  for (final filename in filenames) {
-    final file = File(filename);
-    if (file.existsSync()) file.deleteSync();
-  }
-}
-
-Future<void> main(List<String> arguments) async {
-  final parser = ArgParser();
-  parser.addFlag('clean');
-  final args = parser.parse(arguments);
-
-  // Allow running this script directly from any path (or an IDE).
-  Directory.current = Platform.script.resolve('.').toFilePath();
-  if (!Platform.isMacOS) {
-    throw const OSError('Objective C tests are only supported on MacOS');
-  }
-
-  if (args.flag('clean')) {
-    return await clean(_getTestNames());
-  }
-
-  return await build(args.rest.isNotEmpty ? args.rest : _getTestNames());
-}
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
index 0c90c24..1ca3e60 100644
--- a/pkgs/ffigen/test/native_objc_test/static_func_native_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/static_func_native_config.yaml
@@ -4,6 +4,7 @@
 output: 'static_func_native_test_bindings.dart'
 exclude-all-by-default: true
 ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 functions:
   include:
     - foo
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
index ac6dd8d..76afa9f 100644
--- a/pkgs/ffigen/test/native_objc_test/static_func_native_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/static_func_native_test.dart
@@ -23,16 +23,7 @@
 void main() {
   group('static functions', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     Pointer<Int32> staticFuncOfObjectRefCountTest(Allocator alloc) {
diff --git a/pkgs/ffigen/test/native_objc_test/static_func_native_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/static_func_native_test_bindings.dart
index 9e36181..f544b6c 100644
--- a/pkgs/ffigen/test/native_objc_test/static_func_native_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/static_func_native_test_bindings.dart
@@ -2,6 +2,9 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/objc_test')
+library;
+
 import 'dart:ffi' as ffi;
 import 'package:objective_c/objective_c.dart' as objc;
 import 'package:ffi/ffi.dart' as pkg_ffi;
diff --git a/pkgs/ffigen/test/native_objc_test/static_func_test.dart b/pkgs/ffigen/test/native_objc_test/static_func_test.dart
index afd82d7..a8027f6 100644
--- a/pkgs/ffigen/test/native_objc_test/static_func_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/static_func_test.dart
@@ -22,19 +22,11 @@
 
 void main() {
   late StaticFuncTestObjCLibrary lib;
-
   group('static functions', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
+      lib = StaticFuncTestObjCLibrary(
+        DynamicLibrary.open(findDylib("objc_test")),
       );
-      verifySetupFile(dylib);
-      lib = StaticFuncTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
     });
 
     Pointer<Int32> staticFuncOfObjectRefCountTest(Allocator alloc) {
diff --git a/pkgs/ffigen/test/native_objc_test/string_config.yaml b/pkgs/ffigen/test/native_objc_test/string_config.yaml
index 395542a..fe7b6e7 100644
--- a/pkgs/ffigen/test/native_objc_test/string_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/string_config.yaml
@@ -6,6 +6,8 @@
 objc-interfaces:
   include:
     - StringUtil
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'string_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/string_test.dart b/pkgs/ffigen/test/native_objc_test/string_test.dart
index d631410..9952b94 100644
--- a/pkgs/ffigen/test/native_objc_test/string_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/string_test.dart
@@ -18,16 +18,7 @@
 void main() {
   group('string', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     for (final s in ['Hello', '🇵🇬', 'Embedded\u0000Null']) {
diff --git a/pkgs/ffigen/test/native_objc_test/swift_class_config.yaml b/pkgs/ffigen/test/native_objc_test/swift_class_config.yaml
index c9e3c64..59f5f67 100644
--- a/pkgs/ffigen/test/native_objc_test/swift_class_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/swift_class_config.yaml
@@ -3,6 +3,8 @@
 language: objc
 output: 'swift_class_test_bindings.dart'
 exclude-all-by-default: true
+ffi-native:
+  asset-id: 'package:ffigen/swift_class_test'
 objc-interfaces:
   include:
     - MySwiftClass
diff --git a/pkgs/ffigen/test/native_objc_test/swift_class_test.dart b/pkgs/ffigen/test/native_objc_test/swift_class_test.dart
index 0b321fb..2a598dc 100644
--- a/pkgs/ffigen/test/native_objc_test/swift_class_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/swift_class_test.dart
@@ -16,16 +16,8 @@
 void main() {
   group('swift_class_test', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'swift_class_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
+      loadSwiftLibrary();
     });
 
     test('Renamed class', () {
diff --git a/pkgs/ffigen/test/native_objc_test/swift_class_test.swift b/pkgs/ffigen/test/native_objc_test/swift_class_test.swift
index 26ae016..6e920a1 100644
--- a/pkgs/ffigen/test/native_objc_test/swift_class_test.swift
+++ b/pkgs/ffigen/test/native_objc_test/swift_class_test.swift
@@ -1,5 +1,8 @@
 import Foundation
 
+@_cdecl("ffigen_load_swift_test")
+public func ffigen_load_swift_test() {}
+
 @objc public class MySwiftClass: NSObject {
   var val = 123;
   @objc public func getValue() -> Int {
diff --git a/pkgs/ffigen/test/native_objc_test/transitive_test.h b/pkgs/ffigen/test/native_objc_test/transitive_test.h
index 9d23455..b5bd49e 100644
--- a/pkgs/ffigen/test/native_objc_test/transitive_test.h
+++ b/pkgs/ffigen/test/native_objc_test/transitive_test.h
@@ -161,7 +161,7 @@
 -(int)bug2935TransitiveBlockInterfaceMethod;
 @end
 
-@protocol Bug2935TransitiveProtocol<NSObject> {}
+@protocol Bug2935TransitiveProtocol<NSObject>
 -(void)bug2935TransitiveProtocolMethod:
     (void (^)(Bug2935TransitiveBlockInterface* itf)) block;
 @end
diff --git a/pkgs/ffigen/test/native_objc_test/typedef_config.yaml b/pkgs/ffigen/test/native_objc_test/typedef_config.yaml
index 976ddea..2fcde43 100644
--- a/pkgs/ffigen/test/native_objc_test/typedef_config.yaml
+++ b/pkgs/ffigen/test/native_objc_test/typedef_config.yaml
@@ -10,6 +10,8 @@
 typedefs:
   include:
     - SomeClassPtr
+ffi-native:
+  asset-id: 'package:ffigen/objc_test'
 headers:
   entry-points:
     - 'typedef_test.m'
diff --git a/pkgs/ffigen/test/native_objc_test/typedef_test.dart b/pkgs/ffigen/test/native_objc_test/typedef_test.dart
index 011364e..6857976 100644
--- a/pkgs/ffigen/test/native_objc_test/typedef_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/typedef_test.dart
@@ -16,16 +16,7 @@
 void main() {
   group('typedef', () {
     setUpAll(() {
-      final dylib = File(
-        path.join(
-          packagePathForTests,
-          'test',
-          'native_objc_test',
-          'objc_test.dylib',
-        ),
-      );
-      verifySetupFile(dylib);
-      DynamicLibrary.open(dylib.absolute.path);
+      loadLibrary();
     });
 
     test('Regression test for #386', () {
diff --git a/pkgs/ffigen/test/native_objc_test/util.dart b/pkgs/ffigen/test/native_objc_test/util.dart
index 3a02145..43a57bc 100644
--- a/pkgs/ffigen/test/native_objc_test/util.dart
+++ b/pkgs/ffigen/test/native_objc_test/util.dart
@@ -21,7 +21,12 @@
 
 import '../test_utils.dart';
 
-void verifyBindings(String testName, [Logger? logger]) {
+void verifyBindings(
+  String testName, {
+  Logger? logger,
+  bool Function(String expected, String actual)? dartVerify,
+  bool Function(String expected, String actual)? objCVerify,
+}) {
   final thisDir = p.join(packagePathForTests, 'test', 'native_objc_test');
   final configFile = p.join(thisDir, '${testName}_config.yaml');
 
@@ -34,14 +39,14 @@
     'test',
     'native_objc_test',
     bindingsName,
-  ]);
+  ], verify: dartVerify);
 
   final mFileName = context.config.output.objCFile.pathSegments.last;
   matchObjCFileWithExpected(context, library, mFileName, [
     'test',
     'native_objc_test',
     mFileName,
-  ]);
+  ], verify: objCVerify);
 }
 
 final _executeInternalCommand = () {
@@ -119,3 +124,19 @@
 
 bool isValidClass(Pointer<Void> clazz) =>
     internal_for_testing.isValidClass(clazz.cast(), forceReloadClasses: true);
+
+String findDylib(String name) =>
+    p.join(packagePathForTests, '.dart_tool', 'lib', '$name.dylib');
+
+// TODO(https://github.com/dart-lang/native/issues/3338): Remove these.
+@Native<Void Function()>(
+  symbol: 'ffigen_load_objc_test',
+  assetId: 'package:ffigen/objc_test',
+)
+external void loadLibrary();
+
+@Native<Void Function()>(
+  symbol: 'ffigen_load_swift_test',
+  assetId: 'package:ffigen/swift_class_test',
+)
+external void loadSwiftLibrary();
diff --git a/pkgs/ffigen/test/native_objc_test/verify_bindings_test.dart b/pkgs/ffigen/test/native_objc_test/verify_bindings_test.dart
index 583471a..f5b5938 100644
--- a/pkgs/ffigen/test/native_objc_test/verify_bindings_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/verify_bindings_test.dart
@@ -28,6 +28,13 @@
       'verify_bindings_test.dart',
     };
 
+    const customVerifiers = {
+      'protocol_test.dart': (
+        _verifyProtocolTestDartBindings,
+        _verifyProtocolTestObjCBindings,
+      ),
+    };
+
     final testFiles =
         testDir
             .listSync()
@@ -42,8 +49,61 @@
       final configName = testFile.replaceFirst('_test.dart', '');
 
       test('verifyBindings for $testFile', () {
-        verifyBindings(configName);
+        final verifiers = customVerifiers[testFile];
+        verifyBindings(
+          configName,
+          dartVerify: verifiers?.$1,
+          objCVerify: verifiers?.$2,
+        );
       });
     }
   });
 }
+
+bool _verifyProtocolTestDartBindings(String expected, String actual) {
+  expect(
+    actual,
+    contains('extension type ProtocolConsumer._(objc.ObjCObject '),
+  );
+  expect(
+    actual,
+    contains('extension type ObjCProtocolImpl._(objc.ObjCObject '),
+  );
+  expect(actual, contains('extension type MyProtocol._(objc.ObjCProtocol '));
+  expect(
+    actual,
+    contains('extension type SecondaryProtocol._(objc.ObjCProtocol '),
+  );
+  expect(actual, contains(r'interface class MyProtocol$Builder {'));
+  expect(actual, contains(r'interface class SecondaryProtocol$Builder {'));
+  expect(
+    actual,
+    contains(
+      'objc.NSString instanceMethod('
+      'objc.NSString s, {required double withDouble})',
+    ),
+  );
+  expect(actual, contains('int optionalMethod(SomeStruct s)'));
+  expect(
+    actual,
+    contains(
+      'int otherMethod('
+      'int a, {required int b, required int c, required int d})',
+    ),
+  );
+  expect(actual, contains('int fooMethod()'));
+  expect(actual, contains('extension type EmptyProtocol._(objc.ObjCProtocol '));
+  expect(actual, isNot(contains('EmptyProtocol is a stub')));
+  expect(actual, contains('SuperProtocol is a stub'));
+  expect(actual, contains('FilteredProtocol is a stub'));
+  return true;
+}
+
+bool _verifyProtocolTestObjCBindings(String expected, String actual) {
+  expect(actual, contains('@protocol(EmptyProtocol)'));
+  expect(actual, contains('@protocol(MyProtocol)'));
+  expect(actual, contains('@protocol(SecondaryProtocol)'));
+  expect(actual, contains('@protocol(UnusedProtocol)'));
+  expect(actual, contains('BLOCKING_BLOCK_IMPL'));
+  return true;
+}
diff --git a/pkgs/ffigen/test/native_test/_expected_native_test_bindings.dart b/pkgs/ffigen/test/native_test/_expected_native_test_bindings.dart
index 090b9f5..0d0e9ae 100644
--- a/pkgs/ffigen/test/native_test/_expected_native_test_bindings.dart
+++ b/pkgs/ffigen/test/native_test/_expected_native_test_bindings.dart
@@ -2,233 +2,74 @@
 //
 // Generated by `package:ffigen`.
 // ignore_for_file: type=lint, unused_import
+@ffi.DefaultAsset('package:ffigen/native_test')
+library;
+
 import 'dart:ffi' as ffi;
 
-/// Native tests.
-class NativeLibrary {
-  /// Holds the symbol lookup function.
-  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
-  _lookup;
+@ffi.Native<ffi.Bool Function(ffi.Bool)>()
+external bool Function1Bool(bool x);
 
-  /// The symbols are looked up in [dynamicLibrary].
-  NativeLibrary(ffi.DynamicLibrary dynamicLibrary)
-    : _lookup = dynamicLibrary.lookup;
+@ffi.Native<ffi.Double Function(ffi.Double)>()
+external double Function1Double(double x);
 
-  /// The symbols are looked up with [lookup].
-  NativeLibrary.fromLookup(
-    ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup,
-  ) : _lookup = lookup;
+@ffi.Native<ffi.Float Function(ffi.Float)>()
+external double Function1Float(double x);
 
-  bool Function1Bool(bool x) {
-    return _Function1Bool(x);
-  }
+@ffi.Native<ffi.Int16 Function(ffi.Int16)>()
+external int Function1Int16(int x);
 
-  late final _Function1BoolPtr =
-      _lookup<ffi.NativeFunction<ffi.Bool Function(ffi.Bool)>>('Function1Bool');
-  late final _Function1Bool =
-      _Function1BoolPtr.asFunction<bool Function(bool)>();
+@ffi.Native<ffi.Int32 Function(ffi.Int32)>()
+external int Function1Int32(int x);
 
-  double Function1Double(double x) {
-    return _Function1Double(x);
-  }
+@ffi.Native<ffi.Int64 Function(ffi.Int64)>()
+external int Function1Int64(int x);
 
-  late final _Function1DoublePtr =
-      _lookup<ffi.NativeFunction<ffi.Double Function(ffi.Double)>>(
-        'Function1Double',
-      );
-  late final _Function1Double =
-      _Function1DoublePtr.asFunction<double Function(double)>();
+@ffi.Native<ffi.Int8 Function(ffi.Int8)>()
+external int Function1Int8(int x);
 
-  double Function1Float(double x) {
-    return _Function1Float(x);
-  }
+@ffi.Native<ffi.IntPtr Function(ffi.IntPtr)>()
+external int Function1IntPtr(int x);
 
-  late final _Function1FloatPtr =
-      _lookup<ffi.NativeFunction<ffi.Float Function(ffi.Float)>>(
-        'Function1Float',
-      );
-  late final _Function1Float =
-      _Function1FloatPtr.asFunction<double Function(double)>();
+@ffi.Native<ffi.Int Function(Struct3)>()
+external int Function1StructPassByValue(Struct3 sum_a_b_c);
 
-  int Function1Int16(int x) {
-    return _Function1Int16(x);
-  }
+@ffi.Native<Struct3 Function(ffi.Int, ffi.Int, ffi.Int)>()
+external Struct3 Function1StructReturnByValue(int a, int b, int c);
 
-  late final _Function1Int16Ptr =
-      _lookup<ffi.NativeFunction<ffi.Int16 Function(ffi.Int16)>>(
-        'Function1Int16',
-      );
-  late final _Function1Int16 =
-      _Function1Int16Ptr.asFunction<int Function(int)>();
+@ffi.Native<ffi.Uint16 Function(ffi.Uint16)>()
+external int Function1Uint16(int x);
 
-  int Function1Int32(int x) {
-    return _Function1Int32(x);
-  }
+@ffi.Native<ffi.Uint32 Function(ffi.Uint32)>()
+external int Function1Uint32(int x);
 
-  late final _Function1Int32Ptr =
-      _lookup<ffi.NativeFunction<ffi.Int32 Function(ffi.Int32)>>(
-        'Function1Int32',
-      );
-  late final _Function1Int32 =
-      _Function1Int32Ptr.asFunction<int Function(int)>();
+@ffi.Native<ffi.Uint64 Function(ffi.Uint64)>()
+external int Function1Uint64(int x);
 
-  int Function1Int64(int x) {
-    return _Function1Int64(x);
-  }
+@ffi.Native<ffi.Uint8 Function(ffi.Uint8)>()
+external int Function1Uint8(int x);
 
-  late final _Function1Int64Ptr =
-      _lookup<ffi.NativeFunction<ffi.Int64 Function(ffi.Int64)>>(
-        'Function1Int64',
-      );
-  late final _Function1Int64 =
-      _Function1Int64Ptr.asFunction<int Function(int)>();
+@ffi.Native<ffi.UintPtr Function(ffi.UintPtr)>()
+external int Function1UintPtr(int x);
 
-  int Function1Int8(int x) {
-    return _Function1Int8(x);
-  }
+@ffi.Native<ffi.UnsignedInt Function(ffi.UnsignedInt)>(symbol: 'funcWithEnum1')
+external int _funcWithEnum1(int value);
 
-  late final _Function1Int8Ptr =
-      _lookup<ffi.NativeFunction<ffi.Int8 Function(ffi.Int8)>>('Function1Int8');
-  late final _Function1Int8 = _Function1Int8Ptr.asFunction<int Function(int)>();
+Enum1 funcWithEnum1(Enum1 value) =>
+    Enum1.fromValue(_funcWithEnum1(value.value));
 
-  int Function1IntPtr(int x) {
-    return _Function1IntPtr(x);
-  }
+@ffi.Native<ffi.UnsignedInt Function(ffi.UnsignedInt)>()
+external int funcWithEnum2(int value);
 
-  late final _Function1IntPtrPtr =
-      _lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.IntPtr)>>(
-        'Function1IntPtr',
-      );
-  late final _Function1IntPtr =
-      _Function1IntPtrPtr.asFunction<int Function(int)>();
+@ffi.Native<ffi.Pointer<Struct1> Function()>()
+external ffi.Pointer<Struct1> getStruct1();
 
-  int Function1StructPassByValue(Struct3 sum_a_b_c) {
-    return _Function1StructPassByValue(sum_a_b_c);
-  }
+@ffi.Native<StructWithEnums Function()>()
+external StructWithEnums getStructWithEnums();
 
-  late final _Function1StructPassByValuePtr =
-      _lookup<ffi.NativeFunction<ffi.Int Function(Struct3)>>(
-        'Function1StructPassByValue',
-      );
-  late final _Function1StructPassByValue =
-      _Function1StructPassByValuePtr.asFunction<int Function(Struct3)>();
-
-  Struct3 Function1StructReturnByValue(int a, int b, int c) {
-    return _Function1StructReturnByValue(a, b, c);
-  }
-
-  late final _Function1StructReturnByValuePtr =
-      _lookup<ffi.NativeFunction<Struct3 Function(ffi.Int, ffi.Int, ffi.Int)>>(
-        'Function1StructReturnByValue',
-      );
-  late final _Function1StructReturnByValue =
-      _Function1StructReturnByValuePtr.asFunction<
-        Struct3 Function(int, int, int)
-      >();
-
-  int Function1Uint16(int x) {
-    return _Function1Uint16(x);
-  }
-
-  late final _Function1Uint16Ptr =
-      _lookup<ffi.NativeFunction<ffi.Uint16 Function(ffi.Uint16)>>(
-        'Function1Uint16',
-      );
-  late final _Function1Uint16 =
-      _Function1Uint16Ptr.asFunction<int Function(int)>();
-
-  int Function1Uint32(int x) {
-    return _Function1Uint32(x);
-  }
-
-  late final _Function1Uint32Ptr =
-      _lookup<ffi.NativeFunction<ffi.Uint32 Function(ffi.Uint32)>>(
-        'Function1Uint32',
-      );
-  late final _Function1Uint32 =
-      _Function1Uint32Ptr.asFunction<int Function(int)>();
-
-  int Function1Uint64(int x) {
-    return _Function1Uint64(x);
-  }
-
-  late final _Function1Uint64Ptr =
-      _lookup<ffi.NativeFunction<ffi.Uint64 Function(ffi.Uint64)>>(
-        'Function1Uint64',
-      );
-  late final _Function1Uint64 =
-      _Function1Uint64Ptr.asFunction<int Function(int)>();
-
-  int Function1Uint8(int x) {
-    return _Function1Uint8(x);
-  }
-
-  late final _Function1Uint8Ptr =
-      _lookup<ffi.NativeFunction<ffi.Uint8 Function(ffi.Uint8)>>(
-        'Function1Uint8',
-      );
-  late final _Function1Uint8 =
-      _Function1Uint8Ptr.asFunction<int Function(int)>();
-
-  int Function1UintPtr(int x) {
-    return _Function1UintPtr(x);
-  }
-
-  late final _Function1UintPtrPtr =
-      _lookup<ffi.NativeFunction<ffi.UintPtr Function(ffi.UintPtr)>>(
-        'Function1UintPtr',
-      );
-  late final _Function1UintPtr =
-      _Function1UintPtrPtr.asFunction<int Function(int)>();
-
-  Enum1 funcWithEnum1(Enum1 value) {
-    return Enum1.fromValue(_funcWithEnum1(value.value));
-  }
-
-  late final _funcWithEnum1Ptr =
-      _lookup<ffi.NativeFunction<ffi.UnsignedInt Function(ffi.UnsignedInt)>>(
-        'funcWithEnum1',
-      );
-  late final _funcWithEnum1 = _funcWithEnum1Ptr.asFunction<int Function(int)>();
-
-  int funcWithEnum2(int value) {
-    return _funcWithEnum2(value);
-  }
-
-  late final _funcWithEnum2Ptr =
-      _lookup<ffi.NativeFunction<ffi.UnsignedInt Function(ffi.UnsignedInt)>>(
-        'funcWithEnum2',
-      );
-  late final _funcWithEnum2 = _funcWithEnum2Ptr.asFunction<int Function(int)>();
-
-  ffi.Pointer<Struct1> getStruct1() {
-    return _getStruct1();
-  }
-
-  late final _getStruct1Ptr =
-      _lookup<ffi.NativeFunction<ffi.Pointer<Struct1> Function()>>(
-        'getStruct1',
-      );
-  late final _getStruct1 = _getStruct1Ptr
-      .asFunction<ffi.Pointer<Struct1> Function()>();
-
-  StructWithEnums getStructWithEnums() {
-    return _getStructWithEnums();
-  }
-
-  late final _getStructWithEnumsPtr =
-      _lookup<ffi.NativeFunction<StructWithEnums Function()>>(
-        'getStructWithEnums',
-      );
-  late final _getStructWithEnums = _getStructWithEnumsPtr
-      .asFunction<StructWithEnums Function()>();
-
-  late final ffi.Pointer<ffi.Int> _globalArray = _lookup<ffi.Int>(
-    'globalArray',
-  );
-
-  ffi.Pointer<ffi.Int> get globalArray => _globalArray;
-}
+@ffi.Array.multi([3])
+@ffi.Native<ffi.Array<ffi.Int>>()
+external ffi.Array<ffi.Int> globalArray;
 
 enum Enum1 {
   enum1Value1(0),
diff --git a/pkgs/ffigen/test/native_test/build_test_dylib.dart b/pkgs/ffigen/test/native_test/build_test_dylib.dart
deleted file mode 100644
index bc3eeb8..0000000
--- a/pkgs/ffigen/test/native_test/build_test_dylib.dart
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2020, 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.
-
-/// =======================================================================
-/// ==== Script to generate dynamic library for native_function_tests =====
-/// =======================================================================
-/// This Script effectively calls the following (but user can provide
-/// command line args which will replace the defaults shown below)-
-///
-/// Linux:
-/// ```
-/// clang -shared -fpic native_test.c -o native_test.so
-/// ```
-/// MacOS:
-/// ```
-/// clang -shared -fpic native_test.c -o native_test.dylib
-/// ```
-/// Windows:
-/// ```
-/// call clang -shared native_test.c -o native_test.dll -Wl,"/DEF:native_test.def"
-/// del native_test.exp
-/// del native_test.lib
-/// ```
-/// =======================================================================
-/// =======================================================================
-/// =======================================================================
-library;
-
-import 'dart:io';
-
-const macOS = 'macos';
-const windows = 'windows';
-const linux = 'linux';
-
-Map<String, Options> platformOptions = {
-  linux: Options(
-    outputfilename: 'native_test.so',
-    sharedFlag: '-shared',
-    inputHeader: 'native_test.c',
-    fPIC: '-fpic',
-  ),
-  windows: Options(
-    outputfilename: 'native_test.dll',
-    sharedFlag: '-shared',
-    inputHeader: 'native_test.c',
-    moduleDefPath: '-Wl,/DEF:native_test.def',
-  ),
-  macOS: Options(
-    outputfilename: 'native_test.dylib',
-    sharedFlag: '-shared',
-    inputHeader: 'native_test.c',
-    fPIC: '-fpic',
-  ),
-};
-
-void main(List<String> arguments) {
-  print('Building Dynamic Library for Native Tests... ');
-  final options = getPlatformOptions()!;
-
-  // Run clang compiler to generate the dynamic library.
-  final processResult = runClangProcess(options);
-  printSuccess(processResult, options);
-}
-
-/// Calls the clang compiler.
-ProcessResult runClangProcess(Options options) {
-  final result = Process.runSync('clang', [
-    options.sharedFlag,
-    options.fPIC,
-    options.inputHeader,
-    '-o',
-    options.outputfilename,
-    options.moduleDefPath,
-    '-Wno-nullability-completeness',
-  ]);
-  return result;
-}
-
-/// Prints success message (or process error if any).
-void printSuccess(ProcessResult result, Options options) {
-  print(result.stdout);
-  if ((result.stderr as String).isEmpty) {
-    print('Generated file: ${options.outputfilename}');
-  } else {
-    print(result.stderr);
-  }
-}
-
-/// Get options based on current platform.
-Options? getPlatformOptions() {
-  if (Platform.isMacOS) {
-    return platformOptions[macOS];
-  } else if (Platform.isWindows) {
-    return platformOptions[windows];
-  } else if (Platform.isLinux) {
-    return platformOptions[linux];
-  } else {
-    throw Exception('Unknown Platform.');
-  }
-}
-
-/// Hold options which would be passed to clang.
-class Options {
-  /// Name of dynamic library to generate.
-  final String outputfilename;
-
-  /// Tells compiler to generate a shared library.
-  final String sharedFlag;
-
-  /// Flag for generating Position Independant Code (Not used on windows).
-  final String fPIC;
-
-  /// Input file.
-  final String inputHeader;
-
-  /// Path to `.def` file containing symbols to export, windows use only.
-  final String moduleDefPath;
-
-  Options({
-    required this.outputfilename,
-    required this.sharedFlag,
-    required this.inputHeader,
-    this.fPIC = '',
-    this.moduleDefPath = '',
-  });
-}
diff --git a/pkgs/ffigen/test/native_test/config.yaml b/pkgs/ffigen/test/native_test/config.yaml
index 13be88e..3394d49 100644
--- a/pkgs/ffigen/test/native_test/config.yaml
+++ b/pkgs/ffigen/test/native_test/config.yaml
@@ -9,6 +9,8 @@
 name: NativeLibrary
 description: 'Native tests.'
 output: '_expected_native_test_bindings.dart'
+ffi-native:
+  asset-id: 'package:ffigen/native_test'
 headers:
   entry-points:
     - 'native_test.c'
diff --git a/pkgs/ffigen/test/native_test/native_test.dart b/pkgs/ffigen/test/native_test/native_test.dart
index e825268..08af977 100644
--- a/pkgs/ffigen/test/native_test/native_test.dart
+++ b/pkgs/ffigen/test/native_test/native_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:ffi';
-import 'dart:io';
 import 'dart:math';
 
 import 'package:ffigen/src/header_parser.dart' show parse;
@@ -12,20 +11,15 @@
 import '../test_utils.dart';
 import '_expected_native_test_bindings.dart';
 
+@Native<Void Function()>(
+  symbol: 'Function1Bool',
+  assetId: 'package:ffigen/native_test',
+)
+external void loadLibrary();
+
 void main() {
-  late NativeLibrary bindings;
   group('native_test', () {
-    setUpAll(() {
-      var dylibName = 'test/native_test/native_test.so';
-      if (Platform.isMacOS) {
-        dylibName = 'test/native_test/native_test.dylib';
-      } else if (Platform.isWindows) {
-        dylibName = r'test\native_test\native_test.dll';
-      }
-      final dylib = File(path.join(packagePathForTests, dylibName));
-      verifySetupFile(dylib);
-      bindings = NativeLibrary(DynamicLibrary.open(dylib.absolute.path));
-    });
+    setUpAll(loadLibrary);
 
     test('generate_bindings', () {
       final context = testContext(
@@ -43,64 +37,52 @@
     });
 
     test('bool', () {
-      expect(bindings.Function1Bool(true), false);
-      expect(bindings.Function1Bool(false), true);
+      expect(Function1Bool(true), false);
+      expect(Function1Bool(false), true);
     });
     test('uint8_t', () {
-      expect(bindings.Function1Uint8(pow(2, 8).toInt()), 42);
+      expect(Function1Uint8(pow(2, 8).toInt()), 42);
     });
     test('uint16_t', () {
-      expect(bindings.Function1Uint16(pow(2, 16).toInt()), 42);
+      expect(Function1Uint16(pow(2, 16).toInt()), 42);
     });
     test('uint32_t', () {
-      expect(bindings.Function1Uint32(pow(2, 32).toInt()), 42);
+      expect(Function1Uint32(pow(2, 32).toInt()), 42);
     });
     test('uint64_t', () {
-      expect(bindings.Function1Uint64(pow(2, 64).toInt()), 42);
+      expect(Function1Uint64(pow(2, 64).toInt()), 42);
     });
     test('int8_t', () {
-      expect(
-        bindings.Function1Int8(pow(2, 7).toInt()),
-        -pow(2, 7).toInt() + 42,
-      );
+      expect(Function1Int8(pow(2, 7).toInt()), -pow(2, 7).toInt() + 42);
     });
     test('int16_t', () {
-      expect(
-        bindings.Function1Int16(pow(2, 15).toInt()),
-        -pow(2, 15).toInt() + 42,
-      );
+      expect(Function1Int16(pow(2, 15).toInt()), -pow(2, 15).toInt() + 42);
     });
     test('int32_t', () {
-      expect(
-        bindings.Function1Int32(pow(2, 31).toInt()),
-        -pow(2, 31).toInt() + 42,
-      );
+      expect(Function1Int32(pow(2, 31).toInt()), -pow(2, 31).toInt() + 42);
     });
     test('int64_t', () {
-      expect(
-        bindings.Function1Int64(pow(2, 63).toInt()),
-        -pow(2, 63).toInt() + 42,
-      );
+      expect(Function1Int64(pow(2, 63).toInt()), -pow(2, 63).toInt() + 42);
     });
     test('intptr_t', () {
-      expect(bindings.Function1IntPtr(0), 42);
+      expect(Function1IntPtr(0), 42);
     });
     test('float', () {
-      expect(bindings.Function1Float(0), 42.0);
+      expect(Function1Float(0), 42.0);
     });
     test('double', () {
-      expect(bindings.Function1Double(0), 42.0);
+      expect(Function1Double(0), 42.0);
     });
     test('array global', () {
-      bindings.globalArray[0] = 1;
-      bindings.globalArray[1] = 2;
-      expect(bindings.globalArray[0], 1);
-      expect(bindings.globalArray[1], 2);
-      bindings.globalArray[1] = 3;
-      expect(bindings.globalArray[1], 3);
+      globalArray[0] = 1;
+      globalArray[1] = 2;
+      expect(globalArray[0], 1);
+      expect(globalArray[1], 2);
+      globalArray[1] = 3;
+      expect(globalArray[1], 3);
     });
     test('Array Test: Order of access', () {
-      final struct1 = bindings.getStruct1();
+      final struct1 = getStruct1();
       var expectedValue = 1;
       final dimensions = [3, 1, 2];
       for (var i = 0; i < dimensions[0]; i++) {
@@ -114,7 +96,7 @@
     });
 
     test('Array Workaround: Range Errors', () {
-      final struct1 = bindings.getStruct1();
+      final struct1 = getStruct1();
       // Index (get) above range.
       expect(
         () => struct1.ref.data[4][0][0],
@@ -172,13 +154,13 @@
     test('Struct By Value', () {
       final r = Random();
       final a = r.nextInt(100), b = r.nextInt(100), c = r.nextInt(100);
-      final s = bindings.Function1StructReturnByValue(a, b, c);
-      expect(bindings.Function1StructPassByValue(s), a + b + c);
+      final s = Function1StructReturnByValue(a, b, c);
+      expect(Function1StructPassByValue(s), a + b + c);
     });
 
     test('Enum1 is a Dart enum', () {
       final enum1 = Enum1.enum1Value1;
-      final result = bindings.funcWithEnum1(enum1);
+      final result = funcWithEnum1(enum1);
       expect(enum1, isA<Enum1>());
       expect(enum1.value, isA<int>());
       expect(result, enum1);
@@ -186,13 +168,13 @@
 
     test('Enum2 is a Dart integer', () {
       final enum2 = Enum2.enum2Value1;
-      final result = bindings.funcWithEnum2(enum2);
+      final result = funcWithEnum2(enum2);
       expect(enum2, isA<int>());
       expect(result, enum2);
     });
 
     test('Enum in a struct', () {
-      final struct1 = bindings.getStructWithEnums();
+      final struct1 = getStructWithEnums();
       Enum1 enum1;
       enum1 = struct1.enum1;
       expect(enum1, Enum1.enum1Value1);
diff --git a/pkgs/ffigen/test/setup.dart b/pkgs/ffigen/test/setup.dart
deleted file mode 100644
index 812765a..0000000
--- a/pkgs/ffigen/test/setup.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-// Runs all the test setup scripts. Usage:
-// dart run test/setup.dart
-
-import 'dart:async';
-import 'dart:io';
-
-Future<void> _run(String subdir, String script, List<String> flags) async {
-  final dir = Platform.script.resolve('$subdir/');
-  print('\nRunning $script in ${dir.toFilePath()}');
-  final args = [
-    '--enable-asserts',
-    'run',
-    dir.resolve(script).toFilePath(),
-    ...flags,
-  ];
-  final process = await Process.start(
-    Platform.executable,
-    args,
-    workingDirectory: dir.toFilePath(),
-  );
-  unawaited(stdout.addStream(process.stdout));
-  unawaited(stderr.addStream(process.stderr));
-  final result = await process.exitCode;
-  if (result != 0) {
-    throw ProcessException(Platform.executable, args, '$script failed', result);
-  }
-}
-
-Future<void> main(List<String> arguments) async {
-  await _run('native_test', 'build_test_dylib.dart', []);
-  if (Platform.isMacOS) {
-    await _run('native_objc_test', 'setup.dart', []);
-  }
-  print('\nSuccess :)\n');
-}
diff --git a/pkgs/ffigen/test/test_utils.dart b/pkgs/ffigen/test/test_utils.dart
index 4a7b394..7ab77f3 100644
--- a/pkgs/ffigen/test/test_utils.dart
+++ b/pkgs/ffigen/test/test_utils.dart
@@ -92,17 +92,6 @@
   }
 }
 
-/// Check whether a file generated by test/setup.dart exists and throw a helpful
-/// exception if it does not.
-void verifySetupFile(File file) {
-  if (!file.existsSync()) {
-    throw NotFoundException(
-      'The file ${file.path} does not exist.\n\n'
-      'You may need to run: dart run test/setup.dart\n',
-    );
-  }
-}
-
 // Remove '\r' for Windows compatibility, then apply user's normalizer.
 String _normalizeGeneratedCode(
   String generated,
diff --git a/pkgs/objective_c/hook/build.dart b/pkgs/objective_c/hook/build.dart
index cc6434a..36f4096 100644
--- a/pkgs/objective_c/hook/build.dart
+++ b/pkgs/objective_c/hook/build.dart
@@ -55,9 +55,7 @@
       }
     }
 
-    final includeTestUtils =
-        input.userDefines['include_test_utils'] as bool? ?? false;
-    if (includeTestUtils) {
+    if (input.userDefines['include_test_utils'] == true) {
       cFiles.add(input.packageRoot.resolve('test/util.c').toFilePath());
       mFiles.add(input.packageRoot.resolve('test/gc_inject.m').toFilePath());
     }