Skip to content

Commit 318f841

Browse files
authored
[ffigen] Use isolate ownership API to fix blocking block deadlocks (#2068)
1 parent ec98149 commit 318f841

25 files changed

+584
-331
lines changed

.github/workflows/ffigen.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
8181
- uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff
8282
with:
83-
channel: 'stable'
83+
channel: stable
8484
- name: Install dependencies
8585
run: flutter pub get && flutter pub get --directory="../objective_c" && flutter pub get --directory="../jni"
8686
- name: Install clang-format
@@ -108,6 +108,28 @@ jobs:
108108
github-token: ${{ secrets.GITHUB_TOKEN }}
109109
parallel-finished: true
110110

111+
test-mac-latest:
112+
needs: analyze
113+
runs-on: 'macos-latest'
114+
defaults:
115+
run:
116+
working-directory: pkgs/ffigen/
117+
steps:
118+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
119+
- uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff
120+
with:
121+
channel: master
122+
- name: Install dependencies
123+
run: flutter pub get && flutter pub get --directory="../objective_c" && flutter pub get --directory="../jni"
124+
- name: Install clang-format
125+
uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30
126+
with:
127+
brew: clang-format
128+
- name: Build test dylib and bindings
129+
run: dart test/setup.dart
130+
- name: Run VM tests and collect coverage
131+
run: dart test
132+
111133
test-mac-flutter:
112134
needs: analyze
113135
runs-on: 'macos-latest'

pkgs/ffigen/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
jnigen. The main change is that `$` is used as a delimiter now, to avoid
1818
renamed identifiers from colliding with other identifiers. For example, `foo`
1919
is renamed to `foo$1` if there's already a `foo` in the namespace.
20+
- Fix [a bug](https://github.com/dart-lang/native/issues/1967) where blocking
21+
blocks could deadlock if invoked from the Flutter UI thread. Note that this
22+
relies on changes in Flutter that are currently only available in the main
23+
channel. These changes will likely be released in Flutter 3.31.0.
2024

2125
## 17.0.0
2226

pkgs/ffigen/lib/src/code_generator/imports.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,5 @@ final objCBlockType =
169169
ImportedType(objcPkgImport, 'ObjCBlockImpl', 'ObjCBlockImpl', 'id');
170170
final objCProtocolType =
171171
ImportedType(objcPkgImport, 'ObjCProtocol', 'ObjCProtocol', 'void');
172+
final objCContextType = ImportedType(
173+
objcPkgImport, 'DOBJC_Context', 'DOBJC_Context', 'DOBJC_Context');

pkgs/ffigen/lib/src/code_generator/objc_block.dart

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class ObjCBlock extends BindingType {
148148
final newClosureBlock = ObjCBuiltInFunctions.newClosureBlock.gen(w);
149149
final getBlockClosure = ObjCBuiltInFunctions.getBlockClosure.gen(w);
150150
final releaseFn = ObjCBuiltInFunctions.objectRelease.gen(w);
151-
final wrapBlockingBlockFn = ObjCBuiltInFunctions.wrapBlockingBlock.gen(w);
151+
final objCContext = ObjCBuiltInFunctions.objCContext.gen(w);
152152
final signalWaiterFn = ObjCBuiltInFunctions.signalWaiter.gen(w);
153153
final returnFfiDartType = returnType.getFfiDartType(w);
154154
final voidPtrCType = voidPtr.getCType(w);
@@ -312,7 +312,7 @@ abstract final class $name {
312312
final rawListener = $newClosureBlock(
313313
$blockingListenerCallable.nativeFunction.cast(),
314314
$listenerConvFn, keepIsolateAlive);
315-
final wrapper = $wrapBlockingBlockFn($wrapBlockingFn, raw, rawListener);
315+
final wrapper = $wrapBlockingFn(raw, rawListener, $objCContext);
316316
$releaseFn(raw.cast());
317317
$releaseFn(rawListener.cast());
318318
return $blockType(wrapper, retain: false, release: true);
@@ -406,19 +406,14 @@ typedef ${returnType.getNativeType()} (^$blockingName)($blockingArgStr);
406406
__attribute__((visibility("default"))) __attribute__((used))
407407
$listenerName $blockingWrapper(
408408
$blockingName block, $blockingName listenerBlock,
409-
void* (*newWaiter)(), void (*awaitWaiter)(void*)) NS_RETURNS_RETAINED {
410-
NSThread *targetThread = [NSThread currentThread];
411-
return ^void($argStr) {
412-
if ([NSThread currentThread] == targetThread) {
413-
${generateRetain('block')};
414-
block(${blockingRetains.join(', ')});
415-
} else {
416-
void* waiter = newWaiter();
417-
${generateRetain('listenerBlock')};
418-
listenerBlock(${blockingListenerRetains.join(', ')});
419-
awaitWaiter(waiter);
420-
}
421-
};
409+
DOBJC_Context* ctx) NS_RETURNS_RETAINED {
410+
BLOCKING_BLOCK_IMPL(ctx, ^void($argStr), {
411+
${generateRetain('block')};
412+
block(${blockingRetains.join(', ')});
413+
}, {
414+
${generateRetain('listenerBlock')};
415+
listenerBlock(${blockingListenerRetains.join(', ')});
416+
});
422417
}
423418
''';
424419
}

pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ObjCBuiltInFunctions {
3434
static const getProtocol = ObjCImport('getProtocol');
3535
static const objectRelease = ObjCImport('objectRelease');
3636
static const signalWaiter = ObjCImport('signalWaiter');
37-
static const wrapBlockingBlock = ObjCImport('wrapBlockingBlock');
37+
static const objCContext = ObjCImport('objCContext');
3838
static const objectBase = ObjCImport('ObjCObjectBase');
3939
static const protocolBase = ObjCImport('ObjCProtocolBase');
4040
static const blockType = ObjCImport('ObjCBlock');
@@ -261,16 +261,8 @@ class ObjCBuiltInFunctions {
261261
type: PointerType(objCBlockType),
262262
objCConsumed: false),
263263
Parameter(
264-
name: 'newWaiter',
265-
type: PointerType(NativeFunc(FunctionType(
266-
returnType: PointerType(voidType), parameters: []))),
267-
objCConsumed: false),
268-
Parameter(
269-
name: 'awaitWaiter',
270-
type: PointerType(
271-
NativeFunc(FunctionType(returnType: voidType, parameters: [
272-
Parameter(type: PointerType(voidType), objCConsumed: false),
273-
]))),
264+
name: 'context',
265+
type: PointerType(objCContextType),
274266
objCConsumed: false),
275267
],
276268
],

pkgs/ffigen/lib/src/code_generator/writer.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,50 @@ class Writer {
416416
for (final entryPoint in nativeEntryPoints) {
417417
s.write(_objcImport(entryPoint, outDir));
418418
}
419-
s.write('''
419+
s.write(r'''
420420
421421
#if !__has_feature(objc_arc)
422422
#error "This file must be compiled with ARC enabled"
423423
#endif
424424
425+
typedef struct {
426+
int64_t version;
427+
void* (*newWaiter)(void);
428+
void (*awaitWaiter)(void*);
429+
void* (*currentIsolate)(void);
430+
void (*enterIsolate)(void*);
431+
void (*exitIsolate)(void);
432+
int64_t (*getMainPortId)(void);
433+
bool (*getCurrentThreadOwnsIsolate)(int64_t);
434+
} DOBJC_Context;
435+
425436
id objc_retainBlock(id);
437+
438+
#define BLOCKING_BLOCK_IMPL(ctx, BLOCK_SIG, INVOKE_DIRECT, INVOKE_LISTENER) \
439+
assert(ctx->version >= 1); \
440+
void* targetIsolate = ctx->currentIsolate(); \
441+
int64_t targetPort = ctx->getMainPortId == NULL ? 0 : ctx->getMainPortId(); \
442+
return BLOCK_SIG { \
443+
void* currentIsolate = ctx->currentIsolate(); \
444+
bool mayEnterIsolate = \
445+
currentIsolate == NULL && \
446+
ctx->getCurrentThreadOwnsIsolate != NULL && \
447+
ctx->getCurrentThreadOwnsIsolate(targetPort); \
448+
if (currentIsolate == targetIsolate || mayEnterIsolate) { \
449+
if (mayEnterIsolate) { \
450+
ctx->enterIsolate(targetIsolate); \
451+
} \
452+
INVOKE_DIRECT; \
453+
if (mayEnterIsolate) { \
454+
ctx->exitIsolate(); \
455+
} \
456+
} else { \
457+
void* waiter = ctx->newWaiter(); \
458+
INVOKE_LISTENER; \
459+
ctx->awaitWaiter(waiter); \
460+
} \
461+
};
462+
426463
''');
427464

428465
var empty = true;
@@ -433,6 +470,11 @@ id objc_retainBlock(id);
433470
s.write(bindingString.string);
434471
}
435472
}
473+
474+
s.write('''
475+
#undef BLOCKING_BLOCK_IMPL
476+
''');
477+
436478
return empty ? null : s.toString();
437479
}
438480
}

