Skip to content

Commit

Permalink
Improve ergonomics of protocol bindings (#1257)
Browse files Browse the repository at this point in the history
* Implement option 5

* stricter types

* Implement Brian's suggestion

* Fix analysis
  • Loading branch information
liamappelbe authored Jul 7, 2024
1 parent e20fab4 commit b065e13
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 64 deletions.
47 changes: 39 additions & 8 deletions pkgs/ffigen/lib/src/code_generator/objc_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,19 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods {

final buildArgs = <String>[];
final buildImplementations = StringBuffer();
final buildListenerImplementations = StringBuffer();
final methodFields = StringBuffer();

final methodNamer = createMethodRenamer(w);

bool anyListeners = false;
for (final method in methods) {
final methodName = method.getDartMethodName(methodNamer);
final fieldName = methodName;
final argName = methodName;
final block = method.protocolBlock;
final blockType = block.getDartType(w);
final methodCtor =
final methodClass =
block.hasListener ? protocolListenableMethod : protocolMethod;

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

String listenerBuilder = '';
String maybeImplementAsListener = 'implement';
if (block.hasListener) {
listenerBuilder = '(Function func) => $blockType.listener($wrapper),';
listenerBuilder = '($funcType func) => $blockType.listener($wrapper),';
maybeImplementAsListener = 'implementAsListener';
anyListeners = true;
}

buildImplementations.write('''
builder.implementMethod($name.$fieldName, $argName);''');
$name.$fieldName.implement(builder, $argName);''');
buildListenerImplementations.write('''
$name.$fieldName.$maybeImplementAsListener(builder, $argName);''');

methodFields.write(makeDartDoc(method.dartDoc ?? method.originalName));
methodFields.write('''static final $fieldName = $methodCtor(
methodFields.write('''static final $fieldName = $methodClass<$funcType>(
${method.selObject!.name},
$getSignature(
${_protocolPointer.name},
${method.selObject!.name},
isRequired: ${method.isRequired},
isInstanceMethod: ${method.isInstanceMethod},
),
(Function func) => func is $funcType,
(Function func) => $blockType.fromFunction($wrapper),
($funcType func) => $blockType.fromFunction($wrapper),
$listenerBuilder
);
''');
}

final args = buildArgs.isEmpty ? '' : '{${buildArgs.join(', ')}}';
final mainString = '''
${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
final builders = '''
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
static $objectBase implement($args) {
Expand All @@ -110,7 +115,33 @@ ${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
static void addToBuilder($protocolBuilder builder, $args) {
$buildImplementations
}
''';

String listenerBuilders = '';
if (anyListeners) {
listenerBuilders = '''
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All
/// methods that can be implemented as listeners will be.
static $objectBase implementAsListener($args) {
final builder = $protocolBuilder();
$buildListenerImplementations
return builder.build();
}
/// Adds the implementation of the $originalName protocol to an existing
/// [$protocolBuilder]. All methods that can be implemented as listeners will
/// be.
static void addToBuilderAsListener($protocolBuilder builder, $args) {
$buildListenerImplementations
}
''';
}

final mainString = '''
${makeDartDoc(dartDoc ?? originalName)}abstract final class $name {
$builders
$listenerBuilders
$methodFields
}
''';
Expand Down
69 changes: 65 additions & 4 deletions pkgs/ffigen/test/native_objc_test/protocol_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ void main() {
final consumer = ProtocolConsumer.new1();

final protocolBuilder = ObjCProtocolBuilder();
protocolBuilder.implementMethod(MyProtocol.instanceMethod_withDouble_,
MyProtocol.instanceMethod_withDouble_.implement(protocolBuilder,
(NSString s, double x) {
return 'ProtocolBuilder: $s: $x'.toNSString();
});
protocolBuilder.implementMethod(SecondaryProtocol.otherMethod_b_c_d_,
SecondaryProtocol.otherMethod_b_c_d_.implement(protocolBuilder,
(int a, int b, int c, int d) {
return a * b * c * d;
});
Expand Down Expand Up @@ -142,6 +142,68 @@ void main() {
final intResult = consumer.callOptionalMethod_(myProtocol);
expect(intResult, -999);
});

test('Method implementation as listener', () async {
final consumer = ProtocolConsumer.new1();

final listenerCompleter = Completer<int>();
final myProtocol = MyProtocol.implementAsListener(
instanceMethod_withDouble_: (NSString s, double x) {
return 'MyProtocol: $s: $x'.toNSString();
},
optionalMethod_: (SomeStruct s) {
return s.y - s.x;
},
voidMethod_: (int x) {
listenerCompleter.complete(x);
},
);

// Required instance method.
final result = consumer.callInstanceMethod_(myProtocol);
expect(result.toString(), 'MyProtocol: Hello from ObjC: 3.14');

// Optional instance method.
final intResult = consumer.callOptionalMethod_(myProtocol);
expect(intResult, 333);

// Listener method.
consumer.callMethodOnRandomThread_(myProtocol);
expect(await listenerCompleter.future, 123);
});

test('Multiple protocol implementation as listener', () async {
final consumer = ProtocolConsumer.new1();

final listenerCompleter = Completer<int>();
final protocolBuilder = ObjCProtocolBuilder();
MyProtocol.addToBuilderAsListener(
protocolBuilder,
instanceMethod_withDouble_: (NSString s, double x) {
return 'ProtocolBuilder: $s: $x'.toNSString();
},
voidMethod_: (int x) {
listenerCompleter.complete(x);
},
);
SecondaryProtocol.addToBuilder(protocolBuilder,
otherMethod_b_c_d_: (int a, int b, int c, int d) {
return a * b * c * d;
});
final protocolImpl = protocolBuilder.build();

// Required instance method.
final result = consumer.callInstanceMethod_(protocolImpl);
expect(result.toString(), 'ProtocolBuilder: Hello from ObjC: 3.14');

// Required instance method from secondary protocol.
final otherIntResult = consumer.callOtherMethod_(protocolImpl);
expect(otherIntResult, 24);

// Listener method.
consumer.callMethodOnRandomThread_(protocolImpl);
expect(await listenerCompleter.future, 123);
});
});

group('Manual DartProxy implementation', () {
Expand Down Expand Up @@ -213,8 +275,7 @@ void main() {
int count = 0;

final protocolBuilder = ObjCProtocolBuilder();
protocolBuilder.implementMethodAsListener(MyProtocol.voidMethod_,
(int x) {
MyProtocol.voidMethod_.implementAsListener(protocolBuilder, (int x) {
expect(x, 123);
++count;
if (count == 1000) completer.complete();
Expand Down
19 changes: 0 additions & 19 deletions pkgs/objective_c/lib/src/internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,25 +285,6 @@ Function getBlockClosure(Pointer<c.ObjCBlock> block) {
return _blockClosureRegistry[id]!;
}

/// Only for use by ffigen bindings.
class ObjCProtocolMethod {
final Pointer<c.ObjCSelector> sel;
final objc.NSMethodSignature signature;
final bool Function(Function) isCorrectFunctionType;
final ObjCBlockBase Function(Function) createBlock;

ObjCProtocolMethod(
this.sel, this.signature, this.isCorrectFunctionType, this.createBlock);
}

/// Only for use by ffigen bindings.
class ObjCProtocolListenableMethod extends ObjCProtocolMethod {
final ObjCBlockBase Function(Function) createListenerBlock;

ObjCProtocolListenableMethod(super.sel, super.signature,
super.isCorrectFunctionType, super.createBlock, this.createListenerBlock);
}

// Not exported by ../objective_c.dart, because they're only for testing.
bool blockHasRegisteredClosure(Pointer<c.ObjCBlock> block) =>
_blockClosureRegistry.containsKey(block.ref.target.address);
Expand Down
93 changes: 60 additions & 33 deletions pkgs/objective_c/lib/src/protocol_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,82 @@
// 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 'dart:ffi';

import 'c_bindings_generated.dart' as c;
import 'objective_c_bindings_generated.dart' as objc;
import 'internal.dart'
show ObjCBlockBase, ObjCProtocolMethod, ObjCProtocolListenableMethod;
import 'internal.dart' show ObjCBlockBase;

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

/// Implement an ObjC protocol [method] using a Dart [function].
/// Add a method implementation to the protocol.
///
/// It is not recommended to call this method directly. Instead, use the
/// implement methods on [ObjCProtocolMethod] and its subclasses.
void implementMethod(Pointer<c.ObjCSelector> sel,
objc.NSMethodSignature signature, ObjCBlockBase block) =>
_builder.implementMethod_withSignature_andBlock_(
sel, signature, block.pointer.cast());

/// Builds the object.
///
/// This can be called multiple times to construct multiple object instances
/// that all implement the same protocol methods using the same functions.
objc.DartProxy build() => objc.DartProxy.newFromBuilder_(_builder);
}

/// A method in an ObjC protocol.
///
/// Do not try to construct this class directly. The recommended way of getting
/// a method object is to use ffigen to generate bindings for the protocol you
/// want to implement. The generated bindings will include a
/// [ObjCProtocolMethod] for each method of the protocol.
class ObjCProtocolMethod<T extends Function> {
final Pointer<c.ObjCSelector> _sel;
final objc.NSMethodSignature _signature;
final ObjCBlockBase Function(T) _createBlock;

/// Only for use by ffigen bindings.
ObjCProtocolMethod(this._sel, this._signature, this._createBlock);

/// Implement this method on the protocol [builder] using a Dart [function].
///
/// The implemented method must be invoked by ObjC code running on the same
/// thread as the isolate that called [implementMethod]. Invoking the method
/// on the wrong thread will result in a crash.
///
/// The recommended way of getting the [method] object is to use ffigen to
/// generate bindings for the protocol you want to implement. The generated
/// bindings will include a [ObjCProtocolMethod] for each method of the
/// protocol.
void implementMethod(ObjCProtocolMethod method, Function? function) =>
_implement(method, function, method.createBlock);

/// Implement an ObjC protocol [method] as a listener using a Dart [function].
void implement(ObjCProtocolBuilder builder, T? function) {
if (function != null) {
builder.implementMethod(_sel, _signature, _createBlock(function));
}
}
}

/// A method in an ObjC protocol that can be implemented as a listener.
///
/// Do not try to construct this class directly. The recommended way of getting
/// a method object is to use ffigen to generate bindings for the protocol you
/// want to implement. The generated bindings will include a
/// [ObjCProtocolMethod] for each method of the protocol.
class ObjCProtocolListenableMethod<T extends Function>
extends ObjCProtocolMethod<T> {
final ObjCBlockBase Function(T) _createListenerBlock;

/// Only for use by ffigen bindings.
ObjCProtocolListenableMethod(super._sel, super._signature, super._createBlock,
this._createListenerBlock);

/// Implement this method on the protocol [builder] as a listener using a Dart
/// [function].
///
/// This is based on FFI's NativeCallable.listener, and has the same
/// capabilities and limitations. This method can be invoked by ObjC from any
/// thread, but only supports void functions, and is not run synchronously.
/// See NativeCallable.listener for more details.
///
/// The recommended way of getting the [method] object is to use ffigen to
/// generate bindings for the protocol you want to implement. The generated
/// bindings will include a [ObjCProtocolMethod] for each method of the
/// protocol. If that method can be implemented as a listener, the [method]
/// object will be a [ObjCProtocolListenableMethod].
void implementMethodAsListener(
ObjCProtocolListenableMethod method, Function? function) =>
_implement(method, function, method.createListenerBlock);

/// Builds the object.
///
/// This can be called multiple times to construct multiple object instances
/// that all implement the same protocol methods using the same functions.
objc.DartProxy build() => objc.DartProxy.newFromBuilder_(_builder);

void _implement(ObjCProtocolMethod method, Function? function,
ObjCBlockBase Function(Function) blockMaker) {
void implementAsListener(ObjCProtocolBuilder builder, T? function) {
if (function != null) {
assert(method.isCorrectFunctionType(function));
_builder.implementMethod_withSignature_andBlock_(
method.sel, method.signature, blockMaker(function).pointer.cast());
builder.implementMethod(_sel, _signature, _createListenerBlock(function));
}
}
}

0 comments on commit b065e13

Please sign in to comment.