Skip to content

Commit 0f9c63c

Browse files
authored
[ffigen] Switch to named params for ObjC methods (#2298)
1 parent 3349a78 commit 0f9c63c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2062
-1827
lines changed

pkgs/ffigen/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
## 19.0.0-wip
1+
## 19.0.0
22

33
- Use package:objective_c 8.0.0.
4+
- __Breaking change__: Major change to the way ObjC methods are code-genned.
5+
Methods now use named parameters, making them more readable and closer to how
6+
they're written in ObjC. For example, the `NSData` method
7+
`dataWithBytes:length:` used to be generated as
8+
`dataWithBytes_length_(Pointer<Void> bytes, int length)`, but is now generated
9+
as `dataWithBytes(Pointer<Void> bytes, {required int length})`. Protocol
10+
methods are not affected.
11+
- Migration tip: A quick way to find affected methods is to search for `_(`.
412
- Make it easier for a downstream clone to change behavior of certain utils.
513
- Fix [a bug](https://github.com/dart-lang/native/issues/1268) where types could
614
occasionally show up as a generic ObjCObjectBase, when they were supposed to

pkgs/ffigen/example/objective_c/avf_audio_bindings.dart

Lines changed: 69 additions & 69 deletions
Large diffs are not rendered by default.

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,55 @@
77
#error "This file must be compiled with ARC enabled"
88
#endif
99

10+
#pragma clang diagnostic push
11+
#pragma clang diagnostic ignored "-Wundeclared-selector"
12+
13+
typedef struct {
14+
int64_t version;
15+
void* (*newWaiter)(void);
16+
void (*awaitWaiter)(void*);
17+
void* (*currentIsolate)(void);
18+
void (*enterIsolate)(void*);
19+
void (*exitIsolate)(void);
20+
int64_t (*getMainPortId)(void);
21+
bool (*getCurrentThreadOwnsIsolate)(int64_t);
22+
} DOBJC_Context;
23+
1024
id objc_retainBlock(id);
1125

26+
#define BLOCKING_BLOCK_IMPL(ctx, BLOCK_SIG, INVOKE_DIRECT, INVOKE_LISTENER) \
27+
assert(ctx->version >= 1); \
28+
void* targetIsolate = ctx->currentIsolate(); \
29+
int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \
30+
return BLOCK_SIG { \
31+
void* currentIsolate = ctx->currentIsolate(); \
32+
bool mayEnterIsolate = \
33+
currentIsolate == NULL && \
34+
ctx->getCurrentThreadOwnsIsolate != NULL && \
35+
ctx->getCurrentThreadOwnsIsolate(targetPort); \
36+
if (currentIsolate == targetIsolate || mayEnterIsolate) { \
37+
if (mayEnterIsolate) { \
38+
ctx->enterIsolate(targetIsolate); \
39+
} \
40+
INVOKE_DIRECT; \
41+
if (mayEnterIsolate) { \
42+
ctx->exitIsolate(); \
43+
} \
44+
} else { \
45+
void* waiter = ctx->newWaiter(); \
46+
INVOKE_LISTENER; \
47+
ctx->awaitWaiter(waiter); \
48+
} \
49+
};
50+
51+
1252
Protocol* _AVFAudio_AVAudioPlayerDelegate(void) { return @protocol(AVAudioPlayerDelegate); }
1353

1454
typedef id (^ProtocolTrampoline)(void * sel);
1555
__attribute__((visibility("default"))) __attribute__((used))
1656
id _AVFAudio_protocolTrampoline_1mbt9g9(id target, void * sel) {
1757
return ((ProtocolTrampoline)((id (*)(id, SEL, SEL))objc_msgSend)(target, @selector(getDOBJCDartProtocolMethodForSelector:), sel))(sel);
1858
}
59+
#undef BLOCKING_BLOCK_IMPL
60+
61+
#pragma clang diagnostic pop

pkgs/ffigen/example/objective_c/play_audio.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ void main(List<String> args) async {
1818
for (final file in args) {
1919
final fileStr = NSString(file);
2020
print('Loading $file');
21-
final fileUrl = NSURL.fileURLWithPath_(fileStr);
21+
final fileUrl = NSURL.fileURLWithPath(fileStr);
2222
final player =
23-
AVAudioPlayer.alloc().initWithContentsOfURL_error_(fileUrl, nullptr);
23+
AVAudioPlayer.alloc().initWithContentsOfURL(fileUrl, error: nullptr);
2424
if (player == null) {
2525
print('Failed to load audio');
2626
continue;

pkgs/ffigen/example/swift/example.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ void main() {
1414

1515
// TODO(https://github.com/dart-lang/ffigen/issues/443): Add a test for this.
1616
DynamicLibrary.open('libswiftapi.dylib');
17-
final object = SwiftClass.new1();
17+
final object = SwiftClass();
1818
print(object.sayHello().toDartString());
1919
print('field = ${object.someField}');
2020
object.someField = 456;

pkgs/ffigen/example/swift/swift_api_bindings.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ class SwiftClass extends objc.NSObject {
219219

220220
/// init
221221
SwiftClass init() {
222-
objc.checkOsVersion('SwiftClass.init',
222+
objc.checkOsVersionInternal('SwiftClass.init',
223223
iOS: (false, (2, 0, 0)), macOS: (false, (10, 0, 0)));
224224
final _ret =
225225
_objc_msgSend_151sglz(this.ref.retainAndReturnPointer(), _sel_init);
@@ -233,7 +233,7 @@ class SwiftClass extends objc.NSObject {
233233
}
234234

235235
/// allocWithZone:
236-
static SwiftClass allocWithZone_(ffi.Pointer<objc.NSZone> zone) {
236+
static SwiftClass allocWithZone(ffi.Pointer<objc.NSZone> zone) {
237237
final _ret =
238238
_objc_msgSend_1cwp428(_class_SwiftClass, _sel_allocWithZone_, zone);
239239
return SwiftClass.castFromPointer(_ret, retain: false, release: true);

pkgs/ffigen/example/swift/swift_api_bindings.dart.m

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,53 @@
77
#error "This file must be compiled with ARC enabled"
88
#endif
99

10+
#pragma clang diagnostic push
11+
#pragma clang diagnostic ignored "-Wundeclared-selector"
12+
13+
typedef struct {
14+
int64_t version;
15+
void* (*newWaiter)(void);
16+
void (*awaitWaiter)(void*);
17+
void* (*currentIsolate)(void);
18+
void (*enterIsolate)(void*);
19+
void (*exitIsolate)(void);
20+
int64_t (*getMainPortId)(void);
21+
bool (*getCurrentThreadOwnsIsolate)(int64_t);
22+
} DOBJC_Context;
23+
1024
id objc_retainBlock(id);
1125

26+
#define BLOCKING_BLOCK_IMPL(ctx, BLOCK_SIG, INVOKE_DIRECT, INVOKE_LISTENER) \
27+
assert(ctx->version >= 1); \
28+
void* targetIsolate = ctx->currentIsolate(); \
29+
int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \
30+
return BLOCK_SIG { \
31+
void* currentIsolate = ctx->currentIsolate(); \
32+
bool mayEnterIsolate = \
33+
currentIsolate == NULL && \
34+
ctx->getCurrentThreadOwnsIsolate != NULL && \
35+
ctx->getCurrentThreadOwnsIsolate(targetPort); \
36+
if (currentIsolate == targetIsolate || mayEnterIsolate) { \
37+
if (mayEnterIsolate) { \
38+
ctx->enterIsolate(targetIsolate); \
39+
} \
40+
INVOKE_DIRECT; \
41+
if (mayEnterIsolate) { \
42+
ctx->exitIsolate(); \
43+
} \
44+
} else { \
45+
void* waiter = ctx->newWaiter(); \
46+
INVOKE_LISTENER; \
47+
ctx->awaitWaiter(waiter); \
48+
} \
49+
};
50+
51+
1252
typedef id (^ProtocolTrampoline)(void * sel);
1353
__attribute__((visibility("default"))) __attribute__((used))
1454
id _SwiftLibrary_protocolTrampoline_1mbt9g9(id target, void * sel) {
1555
return ((ProtocolTrampoline)((id (*)(id, SEL, SEL))objc_msgSend)(target, @selector(getDOBJCDartProtocolMethodForSelector:), sel))(sel);
1656
}
57+
#undef BLOCKING_BLOCK_IMPL
58+
59+
#pragma clang diagnostic pop

pkgs/ffigen/lib/src/code_generator/func.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,6 @@ class Parameter extends AstNode {
259259
super.visitChildren(visitor);
260260
visitor.visit(type);
261261
}
262+
263+
bool get isNullable => type.typealiasType is ObjCNullable;
262264
}

pkgs/ffigen/lib/src/code_generator/objc_methods.dart

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,9 @@ class ObjCMethod extends AstNode {
201201
final ObjCBuiltInFunctions builtInFunctions;
202202
final String? dartDoc;
203203
final String originalName;
204-
final String name;
204+
String name;
205205
String? dartMethodName;
206+
late final String protocolMethodName;
206207
final ObjCProperty? property;
207208
Type returnType;
208209
final List<Parameter> params;
@@ -244,6 +245,43 @@ class ObjCMethod extends AstNode {
244245
}) : params = params_ ?? [],
245246
selObject = builtInFunctions.getSelObject(originalName);
246247

248+
// Must be called after all params are added to the method.
249+
void finalizeParams() {
250+
protocolMethodName = name.replaceAll(':', '_');
251+
252+
// Split the name at the ':'. The first chunk is the name of the method, and
253+
// the rest of the chunks are named parameters. Eg NSString's
254+
// - compare:options:range:
255+
// method becomes
256+
// NSComparisonResult compare(NSString string,
257+
// {required NSStringCompareOptions options, required NSRange range})
258+
final chunks = name.split(':');
259+
260+
// Details:
261+
// - The first chunk is always the new Dart method name.
262+
// - The rest of the chunks correspond to the params, so there's always
263+
// one more chunk than the number of params.
264+
// - The correspondence between the chunks and the params is non-trivial:
265+
// - The ObjC name always ends with a ':' unless there are no ':' at all.
266+
// - The first param is an ordinary param, not a named param.
267+
final correctNumParams = chunks.length == params.length + 1;
268+
final lastChunkIsEmpty = chunks.length == 1 || chunks.last.isEmpty;
269+
if (correctNumParams && lastChunkIsEmpty) {
270+
// Take the first chunk as the name, ignore the last chunk, and map the
271+
// rest to each of the params after the first.
272+
name = chunks[0];
273+
for (var i = 1; i < params.length; ++i) {
274+
params[i].name = chunks[i];
275+
}
276+
} else {
277+
// There are a few methods that don't obey these rules, eg due to variadic
278+
// parameters. Most of these are omitted from the bindings as they're not
279+
// supported yet. But as a fallback, just replace all the ':' in the name
280+
// with '_', like we do for protocol methods.
281+
name = protocolMethodName;
282+
}
283+
}
284+
247285
bool get isProperty =>
248286
kind == ObjCMethodKind.propertyGetter ||
249287
kind == ObjCMethodKind.propertySetter;
@@ -271,9 +309,11 @@ class ObjCMethod extends AstNode {
271309
)..fillProtocolTrampoline();
272310
}
273311

274-
String getDartMethodName(UniqueNamer uniqueNamer,
275-
{bool usePropertyNaming = true}) {
276-
if (property != null && usePropertyNaming) {
312+
String getDartProtocolMethodName(UniqueNamer uniqueNamer) =>
313+
uniqueNamer.makeUnique(protocolMethodName);
314+
315+
String getDartMethodName(UniqueNamer uniqueNamer) {
316+
if (property != null) {
277317
// A getter and a setter are allowed to have the same name, so we can't
278318
// just run the name through uniqueNamer. Instead they need to share
279319
// the dartName, which is run through uniqueNamer.
@@ -282,12 +322,8 @@ class ObjCMethod extends AstNode {
282322
}
283323
return property!.dartName!;
284324
}
285-
// Objective C methods can look like:
286-
// foo
287-
// foo:
288-
// foo:someArgName:
289-
// So replace all ':' with '_'.
290-
return uniqueNamer.makeUnique(name.replaceAll(':', '_'));
325+
326+
return uniqueNamer.makeUnique(name);
291327
}
292328

293329
bool sameAs(ObjCMethod other) {
@@ -345,20 +381,35 @@ class ObjCMethod extends AstNode {
345381
return returnType.getDartType(w);
346382
}
347383

384+
static String _paramToStr(Writer w, Parameter p) =>
385+
'${p.isCovariant ? 'covariant ' : ''}${p.type.getDartType(w)} ${p.name}';
386+
387+
static String _paramToNamed(Writer w, Parameter p) =>
388+
'${p.isNullable ? '' : 'required '}${_paramToStr(w, p)}';
389+
390+
static String _joinParamStr(Writer w, List<Parameter> params) {
391+
if (params.isEmpty) return '';
392+
if (params.length == 1) return _paramToStr(w, params.first);
393+
final named = params.sublist(1).map((p) => _paramToNamed(w, p)).join(',');
394+
return '${_paramToStr(w, params.first)}, {$named}';
395+
}
396+
348397
String generateBindings(
349398
Writer w, ObjCInterface target, UniqueNamer methodNamer) {
350-
dartMethodName ??= getDartMethodName(methodNamer);
399+
if (dartMethodName == null) {
400+
dartMethodName = getDartMethodName(methodNamer);
401+
final paramNamer = UniqueNamer(parent: methodNamer);
402+
for (final p in params) {
403+
p.name = paramNamer.makeUnique(p.name);
404+
}
405+
}
351406
final methodName = dartMethodName!;
352407
final upperName = methodName[0].toUpperCase() + methodName.substring(1);
353408
final s = StringBuffer();
354409

355410
final targetType = target.getDartType(w);
356411
final returnTypeStr = _getConvertedReturnType(w, targetType);
357-
final paramStr = <String>[
358-
for (final p in params)
359-
'${p.isCovariant ? 'covariant ' : ''}'
360-
'${p.type.getDartType(w)} ${p.name}',
361-
].join(', ');
412+
final paramStr = _joinParamStr(w, params);
362413

363414
// The method declaration.
364415
s.write('\n ${makeDartDoc(dartDoc)} ');

pkgs/ffigen/lib/src/code_generator/objc_protocol.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ interface class $name extends $protocolBase $impls{
112112

113113
var anyListeners = false;
114114
for (final method in methods) {
115-
final methodName =
116-
method.getDartMethodName(methodNamer, usePropertyNaming: false);
115+
final methodName = method.getDartProtocolMethodName(methodNamer);
117116
final fieldName = methodName;
118117
final argName = methodName;
119118
final block = method.protocolBlock!;

pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ void _parseSuperType(clang_types.CXCursor cursor, ObjCInterface itf) {
159159
returnType: fieldType,
160160
family: null,
161161
apiAvailability: apiAvailability,
162-
);
162+
)..finalizeParams();
163163

164164
ObjCMethod? setter;
165165
if (!isReadOnly) {
@@ -181,6 +181,7 @@ void _parseSuperType(clang_types.CXCursor cursor, ObjCInterface itf) {
181181
);
182182
setter.params
183183
.add(Parameter(name: 'value', type: fieldType, objCConsumed: false));
184+
setter.finalizeParams();
184185
}
185186
return (getter, setter);
186187
}
@@ -244,6 +245,7 @@ ObjCMethod? parseObjCMethod(clang_types.CXCursor cursor, Declaration itfDecl,
244245
default:
245246
}
246247
});
248+
method.finalizeParams();
247249
return hasError ? null : method;
248250
}
249251

pkgs/ffigen/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# BSD-style license that can be found in the LICENSE file.
44

55
name: ffigen
6-
version: 19.0.0-wip
6+
version: 19.0.0
77
description: >
88
Generator for FFI bindings, using LibClang to parse C, Objective-C, and Swift
99
files.

pkgs/ffigen/test/example_tests/objective_c_example_test.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ void main() {
2929
expect(output, contains('class AVAudioPlayer extends objc.NSObject {'));
3030
expect(
3131
output,
32-
contains(
33-
'AVAudioPlayer? initWithContentsOfURL_error_(objc.NSURL url, '
34-
'ffi.Pointer<ffi.Pointer<objc.ObjCObject>> outError) {'));
32+
contains('AVAudioPlayer? initWithContentsOfURL(objc.NSURL url, '
33+
'{required ffi.Pointer<ffi.Pointer<objc.ObjCObject>> error}) {'));
3534
expect(output, contains('double get duration {'));
3635
expect(output, contains('bool play() {'));
3736
});

0 commit comments

Comments
 (0)