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] Allow blocks and protocols to keep their isolates alive #2017

Merged
merged 6 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
## 18.0.0-dev
## 18.0.0-wip

- Add variable substitutions that can be used in the `headers.entry-points` to
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 and protocol constructors that
allows a block or protocol to keep its owner isolate alive.
- __Breaking change__: `keepIsolateAlive` defaults to true, so all existing ObjC
blocks and protocols now keep their isolates alive by default.

## 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 = true}) =>
$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 = true}) {
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 = true}) {
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
18 changes: 14 additions & 4 deletions pkgs/ffigen/lib/src/code_generator/objc_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,18 @@ interface class $name extends $protocolBase $impls{
''');
}

final args = buildArgs.isEmpty ? '' : '{${buildArgs.join(', ')}}';
buildArgs.add('bool \$keepIsolateAlive = true');
final args = '{${buildArgs.join(', ')}}';
final builders = '''
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implement($args) {
final builder = $protocolBuilder();
$buildImplementations
return $name.castFrom(builder.build());
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}

/// Adds the implementation of the $originalName protocol to an existing
Expand All @@ -199,10 +203,13 @@ interface class $name extends $protocolBase $impls{
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All
/// methods that can be implemented as listeners will be.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implementAsListener($args) {
final builder = $protocolBuilder();
$buildListenerImplementations
return $name.castFrom(builder.build());
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}

/// Adds the implementation of the $originalName protocol to an existing
Expand All @@ -215,10 +222,13 @@ interface class $name extends $protocolBase $impls{
/// 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.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implementAsBlocking($args) {
final builder = $protocolBuilder();
$buildBlockingImplementations
return $name.castFrom(builder.build());
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}

/// Adds the implementation of the $originalName protocol to an existing
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: 18.0.0-dev
version: 18.0.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: ^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
Loading
Loading