Skip to content

Commit 3ff7b44

Browse files
authored
[ffigen] Move all instance methods to extension methods (#2434)
1 parent db42150 commit 3ff7b44

19 files changed

+413
-188
lines changed

pkgs/ffigen/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
`Config` classes. Rename `FfiGen.run` to `.generate`, and make it an extension
55
method on the `Config` class. So `FfiGen().run(config)` becomes
66
`config.generate(logger)`.
7+
- __Breaking change__: Minor breaking change in the way that ObjC interface
8+
methods are generated. Interface methods are now generated as extension
9+
methods instead of being part of the class. This shouldn't require any code
10+
changes unless you are using `show` or `hide` when importing the interface.
11+
- If you are using `show`/`hide` to show or hide a particular interface, eg
12+
`Foo`, you'll now also need to show or hide `Foo$Methods`.
13+
- In rare cases the runtime type of the Dart wrapper object around the ObjC
14+
object may change, but the underlying ObjC object will still be the same.
15+
In any case, you should be using `Foo.isInstance(x)` instead of `x is Foo`
16+
to check the runtime type of an ObjC object.
717

818
## 19.1.0
919

pkgs/ffigen/dartdoc_options.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dartdoc:
2+
categories:
3+
Errors:
4+
markdown: doc/errors.md
5+
name: Common errors in ffigen

pkgs/ffigen/doc/errors.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,30 @@ compiler-opts:
2424
As a last resort, you can pass in `--ignore-source-errors` or set `ignore-source-errors: true` in yaml config.
2525

2626
**Warning: This will likely lead to incorrect bindings!**
27+
28+
## Errors in Objective-C bindings
29+
30+
### Method not defined
31+
32+
ObjC's method naming is a bit different to Dart's. For example, `NSString` has
33+
a method `stringWithCharacters:length:`. To make invocation feel as similar to
34+
ObjC as possible, we generate this as a method named `stringWithCharacters`,
35+
with a named required parameter called `length`.
36+
37+
So one reason you might be having trouble invoking the method is that this
38+
naming convention is different to ObjC. The generated methods have a comment
39+
giving their full ObjC name, so you can try searching for that string to find
40+
the corresponding Dart method.
41+
42+
Since Dart doesn't support method overloading, the method may have been renamed
43+
to avoid collisions. Again the best approach is to search for the ObjC method
44+
name.
45+
46+
When ffigen generates bindings for an ObjC interface, eg `Foo`, it generates a
47+
Dart class `Foo`, which contains the constructor and static methods. The
48+
instance methods are generated in an extension, `Foo$Methods`.
49+
50+
So if you've searched for the generated method and found it, and still can't
51+
invoke it, it's possible that you've imported the class, `Foo`, but not the
52+
extension `Foo$Methods`. This can happen if you're using `show`/`hide` when you
53+
import the bindings. Make sure to import both `Foo` and `Foo$Methods`.

pkgs/ffigen/lib/ffigen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
///
77
/// For most use cases the YAML based API is simpler. See
88
/// https://pub.dev/packages/ffigen for details.
9+
///
10+
/// {@category Errors}
911
library;
1012

1113
export 'src/code_generator/imports.dart' show ImportedType, LibraryImport;

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,28 @@ class $name extends ${superType?.getDartType(w) ?? wrapObjType} $protoImpl{
111111
{bool retain = false, bool release = false}) :
112112
this._(other, retain: retain, release: release);
113113
114-
${generateAsStub ? '' : _generateMethods(w)}
114+
${generateAsStub ? '' : _generateStaticMethods(w)}
115115
}
116116
117117
''');
118118

119+
if (!generateAsStub) {
120+
final extName = w.topLevelUniqueNamer.makeUnique('$name\$Methods');
121+
s.write('''
122+
extension $extName on $name {
123+
${generateInstanceMethodBindings(w, this)}
124+
}
125+
126+
''');
127+
}
128+
119129
return BindingString(
120130
type: BindingStringType.objcInterface,
121131
string: s.toString(),
122132
);
123133
}
124134

125-
String _generateMethods(Writer w) {
135+
String _generateStaticMethods(Writer w) {
126136
final wrapObjType = ObjCBuiltInFunctions.objectBase.gen(w);
127137
final s = StringBuffer();
128138

@@ -132,7 +142,8 @@ ${generateAsStub ? '' : _generateMethods(w)}
132142
return ${_isKindOfClassMsgSend.invoke(w, 'obj.ref.pointer', _isKindOfClass.name, [classObject.name])};
133143
}
134144
''');
135-
s.write(generateMethodBindings(w, this));
145+
146+
s.write(generateStaticMethodBindings(w, this));
136147

