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 blocks #1796

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
## 16.0.1-wip
## 16.1.0-wip

- Ensure that required symbols are available to FFI even when the final binary
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.

## 16.0.0

Expand Down
1 change: 1 addition & 0 deletions pkgs/ffigen/lib/src/code_generator/dart_keywords.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const keywords = {
'default',
'deferred',
'do',
'Duration',
'dynamic',
'else',
'enum',
Expand Down
227 changes: 172 additions & 55 deletions pkgs/ffigen/lib/src/code_generator/objc_block.dart

Large diffs are not rendered by default.

75 changes: 53 additions & 22 deletions pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class ObjCBuiltInFunctions {
ObjCImport('getProtocolMethodSignature');
static const getProtocol = ObjCImport('getProtocol');
static const objectRelease = ObjCImport('objectRelease');
static const signalWaiter = ObjCImport('signalWaiter');
static const wrapBlockingBlock = ObjCImport('wrapBlockingBlock');
static const objectBase = ObjCImport('ObjCObjectBase');
static const blockType = ObjCImport('ObjCBlock');
static const consumedType = ObjCImport('Consumed');
Expand Down Expand Up @@ -207,28 +209,54 @@ class ObjCBuiltInFunctions {
Parameter(type: _methodSigType(p.type), objCConsumed: p.objCConsumed))
.toList();

final _blockTrampolines = <String, ObjCListenerBlockTrampoline>{};
ObjCListenerBlockTrampoline? getListenerBlockTrampoline(ObjCBlock block) {
final _blockTrampolines = <String, ObjCBlockWrapperFuncs>{};
ObjCBlockWrapperFuncs? getBlockTrampolines(ObjCBlock block) {
final id = _methodSigId(block.returnType, block.params);
final idHash = fnvHash32(id).toRadixString(36);

return _blockTrampolines[id] ??= ObjCListenerBlockTrampoline(Func(
name: '_${wrapperName}_wrapListenerBlock_$idHash',
returnType: PointerType(objCBlockType),
parameters: [
Parameter(
name: 'block',
type: PointerType(objCBlockType),
objCConsumed: false)
],
objCReturnsRetained: true,
isLeaf: true,
isInternal: true,
useNameForLookup: true,
ffiNativeConfig: const FfiNativeConfig(enabled: true),
));
return _blockTrampolines[id] ??= ObjCBlockWrapperFuncs(
_blockTrampolineFunc('_${wrapperName}_wrapListenerBlock_$idHash'),
_blockTrampolineFunc('_${wrapperName}_wrapBlockingBlock_$idHash',
blocking: true),
);
}

Func _blockTrampolineFunc(String name, {bool blocking = false}) => Func(
name: name,
returnType: PointerType(objCBlockType),
parameters: [
Parameter(
name: 'block',
type: PointerType(objCBlockType),
objCConsumed: false),
if (blocking) ...[
Parameter(
name: 'listnerBlock',
type: PointerType(objCBlockType),
objCConsumed: false),
Parameter(
name: 'timeoutSeconds', type: doubleType, objCConsumed: false),
Parameter(
name: 'newWaiter',
type: PointerType(NativeFunc(FunctionType(
returnType: PointerType(voidType), parameters: []))),
objCConsumed: false),
Parameter(
name: 'awaitWaiter',
type: PointerType(
NativeFunc(FunctionType(returnType: voidType, parameters: [
Parameter(type: PointerType(voidType), objCConsumed: false),
Parameter(type: doubleType, objCConsumed: false),
]))),
objCConsumed: false),
],
],
objCReturnsRetained: true,
isLeaf: true,
isInternal: true,
useNameForLookup: true,
ffiNativeConfig: const FfiNativeConfig(enabled: true),
);

static bool isInstanceType(Type type) {
if (type is ObjCInstanceType) return true;
final baseType = type.typealiasType;
Expand All @@ -237,15 +265,18 @@ class ObjCBuiltInFunctions {
}

/// A native trampoline function for a listener block.
class ObjCListenerBlockTrampoline extends AstNode {
final Func func;
class ObjCBlockWrapperFuncs extends AstNode {
final Func listenerWrapper;
final Func blockingWrapper;
bool objCBindingsGenerated = false;
ObjCListenerBlockTrampoline(this.func);

ObjCBlockWrapperFuncs(this.listenerWrapper, this.blockingWrapper);

@override
void visitChildren(Visitor visitor) {
super.visitChildren(visitor);
visitor.visit(func);
visitor.visit(listenerWrapper);
visitor.visit(blockingWrapper);
}
}

Expand Down
1 change: 1 addition & 0 deletions pkgs/ffigen/lib/src/code_generator/writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ class Writer {
final s = StringBuffer();
s.write('''
#include <stdint.h>
#import <Foundation/Foundation.h>
''');

for (final entryPoint in nativeEntryPoints) {
Expand Down
4 changes: 2 additions & 2 deletions pkgs/ffigen/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# BSD-style license that can be found in the LICENSE file.

name: ffigen
version: 16.0.1-wip
version: 16.1.0-wip
description: >
Generator for FFI bindings, using LibClang to parse C, Objective-C, and Swift
files.
Expand Down Expand Up @@ -41,7 +41,7 @@ dev_dependencies:
dart_flutter_team_lints: ^2.0.0
json_schema: ^5.1.1
leak_tracker: ^10.0.7
objective_c: ^4.0.0
objective_c: ^4.1.0
test: ^1.16.2

dependency_overrides:
Expand Down
140 changes: 140 additions & 0 deletions pkgs/ffigen/test/native_objc_test/block_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ typedef StructListenerBlock = ObjCBlock_ffiVoid_Vec2_Vec4_NSObject;
typedef NSStringListenerBlock = ObjCBlock_ffiVoid_NSString;
typedef NoTrampolineListenerBlock = ObjCBlock_ffiVoid_Int32_Vec4_ffiChar;
typedef BlockBlock = ObjCBlock_IntBlock_IntBlock;
typedef IntPtrBlock = ObjCBlock_ffiVoid_Int32;
typedef ResultBlock = ObjCBlock_ffiVoid_Int321;

void main() {
late final BlockTestObjCLibrary lib;
Expand Down Expand Up @@ -113,6 +115,82 @@ void main() {
expect(value, 123);
});

void waitSync(Duration d) {
final t = Stopwatch();
t.start();
while (t.elapsed < d) {
// Waiting...
}
}

test('Blocking block same thread', () {
int value = 0;
final block = VoidBlock.blocking(() {
waitSync(Duration(milliseconds: 100));
value = 123;
});
BlockTester.callOnSameThread_(block);
expect(value, 123);
});

test('Blocking block new thread', () async {
final block = IntPtrBlock.blocking((Pointer<Int32> result) {
waitSync(Duration(milliseconds: 100));
result.value = 123456;
}, timeout: Duration(seconds: 60));
final resultCompleter = Completer<int>();
final resultBlock = ResultBlock.listener((int result) {
resultCompleter.complete(result);
});
BlockTester.blockingBlockTest_resultBlock_(block, resultBlock);
expect(await resultCompleter.future, 123456);
});

test('Blocking block timeout', () async {
int value = 0;
final block = VoidBlock.blocking(() {
waitSync(Duration(milliseconds: 300));
value = 123456;
}, timeout: Duration(milliseconds: 100));
BlockTester.callOnNewThread_(block).start();
expect(value, 0);
await Future.delayed(Duration(milliseconds: 1000));
expect(value, 123456);
});

test('Blocking block same thread throws', () {
int value = 0;
final block = VoidBlock.blocking(() {
value = 123;
throw "Hello";
});
BlockTester.callOnSameThread_(block);
expect(value, 123);
});

test('Blocking block new thread throws', () async {
final block = IntPtrBlock.blocking((Pointer<Int32> result) {
result.value = 123456;
throw "Hello";
}, timeout: Duration(seconds: 60));
final resultCompleter = Completer<int>();
final resultBlock = ResultBlock.listener((int result) {
resultCompleter.complete(result);
});
BlockTester.blockingBlockTest_resultBlock_(block, resultBlock);
expect(await resultCompleter.future, 123456);
});

test('Blocking block manual invocation', () {
int value = 0;
final block = VoidBlock.blocking(() {
waitSync(Duration(milliseconds: 100));
value = 123;
});
block();
expect(value, 123);
});

test('Float block', () {
final block = FloatBlock.fromFunction((double x) {
return x + 4.56;
Expand Down Expand Up @@ -664,6 +742,68 @@ void main() {
expect(blockRetainCount(blockBlock), 0);
}, skip: !canDoGC);

test('Blocking block ref counting same thread', () async {
DummyObject? dummyObject = DummyObject.new1();
DartObjectListenerBlock? block =
ObjectListenerBlock.blocking((DummyObject obj) {
// Object passed as argument.
expect(objectRetainCount(obj.ref.pointer), greaterThan(0));

// Object bound in block's lambda.
expect(dummyObject, isNotNull);
});

final tester = BlockTester.newFromListener_(block);
final rawBlock = block!.ref.pointer;
expect(blockRetainCount(rawBlock), 2);

final rawDummyObject = dummyObject!.ref.pointer;
expect(objectRetainCount(rawDummyObject), 1);

dummyObject = null;
block = null;
tester.invokeAndReleaseListener_(null);
doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();

expect(blockRetainCount(rawBlock), 0);
expect(objectRetainCount(rawDummyObject), 0);
}, skip: !canDoGC);

test('Blocking block ref counting new thread', () async {
final completer = Completer<void>();
DummyObject? dummyObject = DummyObject.new1();
DartObjectListenerBlock? block =
ObjectListenerBlock.blocking((DummyObject obj) {
// Object passed as argument.
expect(objectRetainCount(obj.ref.pointer), greaterThan(0));

// Object bound in block's lambda.
expect(dummyObject, isNotNull);

completer.complete();
});

final tester = BlockTester.newFromListener_(block);
final rawBlock = block!.ref.pointer;
expect(blockRetainCount(rawBlock), 2);

final rawDummyObject = dummyObject!.ref.pointer;
expect(objectRetainCount(rawDummyObject), 1);

tester.invokeAndReleaseListenerOnNewThread();
await completer.future;
dummyObject = null;
block = null;
doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();

expect(blockRetainCount(rawBlock), 0);
expect(objectRetainCount(rawDummyObject), 0);
}, skip: !canDoGC);

test('Block fields have sensible values', () {
final block = IntBlock.fromFunction(makeAdder(4000));
final blockPtr = block.ref.pointer;
Expand Down
6 changes: 5 additions & 1 deletion pkgs/ffigen/test/native_objc_test/block_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ typedef void (^NullableListenerBlock)(DummyObject* _Nullable);
typedef void (^StructListenerBlock)(struct Vec2, Vec4, NSObject*);
typedef void (^NSStringListenerBlock)(NSString*);
typedef void (^NoTrampolineListenerBlock)(int32_t, Vec4, const char*);
typedef void (^IntPtrBlock)(int32_t*);
typedef void (^ResultBlock)(int32_t);

// Wrapper around a block, so that our Dart code can test creating and invoking
// blocks in Objective C code.
Expand Down Expand Up @@ -80,5 +82,7 @@ typedef void (^NoTrampolineListenerBlock)(int32_t, Vec4, const char*);
+ (IntBlock)newBlock:(BlockBlock)block withMult:(int)mult NS_RETURNS_RETAINED;
+ (BlockBlock)newBlockBlock:(int)mult NS_RETURNS_RETAINED;
- (void)invokeAndReleaseListenerOnNewThread;
- (void)invokeAndReleaseListener:(id)_;
- (void)invokeAndReleaseListener:(_Nullable id)_;
+ (void)blockingBlockTest:(IntPtrBlock)blockingBlock
resultBlock:(ResultBlock)resultBlock;
@end
9 changes: 9 additions & 0 deletions pkgs/ffigen/test/native_objc_test/block_test.m
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,13 @@ - (void)invokeAndReleaseListener:(id)_ {
myListener = nil;
}

+ (void)blockingBlockTest:(IntPtrBlock)blockingBlock
resultBlock:(ResultBlock)resultBlock {
[[[NSThread alloc] initWithBlock:^void() {
int32_t result;
blockingBlock(&result);
resultBlock(result);
}] start];
}

@end
3 changes: 2 additions & 1 deletion pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 4.0.1-wip
## 4.1.0-wip

- Use ffigen 16.1.0
- 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`.
Expand Down
4 changes: 4 additions & 0 deletions pkgs/objective_c/ffigen_c.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ functions:
- 'DOBJC_disposeObjCBlockWithClosure'
- 'DOBJC_newFinalizableBool'
- 'DOBJC_newFinalizableHandle'
- 'DOBJC_awaitWaiter'
rename:
'DOBJC_disposeObjCBlockWithClosure': 'disposeObjCBlockWithClosure'
'DOBJC_isValidBlock': 'isValidBlock'
'DOBJC_newFinalizableHandle': 'newFinalizableHandle'
'DOBJC_deleteFinalizableHandle': 'deleteFinalizableHandle'
'DOBJC_newFinalizableBool': 'newFinalizableBool'
'DOBJC_newWaiter': 'newWaiter'
'DOBJC_signalWaiter': 'signalWaiter'
'DOBJC_awaitWaiter': 'awaitWaiter'
'sel_registerName': 'registerName'
'sel_getName': 'getName'
'objc_getClass': 'getClass'
Expand Down
3 changes: 2 additions & 1 deletion pkgs/objective_c/lib/objective_c.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export 'src/c_bindings_generated.dart'
ObjCSelector,
blockRetain,
objectRelease,
objectRetain;
objectRetain,
signalWaiter;
export 'src/internal.dart'
hide
ObjCBlockBase,
Expand Down
Loading
Loading