pkgs/ffigen/test/native_objc_test/block_test.dart

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ typedef BlockBlock = ObjCBlock_IntBlock_IntBlock;
4141
typedef IntPtrBlock = ObjCBlock_ffiVoid_Int32;
4242
typedef ResultBlock = ObjCBlock_ffiVoid_Int32$1;
4343

44+
bool get hasIsolateOwnershipApi =>
45+
DynamicLibrary.process().providesSymbol('Dart_SetCurrentThreadOwnsIsolate');
46+
void setCurrentThreadOwnsIsolate() =>
47+
DynamicLibrary.process().lookupFunction<Void Function(), void Function()>(
48+
'Dart_SetCurrentThreadOwnsIsolate')();
49+
4450
void main() {
4551
late final BlockTestObjCLibrary lib;
4652

@@ -52,7 +58,9 @@ void main() {
5258
verifySetupFile(dylib);
5359
lib = BlockTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
5460

55-
generateBindingsForCoverage('block');
61+
// generateBindingsForCoverage('block');
62+
63+
BlockTester.setup_(NativeApi.initializeApiDLData);
5664
});
5765

5866
test('BlockTester is working', () {
@@ -1051,6 +1059,22 @@ void main() {
10511059

10521060
receivePort.close();
10531061
}, skip: !canDoGC);
1062+
1063+
test('Blocking block deadlock', () async {
1064+
// Regression test for https://github.com/dart-lang/native/issues/1967
1065+
// Run the test on a new isolate so we can safely claim ownership of it.
1066+
final value = await Isolate.run(() {
1067+
setCurrentThreadOwnsIsolate();
1068+
1069+
var innerValue = 0;
1070+
final block = VoidBlock.blocking(() {
1071+
innerValue = 123;
1072+
});
1073+
BlockTester.callOnSameThreadOutsideIsolate_(block);
1074+
return innerValue;
1075+
});
1076+
expect(value, 123);
1077+
}, skip: !hasIsolateOwnershipApi);
10541078
});
10551079
}
10561080