137148
final newMethod = methods
138149
.where(

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,21 @@ mixin ObjCMethods {
115115
_methods = newMethods;
116116
}
117117

118-
String generateMethodBindings(Writer w, ObjCInterface target) {
118+
String generateMethodBindings(Writer w, ObjCInterface target) =>
119+
_generateMethods(w, target, null);
120+
121+
String generateStaticMethodBindings(Writer w, ObjCInterface target) =>
122+
_generateMethods(w, target, true);
123+
124+
String generateInstanceMethodBindings(Writer w, ObjCInterface target) =>
125+
_generateMethods(w, target, false);
126+
127+
String _generateMethods(Writer w, ObjCInterface target, bool? staticMethods) {
119128
final methodNamer = createMethodRenamer(w);
120129
return [
121-
for (final m in methods) m.generateBindings(w, target, methodNamer),
130+
for (final m in methods)
131+
if (staticMethods == null || staticMethods == m.isClassMethod)
132+
m.generateBindings(w, target, methodNamer),
122133
].join('\n');
123134
}
124135
}
@@ -359,7 +370,7 @@ class ObjCMethod extends AstNode {
359370
}
360371

361372
static String _paramToStr(Writer w, Parameter p) =>
362-
'${p.isCovariant ? 'covariant ' : ''}${p.type.getDartType(w)} ${p.name}';
373+
'${p.type.getDartType(w)} ${p.name}';
363374

364375
static String _paramToNamed(Writer w, Parameter p) =>
365376
'${p.isNullable ? '' : 'required '}${_paramToStr(w, p)}';

pkgs/ffigen/test/native_objc_test/bad_override_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ void main() {
8888

8989
parent = BadOverrideChild();
9090
expect(parent.covariantArg(square).toDartString(), 'Rectangle: Square');
91-
expect(() => parent.covariantArg(triangle), throwsA(isA<TypeError>()));
91+
expect(
92+
parent.covariantArg(triangle).toDartString(),
93+
'Rectangle: Triangle',
94+
);
9295
});
9396
});
9497
}

pkgs/ffigen/test/native_objc_test/category_config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ objc-categories:
1515
- CatImplementsProto
1616
- InstanceTypeCategory
1717
- InterfaceOnBuiltInType
18+
- StaticAndInstanceMethodsWithSameNameCategory
1819
headers:
1920
entry-points:
2021
- 'category_test.h'

pkgs/ffigen/test/native_objc_test/category_test.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,8 @@
5454
@interface NSString (NSStringCategory)
5555
-(int32_t)excludedExtensionMethod;
5656
@end
57+
58+
@interface Thing (StaticAndInstanceMethodsWithSameNameCategory)
59+
-(int32_t)sameNameMethod;
60+
+(int32_t)sameNameMethod;
61+
@end

pkgs/ffigen/test/native_objc_test/inherited_instancetype_test.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ void main() {
6060
final ChildClass child = ChildClass.alloc().init();
6161
final BaseClass base = child;
6262

63-
// Calling base.getSelf() should still go through ChildClass.getSelf, so
64-
// the result will have a compile time type of BaseClass, but a runtime
65-
// type of ChildClass.
63+
// Calling base.getSelf() goes through BaseClass.getSelf on the Dart side,
64+
// but is dynamically dispatched to the ObjC method ChildClass.getSelf. So
65+
// the Dart wrapper object is a BaseClass, but the underlying ObjC object
66+
// is a ChildClass.
6667
final BaseClass sameChild = base.getSelf();
67-
expect(sameChild, isA<ChildClass>());
68+
expect(sameChild, isA<BaseClass>());
69+
expect(ChildClass.isInstance(sameChild), isTrue);
6870
});
6971
});
7072
}

0 commit comments

Comments
 (0)