Skip to content

Commit b065e13

Browse files
authored
Improve ergonomics of protocol bindings (#1257)
* Implement option 5 * stricter types * Implement Brian's suggestion * Fix analysis
1 parent e20fab4 commit b065e13

File tree

4 files changed

+164
-64
lines changed

4 files changed

+164
-64
lines changed

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,19 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods {
3737

3838
final buildArgs = <String>[];
3939
final buildImplementations = StringBuffer();
40+
final buildListenerImplementations = StringBuffer();
4041
final methodFields = StringBuffer();
4142

4243
final methodNamer = createMethodRenamer(w);
4344

45+
bool anyListeners = false;
4446
for (final method in methods) {
4547
final methodName = method.getDartMethodName(methodNamer);
4648
final fieldName = methodName;
4749
final argName = methodName;
4850
final block = method.protocolBlock;
4951
final blockType = block.getDartType(w);
50-
final methodCtor =
52+
final methodClass =
5153
block.hasListener ? protocolListenableMethod : protocolMethod;
5254

5355
// The function type omits the first arg of the block, which is unused.
@@ -71,32 +73,35 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods {
7173
final wrapper = '($blockFirstArg _, $argsReceived) => func($argsPassed)';
7274

7375
String listenerBuilder = '';
76+
String maybeImplementAsListener = 'implement';
7477
if (block.hasListener) {
75-
listenerBuilder = '(Function func) => $blockType.listener($wrapper),';
78+
listenerBuilder = '($funcType func) => $blockType.listener($wrapper),';
79+
maybeImplementAsListener = 'implementAsListener';
80+
anyListeners = true;
7681
}
7782

7883
buildImplementations.write('''
79-
builder.implementMethod($name.$fieldName, $argName);''');
84+
$name.$fieldName.implement(builder, $argName);''');
85+
buildListenerImplementations.write('''
86+
$name.$fieldName.$maybeImplementAsListener(builder, $argName);''');
8087

8188
methodFields.write(makeDartDoc(method.dartDoc ?? method.originalName));
82-
methodFields.write('''static final $fieldName = $methodCtor(
89+
methodFields.write('''static final $fieldName = $methodClass<$funcType>(
8390
${method.selObject!.name},
8491
$getSignature(
8592
${_protocolPointer.name},
8693
${method.selObject!.name},
8794
isRequired: ${method.isRequired},
8895
isInstanceMethod: ${method.isInstanceMethod},
8996
),
90-
(Function func) => func is $funcType,
91-
(Function func) => $blockType.fromFunction($wrapper),
97+
($funcType func) => $blockType.fromFunction($wrapper),
9298
$listenerBuilder
9399
);
94100
''');
95101
}
96102

97103
final args = buildArgs.isEmpty ? '' : '{${buildArgs.join(', ')}}';
98-
final mainString = '''
99-
${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
104+
final builders = '''
100105
/// Builds an object that implements the $originalName protocol. To implement
101106
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
102107
static $objectBase implement($args) {
@@ -110,7 +115,33 @@ ${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
110115
static void addToBuilder($protocolBuilder builder, $args) {
111116
$buildImplementations
112117
}
118+
''';
119+
120+
String listenerBuilders = '';
121+
if (anyListeners) {
122+
listenerBuilders = '''
123+
/// Builds an object that implements the $originalName protocol. To implement
124+
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All
125+
/// methods that can be implemented as listeners will be.
126+
static $objectBase implementAsListener($args) {
127+
final builder = $protocolBuilder();
128+
$buildListenerImplementations
129+
return builder.build();
130+
}
113131
132+
/// Adds the implementation of the $originalName protocol to an existing
133+
/// [$protocolBuilder]. All methods that can be implemented as listeners will
134+
/// be.
135+
static void addToBuilderAsListener($protocolBuilder builder, $args) {
136+
$buildListenerImplementations
137+
}
138+
''';
139+
}
140+
141+
final mainString = '''
142+
${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
143+
$builders
144+
$listenerBuilders
114145
$methodFields
115146
}
116147
''';

pkgs/ffigen/test/native_objc_test/protocol_test.dart

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ void main() {
110110
final consumer = ProtocolConsumer.new1();
111111

112112
final protocolBuilder = ObjCProtocolBuilder();
113-
protocolBuilder.implementMethod(MyProtocol.instanceMethod_withDouble_,
113+
MyProtocol.instanceMethod_withDouble_.implement(protocolBuilder,
114114
(NSString s, double x) {
115115
return 'ProtocolBuilder: $s: $x'.toNSString();
116116
});
117-
protocolBuilder.implementMethod(SecondaryProtocol.otherMethod_b_c_d_,
117+
SecondaryProtocol.otherMethod_b_c_d_.implement(protocolBuilder,
118118
(int a, int b, int c, int d) {
119119
return a * b * c * d;
120120
});
@@ -142,6 +142,68 @@ void main() {
142142
final intResult = consumer.callOptionalMethod_(myProtocol);
143143
expect(intResult, -999);
144144
});
145+
146+
test('Method implementation as listener', () async {
147+
final consumer = ProtocolConsumer.new1();
148+
149+
final listenerCompleter = Completer<int>();
150+
final myProtocol = MyProtocol.implementAsListener(
151+
instanceMethod_withDouble_: (NSString s, double x) {
152+
return 'MyProtocol: $s: $x'.toNSString();
153+
},
154+
optionalMethod_: (SomeStruct s) {
155+
return s.y - s.x;
156+
},
157+
voidMethod_: (int x) {
158+
listenerCompleter.complete(x);
159+
},
160+
);
161+
162+
// Required instance method.
163+
final result = consumer.callInstanceMethod_(myProtocol);
164+
expect(result.toString(), 'MyProtocol: Hello from ObjC: 3.14');
165+
166+
// Optional instance method.
167+
final intResult = consumer.callOptionalMethod_(myProtocol);
168+
expect(intResult, 333);
169+
170+
// Listener method.
171+
consumer.callMethodOnRandomThread_(myProtocol);
172+
expect(await listenerCompleter.future, 123);
173+
});
174+
175+
test('Multiple protocol implementation as listener', () async {
176+
final consumer = ProtocolConsumer.new1();
177+
178+
final listenerCompleter = Completer<int>();
179+
final protocolBuilder = ObjCProtocolBuilder();
180+
MyProtocol.addToBuilderAsListener(
181+
protocolBuilder,
182+
instanceMethod_withDouble_: (NSString s, double x) {
183+
return 'ProtocolBuilder: $s: $x'.toNSString();
184+
},
185+
voidMethod_: (int x) {
186+
listenerCompleter.complete(x);
187+
},
188+
);
189+
SecondaryProtocol.addToBuilder(protocolBuilder,
190+
otherMethod_b_c_d_: (int a, int b, int c, int d) {
191+
return a * b * c * d;
192+
});
193+
final protocolImpl = protocolBuilder.build();
194+
195+
// Required instance method.
196+
final result = consumer.callInstanceMethod_(protocolImpl);
197+
expect(result.toString(), 'ProtocolBuilder: Hello from ObjC: 3.14');
198+
199+
// Required instance method from secondary protocol.
200+
final otherIntResult = consumer.callOtherMethod_(protocolImpl);
201+
expect(otherIntResult, 24);
202+
203+
// Listener method.
204+
consumer.callMethodOnRandomThread_(protocolImpl);
205+
expect(await listenerCompleter.future, 123);
206+
});
145207
});
146208

147209
group('Manual DartProxy implementation', () {
@@ -213,8 +275,7 @@ void main() {
213275
int count = 0;
214276

215277
final protocolBuilder = ObjCProtocolBuilder();
216-
protocolBuilder.implementMethodAsListener(MyProtocol.voidMethod_,
217-
(int x) {
278+
MyProtocol.voidMethod_.implementAsListener(protocolBuilder, (int x) {
218279
expect(x, 123);
219280
++count;
220281
if (count == 1000) completer.complete();

pkgs/objective_c/lib/src/internal.dart

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -285,25 +285,6 @@ Function getBlockClosure(Pointer<c.ObjCBlock> block) {
285285
return _blockClosureRegistry[id]!;
286286
}
287287

288-
/// Only for use by ffigen bindings.
289-
class ObjCProtocolMethod {
290-
final Pointer<c.ObjCSelector> sel;
291-
final objc.NSMethodSignature signature;
292-
final bool Function(Function) isCorrectFunctionType;
293-
final ObjCBlockBase Function(Function) createBlock;
294-
295-
ObjCProtocolMethod(
296-
this.sel, this.signature, this.isCorrectFunctionType, this.createBlock);
297-
}
298-
299-
/// Only for use by ffigen bindings.
300-
class ObjCProtocolListenableMethod extends ObjCProtocolMethod {
301-
final ObjCBlockBase Function(Function) createListenerBlock;
302-
303-
ObjCProtocolListenableMethod(super.sel, super.signature,
304-
super.isCorrectFunctionType, super.createBlock, this.createListenerBlock);
305-
}
306-
307288
// Not exported by ../objective_c.dart, because they're only for testing.
308289
bool blockHasRegisteredClosure(Pointer<c.ObjCBlock> block) =>
309290
_blockClosureRegistry.containsKey(block.ref.target.address);

pkgs/objective_c/lib/src/protocol_builder.dart

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,82 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:ffi';
6+
7+
import 'c_bindings_generated.dart' as c;
58
import 'objective_c_bindings_generated.dart' as objc;
6-
import 'internal.dart'
7-
show ObjCBlockBase, ObjCProtocolMethod, ObjCProtocolListenableMethod;
9+
import 'internal.dart' show ObjCBlockBase;
810

911
/// Helper class for building Objective C objects that implement protocols.
1012
class ObjCProtocolBuilder {
1113
final _builder = objc.DartProxyBuilder.new1();
1214

13-
/// Implement an ObjC protocol [method] using a Dart [function].
15+
/// Add a method implementation to the protocol.
16+
///
17+
/// It is not recommended to call this method directly. Instead, use the
18+
/// implement methods on [ObjCProtocolMethod] and its subclasses.
19+
void implementMethod(Pointer<c.ObjCSelector> sel,
20+
objc.NSMethodSignature signature, ObjCBlockBase block) =>
21+
_builder.implementMethod_withSignature_andBlock_(
22+
sel, signature, block.pointer.cast());
23+
24+
/// Builds the object.
25+
///
26+
/// This can be called multiple times to construct multiple object instances
27+
/// that all implement the same protocol methods using the same functions.
28+
objc.DartProxy build() => objc.DartProxy.newFromBuilder_(_builder);
29+
}
30+
31+
/// A method in an ObjC protocol.
32+
///
33+
/// Do not try to construct this class directly. The recommended way of getting
34+
/// a method object is to use ffigen to generate bindings for the protocol you
35+
/// want to implement. The generated bindings will include a
36+
/// [ObjCProtocolMethod] for each method of the protocol.
37+
class ObjCProtocolMethod<T extends Function> {
38+
final Pointer<c.ObjCSelector> _sel;
39+
final objc.NSMethodSignature _signature;
40+
final ObjCBlockBase Function(T) _createBlock;
41+
42+
/// Only for use by ffigen bindings.
43+
ObjCProtocolMethod(this._sel, this._signature, this._createBlock);
44+
45+
/// Implement this method on the protocol [builder] using a Dart [function].
1446
///
1547
/// The implemented method must be invoked by ObjC code running on the same
1648
/// thread as the isolate that called [implementMethod]. Invoking the method
1749
/// on the wrong thread will result in a crash.
18-
///
19-
/// The recommended way of getting the [method] object is to use ffigen to
20-
/// generate bindings for the protocol you want to implement. The generated
21-
/// bindings will include a [ObjCProtocolMethod] for each method of the
22-
/// protocol.
23-
void implementMethod(ObjCProtocolMethod method, Function? function) =>
24-
_implement(method, function, method.createBlock);
25-
26-
/// Implement an ObjC protocol [method] as a listener using a Dart [function].
50+
void implement(ObjCProtocolBuilder builder, T? function) {
51+
if (function != null) {
52+
builder.implementMethod(_sel, _signature, _createBlock(function));
53+
}
54+
}
55+
}
56+
57+
/// A method in an ObjC protocol that can be implemented as a listener.
58+
///
59+
/// Do not try to construct this class directly. The recommended way of getting
60+
/// a method object is to use ffigen to generate bindings for the protocol you
61+
/// want to implement. The generated bindings will include a
62+
/// [ObjCProtocolMethod] for each method of the protocol.
63+
class ObjCProtocolListenableMethod<T extends Function>
64+
extends ObjCProtocolMethod<T> {
65+
final ObjCBlockBase Function(T) _createListenerBlock;
66+
67+
/// Only for use by ffigen bindings.
68+
ObjCProtocolListenableMethod(super._sel, super._signature, super._createBlock,
69+
this._createListenerBlock);
70+
71+
/// Implement this method on the protocol [builder] as a listener using a Dart
72+
/// [function].
2773
///
2874
/// This is based on FFI's NativeCallable.listener, and has the same
2975
/// capabilities and limitations. This method can be invoked by ObjC from any
3076
/// thread, but only supports void functions, and is not run synchronously.
3177
/// See NativeCallable.listener for more details.
32-
///
33-
/// The recommended way of getting the [method] object is to use ffigen to
34-
/// generate bindings for the protocol you want to implement. The generated
35-
/// bindings will include a [ObjCProtocolMethod] for each method of the
36-
/// protocol. If that method can be implemented as a listener, the [method]
37-
/// object will be a [ObjCProtocolListenableMethod].
38-
void implementMethodAsListener(
39-
ObjCProtocolListenableMethod method, Function? function) =>
40-
_implement(method, function, method.createListenerBlock);
41-
42-
/// Builds the object.
43-
///
44-
/// This can be called multiple times to construct multiple object instances
45-
/// that all implement the same protocol methods using the same functions.
46-
objc.DartProxy build() => objc.DartProxy.newFromBuilder_(_builder);
47-
48-
void _implement(ObjCProtocolMethod method, Function? function,
49-
ObjCBlockBase Function(Function) blockMaker) {
78+
void implementAsListener(ObjCProtocolBuilder builder, T? function) {
5079
if (function != null) {
51-
assert(method.isCorrectFunctionType(function));
52-
_builder.implementMethod_withSignature_andBlock_(
53-
method.sel, method.signature, blockMaker(function).pointer.cast());
80+
builder.implementMethod(_sel, _signature, _createListenerBlock(function));
5481
}
5582
}
5683
}

0 commit comments

Comments
 (0)