-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
[vm/ffi] Introduce pragma's 'vm:shareable' and 'vm:shared' and make NativeFinalizer
s live in an isolate group instead of isolate.
#55062
Comments
My suggestion to use deeply immutable objects and finalizers was about a specific use case. So let's look at this use case first: class JObject {
final _JGlobalRef ref;
}
class JFoo extends JObject {
void foo() {}
void baz() {}
}
void foo(JFoo foo) async {
await runOnPlatformThread(() {
foo.bar();
foo.baz();
});
} The thought is: If If we go this route we have to first think carefully about what this means:
@HosseinYousefi wdyt? If we do it like this, the next question is how to implement it.
|
Yes, I think this is the way forward. The concern is if we create this object in a helper isolate, send it to the main isolate, and then exit the helper isolate, the static native finalizer in the helper isolate eagerly gets called for all of the attached objects.
I didn't know about these. But they could work since they're isolate-group global. |
If
That's a great alternative. 👍 |
This |
When would we free that though? |
With another finalizable handle 😆 which would always be attached and always be managed by GC. @immutabale
class _JavaGlobalRefValue implements Finalizable {
final Pointer<Void> javaHandlePointer;
final Atomic<IntPtr> state = Atomic<IntPtr>(); // -1 if freed, 0 if unused, positive: number of users
}
@immutable
class _JGlobalRef {
static const kFreed = -1;
final _JGlobalRefValue javaHandle;
final Pointer<Void> dartFinalizableHandle;
_JGlobalRef(localHandle)
: javaHandle = _JavaGlobalRefValue(javaNewGlobalHandle(localHandle)),
dartFinalizableHandle = createFinalizableHandle(javaHandle, ...); // global handle freed by default by GC
void startUse() {
int currentValue = 0;
while (true) {
(currentValue, success) = javaHandle.state.compareAndSwap(currentValue, currentValue + 1);
if (success) return;
if (currentValue == kFreed) throw 'Global ref is no longer valid.';
}
}
void endUse() {
final oldValue = javaHandle.state.atomicDecrement();
assert(oldValue > 0);
}
void eagerFree() {
if (!javaHandle.state.compareAndSwap(0, kFreed)) throw 'Ref is currently in use (maybe by another thread?)';
deleteFinalizableHandle(dartFinalizableHandle, javaHandle);
javaDeleteGlobalRef(javaHandle.javaHandlePointer);
}
}
JObject foo(JFoo obj) {
obj.ref.startUse(); // if it doesn't throw, safe to use
try {
<perform jni call>
} finally {
obj.ref.endUse();
}
} Doing this would shift incorrectly eager freeing (user eagerly frees java global ref even though the same isolate or other isolates will use the global ref afterwards)
In the end for an app to work properly the code that does eager freeing must know what it's doing. |
👍 Yes, and then we don't allow eager free on As long as we haven't implemented @HosseinYousefi WDYT? I believe you should then be unblocked after https://dart-review.googlesource.com/c/sdk/+/354902 (still WIP at this point). |
NativeFinalizer
s live in an isolate group instead of isolate.NativeFinalizer
s live in an isolate group instead of isolate.
This CL introduces a way to mark all instances of a class as deeply immutable. In order to statically verify that all instances of a deeply immutable class are immutable, a deeply immutable classes must have the following properties: 1. All instance fields must 1. have a deeply immutable type, 2. be final, and 3. be non-late. 2. The class must be `final` or `sealed`. This ensures no non-deeply-immutable subtypes are added by external code. 3. All subtypes must be deeply immutable. This ensures 1.1 can be trusted. 4. The super type must be deeply immutable (except for Object). Note that instances of some classes in the VM are deeply immutable while their class cannot be marked immutable. * SendPort, Capability, RegExp, and StackTrace are not `final` and can be implemented by external code. * UnmodifiableTypedDataViews do not have a public type. (It was recently deprecated.) See runtime/docs/deeply_immutable.md for more details. Use case: This enables attaching a `Dart_FinalizableHandle` to a deeply immutable object and the deeply immutable object with other isolates in the same isolate group. (Note that `NativeFinalizer`s live in an isolate, and not an isolate group. So this should currently _not_ be used with `NativeFinalizer`s. See #55062 for making a `NativeFinalizer.shared(` that would live in an isolate group instead of in an isolate.) Implementation details: Before this CL, the `ImmutableBit` in the object header was only ever set to true for predefined class ids (and for const objects). After this CL, the bit can also be set to true for non const instances of user-defined classes. The object allocation and initialization code has been changed to deal with this new case. The immutability of a class is saved in the class state bits. On object allocation and initialization the immutability bit is read from the class for non-predefined class ids. TEST=runtime/tests/vm/dart/isolates/fast_object_copy2_test.dart TEST=runtime/vm/isolate_reload_test.cc TEST=tests/lib/isolate/deeply_immutable_* Bug: #55120 Bug: #54885 Change-Id: Ib97fe589cb4f81673cb928c93e3093838d82132d Cq-Include-Trybots: luci.dart.try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-appjit-linux-debug-x64-try,vm-asan-linux-release-x64-try,vm-checked-mac-release-arm64-try,vm-eager-optimization-linux-release-ia32-try,vm-eager-optimization-linux-release-x64-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64c-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-fuchsia-release-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-linux-debug-ia32-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-reload-rollback-linux-debug-x64-try,vm-ubsan-linux-release-x64-try Cq-Include-Trybots: dart-internal/g3.dart-internal.try:g3-cbuild-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/354902 Commit-Queue: Daco Harkes <dacoharkes@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
A test exercising isolate group finalizers with `Finalizable` and `pragma('vm:deeply-immutable')`. Note that we cannot fully do without C code due to the signature of `Dart_HandleFinalizer` which also passes a `void* isolate_callback_data` in addition to the peer. TEST=tests/ffi/deeply_immutable_c_api_finalizer_test.dart Bug: #55062 Bug: #55120 Change-Id: Ibaf4899ca678ffb0c5d227ac4f10deb38d49fe6f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/356720 Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Hossein Yousefi <yousefi@google.com> Commit-Queue: Daco Harkes <dacoharkes@google.com>
Maybe the implementation is easier to be done in C. All the JNI APIs would get a " |
My understanding of using Dart code to prevent use after free is the following:
@HosseinYousefi Are you suggesting we keep that structure, but we do the atomic counter increase/decrease all in the JNI calls? And still the Dart wrapper object ( Maybe what @mkustermann was trying to suggest here was to prevent the eager |
For
NativeFinalizer
s it would be really useful if they could live in an isolate group rather than in an isolate. #55050 (comment) (TL;DR: MigratingNativeFinalizer
s to the longest living isolate, or doing refcounting can both not be encapsulated. Artificially keeping allNativeFinalizer
objects alive until the isolate group dies leads to a memory leak.)In order to achieve
NativeFinalizer
s living in an isolate group instead of an isolated, we'd like to be able to mark static fields as shared across an isolate group.We introduce two pragmas:
pragma('vm:sharable')
Marks a type as being sharable, cannot be used by users, only by Dart SDK code. Dart SDK developers would need to make sure that all code reachable from such type is concurrency safe.pragma('vm:shared')
Marks a static field as shared across isolates in an isolate groups. Can be used by users. The type of the field must be markedpragma('vm:sharable')
, all its subtypes must be shareable too, and the class must be final or sealed.Users would then be able to write
Whichever isolate lives the longest would keep the
NativeFinalizer
alive. No need to migrate theNativeFinalizer
to the longest living isolate withIsolate.exit
#55050 (comment).We might need to introduce a
NativeFinalizer.shared(...)
constructor to signify that this finalizer is shared across isolates. OrNativeFinalizer.concurrent(...)
.@mraleph This is implementing a subset of dart-lang/language#3531. But not as a general language feature but for a single type in the VM for which this enables some use cases.
cc @mkustermann @HosseinYousefi
The text was updated successfully, but these errors were encountered: