Skip to content

Commit f7c7d52

Browse files
authored
[ffigen] Migrate ObjC example to Dart API (#2709)
1 parent b17023d commit f7c7d52

File tree

10 files changed

+91
-198
lines changed

10 files changed

+91
-198
lines changed

pkgs/ffigen/doc/apple_apis.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ headers:
1919
entry-points:
2020
- '$MACOS_SDK/System/Library/Frameworks/Foundation.framework/Headers/NSDate.h'
2121
```
22+
23+
In the Dart API you can use these getters:
24+
`xcodePath`, `iosSdkPath`, and `macSdkPath`.

pkgs/ffigen/example/objective_c/README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,29 @@ dart play_audio.dart test.mp3
1010
## Config notes
1111

1212
The FFIgen config for an Objective C library looks very similar to a C library.
13-
The most important difference is that you must set `language: objc`. If you want
14-
to filter which interfaces are included you can use the `objc-interfaces:`
15-
option. This works similarly to the other filtering options.
13+
The most important difference is that you must set `FfiGenerator.objectiveC`.
14+
If you want to filter which interfaces are included you can use the
15+
`FfiGenerator.objectiveC.interfaces` option.
16+
This works similarly to the other filtering options.
1617

1718
It is recommended that you filter out just about everything you're not
1819
interested in binding (see the FFIgen config in [pubspec.yaml](./pubspec.yaml)).
1920
Virtually all Objective C libraries depend on Apple's internal libraries, which
2021
are huge. Filtering can reduce the generated bindings from millions of lines to
21-
tens of thousands. You can use the `exclude-all-by-default` flag, or exclude
22-
individual sets of declarations like this:
23-
24-
```yaml
25-
functions:
26-
exclude:
27-
- '.*'
28-
# Same for structs/unions/enums etc.
29-
```
22+
thousands.
3023

3124
In this example, we're only interested in `AVAudioPlayer`, so we've filtered out
32-
everything else. But FFIgen will automatically pull in anything referenced by
33-
any of the fields or methods of `AVAudioPlayer`, so we're still able to use
34-
`NSURL` etc to load our audio file.
25+
everything else. FFIgen will automatically pull in anything referenced by
26+
any of the fields or methods of `AVAudioPlayer`, but by default they're
27+
generated as stubs. To generate full bindings for the transient dependencies,
28+
add them to your include set, or set `Interfaces.includeTransitive` to `true`.
3529

3630
## Generating bindings
3731

3832
At the root of this example (`example/objective_c`), run:
3933

4034
```
41-
dart run ffigen --config config.yaml
35+
dart run generate_code.dart
4236
```
4337

4438
This will generate [avf_audio_bindings.dart](./avf_audio_bindings.dart).

pkgs/ffigen/example/objective_c/avf_audio_bindings.dart

Lines changed: 2 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -12,96 +12,9 @@ import 'dart:ffi' as ffi;
1212
import 'package:objective_c/objective_c.dart' as objc;
1313
import 'package:ffi/ffi.dart' as pkg_ffi;
1414

15-
final class AudioStreamBasicDescription extends ffi.Struct {
16-
@ffi.Double()
17-
external double mSampleRate;
15+
final class AudioStreamBasicDescription extends ffi.Opaque {}
1816

19-
@ffi.UnsignedInt()
20-
external int mFormatID;
21-
22-
@ffi.UnsignedInt()
23-
external int mFormatFlags;
24-
25-
@ffi.UnsignedInt()
26-
external int mBytesPerPacket;
27-
28-
@ffi.UnsignedInt()
29-
external int mFramesPerPacket;
30-
31-
@ffi.UnsignedInt()
32-
external int mBytesPerFrame;
33-
34-
@ffi.UnsignedInt()
35-
external int mChannelsPerFrame;
36-
37-
@ffi.UnsignedInt()
38-
external int mBitsPerChannel;
39-
40-
@ffi.UnsignedInt()
41-
external int mReserved;
42-
}
43-
44-
sealed class AudioChannelBitmap {
45-
static const kAudioChannelBit_Left = 1;
46-
static const kAudioChannelBit_Right = 2;
47-
static const kAudioChannelBit_Center = 4;
48-
static const kAudioChannelBit_LFEScreen = 8;
49-
static const kAudioChannelBit_LeftSurround = 16;
50-
static const kAudioChannelBit_RightSurround = 32;
51-
static const kAudioChannelBit_LeftCenter = 64;
52-
static const kAudioChannelBit_RightCenter = 128;
53-
static const kAudioChannelBit_CenterSurround = 256;
54-
static const kAudioChannelBit_LeftSurroundDirect = 512;
55-
static const kAudioChannelBit_RightSurroundDirect = 1024;
56-
static const kAudioChannelBit_TopCenterSurround = 2048;
57-
static const kAudioChannelBit_VerticalHeightLeft = 4096;
58-
static const kAudioChannelBit_VerticalHeightCenter = 8192;
59-
static const kAudioChannelBit_VerticalHeightRight = 16384;
60-
static const kAudioChannelBit_TopBackLeft = 32768;
61-
static const kAudioChannelBit_TopBackCenter = 65536;
62-
static const kAudioChannelBit_TopBackRight = 131072;
63-
static const kAudioChannelBit_LeftTopFront = 4096;
64-
static const kAudioChannelBit_CenterTopFront = 8192;
65-
static const kAudioChannelBit_RightTopFront = 16384;
66-
static const kAudioChannelBit_LeftTopMiddle = 2097152;
67-
static const kAudioChannelBit_CenterTopMiddle = 2048;
68-
static const kAudioChannelBit_RightTopMiddle = 8388608;
69-
static const kAudioChannelBit_LeftTopRear = 16777216;
70-
static const kAudioChannelBit_CenterTopRear = 33554432;
71-
static const kAudioChannelBit_RightTopRear = 67108864;
72-
}
73-
74-
sealed class AudioChannelFlags {
75-
static const kAudioChannelFlags_AllOff = 0;
76-
static const kAudioChannelFlags_RectangularCoordinates = 1;
77-
static const kAudioChannelFlags_SphericalCoordinates = 2;
78-
static const kAudioChannelFlags_Meters = 4;
79-
}
80-
81-
final class AudioChannelDescription extends ffi.Struct {
82-
@ffi.UnsignedInt()
83-
external int mChannelLabel;
84-
85-
@ffi.UnsignedInt()
86-
external int mChannelFlags;
87-
88-
@ffi.Array.multi([3])
89-
external ffi.Array<ffi.Float> mCoordinates;
90-
}
91-
92-
final class AudioChannelLayout extends ffi.Struct {
93-
@ffi.UnsignedInt()
94-
external int mChannelLayoutTag;
95-
96-
@ffi.UnsignedInt()
97-
external int mChannelBitmap;
98-
99-
@ffi.UnsignedInt()
100-
external int mNumberChannelDescriptions;
101-
102-
@ffi.Array.multi([1])
103-
external ffi.Array<AudioChannelDescription> mChannelDescriptions;
104-
}
17+
final class AudioChannelLayout extends ffi.Opaque {}
10518

10619
final class opaqueCMFormatDescription extends ffi.Opaque {}
10720

@@ -579,36 +492,6 @@ late final _sel_channelAssignments = objc.registerName("channelAssignments");
579492
late final _sel_setChannelAssignments_ = objc.registerName(
580493
"setChannelAssignments:",
581494
);
582-
583-
/// WARNING: CASpatialAudioExperience is a stub. To generate bindings for this class, include
584-
/// CASpatialAudioExperience in your config's objc-interfaces list.
585-
///
586-
/// CASpatialAudioExperience
587-
class CASpatialAudioExperience extends objc.ObjCObjectBase {
588-
CASpatialAudioExperience._(
589-
ffi.Pointer<objc.ObjCObject> pointer, {
590-
bool retain = false,
591-
bool release = false,
592-
}) : super(pointer, retain: retain, release: release);
593-
594-
/// Constructs a [CASpatialAudioExperience] that points to the same underlying object as [other].
595-
CASpatialAudioExperience.castFrom(objc.ObjCObjectBase other)
596-
: this._(other.ref.pointer, retain: true, release: true);
597-
598-
/// Constructs a [CASpatialAudioExperience] that wraps the given raw object pointer.
599-
CASpatialAudioExperience.castFromPointer(
600-
ffi.Pointer<objc.ObjCObject> other, {
601-
bool retain = false,
602-
bool release = false,
603-
}) : this._(other, retain: retain, release: release);
604-
}
605-
606-
late final _sel_intendedSpatialExperience = objc.registerName(
607-
"intendedSpatialExperience",
608-
);
609-
late final _sel_setIntendedSpatialExperience_ = objc.registerName(
610-
"setIntendedSpatialExperience:",
611-
);
612495
late final _sel_init = objc.registerName("init");
613496
late final _sel_new = objc.registerName("new");
614497
late final _sel_allocWithZone_ = objc.registerName("allocWithZone:");
@@ -930,24 +813,6 @@ extension AVAudioPlayer$Methods on AVAudioPlayer {
930813
: AVAudioPlayer.castFromPointer($ret, retain: false, release: true);
931814
}
932815

933-
/// intendedSpatialExperience
934-
CASpatialAudioExperience get intendedSpatialExperience {
935-
objc.checkOsVersionInternal(
936-
'AVAudioPlayer.intendedSpatialExperience',
937-
iOS: (true, null),
938-
macOS: (true, null),
939-
);
940-
final $ret = _objc_msgSend_151sglz(
941-
this.ref.pointer,
942-
_sel_intendedSpatialExperience,
943-
);
944-
return CASpatialAudioExperience.castFromPointer(
945-
$ret,
946-
retain: true,
947-
release: true,
948-
);
949-
}
950-
951816
/// isMeteringEnabled
952817
bool get isMeteringEnabled {
953818
objc.checkOsVersionInternal(
@@ -1134,20 +999,6 @@ extension AVAudioPlayer$Methods on AVAudioPlayer {
1134999
_objc_msgSend_1s56lr9(this.ref.pointer, _sel_setEnableRate_, value);
11351000
}
11361001

1137-
/// setIntendedSpatialExperience:
1138-
set intendedSpatialExperience(CASpatialAudioExperience value) {
1139-
objc.checkOsVersionInternal(
1140-
'AVAudioPlayer.setIntendedSpatialExperience:',
1141-
iOS: (true, null),
1142-
macOS: (true, null),
1143-
);
1144-
_objc_msgSend_xtuoz7(
1145-
this.ref.pointer,
1146-
_sel_setIntendedSpatialExperience_,
1147-
value.ref.pointer,
1148-
);
1149-
}
1150-
11511002
/// setMeteringEnabled:
11521003
set isMeteringEnabled(bool value) {
11531004
objc.checkOsVersionInternal(

pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
};
5050

5151

52-
Protocol* _AVFAudio_AVAudioPlayerDelegate(void) { return @protocol(AVAudioPlayerDelegate); }
52+
Protocol* _NativeLibrary_AVAudioPlayerDelegate(void) { return @protocol(AVAudioPlayerDelegate); }
5353
#undef BLOCKING_BLOCK_IMPL
5454

5555
#pragma clang diagnostic pop

pkgs/ffigen/example/objective_c/config.yaml

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:ffigen/ffigen.dart';
6+
7+
final config = FfiGenerator(
8+
headers: Headers(
9+
// The entryPoints are the files that FFIgen should scan to find the APIs
10+
// you want to generate bindings for. You can use the macSdkPath or
11+
// iosSdkPath getters to find the Apple SDKs.
12+
entryPoints: [
13+
Uri.file(
14+
'$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h',
15+
),
16+
],
17+
),
18+
19+
// To tell FFIgen to generate Objective-C bindings, rather than C bindings,
20+
// set the objectiveC field to a non-null value.
21+
objectiveC: ObjectiveC(
22+
// The interfaces field is used to tell FFIgen which interfaces to generate
23+
// bindings for. There's also a protocols and a categories field.
24+
interfaces: Interfaces.includeSet({'AVAudioPlayer'}),
25+
),
26+
27+
output: Output(
28+
// The Dart file where the bindings will be generated.
29+
dartFile: Uri.file('avf_audio_bindings.dart'),
30+
31+
// Preamble text to put at the top of the generated file.
32+
preamble: '''
33+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
34+
// for details. All rights reserved. Use of this source code is governed by a
35+
// BSD-style license that can be found in the LICENSE file.
36+
37+
// 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
38+
''',
39+
),
40+
);
41+
42+
void main() => config.generate();

pkgs/ffigen/lib/ffigen.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,7 @@ export 'src/config_provider.dart'
4444
VarArgFunction,
4545
Versions,
4646
YamlConfig,
47-
defaultCompilerOpts;
47+
defaultCompilerOpts,
48+
iosSdkPath,
49+
macSdkPath,
50+
xcodePath;

pkgs/ffigen/lib/src/config_provider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ library;
88
export 'config_provider/config.dart';
99
export 'config_provider/config_types.dart';
1010
export 'config_provider/path_finder.dart';
11+
export 'config_provider/utils.dart' show iosSdkPath, macSdkPath, xcodePath;
1112
export 'config_provider/yaml_config.dart';

pkgs/ffigen/lib/src/config_provider/utils.dart

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export 'overrideable_utils.dart';
99

1010
/// Replaces any variable names in the path with the corresponding value.
1111
String substituteVars(String path) {
12-
for (final variable in _variables) {
12+
for (final variable in [_xcode, _iosSdk, _macSdk]) {
1313
final key = '\$${variable.key}';
1414
if (path.contains(key)) {
1515
path = path.replaceAll(key, variable.value);
@@ -27,11 +27,17 @@ class _LazyVariable {
2727
String get value => _value ??= firstLineOfStdout(_cmd, _args);
2828
}
2929

30-
final _variables = <_LazyVariable>[
31-
_LazyVariable('XCODE', 'xcode-select', ['-p']),
32-
_LazyVariable('IOS_SDK', 'xcrun', ['--show-sdk-path', '--sdk', 'iphoneos']),
33-
_LazyVariable('MACOS_SDK', 'xcrun', ['--show-sdk-path', '--sdk', 'macosx']),
34-
];
30+
final _xcode = _LazyVariable('XCODE', 'xcode-select', ['-p']);
31+
final _iosSdk = _LazyVariable('IOS_SDK', 'xcrun', [
32+
'--show-sdk-path',
33+
'--sdk',
34+
'iphoneos',
35+
]);
36+
final _macSdk = _LazyVariable('MACOS_SDK', 'xcrun', [
37+
'--show-sdk-path',
38+
'--sdk',
39+
'macosx',
40+
]);
3541

3642
String firstLineOfStdout(String cmd, List<String> args) {
3743
final result = Process.runSync(cmd, args);
@@ -41,3 +47,18 @@ String firstLineOfStdout(String cmd, List<String> args) {
4147
.where((line) => line.isNotEmpty)
4248
.first;
4349
}
50+
51+
/// The directory where Xcode's APIs are installed.
52+
///
53+
/// This is the result of the command `xcode-select -p`.
54+
String get xcodePath => _xcode.value;
55+
56+
/// The directory within [xcodePath] where the iOS SDK is installed.
57+
///
58+
/// This is the result of the command `xcrun --show-sdk-path --sdk iphoneos`.
59+
String get iosSdkPath => _iosSdk.value;
60+
61+
/// The directory within [xcodePath] where the macOS SDK is installed.
62+
///
63+
/// This is the result of the command `xcrun --show-sdk-path --sdk macosx`.
64+
String get macSdkPath => _macSdk.value;

pkgs/ffigen/test/example_tests/objective_c_example_test.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ library;
88

99
import 'package:ffigen/src/header_parser.dart';
1010
import 'package:logging/logging.dart';
11-
import 'package:path/path.dart' as path;
1211
import 'package:test/test.dart';
1312

13+
import '../../example/objective_c/generate_code.dart' show config;
1414
import '../test_utils.dart';
1515

1616
void main() {
@@ -20,9 +20,6 @@ void main() {
2020
});
2121

2222
test('objective_c', () {
23-
final config = testConfigFromPath(
24-
path.join(packagePathForTests, 'example', 'objective_c', 'config.yaml'),
25-
);
2623
final output = parse(testContext(config)).generate();
2724

2825
// Verify that the output contains all the methods and classes that the

0 commit comments

Comments
 (0)