Skip to content

Commit

Permalink
[ffigen] Blocking protocol methods (#1870)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamappelbe authored Jan 9, 2025
1 parent 14368a8 commit a646680
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 5 deletions.
1 change: 1 addition & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 27 additions & 3 deletions pkgs/ffigen/lib/src/code_generator/objc_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods {
final buildArgs = <String>[];
final buildImplementations = StringBuffer();
final buildListenerImplementations = StringBuffer();
final buildBlockingImplementations = StringBuffer();
final methodFields = StringBuffer();

final methodNamer = createMethodRenamer(w);
Expand Down Expand Up @@ -83,18 +84,25 @@ 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;
}

buildImplementations.write('''
$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>(
Expand All @@ -107,7 +115,7 @@ class ObjCProtocol extends NoLookUpBinding with ObjCMethods {
isInstanceMethod: ${method.isInstanceMethod},
),
($funcType func) => $blockUtils.fromFunction($wrapper),
$listenerBuilder
$listenerBuilders
);
''');
}
Expand Down Expand Up @@ -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
}
''';
}

Expand Down
63 changes: 63 additions & 0 deletions pkgs/ffigen/test/native_objc_test/protocol_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();
final myProtocol = MyProtocol.implementAsBlocking(
instanceMethod_withDouble_: (NSString s, double x) {
throw UnimplementedError();
},
voidMethod_: (int x) {
listenerCompleter.complete(x);
},
intPtrMethod_: (Pointer<Int32> 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<int>();
final protocolBuilder = ObjCProtocolBuilder();
MyProtocol.addToBuilderAsBlocking(
protocolBuilder,
instanceMethod_withDouble_: (NSString s, double x) {
throw UnimplementedError();
},
voidMethod_: (int x) {
listenerCompleter.complete(x);
},
intPtrMethod_: (Pointer<Int32> 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', () {
Expand Down
6 changes: 5 additions & 1 deletion pkgs/ffigen/test/native_objc_test/protocol_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ typedef struct {
- (int32_t)disabledMethod;
#endif

@optional
- (void)intPtrMethod:(int32_t*)ptr;

@end


Expand All @@ -70,7 +73,8 @@ typedef struct {
- (NSString*)callInstanceMethod:(id<MyProtocol>)protocol;
- (int32_t)callOptionalMethod:(id<MyProtocol>)protocol;
- (int32_t)callOtherMethod:(id<SecondaryProtocol>)protocol;
- (void)callMethodOnRandomThread:(id<SecondaryProtocol>)protocol;
- (void)callMethodOnRandomThread:(id<MyProtocol>)protocol;
- (void)callBlockingMethodOnRandomThread:(id<MyProtocol>)protocol;
@end


Expand Down
8 changes: 8 additions & 0 deletions pkgs/ffigen/test/native_objc_test/protocol_test.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ - (void)callMethodOnRandomThread:(id<MyProtocol>)protocol {
[protocol voidMethod:123];
});
}

- (void)callBlockingMethodOnRandomThread:(id<MyProtocol>)protocol {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
int32_t x;
[protocol intPtrMethod:&x];
[protocol voidMethod:x];
});
}
@end


Expand Down
1 change: 1 addition & 0 deletions pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions pkgs/objective_c/lib/src/objective_c_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void Function(NSStream, NSStreamEvent)>(
Expand All @@ -6397,6 +6417,10 @@ abstract final class NSStreamDelegate {
ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent.listener(
(ffi.Pointer<ffi.Void> _, NSStream arg1, NSStreamEvent arg2) =>
func(arg1, arg2)),
(void Function(NSStream, NSStreamEvent) func) =>
ObjCBlock_ffiVoid_ffiVoid_NSStream_NSStreamEvent.blocking(
(ffi.Pointer<ffi.Void> _, NSStream arg1, NSStreamEvent arg2) =>
func(arg1, arg2)),
);
}

Expand Down
15 changes: 14 additions & 1 deletion pkgs/objective_c/lib/src/protocol_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ class ObjCProtocolMethod<T extends Function> {
class ObjCProtocolListenableMethod<T extends Function>
extends ObjCProtocolMethod<T> {
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].
Expand All @@ -92,4 +93,16 @@ class ObjCProtocolListenableMethod<T extends Function>
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));
}
}
}

0 comments on commit a646680

Please sign in to comment.