-
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] NativeCallable.onTemporaryIsolate
#54530
Comments
This has become more relevant recently, as a way of eliminating ffigen's generated ObjC code. Small ObjC trampolines are generated to fix dart-lang/native#835, but if we had temp isolate callbacks we could write these trampolines in Dart. Are we happy with the |
I'm happy with the longer name, it's more clear. cc @lrhn for naming! Cross linking the other open issue for |
"Avoid abbreviations" is a style rule, so "temp" isn't stylistic. I'd drop "temporary" completely. Is the isolates spawned first, out spawned synchronously when trying to fix the callback? How will you prevent the call from starting async computations? It's presumably an isolate in the same isolate group, so you can't control access to Does the isolate do |
I wonder if those imply that it's an ordinary isolate with no limitations?
There's existing temporary isolate infrastructure in place for So I'd like to avoid making any promises about whether it's a fresh isolate or not, because we may want to change the implementation in future. |
Even if it's not fresh, we might want to not leak any state. (e.g. reset all global fields?) I can't find a pattern in our names yet:
So one name is behaves like or should be used as, and the other name is more about what it is. I guess sticking closer to what it is makes more sense. A native callable is run, so the kind of native callable is how it is run.
Maybe the name doesn't really matter and we just pick something and add good documentation. 🤷 |
|
That exhausts the sync/async behaviors, so About resetting globals, also needs to close all ports and clear the event queues, to make sure nothing survives. Then send |
I would strongly prefer we align semantics and naming with extensions we are exploring in context of shared memory multithreading proposal, so that we avoid introducing duplication and incurring additional migration / cognitive costs later. Specifically, this means going for a variation of Though I must admit that I don't like the name shared for callables because it is not descriptive enough - though I don't really see any good alternative. We need some word that describes callback which:
There is no single word which clearly describes these capabilities and limitations. |
Ah, this means it should be an error to access global fields completely in @mraleph Could we theoretically support having unawaited futures in such code? E.g. would it be possible to have a message queue for the isolate group? (The native callable itself must be synchronous though due to the return value. So only unawaited futures.)
I like |
So ideally we'd have a name that allows us to implement this now using the existing temporary isolate infra, but later upgrade to the shared isolate proposal when shared memory lands. How about |
That would be non-constant fields at least. Constants are probably fine. Good luck with that! 😉
You can't have futures at all. The first thing a I have no real hope that we can find a way to introduce such a restricted Dart isolate, and still be able to run actual Dart code in it. There are too many places where code assumes that it can cache things in static variable, either storing them or just lazily creating a value the first time its needed. |
How about
You can experiment with using shared memory for callback temp isolates now - it's available on dev/main under
The proposal talked about introducing shared-friendly versions of futures, streams, zones. But those won't mesh with normal futures if I understand correctly. |
These are great points. It sounds like if we want something like temporary isolate callbacks (before shared memory lands), we'll have to allow them to read and write static variables. There's no technical issue with that afaik, it would just be confusing for users if they expect those static variables to have the same value as they do on the original isolate, or to have persistent values between invocations. But that odd behavior would be preferable to not having access to all these core libraries, and adding static variables to an internal algorithm being a breaking change. Obviously we'd have to document it very clearly. We'll still have this issue with shared memory, because the static variables would have to be declared as shared. So unless we go through and annotate every internal static variable in the core libraries as shared (which would have performance implications), we'll still run into all these issues @lrhn points out. |
Yep, and that's intentional. If you want full Dart semantics you need to have a proper Dart isolate (with an event loop and such). Doing async in a temporary isolate is a foot gun anyway - we need to define how it works and there are multiple different ways it could work (e.g. you can decide to block until all events are drained and ports are closed or you could decide that execution goes to the default thread pool). If we implicitly default to either of these we will have surprising behavior. So I don't want us to be implicit, I want APIs to be explicit and restrictive. I have the same reservations about static state in general - I don't believe that behavior where you impliciltly get a fresh copy of the static state every time callback is called is any good. Outside of async we can audit and tweak core libraries in a way that unlocks using things which do not require event loop. If needed by using shared memory capabilities not exposed to the Dart developer. It is not that much different from how we canonicalize constants across multiple isolates - this memory is shared but a regular Dart developer can't achieve the same thing.
I strongly disagree with this. I do not believe this odd behavior would be preferable. I suggest to look at this from a different angle - we need to stop thinking about this as simply a Dart problem. It is an interop problem. When you write C code (which uses Dart VM C API) you can write callbacks which get executed on any thread but these callbacks can't touch the Dart world unless they explicitly enter a Dart isolate using It is a fundamental capability from which many other capabilities (e.g. |
"Full Dart semantics" is a very wide concept.
What worries me isn't the goal, but the details, where we draw the line between "non-isolate-compatible" code and "isolate-required" code. We can obviously disable particular features (anything async, like running in a zone that throws on any attempt to schedule, maybe even on trying to access Not accessing static state is harder, basically impossible. That includes all non-constant top-level/static variables. Let's assume we can somehow allow some of those, the ones that are really just caching a compute-on-demand result, lazy rather than mutable, so it won't matter if they're recomputed or not, and so it doesn't matter whether if they're reset on each call or not. It's indistinguishable, allowing us to reuse or reinitialize an isolate without changing any behavior. Then there is internal static state (like the list used for cycle-detection in Maybe we could have a standard practice of the core platform libraries not containing persistent (visibly) mutable state, so that it's never possible to see if you're running against a fresh version or an existing one. Don't know if we can extend that to other platform libraries too. Interop-libraries have external state they access, they usually don't need their own. (The We may be able to draw a line somewhere and ensure that most of the (non-async) platform libraries can work consistently in a restricted isolate, whether fresh or reused. I doubt we can make them work without access to statics at all. Then there are user libraries, which won't ever consider that they'll be used in a restricted setting. They'd be prone to breaking arbitrarily. TL;DR: I'm worried the boundary between what works in a restricted context, and what doesn't, will be semi-arbitrary and unstable over time. It'll require real work to ensure that just the core platform libraries can work consistently, if at all. |
We might never build this, but let's have a tracking bug to point to.
https://api.dart.dev/stable/3.2.4/dart-ffi/NativeCallable/NativeCallable.listener.html enables async callbacks that return immediately upon calling and schedule the callback to be run on the Dart event loop of the right isolate. No return values can be returned.
https://api.dart.dev/stable/3.2.4/dart-ffi/NativeCallable/NativeCallable.isolateLocal.html enables sync callbacks with return values. The current thread must be the one entered in the target isolate.
If we would like to be able to run Dart code synchronously and get a return value, but we don't have access to a Dart isolate in the current thread, we could conceivably spawn a new temporary isolate.
This would incur some serious limitations:
We're currently not exploring this. @liamappelbe
A way more general approach would be to have shared memory multithreading:
The text was updated successfully, but these errors were encountered: