From fb201ad7bbe160da606c77389aa1bfd4befd4b86 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 16 Oct 2025 13:50:46 +1100 Subject: [PATCH 1/5] [ffigen] Migrate ObjC example to Dart API --- .../objective_c/avf_audio_bindings.dart | 153 +----------------- .../objective_c/avf_audio_bindings.dart.m | 2 +- pkgs/ffigen/example/objective_c/config.yaml | 19 --- .../example/objective_c/generate_code.dart | 28 ++++ pkgs/ffigen/lib/ffigen.dart | 5 +- .../lib/src/code_generator/objc_protocol.dart | 9 +- pkgs/ffigen/lib/src/config_provider.dart | 1 + .../ffigen/lib/src/config_provider/utils.dart | 22 ++- .../objective_c_example_test.dart | 5 +- 9 files changed, 59 insertions(+), 185 deletions(-) delete mode 100644 pkgs/ffigen/example/objective_c/config.yaml create mode 100644 pkgs/ffigen/example/objective_c/generate_code.dart diff --git a/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart b/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart index 826113e9e9..5ab673e89a 100644 --- a/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart +++ b/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart @@ -12,96 +12,9 @@ import 'dart:ffi' as ffi; import 'package:objective_c/objective_c.dart' as objc; import 'package:ffi/ffi.dart' as pkg_ffi; -final class AudioStreamBasicDescription extends ffi.Struct { - @ffi.Double() - external double mSampleRate; +final class AudioStreamBasicDescription extends ffi.Opaque {} - @ffi.UnsignedInt() - external int mFormatID; - - @ffi.UnsignedInt() - external int mFormatFlags; - - @ffi.UnsignedInt() - external int mBytesPerPacket; - - @ffi.UnsignedInt() - external int mFramesPerPacket; - - @ffi.UnsignedInt() - external int mBytesPerFrame; - - @ffi.UnsignedInt() - external int mChannelsPerFrame; - - @ffi.UnsignedInt() - external int mBitsPerChannel; - - @ffi.UnsignedInt() - external int mReserved; -} - -sealed class AudioChannelBitmap { - static const kAudioChannelBit_Left = 1; - static const kAudioChannelBit_Right = 2; - static const kAudioChannelBit_Center = 4; - static const kAudioChannelBit_LFEScreen = 8; - static const kAudioChannelBit_LeftSurround = 16; - static const kAudioChannelBit_RightSurround = 32; - static const kAudioChannelBit_LeftCenter = 64; - static const kAudioChannelBit_RightCenter = 128; - static const kAudioChannelBit_CenterSurround = 256; - static const kAudioChannelBit_LeftSurroundDirect = 512; - static const kAudioChannelBit_RightSurroundDirect = 1024; - static const kAudioChannelBit_TopCenterSurround = 2048; - static const kAudioChannelBit_VerticalHeightLeft = 4096; - static const kAudioChannelBit_VerticalHeightCenter = 8192; - static const kAudioChannelBit_VerticalHeightRight = 16384; - static const kAudioChannelBit_TopBackLeft = 32768; - static const kAudioChannelBit_TopBackCenter = 65536; - static const kAudioChannelBit_TopBackRight = 131072; - static const kAudioChannelBit_LeftTopFront = 4096; - static const kAudioChannelBit_CenterTopFront = 8192; - static const kAudioChannelBit_RightTopFront = 16384; - static const kAudioChannelBit_LeftTopMiddle = 2097152; - static const kAudioChannelBit_CenterTopMiddle = 2048; - static const kAudioChannelBit_RightTopMiddle = 8388608; - static const kAudioChannelBit_LeftTopRear = 16777216; - static const kAudioChannelBit_CenterTopRear = 33554432; - static const kAudioChannelBit_RightTopRear = 67108864; -} - -sealed class AudioChannelFlags { - static const kAudioChannelFlags_AllOff = 0; - static const kAudioChannelFlags_RectangularCoordinates = 1; - static const kAudioChannelFlags_SphericalCoordinates = 2; - static const kAudioChannelFlags_Meters = 4; -} - -final class AudioChannelDescription extends ffi.Struct { - @ffi.UnsignedInt() - external int mChannelLabel; - - @ffi.UnsignedInt() - external int mChannelFlags; - - @ffi.Array.multi([3]) - external ffi.Array mCoordinates; -} - -final class AudioChannelLayout extends ffi.Struct { - @ffi.UnsignedInt() - external int mChannelLayoutTag; - - @ffi.UnsignedInt() - external int mChannelBitmap; - - @ffi.UnsignedInt() - external int mNumberChannelDescriptions; - - @ffi.Array.multi([1]) - external ffi.Array mChannelDescriptions; -} +final class AudioChannelLayout extends ffi.Opaque {} final class opaqueCMFormatDescription extends ffi.Opaque {} @@ -579,36 +492,6 @@ late final _sel_channelAssignments = objc.registerName("channelAssignments"); late final _sel_setChannelAssignments_ = objc.registerName( "setChannelAssignments:", ); - -/// WARNING: CASpatialAudioExperience is a stub. To generate bindings for this class, include -/// CASpatialAudioExperience in your config's objc-interfaces list. -/// -/// CASpatialAudioExperience -class CASpatialAudioExperience extends objc.ObjCObjectBase { - CASpatialAudioExperience._( - ffi.Pointer pointer, { - bool retain = false, - bool release = false, - }) : super(pointer, retain: retain, release: release); - - /// Constructs a [CASpatialAudioExperience] that points to the same underlying object as [other]. - CASpatialAudioExperience.castFrom(objc.ObjCObjectBase other) - : this._(other.ref.pointer, retain: true, release: true); - - /// Constructs a [CASpatialAudioExperience] that wraps the given raw object pointer. - CASpatialAudioExperience.castFromPointer( - ffi.Pointer other, { - bool retain = false, - bool release = false, - }) : this._(other, retain: retain, release: release); -} - -late final _sel_intendedSpatialExperience = objc.registerName( - "intendedSpatialExperience", -); -late final _sel_setIntendedSpatialExperience_ = objc.registerName( - "setIntendedSpatialExperience:", -); late final _sel_init = objc.registerName("init"); late final _sel_new = objc.registerName("new"); late final _sel_allocWithZone_ = objc.registerName("allocWithZone:"); @@ -930,24 +813,6 @@ extension AVAudioPlayer$Methods on AVAudioPlayer { : AVAudioPlayer.castFromPointer($ret, retain: false, release: true); } - /// intendedSpatialExperience - CASpatialAudioExperience get intendedSpatialExperience { - objc.checkOsVersionInternal( - 'AVAudioPlayer.intendedSpatialExperience', - iOS: (true, null), - macOS: (true, null), - ); - final $ret = _objc_msgSend_151sglz( - this.ref.pointer, - _sel_intendedSpatialExperience, - ); - return CASpatialAudioExperience.castFromPointer( - $ret, - retain: true, - release: true, - ); - } - /// isMeteringEnabled bool get isMeteringEnabled { objc.checkOsVersionInternal( @@ -1134,20 +999,6 @@ extension AVAudioPlayer$Methods on AVAudioPlayer { _objc_msgSend_1s56lr9(this.ref.pointer, _sel_setEnableRate_, value); } - /// setIntendedSpatialExperience: - set intendedSpatialExperience(CASpatialAudioExperience value) { - objc.checkOsVersionInternal( - 'AVAudioPlayer.setIntendedSpatialExperience:', - iOS: (true, null), - macOS: (true, null), - ); - _objc_msgSend_xtuoz7( - this.ref.pointer, - _sel_setIntendedSpatialExperience_, - value.ref.pointer, - ); - } - /// setMeteringEnabled: set isMeteringEnabled(bool value) { objc.checkOsVersionInternal( diff --git a/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m b/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m index c608a65e61..c2b8aefd50 100644 --- a/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m +++ b/pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m @@ -49,7 +49,7 @@ }; -Protocol* _AVFAudio_AVAudioPlayerDelegate(void) { return @protocol(AVAudioPlayerDelegate); } +Protocol* _NativeLibrary_AVAudioPlayerDelegate(void) { return @protocol(AVAudioPlayerDelegate); } #undef BLOCKING_BLOCK_IMPL #pragma clang diagnostic pop diff --git a/pkgs/ffigen/example/objective_c/config.yaml b/pkgs/ffigen/example/objective_c/config.yaml deleted file mode 100644 index 799c2efa13..0000000000 --- a/pkgs/ffigen/example/objective_c/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# yaml-language-server: $schema=../../ffigen.schema.json - -name: AVFAudio -description: Bindings for AVFAudio. -language: objc -output: 'avf_audio_bindings.dart' -exclude-all-by-default: true -objc-interfaces: - include: - - 'AVAudioPlayer' -headers: - entry-points: - - '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h' -preamble: | - // Copyright (c) 2024, 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. - - // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field, void_checks, annotate_overrides, no_leading_underscores_for_local_identifiers, library_private_types_in_public_api diff --git a/pkgs/ffigen/example/objective_c/generate_code.dart b/pkgs/ffigen/example/objective_c/generate_code.dart new file mode 100644 index 0000000000..5a9612a95a --- /dev/null +++ b/pkgs/ffigen/example/objective_c/generate_code.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2025, 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 'package:ffigen/ffigen.dart'; + +final config = FfiGenerator( + headers: Headers( + entryPoints: [ + Uri.file( + '$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h', + ), + ], + ), + objectiveC: ObjectiveC(interfaces: Interfaces.includeSet({'AVAudioPlayer'})), + output: Output( + dartFile: Uri.file('avf_audio_bindings.dart'), + preamble: ''' +// Copyright (c) 2024, 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. + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field, void_checks, annotate_overrides, no_leading_underscores_for_local_identifiers, library_private_types_in_public_api +''', + ), +); + +void main() => config.generate(); diff --git a/pkgs/ffigen/lib/ffigen.dart b/pkgs/ffigen/lib/ffigen.dart index 64a3193fd3..737bd33217 100644 --- a/pkgs/ffigen/lib/ffigen.dart +++ b/pkgs/ffigen/lib/ffigen.dart @@ -44,4 +44,7 @@ export 'src/config_provider.dart' VarArgFunction, Versions, YamlConfig, - defaultCompilerOpts; + defaultCompilerOpts, + iosSdkPath, + macSdkPath, + xcodePath; diff --git a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart index 660289df27..66f7b6304b 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart @@ -110,6 +110,11 @@ interface class $name extends $protocolBase $impls{ '''); if (!generateAsStub) { + final builder = '$name\$Builder'; + s.write(''' + interface class $builder { + '''); + final buildArgs = []; final buildImplementations = StringBuffer(); final buildListenerImplementations = StringBuffer(); @@ -283,11 +288,9 @@ interface class $name extends $protocolBase $impls{ $builders $listenerBuilders $methodFields -'''); - } - s.write(''' } '''); + } return BindingString( type: BindingStringType.objcProtocol, diff --git a/pkgs/ffigen/lib/src/config_provider.dart b/pkgs/ffigen/lib/src/config_provider.dart index df1a0b5b69..31f24a18c4 100644 --- a/pkgs/ffigen/lib/src/config_provider.dart +++ b/pkgs/ffigen/lib/src/config_provider.dart @@ -8,4 +8,5 @@ library; export 'config_provider/config.dart'; export 'config_provider/config_types.dart'; export 'config_provider/path_finder.dart'; +export 'config_provider/utils.dart' show iosSdkPath, macSdkPath, xcodePath; export 'config_provider/yaml_config.dart'; diff --git a/pkgs/ffigen/lib/src/config_provider/utils.dart b/pkgs/ffigen/lib/src/config_provider/utils.dart index a7780ac7e8..cd6aac1ea1 100644 --- a/pkgs/ffigen/lib/src/config_provider/utils.dart +++ b/pkgs/ffigen/lib/src/config_provider/utils.dart @@ -9,7 +9,7 @@ export 'overrideable_utils.dart'; /// Replaces any variable names in the path with the corresponding value. String substituteVars(String path) { - for (final variable in _variables) { + for (final variable in [_xcode, _iosSdk, _macSdk]) { final key = '\$${variable.key}'; if (path.contains(key)) { path = path.replaceAll(key, variable.value); @@ -27,11 +27,17 @@ class _LazyVariable { String get value => _value ??= firstLineOfStdout(_cmd, _args); } -final _variables = <_LazyVariable>[ - _LazyVariable('XCODE', 'xcode-select', ['-p']), - _LazyVariable('IOS_SDK', 'xcrun', ['--show-sdk-path', '--sdk', 'iphoneos']), - _LazyVariable('MACOS_SDK', 'xcrun', ['--show-sdk-path', '--sdk', 'macosx']), -]; +final _xcode = _LazyVariable('XCODE', 'xcode-select', ['-p']); +final _iosSdk = _LazyVariable('IOS_SDK', 'xcrun', [ + '--show-sdk-path', + '--sdk', + 'iphoneos', +]); +final _macSdk = _LazyVariable('MACOS_SDK', 'xcrun', [ + '--show-sdk-path', + '--sdk', + 'macosx', +]); String firstLineOfStdout(String cmd, List args) { final result = Process.runSync(cmd, args); @@ -41,3 +47,7 @@ String firstLineOfStdout(String cmd, List args) { .where((line) => line.isNotEmpty) .first; } + +String get xcodePath => _xcode.value; +String get iosSdkPath => _iosSdk.value; +String get macSdkPath => _macSdk.value; diff --git a/pkgs/ffigen/test/example_tests/objective_c_example_test.dart b/pkgs/ffigen/test/example_tests/objective_c_example_test.dart index ff365913ca..fe65536dfc 100644 --- a/pkgs/ffigen/test/example_tests/objective_c_example_test.dart +++ b/pkgs/ffigen/test/example_tests/objective_c_example_test.dart @@ -8,9 +8,9 @@ library; import 'package:ffigen/src/header_parser.dart'; import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; import 'package:test/test.dart'; +import '../../example/objective_c/generate_code.dart' show config; import '../test_utils.dart'; void main() { @@ -20,9 +20,6 @@ void main() { }); test('objective_c', () { - final config = testConfigFromPath( - path.join(packagePathForTests, 'example', 'objective_c', 'config.yaml'), - ); final output = parse(testContext(config)).generate(); // Verify that the output contains all the methods and classes that the From ffd13d19fd60cd9afb75ed93bb8b073969bd91b0 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 16 Oct 2025 14:10:48 +1100 Subject: [PATCH 2/5] revert junk --- pkgs/ffigen/lib/src/code_generator/objc_protocol.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart index 66f7b6304b..660289df27 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart @@ -110,11 +110,6 @@ interface class $name extends $protocolBase $impls{ '''); if (!generateAsStub) { - final builder = '$name\$Builder'; - s.write(''' - interface class $builder { - '''); - final buildArgs = []; final buildImplementations = StringBuffer(); final buildListenerImplementations = StringBuffer(); @@ -288,9 +283,11 @@ interface class $name extends $protocolBase $impls{ $builders $listenerBuilders $methodFields -} '''); } + s.write(''' +} +'''); return BindingString( type: BindingStringType.objcProtocol, From fd10f20e73e6a754303d792df5ab62f56766ff37 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 16 Oct 2025 14:19:00 +1100 Subject: [PATCH 3/5] Document new API elements --- pkgs/ffigen/lib/src/config_provider/utils.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkgs/ffigen/lib/src/config_provider/utils.dart b/pkgs/ffigen/lib/src/config_provider/utils.dart index cd6aac1ea1..296a965572 100644 --- a/pkgs/ffigen/lib/src/config_provider/utils.dart +++ b/pkgs/ffigen/lib/src/config_provider/utils.dart @@ -48,6 +48,17 @@ String firstLineOfStdout(String cmd, List args) { .first; } +/// The directory where Xcode's APIs are installed. +/// +/// This is the result of the command `xcode-select -p`. String get xcodePath => _xcode.value; + +/// The directory within [xcodePath] where the iOS SDK is installed. +/// +/// This is the result of the command `xcrun --show-sdk-path --sdk iphoneos`. String get iosSdkPath => _iosSdk.value; + +/// The directory within [xcodePath] where the macOS SDK is installed. +/// +/// This is the result of the command `xcrun --show-sdk-path --sdk macosx`. String get macSdkPath => _macSdk.value; From 7c9a7abe9dc7eff950aa01f7b326337aa2920474 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 16 Oct 2025 14:22:26 +1100 Subject: [PATCH 4/5] update doc --- pkgs/ffigen/doc/apple_apis.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgs/ffigen/doc/apple_apis.md b/pkgs/ffigen/doc/apple_apis.md index 2b7dd15235..76d4b189c5 100644 --- a/pkgs/ffigen/doc/apple_apis.md +++ b/pkgs/ffigen/doc/apple_apis.md @@ -19,3 +19,6 @@ headers: entry-points: - '$MACOS_SDK/System/Library/Frameworks/Foundation.framework/Headers/NSDate.h' ``` + +In the Dart API you can use these getters: +`xcodePath`, `iosSdkPath`, and `macSdkPath`. From 752aebb6c3c693d3d5045f378439b7420831153e Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 16 Oct 2025 14:45:20 +1100 Subject: [PATCH 5/5] readme etc --- pkgs/ffigen/example/objective_c/README.md | 26 +++++++------------ .../example/objective_c/generate_code.dart | 16 +++++++++++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pkgs/ffigen/example/objective_c/README.md b/pkgs/ffigen/example/objective_c/README.md index 89f3a37c75..8e67d0d7a4 100644 --- a/pkgs/ffigen/example/objective_c/README.md +++ b/pkgs/ffigen/example/objective_c/README.md @@ -10,35 +10,29 @@ dart play_audio.dart test.mp3 ## Config notes The FFIgen config for an Objective C library looks very similar to a C library. -The most important difference is that you must set `language: objc`. If you want -to filter which interfaces are included you can use the `objc-interfaces:` -option. This works similarly to the other filtering options. +The most important difference is that you must set `FfiGenerator.objectiveC`. +If you want to filter which interfaces are included you can use the +`FfiGenerator.objectiveC.interfaces` option. +This works similarly to the other filtering options. It is recommended that you filter out just about everything you're not interested in binding (see the FFIgen config in [pubspec.yaml](./pubspec.yaml)). Virtually all Objective C libraries depend on Apple's internal libraries, which are huge. Filtering can reduce the generated bindings from millions of lines to -tens of thousands. You can use the `exclude-all-by-default` flag, or exclude -individual sets of declarations like this: - -```yaml -functions: - exclude: - - '.*' -# Same for structs/unions/enums etc. -``` +thousands. In this example, we're only interested in `AVAudioPlayer`, so we've filtered out -everything else. But FFIgen will automatically pull in anything referenced by -any of the fields or methods of `AVAudioPlayer`, so we're still able to use -`NSURL` etc to load our audio file. +everything else. FFIgen will automatically pull in anything referenced by +any of the fields or methods of `AVAudioPlayer`, but by default they're +generated as stubs. To generate full bindings for the transient dependencies, +add them to your include set, or set `Interfaces.includeTransitive` to `true`. ## Generating bindings At the root of this example (`example/objective_c`), run: ``` -dart run ffigen --config config.yaml +dart run generate_code.dart ``` This will generate [avf_audio_bindings.dart](./avf_audio_bindings.dart). diff --git a/pkgs/ffigen/example/objective_c/generate_code.dart b/pkgs/ffigen/example/objective_c/generate_code.dart index 5a9612a95a..7a71fafa81 100644 --- a/pkgs/ffigen/example/objective_c/generate_code.dart +++ b/pkgs/ffigen/example/objective_c/generate_code.dart @@ -6,15 +6,29 @@ import 'package:ffigen/ffigen.dart'; final config = FfiGenerator( headers: Headers( + // The entryPoints are the files that FFIgen should scan to find the APIs + // you want to generate bindings for. You can use the macSdkPath or + // iosSdkPath getters to find the Apple SDKs. entryPoints: [ Uri.file( '$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h', ), ], ), - objectiveC: ObjectiveC(interfaces: Interfaces.includeSet({'AVAudioPlayer'})), + + // To tell FFIgen to generate Objective-C bindings, rather than C bindings, + // set the objectiveC field to a non-null value. + objectiveC: ObjectiveC( + // The interfaces field is used to tell FFIgen which interfaces to generate + // bindings for. There's also a protocols and a categories field. + interfaces: Interfaces.includeSet({'AVAudioPlayer'}), + ), + output: Output( + // The Dart file where the bindings will be generated. dartFile: Uri.file('avf_audio_bindings.dart'), + + // Preamble text to put at the top of the generated file. preamble: ''' // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a