pkgs/ffigen/test/native_objc_test/block_test.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ typedef void (^ResultBlock)(int32_t);
5555
__strong IntBlock myBlock;
5656
__strong ObjectListenerBlock myListener;
5757
}
58+
+ (void)setup:(void*)apiData;
5859
+ (BlockTester*)newFromBlock:(IntBlock)block;
5960
+ (BlockTester*)newFromMultiplier:(int32_t)mult;
6061
+ (BlockTester*)newFromListener:(ObjectListenerBlock)block;
6162
- (int32_t)call:(int32_t)x;
6263
- (IntBlock)getBlock NS_RETURNS_RETAINED;
6364
- (void)pokeBlock;
6465
+ (void)callOnSameThread:(VoidBlock)block;
66+
+ (void)callOnSameThreadOutsideIsolate:(VoidBlock)block;
6567
+ (NSThread*)callOnNewThread:(VoidBlock)block NS_RETURNS_RETAINED;
6668
+ (NSThread*)callWithBlockOnNewThread:(ListenerBlock)block NS_RETURNS_RETAINED;
6769
+ (float)callFloatBlock:(FloatBlock)block;

pkgs/ffigen/test/native_objc_test/block_test.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#import <Foundation/NSThread.h>
88

99
#include "block_test.h"
10+
#include "../../../objective_c/src/include/dart_api_dl.h"
1011

1112
@implementation DummyObject
1213

@@ -33,6 +34,10 @@ - (void)dealloc {
3334

3435
@implementation BlockTester
3536

37+
+ (void)setup:(void*)apiData {
38+
Dart_InitializeApiDL(apiData);
39+
}
40+
3641
+ (BlockTester*)newFromBlock:(IntBlock)block {
3742
BlockTester* bt = [BlockTester new];
3843
bt->myBlock = block;
@@ -72,6 +77,13 @@ + (void)callOnSameThread:(VoidBlock)block {
7277
block();
7378
}
7479

80+
+ (void)callOnSameThreadOutsideIsolate:(VoidBlock)block {
81+
Dart_Isolate isolate = Dart_CurrentIsolate_DL();
82+
Dart_ExitIsolate_DL();
83+
block();
84+
Dart_EnterIsolate_DL(isolate);
85+
}
86+
7587
+ (NSThread*)callOnNewThread:(VoidBlock)block NS_RETURNS_RETAINED {
7688
return [[NSThread alloc] initWithBlock:block];
7789
}

pkgs/ffigen/test/native_objc_test/setup.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ Future<void> _runClang(List<String> flags, String output) async {
2424
print('Generated file: $output');
2525
}
2626

27-
Future<String> _buildObject(String input) async {
27+
Future<String> _buildObject(String input, {bool objc = false}) async {
2828
final output = '$input.o';
2929
await _runClang([
30-
'-x',
31-
'objective-c',
32-
if (!arcDisabledFiles.contains(input)) '-fobjc-arc',
30+
if (objc) ...[
31+
'-x',
32+
'objective-c',
33+
],
34+
if (objc && !arcDisabledFiles.contains(input)) '-fobjc-arc',
3335
'-Wno-nullability-completeness',
3436
'-c',
3537
input,
@@ -48,8 +50,10 @@ Future<void> _linkLib(List<String> inputs, String output) => _runClang([
4850
Future<void> _buildLib(List<String> inputs, String output) async {
4951
final objFiles = <String>[];
5052
for (final input in inputs) {
51-
objFiles.add(await _buildObject(input));
53+
objFiles.add(await _buildObject(input, objc: true));
5254
}
55+
objFiles.add(
56+
await _buildObject('../../../objective_c/src/include/dart_api_dl.c'));
5357
await _linkLib(objFiles, output);
5458
}
5559

0 commit comments

Comments
 (0)