Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ffigen] Blocking protocol methods #1870

Merged
merged 16 commits into from
Jan 9, 2025
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));
}
}
}
Loading