diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 39633bde6..03c4aa986 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md @@ -4,6 +4,7 @@ is linked with `-dead_strip`. - Handle dart typedefs in import/export of symbol files. - Add support for blocking ObjC blocks that can be invoked from any thread. +- Add support for blocking ObjC protocol methods. ## 16.0.0 diff --git a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart index 0c0eeb882..9ccc4c065 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart @@ -49,6 +49,7 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { final buildArgs = []; final buildImplementations = StringBuffer(); final buildListenerImplementations = StringBuffer(); + final buildBlockingImplementations = StringBuffer(); final methodFields = StringBuffer(); final methodNamer = createMethodRenamer(w); @@ -83,11 +84,16 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { final argsPassed = func.parameters.map((p) => p.name).join(', '); final wrapper = '($blockFirstArg _, $argsReceived) => func($argsPassed)'; - var listenerBuilder = ''; + var listenerBuilders = ''; var maybeImplementAsListener = 'implement'; + var maybeImplementAsBlocking = 'implement'; if (block.hasListener) { - listenerBuilder = '($funcType func) => $blockUtils.listener($wrapper),'; + listenerBuilders = ''' + ($funcType func) => $blockUtils.listener($wrapper), + ($funcType func) => $blockUtils.blocking($wrapper), +'''; maybeImplementAsListener = 'implementAsListener'; + maybeImplementAsBlocking = 'implementAsBlocking'; anyListeners = true; } @@ -95,6 +101,8 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { $name.$fieldName.implement(builder, $argName);'''); buildListenerImplementations.write(''' $name.$fieldName.$maybeImplementAsListener(builder, $argName);'''); + buildBlockingImplementations.write(''' + $name.$fieldName.$maybeImplementAsBlocking(builder, $argName);'''); methodFields.write(makeDartDoc(method.dartDoc ?? method.originalName)); methodFields.write('''static final $fieldName = $methodClass<$funcType>( @@ -107,7 +115,7 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { isInstanceMethod: ${method.isInstanceMethod}, ), ($funcType func) => $blockUtils.fromFunction($wrapper), - $listenerBuilder + $listenerBuilders ); '''); } @@ -147,6 +155,22 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods { static void addToBuilderAsListener($protocolBuilder builder, $args) { $buildListenerImplementations } + + /// Builds an object that implements the $originalName protocol. To implement + /// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All + /// methods that can be implemented as blocking listeners will be. + static $objectBase implementAsBlocking($args) { + final builder = $protocolBuilder(); + $buildBlockingImplementations + return builder.build(); + } + + /// Adds the implementation of the $originalName protocol to an existing + /// [$protocolBuilder]. All methods that can be implemented as blocking + /// listeners will be. + static void addToBuilderAsBlocking($protocolBuilder builder, $args) { + $buildBlockingImplementations + } '''; } diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.dart b/pkgs/ffigen/test/native_objc_test/protocol_test.dart index d6176506e..cdc9bef33 100644 --- a/pkgs/ffigen/test/native_objc_test/protocol_test.dart +++ b/pkgs/ffigen/test/native_objc_test/protocol_test.dart @@ -247,6 +247,69 @@ void main() { consumer.callMethodOnRandomThread_(protocolImpl); expect(await listenerCompleter.future, 123); }); + + void waitSync(Duration d) { + final t = Stopwatch(); + t.start(); + while (t.elapsed < d) { + // Waiting... + } + } + + test('Method implementation as blocking', () async { + final consumer = ProtocolConsumer.new1(); + + final listenerCompleter = Completer(); + final myProtocol = MyProtocol.implementAsBlocking( + instanceMethod_withDouble_: (NSString s, double x) { + throw UnimplementedError(); + }, + voidMethod_: (int x) { + listenerCompleter.complete(x); + }, + intPtrMethod_: (Pointer ptr) { + waitSync(Duration(milliseconds: 100)); + ptr.value = 123456; + }, + ); + + // Blocking method. + consumer.callBlockingMethodOnRandomThread_(myProtocol); + expect(await listenerCompleter.future, 123456); + }); + + test('Multiple protocol implementation as blocking', () async { + final consumer = ProtocolConsumer.new1(); + + final listenerCompleter = Completer(); + final protocolBuilder = ObjCProtocolBuilder(); + MyProtocol.addToBuilderAsBlocking( + protocolBuilder, + instanceMethod_withDouble_: (NSString s, double x) { + throw UnimplementedError(); + }, + voidMethod_: (int x) { + listenerCompleter.complete(x); + }, + intPtrMethod_: (Pointer ptr) { + waitSync(Duration(milliseconds: 100)); + ptr.value = 98765; + }, + ); + 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 from secondary protocol. + final otherIntResult = consumer.callOtherMethod_(protocolImpl); + expect(otherIntResult, 24); + + // Blocking method. + consumer.callBlockingMethodOnRandomThread_(protocolImpl); + expect(await listenerCompleter.future, 98765); + }); }); group('Manual DartProxy implementation', () { diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.h b/pkgs/ffigen/test/native_objc_test/protocol_test.h index 21262a248..dc3f07dfd 100644 --- a/pkgs/ffigen/test/native_objc_test/protocol_test.h +++ b/pkgs/ffigen/test/native_objc_test/protocol_test.h @@ -44,6 +44,9 @@ typedef struct { - (int32_t)disabledMethod; #endif +@optional +- (void)intPtrMethod:(int32_t*)ptr; + @end @@ -70,7 +73,8 @@ typedef struct { - (NSString*)callInstanceMethod:(id)protocol; - (int32_t)callOptionalMethod:(id)protocol; - (int32_t)callOtherMethod:(id)protocol; -- (void)callMethodOnRandomThread:(id)protocol; +- (void)callMethodOnRandomThread:(id)protocol; +- (void)callBlockingMethodOnRandomThread:(id)protocol; @end diff --git a/pkgs/ffigen/test/native_objc_test/protocol_test.m b/pkgs/ffigen/test/native_objc_test/protocol_test.m index 91b7a7f40..975effc68 100644 --- a/pkgs/ffigen/test/native_objc_test/protocol_test.m +++ b/pkgs/ffigen/test/native_objc_test/protocol_test.m @@ -31,6 +31,14 @@ - (void)callMethodOnRandomThread:(id)protocol { [protocol voidMethod:123]; }); } + +- (void)callBlockingMethodOnRandomThread:(id)protocol { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + int32_t x; + [protocol intPtrMethod:&x]; + [protocol voidMethod:x]; + }); +} @end diff --git a/pkgs/objective_c/CHANGELOG.md b/pkgs/objective_c/CHANGELOG.md index 25b703cf2..c1f078b39 100644 --- a/pkgs/objective_c/CHANGELOG.md +++ b/pkgs/objective_c/CHANGELOG.md @@ -4,6 +4,7 @@ - Reduces the chances of duplicate symbols by adding a `DOBJC_` prefix. - Ensure that required symbols are available to FFI even when the final binary is linked with `-dead_strip`. +- Add support for blocking ObjC protocol methods. ## 4.0.0 diff --git a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart index 767000c3e..34e37be8b 100644 --- a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart +++ b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart @@ -6378,6 +6378,26 @@ abstract final class NSStreamDelegate { .implementAsListener(builder, stream_handleEvent_); } + /// Builds an object that implements the NSStreamDelegate protocol. To implement + /// multiple protocols, use [addToBuilder] or [objc.ObjCProtocolBuilder] directly. All + /// methods that can be implemented as blocking listeners will be. + static objc.ObjCObjectBase implementAsBlocking( + {void Function(NSStream, NSStreamEvent)? stream_handleEvent_}) { + final builder = objc.ObjCProtocolBuilder(); + NSStreamDelegate.stream_handleEvent_ + .implementAsBlocking(builder, stream_handleEvent_); + return builder.build(); + } + + /// Adds the implementation of the NSStreamDelegate protocol to an existing + /// [objc.ObjCProtocolBuilder]. All methods that can be implemented as blocking + /// listeners will be. + static void addToBuilderAsBlocking(objc.ObjCProtocolBuilder builder, + {void Function(NSStream, NSStreamEvent)? stream_handleEvent_}) { + NSStreamDelegate.stream_handleEvent_ + .implementAsBlocking(builder, stream_handleEvent_); + } + /// stream:handleEvent: static final stream_handleEvent_ = objc.ObjCProtocolListenableMethod( @@ -6397,6 +6417,10 @@ abstract final class NSStreamDelegate { ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent.listener( (ffi.Pointer _, NSStream arg1, NSStreamEvent arg2) => func(arg1, arg2)), + (void Function(NSStream, NSStreamEvent) func) => + ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent.blocking( + (ffi.Pointer _, NSStream arg1, NSStreamEvent arg2) => + func(arg1, arg2)), ); } diff --git a/pkgs/objective_c/lib/src/protocol_builder.dart b/pkgs/objective_c/lib/src/protocol_builder.dart index c70a12b8f..d9fbaa9ee 100644 --- a/pkgs/objective_c/lib/src/protocol_builder.dart +++ b/pkgs/objective_c/lib/src/protocol_builder.dart @@ -75,10 +75,11 @@ class ObjCProtocolMethod { class ObjCProtocolListenableMethod extends ObjCProtocolMethod { final ObjCBlockBase Function(T) _createListenerBlock; + final ObjCBlockBase Function(T) _createBlockingBlock; /// Only for use by ffigen bindings. ObjCProtocolListenableMethod(super._proto, super._sel, super._signature, - super._createBlock, this._createListenerBlock); + super._createBlock, this._createListenerBlock, this._createBlockingBlock); /// Implement this method on the protocol [builder] as a listener using a Dart /// [function]. @@ -92,4 +93,16 @@ class ObjCProtocolListenableMethod builder.implementMethod(_sel, _sig, _createListenerBlock(function)); } } + + /// Implement this method on the protocol [builder] as a blocking listener + /// using a Dart [function]. + /// + /// This callback can be invoked from any native thread, and will block the + /// caller until the callback is handled by the Dart isolate that implemented + /// the method. Async functions are not supported. + void implementAsBlocking(ObjCProtocolBuilder builder, T? function) { + if (function != null) { + builder.implementMethod(_sel, _sig, _createBlockingBlock(function)); + } + } }