Skip to content

Commit

Permalink
[ffigen] Blocking blocks (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamappelbe authored Jan 6, 2025
1 parent 1deeac7 commit 4ebaea4
Show file tree
Hide file tree
Showing 19 changed files with 1,106 additions and 126 deletions.
1 change: 1 addition & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- 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
223 changes: 168 additions & 55 deletions pkgs/ffigen/lib/src/code_generator/objc_block.dart

Large diffs are not rendered by default.

72 changes: 50 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,51 @@ 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: '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),
]))),
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 +262,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
2 changes: 1 addition & 1 deletion pkgs/ffigen/pubspec.yaml
Original file line number Diff line number Diff line change
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
128 changes: 128 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,70 @@ 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;
});
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 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";
});
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 +730,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
1 change: 1 addition & 0 deletions pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 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
16 changes: 16 additions & 0 deletions pkgs/objective_c/lib/src/c_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteMallocBlock;
@ffi.Native<ffi.Array<ffi.Pointer<ffi.Void>>>(symbol: "_NSConcreteStackBlock")
external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteStackBlock;

@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
symbol: "DOBJC_awaitWaiter")
external void awaitWaiter(
ffi.Pointer<ffi.Void> waiter,
);

@ffi.Native<ffi.Pointer<ObjCObject> Function(ffi.Pointer<ObjCObject>)>(
symbol: "objc_retainBlock", isLeaf: true)
external ffi.Pointer<ObjCObject> blockRetain(
Expand Down Expand Up @@ -167,6 +173,10 @@ external Dart_FinalizableHandle newFinalizableHandle(
ffi.Pointer<ObjCObject> object,
);

@ffi.Native<ffi.Pointer<ffi.Void> Function()>(
symbol: "DOBJC_newWaiter", isLeaf: true)
external ffi.Pointer<ffi.Void> newWaiter();

@ffi.Native<ffi.Pointer<ObjCObject> Function(ffi.Pointer<ObjCObject>)>(
symbol: "objc_autorelease", isLeaf: true)
external ffi.Pointer<ObjCObject> objectAutorelease(
Expand All @@ -191,6 +201,12 @@ external ffi.Pointer<ObjCSelector> registerName(
ffi.Pointer<ffi.Char> name,
);

@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
symbol: "DOBJC_signalWaiter", isLeaf: true)
external void signalWaiter(
ffi.Pointer<ffi.Void> waiter,
);

typedef Dart_FinalizableHandle = ffi.Pointer<_Dart_FinalizableHandle>;
typedef ObjCBlockDesc = _ObjCBlockDesc;
typedef ObjCBlockImpl = _ObjCBlockImpl;
Expand Down
Loading

0 comments on commit 4ebaea4

Please sign in to comment.