Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkgs/ffigen/doc/apple_apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
26 changes: 10 additions & 16 deletions pkgs/ffigen/example/objective_c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
153 changes: 2 additions & 151 deletions pkgs/ffigen/example/objective_c/avf_audio_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ffi.Float> 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<AudioChannelDescription> mChannelDescriptions;
}
final class AudioChannelLayout extends ffi.Opaque {}

final class opaqueCMFormatDescription extends ffi.Opaque {}

Expand Down Expand Up @@ -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<objc.ObjCObject> 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<objc.ObjCObject> 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:");
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion pkgs/ffigen/example/objective_c/avf_audio_bindings.dart.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 0 additions & 19 deletions pkgs/ffigen/example/objective_c/config.yaml

This file was deleted.

42 changes: 42 additions & 0 deletions pkgs/ffigen/example/objective_c/generate_code.dart
Original file line number Diff line number Diff line change
@@ -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();
5 changes: 4 additions & 1 deletion pkgs/ffigen/lib/ffigen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ export 'src/config_provider.dart'
VarArgFunction,
Versions,
YamlConfig,
defaultCompilerOpts;
defaultCompilerOpts,
iosSdkPath,
macSdkPath,
xcodePath;
1 change: 1 addition & 0 deletions pkgs/ffigen/lib/src/config_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
33 changes: 27 additions & 6 deletions pkgs/ffigen/lib/src/config_provider/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<String> args) {
final result = Process.runSync(cmd, args);
Expand All @@ -41,3 +47,18 @@ String firstLineOfStdout(String cmd, List<String> 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;
5 changes: 1 addition & 4 deletions pkgs/ffigen/test/example_tests/objective_c_example_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand Down
Loading