diff --git a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart index a9adf66ac0..8632cf2a3c 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart @@ -37,17 +37,19 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { final buildArgs = []; 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. @@ -71,15 +73,20 @@ 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}, @@ -87,16 +94,14 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { 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) { @@ -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 } '''; diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.dart b/pkgs/ffigen/test/native_objc_test/protocol_test.dart index 947b837038..9a284b0a52 100644 --- a/pkgs/ffigen/test/native_objc_test/protocol_test.dart +++ b/pkgs/ffigen/test/native_objc_test/protocol_test.dart @@ -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; }); @@ -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(); + 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(); + 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', () { @@ -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(); diff --git a/pkgs/objective_c/lib/src/internal.dart b/pkgs/objective_c/lib/src/internal.dart index 7ece5ae776..5ff5f585ec 100644 --- a/pkgs/objective_c/lib/src/internal.dart +++ b/pkgs/objective_c/lib/src/internal.dart @@ -285,25 +285,6 @@ Function getBlockClosure(Pointer block) { return _blockClosureRegistry[id]!; } -/// Only for use by ffigen bindings. -class ObjCProtocolMethod { - final Pointer 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 block) => _blockClosureRegistry.containsKey(block.ref.target.address); diff --git a/pkgs/objective_c/lib/src/protocol_builder.dart b/pkgs/objective_c/lib/src/protocol_builder.dart index f1bf5e09f5..f175ff197b 100644 --- a/pkgs/objective_c/lib/src/protocol_builder.dart +++ b/pkgs/objective_c/lib/src/protocol_builder.dart @@ -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 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 { + final Pointer _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 + extends ObjCProtocolMethod { + 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)); } } }