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`. 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/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..7a71fafa81 --- /dev/null +++ b/pkgs/ffigen/example/objective_c/generate_code.dart @@ -0,0 +1,42 @@ +// 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( + // 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', + ), + ], + ), + + // 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 +// 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/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..296a965572 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,18 @@ String firstLineOfStdout(String cmd, List args) { .where((line) => line.isNotEmpty) .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; 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