Skip to content

Commit

Permalink
[ffigen] Allow blocks to keep their isolates alive
Browse files Browse the repository at this point in the history
  • Loading branch information
liamappelbe committed Feb 19, 2025
1 parent 63b2a88 commit ae4e6d1
Show file tree
Hide file tree
Showing 8 changed files with 516 additions and 185 deletions.
2 changes: 2 additions & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
locate Apple APIs: `$XCODE`, `$IOS_SDK`, and `$MACOS_SDK`.
- __Breaking change__: Change the `usrTypeMappings` field of `Config`'s factory
constructor from a `List<ImportedType>` to a `Map<String, ImportedType>`.
- Add a `keepIsolateAlive` parameter to the block constructors that allows a
block to keep its owner isolate alive.

## 17.0.0

Expand Down
34 changes: 21 additions & 13 deletions pkgs/ffigen/lib/src/code_generator/objc_block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,12 @@ abstract final class $name {
/// This block must be invoked by native code running on the same thread as
/// the isolate that registered it. Invoking the block on the wrong thread
/// will result in a crash.
static $blockType fromFunction(${func.dartType} fn) =>
$blockType($newClosureBlock($closureCallable, $convFn),
///
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC.
static $blockType fromFunction(${func.dartType} fn,
{bool keepIsolateAlive = false}) =>
$blockType($newClosureBlock($closureCallable, $convFn, keepIsolateAlive),
retain: false, release: true);
''');

Expand Down Expand Up @@ -274,11 +278,12 @@ abstract final class $name {
/// but only supports void functions, and is not run synchronously. See
/// NativeCallable.listener for more details.
///
/// Note that unlike the default behavior of NativeCallable.listener, listener
/// blocks do not keep the isolate alive.
static $blockType listener(${func.dartType} fn) {
final raw = $newClosureBlock(
$listenerCallable.nativeFunction.cast(), $listenerConvFn);
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC.
static $blockType listener(${func.dartType} fn,
{bool keepIsolateAlive = false}) {
final raw = $newClosureBlock($listenerCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final wrapper = $wrapListenerFn(raw);
$releaseFn(raw.cast());
return $blockType(wrapper, retain: false, release: true);
Expand All @@ -290,14 +295,17 @@ abstract final class $name {
/// caller until the callback is handled by the Dart isolate that created
/// the block. Async functions are not supported.
///
/// This block does not keep the owner isolate alive. If the owner isolate has
/// shut down, and the block is invoked by native code, it may block
/// If `keepIsolateAlive` is true, this block will keep this isolate alive
/// until it is garbage collected by both Dart and ObjC. If the owner isolate
/// has shut down, and the block is invoked by native code, it may block
/// indefinitely, or have other undefined behavior.
static $blockType blocking(${func.dartType} fn) {
final raw = $newClosureBlock(
$blockingCallable.nativeFunction.cast(), $listenerConvFn);
static $blockType blocking(${func.dartType} fn,
{bool keepIsolateAlive = false}) {
final raw = $newClosureBlock($blockingCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final rawListener = $newClosureBlock(
$blockingListenerCallable.nativeFunction.cast(), $listenerConvFn);
$blockingListenerCallable.nativeFunction.cast(),
$listenerConvFn, keepIsolateAlive);
final wrapper = $wrapBlockingBlockFn($wrapBlockingFn, raw, rawListener);
$releaseFn(raw.cast());
$releaseFn(rawListener.cast());
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: ^6.0.0
objective_c: ^7.0.0
test: ^1.16.2

dependency_overrides:
Expand Down
188 changes: 188 additions & 0 deletions pkgs/ffigen/test/native_objc_test/block_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';

import 'package:ffi/ffi.dart';
import 'package:objective_c/objective_c.dart';
Expand Down Expand Up @@ -863,6 +864,193 @@ void main() {
expect(objectRetainCount(objectPtr), 0);
}
});

test('Block.fromFunction, keepIsolateAlive', () async {
final isolateSendPort = Completer<SendPort>();
final blocksCreated = Completer<void>();
final blkKeepAliveDestroyed = Completer<void>();
final receivePort = RawReceivePort((msg) {
if (msg is SendPort) {
isolateSendPort.complete(msg);
} else if (msg == 'Blocks created') {
blocksCreated.complete();
} else if (msg == 'blkKeepAlive destroyed') {
blkKeepAliveDestroyed.complete();
}
});

var isExited = false;
late final RawReceivePort exitPort;
exitPort = RawReceivePort((_) {
isExited = true;
exitPort.close();
});

final isolate = Isolate.spawn((sendPort) {
final blkKeepAlive =
VoidBlock.fromFunction(() {}, keepIsolateAlive: true);
final blkDontKeepAlive =
VoidBlock.fromFunction(() {}, keepIsolateAlive: false);
sendPort.send('Blocks created');

final isolatePort = RawReceivePort((msg) {
if (msg == 'Destroy blkKeepAlive') {
blkKeepAlive.ref.release();
sendPort.send('blkKeepAlive destroyed');
}
})
..keepIsolateAlive = false;

sendPort.send(isolatePort.sendPort);
}, receivePort.sendPort, onExit: exitPort.sendPort);

await blocksCreated.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Both blocks are still alive.
expect(isExited, isFalse);

(await isolateSendPort.future).send('Destroy blkKeepAlive');
await blkKeepAliveDestroyed.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Only blkDontKeepAlive is alive.
expect(isExited, isTrue);

receivePort.close();
}, skip: !canDoGC);

test('Block.listener, keepIsolateAlive', () async {
final isolateSendPort = Completer<SendPort>();
final blocksCreated = Completer<void>();
final blkKeepAliveDestroyed = Completer<void>();
final receivePort = RawReceivePort((msg) {
if (msg is SendPort) {
isolateSendPort.complete(msg);
} else if (msg == 'Blocks created') {
blocksCreated.complete();
} else if (msg == 'blkKeepAlive destroyed') {
blkKeepAliveDestroyed.complete();
}
});

var isExited = false;
late final RawReceivePort exitPort;
exitPort = RawReceivePort((_) {
isExited = true;
exitPort.close();
});

final isolate = Isolate.spawn((sendPort) {
final blkKeepAlive = VoidBlock.listener(() {}, keepIsolateAlive: true);
final blkDontKeepAlive =
VoidBlock.listener(() {}, keepIsolateAlive: false);
sendPort.send('Blocks created');

final isolatePort = RawReceivePort((msg) {
if (msg == 'Destroy blkKeepAlive') {
blkKeepAlive.ref.release();
sendPort.send('blkKeepAlive destroyed');
}
})
..keepIsolateAlive = false;

sendPort.send(isolatePort.sendPort);
}, receivePort.sendPort, onExit: exitPort.sendPort);

await blocksCreated.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Both blocks are still alive.
expect(isExited, isFalse);

(await isolateSendPort.future).send('Destroy blkKeepAlive');
await blkKeepAliveDestroyed.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Only blkDontKeepAlive is alive.
expect(isExited, isTrue);

receivePort.close();
}, skip: !canDoGC);

test('Block.blocking, keepIsolateAlive', () async {
final isolateSendPort = Completer<SendPort>();
final blocksCreated = Completer<void>();
final blkKeepAliveDestroyed = Completer<void>();
final receivePort = RawReceivePort((msg) {
if (msg is SendPort) {
isolateSendPort.complete(msg);
} else if (msg == 'Blocks created') {
blocksCreated.complete();
} else if (msg == 'blkKeepAlive destroyed') {
blkKeepAliveDestroyed.complete();
}
});

var isExited = false;
late final RawReceivePort exitPort;
exitPort = RawReceivePort((_) {
isExited = true;
exitPort.close();
});

final isolate = Isolate.spawn((sendPort) {
final blkKeepAlive = VoidBlock.blocking(() {}, keepIsolateAlive: true);
final blkDontKeepAlive =
VoidBlock.blocking(() {}, keepIsolateAlive: false);
sendPort.send('Blocks created');

final isolatePort = RawReceivePort((msg) {
if (msg == 'Destroy blkKeepAlive') {
blkKeepAlive.ref.release();
sendPort.send('blkKeepAlive destroyed');
}
})
..keepIsolateAlive = false;

sendPort.send(isolatePort.sendPort);
}, receivePort.sendPort, onExit: exitPort.sendPort);

await blocksCreated.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Both blocks are still alive.
expect(isExited, isFalse);

(await isolateSendPort.future).send('Destroy blkKeepAlive');
await blkKeepAliveDestroyed.future;

doGC();
await Future<void>.delayed(Duration.zero); // Let dispose message arrive.
doGC();
await Future<void>.delayed(Duration.zero); // Let exit message arrive.

// Only blkDontKeepAlive is alive.
expect(isExited, isTrue);

receivePort.close();
}, skip: !canDoGC);
});
}

Expand Down
4 changes: 4 additions & 0 deletions pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 7.0.0

- Use ffigen 18.0.0

## 6.0.0

- Use ffigen 17.0.0
Expand Down
32 changes: 21 additions & 11 deletions pkgs/objective_c/lib/src/internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,18 +389,24 @@ BlockPtr _newBlock(VoidPtr invoke, VoidPtr target,
const int _blockHasCopyDispose = 1 << 25;

/// Only for use by ffigen bindings.
BlockPtr newClosureBlock(VoidPtr invoke, Function fn) => _newBlock(
invoke,
_registerBlockClosure(fn),
_closureBlockDesc,
_blockClosureDisposer.sendPort.nativePort,
_blockHasCopyDispose);
BlockPtr newClosureBlock(VoidPtr invoke, Function fn, bool keepIsolateAlive) =>
_newBlock(
invoke,
_registerBlockClosure(fn, keepIsolateAlive),
_closureBlockDesc,
_blockClosureDisposer.sendPort.nativePort,
_blockHasCopyDispose);

/// Only for use by ffigen bindings.
BlockPtr newPointerBlock(VoidPtr invoke, VoidPtr target) =>
_newBlock(invoke, target, _pointerBlockDesc, 0, 0);

final _blockClosureRegistry = <int, Function>{};
typedef _RegEntry = ({
Function closure,
RawReceivePort? keepAlivePort,
});

final _blockClosureRegistry = <int, _RegEntry>{};

int _blockClosureRegistryLastId = 0;

Expand All @@ -409,23 +415,27 @@ final _blockClosureDisposer = () {
return RawReceivePort((dynamic msg) {
final id = msg as int;
assert(_blockClosureRegistry.containsKey(id));
_blockClosureRegistry.remove(id);
final entry = _blockClosureRegistry.remove(id)!;
entry.keepAlivePort?.close();
}, 'ObjCBlockClosureDisposer')
..keepIsolateAlive = false;
}();

VoidPtr _registerBlockClosure(Function closure) {
VoidPtr _registerBlockClosure(Function closure, bool keepIsolateAlive) {
++_blockClosureRegistryLastId;
assert(!_blockClosureRegistry.containsKey(_blockClosureRegistryLastId));
_blockClosureRegistry[_blockClosureRegistryLastId] = closure;
_blockClosureRegistry[_blockClosureRegistryLastId] = (
closure: closure,
keepAlivePort: keepIsolateAlive ? RawReceivePort() : null,
);
return VoidPtr.fromAddress(_blockClosureRegistryLastId);
}

/// Only for use by ffigen bindings.
Function getBlockClosure(BlockPtr block) {
var id = block.ref.target.address;
assert(_blockClosureRegistry.containsKey(id));
return _blockClosureRegistry[id]!;
return _blockClosureRegistry[id]!.closure;
}

typedef NewWaiterFn = NativeFunction<VoidPtr Function()>;
Expand Down
Loading

0 comments on commit ae4e6d1

Please sign in to comment.