[vm/ffi] Test and Document NativeFinalizer Dart API limitations

Explicitly allow calling `Dart_DeletePersistentHandle` and
`Dart_DeleteWeakPersistentHandle` from `NativeFinalizers`.

And exercise this behavior in tests.

TEST=tests/ffi/vmspecific_native_finalizer_api_calls_test.dart
TEST=tests/ffi/vmspecific_native_finalizer_isolates_test.dart

Closes: https://github.com/dart-lang/sdk/issues/62076
CoreLibraryReviewExempt: VM-only. Doc-only.
Change-Id: I4915092e4b13cc55d0d48e7977149fb32059b854
Cq-Include-Trybots: luci.dart.try:vm-aot-linux-debug-x64-try,vm-dyn-linux-debug-x64-try,vm-linux-debug-x64-try,vm-ubsan-linux-release-x64-try,vm-tsan-linux-release-x64-try,vm-msan-linux-release-x64-try,vm-asan-linux-release-x64-try,vm-ffi-mac-debug-simarm64_arm64-try,vm-win-debug-x64-try,vm-reload-linux-debug-x64-try,vm-gcc-linux-x64-try,vm-appjit-linux-debug-x64-try,vm-aot-linux-release-x64-try,vm-aot-dyn-linux-debug-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/464180
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Reviewed-by: Liam Appelbe <liama@google.com>
This commit is contained in:
Daco Harkes
2025-11-25 02:50:47 -08:00
committed by Commit Queue
parent 80217511f9
commit ef30b3cc99
5 changed files with 192 additions and 6 deletions
@@ -1170,6 +1170,26 @@ DART_EXPORT void SetArgumentTo42(void* token) {
*reinterpret_cast<intptr_t*>(token) = 42;
}
DART_EXPORT Dart_PersistentHandle NewPersistentHandle(Dart_Handle object) {
return Dart_NewPersistentHandle(object);
}
DART_EXPORT void DeletePersistentHandleFinalizer(void* handle) {
printf("C: Finalizer deleting persistent handle %p\n", handle);
Dart_DeletePersistentHandle(reinterpret_cast<Dart_PersistentHandle>(handle));
}
DART_EXPORT Dart_WeakPersistentHandle
NewWeakPersistentHandle(Dart_Handle object) {
return Dart_NewWeakPersistentHandle(object, nullptr, 0, [](void*, void*) {});
}
DART_EXPORT void DeleteWeakPersistentHandleFinalizer(void* handle) {
printf("C: Finalizer deleting weak persistent handle %p\n", handle);
Dart_DeleteWeakPersistentHandle(
reinterpret_cast<Dart_WeakPersistentHandle>(handle));
}
////////////////////////////////////////////////////////////////////////////////
// Functions for testing @Native.
+2
View File
@@ -45,6 +45,8 @@ Note that a mutator can be at a safepoint without being suspended. It might be p
Because a safepoint operation excludes execution of Dart code, it is sometimes used for non-GC tasks that requires only this property. For example, when a background compilation has completed and wants to install its result, it uses a safepoint operation to ensure no Dart execution sees the intermediate states during installation.
The state of each thread is represented using `Thread::ExecutionState` enum. A thread is considered to be at a safepoint if its state is `kThreadInNative` (executing external native code) or `kThreadInBlockedState` (blocked on a lock). Conversely, a thread in `kThreadInVM` (executing C++ VM code) or `kThreadInGenerated` (executing compiled Dart code) is not at a safepoint. The VM relies on threads transitioning between these states carefully to ensure that a safepoint operation can begin only when all threads are in a safe state.
## Scavenge
See [Cheney's algorithm](https://en.wikipedia.org/wiki/Cheney's_algorithm).
+10 -6
View File
@@ -347,13 +347,17 @@ abstract final class NativeFinalizer {
/// Creates a finalizer with the given finalization callback.
///
/// The [callback] must be a native function which can be executed outside of
/// a Dart isolate. This means that passing an FFI trampoline (a function
/// pointer obtained via [Pointer.fromFunction]) is not supported.
/// a Dart isolate. This also means that passing an FFI trampoline (a function
/// a function pointer obtained via [Pointer.fromFunction]) is not supported.
///
/// The [callback] might be invoked on an arbitrary thread and not necessary
/// on the same thread that created [NativeFinalizer].
// TODO(https://dartbug.com/47778): Implement isolate independent code and
// update the above comment.
/// The callback is not allowed to re-enter the Dart VM via Dart C APIs, with
/// two exceptions: it is allowed to call `Dart_DeletePersistentHandle` and
/// `Dart_DeleteWeakPersistentHandle`. Calling any other Dart C API function
/// results in undefined behavior, which means it can cause anything from
/// crashes and deadlocks to silent memory corruptions.
///
/// The [callback] might be invoked on an arbitrary thread. It will have a
/// current isolate group but will not have a current isolate.
external factory NativeFinalizer(Pointer<NativeFinalizerFunction> callback);
/// Attaches this finalizer to [value].
@@ -0,0 +1,80 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// SharedObjects=ffi_test_functions
//
// VMOptions=--trace-finalizers
import 'dart:ffi';
import 'dylib_utils.dart';
import 'ffi_test_helpers.dart';
void main() {
testDeletePersistentHandleFromFinalizer();
testDeleteWeakPersistentHandleFromFinalizer();
print('Done.');
}
final ffiTestFunctions = dlopenPlatformSpecific('ffi_test_functions');
// C functions.
final newPersistentHandle = ffiTestFunctions
.lookupFunction<
Pointer<Void> Function(Handle),
Pointer<Void> Function(Object)
>('NewPersistentHandle');
final deletePersistentHandleFinalizer = ffiTestFunctions
.lookup<NativeFunction<Void Function(Pointer<Void>)>>(
'DeletePersistentHandleFinalizer',
);
final newWeakPersistentHandle = ffiTestFunctions
.lookupFunction<
Pointer<Void> Function(Handle),
Pointer<Void> Function(Object)
>('NewWeakPersistentHandle');
final deleteWeakPersistentHandleFinalizer = ffiTestFunctions
.lookup<NativeFunction<Void Function(Pointer<Void>)>>(
'DeleteWeakPersistentHandleFinalizer',
);
class MyClass {
final int a;
MyClass(this.a);
}
class MyFinalizable implements Finalizable {
final int a;
MyFinalizable(this.a);
}
void testDeletePersistentHandleFromFinalizer() {
final finalizer = NativeFinalizer(deletePersistentHandleFinalizer);
final objectToKeepAlive = MyClass(1);
final persistentHandle = newPersistentHandle(objectToKeepAlive);
var gcObject = MyFinalizable(2);
finalizer.attach(gcObject, persistentHandle, detach: gcObject);
// Lose the object, the finalizer should run.
gcObject = MyFinalizable(3);
doGC();
// Test passes if it does not crash.
}
void testDeleteWeakPersistentHandleFromFinalizer() {
final finalizer = NativeFinalizer(deleteWeakPersistentHandleFinalizer);
final objectToKeepAlive = MyClass(1);
final weakHandle = newWeakPersistentHandle(objectToKeepAlive);
var gcObject = MyFinalizable(2);
finalizer.attach(gcObject, weakHandle, detach: gcObject);
// Lose the object, the finalizer should run.
gcObject = MyFinalizable(3);
doGC();
// Test passes if it does not crash.
}
@@ -20,6 +20,8 @@ void main() async {
await testSendAndExitFinalizable();
await testSendAndExitFinalizer();
await testFinalizerRunsOnIsolateShutdown();
await testDeletePersistentHandleOnIsolateShutdown();
await testDeleteWeakPersistentHandleOnIsolateShutdown();
print('End of test, shutting down.');
}
@@ -48,6 +50,60 @@ Future<void> testFinalizerRunsOnIsolateShutdown() async {
});
}
void runIsolateDeletePersistentHandleOnShutdown(int objectAddress) {
final finalizer = NativeFinalizer(deletePersistentHandleFinalizer);
final persistentHandle = Pointer<Void>.fromAddress(objectAddress);
final objectToFinalize = MyFinalizableObject();
finalizer.attach(
objectToFinalize,
persistentHandle,
detach: objectToFinalize,
);
}
Future<void> testDeletePersistentHandleOnIsolateShutdown() async {
final objectToKeepAlive =
Object(); // Keep a strong reference to ensure the handle stays alive
final persistentHandle = newPersistentHandle(objectToKeepAlive);
final portExitMessage = ReceivePort();
await Isolate.spawn(
runIsolateDeletePersistentHandleOnShutdown,
persistentHandle.address,
onExit: portExitMessage.sendPort,
);
await portExitMessage.first; // Wait for the isolate to exit
doGC();
// The test passes if no crash occurred. We can't directly verify the handle deletion
// from outside the isolate, but the lack of a crash indicates success.
print('Persistent handle deletion test on shutdown completed cleanly.');
}
void runIsolateDeleteWeakPersistentHandleOnShutdown(int objectAddress) {
final finalizer = NativeFinalizer(deleteWeakPersistentHandleFinalizer);
final weakHandle = Pointer<Void>.fromAddress(objectAddress);
final objectToFinalize = MyFinalizableObject();
finalizer.attach(objectToFinalize, weakHandle, detach: objectToFinalize);
print('Isolate for weak persistent handle deletion done.');
}
Future<void> testDeleteWeakPersistentHandleOnIsolateShutdown() async {
final objectToKeepAlive =
Object(); // Keep a strong reference to ensure the handle stays alive
final weakHandle = newWeakPersistentHandle(objectToKeepAlive);
final portExitMessage = ReceivePort();
await Isolate.spawn(
runIsolateDeleteWeakPersistentHandleOnShutdown,
weakHandle.address,
onExit: portExitMessage.sendPort,
);
await portExitMessage.first; // Wait for the isolate to exit
doGC();
// The test passes if no crash occurred.
print('Weak persistent handle deletion test on shutdown completed cleanly.');
}
Future<void> testSendAndExitFinalizable() async {
final receivePort = ReceivePort();
await Isolate.spawn((SendPort sendPort) {
@@ -75,3 +131,27 @@ Future<void> testSendAndExitFinalizer() async {
final result = await receivePort.first;
Expect.contains("Invalid argument: is unsendable", result);
}
final newPersistentHandle = ffiTestFunctions
.lookupFunction<
Pointer<Void> Function(Handle),
Pointer<Void> Function(Object)
>('NewPersistentHandle');
final deletePersistentHandleFinalizer = ffiTestFunctions
.lookup<NativeFunction<Void Function(Pointer<Void>)>>(
'DeletePersistentHandleFinalizer',
);
final newWeakPersistentHandle = ffiTestFunctions
.lookupFunction<
Pointer<Void> Function(Handle),
Pointer<Void> Function(Object)
>('NewWeakPersistentHandle');
final deleteWeakPersistentHandleFinalizer = ffiTestFunctions
.lookup<NativeFunction<Void Function(Pointer<Void>)>>(
'DeleteWeakPersistentHandleFinalizer',
);
class MyFinalizableObject implements Finalizable {}