-
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] Support asynchronous callbacks #37022
Comments
Asynchronous callbacks can be invoked on an arbitrary thread. Synchronous callbacks cannot be invoked on an arbitrary thread because it would break the Dart language semantics: only one thread executing Dart per isolate. Note that we need a solution for managing isolate lifetime when registering an asynchronous callback to be called from any thread in C. If the isolate died, and the callback is called, we have undefined behavior. A possible solution suggested by @mkustermann is to provide call-once callbacks, and keep the isolate alive until they are all called exactly once. Or we could have a counter or an arbitrary condition that we can check. |
For inspiration, Kotlin requires the C library to be aware of Kotlin when doing non-main-thread-callbacks:
Source: https://github.com/JetBrains/kotlin-native/blob/master/INTEROP.md#callbacks |
It's not the thread which the callback is tied to, it's the isolate. So our callbacks may be invoked on any thread so long as that thread has called |
I presume |
Another solution to managing lifetime is that the asynchronous callbacks need to be explicitly de-registered, and the isolate lives until all callbacks are de-registered. Another challenge is knowing which |
Does it mean that calling back into Dart works as long as Dart_EnterIsolate is called before invoking the callback? How does the native code know that? |
We have not made a design for this feature yet, we'll explore the possibilities when creating a design. |
Okay, we have a workaround for it:
|
@derolf nice! Would you like to provide a minimal sample and contribute it as a PR for others to learn from for the time being? |
It didn’t work so far because Flutter itself is using SIGUSR1/2 for hot reloading. However, I saw that a NativePort can be sent through FFI to the c-layer. Do you have any example how to send something to that nativePort from C? |
@derolf, to my understanding the NativePort solution only works if you own the Dart embedder yourself. cc @mkustermann Linking flutter/flutter#46887. |
Okay, I implemented a way that works reliable and clean. The queue: On the native side, you need a threadsafe queue that allows to: The dance: Now, you spawn a slave isolate that waits on the queue and then sends a message to the main isolate, and waits again, ... (forever loop) The main isolate receives this message and calls into ffi to execute all pending callbacks. If some thread wants to dispatch a callback, you enqueue it instead of calling it. This will wakeup the slave isolate and that will send the said message to main and main will deliver the callback. That's it. Less than 100 LOC to do it all. |
That is slightly problematic in an event-loop based system. The synchronous blocking will prevent the isolate from processing any other messages (timers, socket i/o, ...). We'll write an example on how this can be done already now with our |
@mkustermann The problem is that none of the The "slave" isolate's one and only job is to do this waiting on the queue. It's doing nothing else. Here's its code (ripped out of the codebase): class _SlaveIsolateMessage {
_SlaveIsolateMessage(this.port, this.isolate);
final SendPort port;
final int isolate;
}
void _slaveIsolate(_SlaveIsolateMessage msg) {
print("_slaveIsolate running for ${msg.isolate}");
while (_dart_ffi_wait_for_callbacks(msg.isolate) != 0) {
print("_slaveIsolate has callbacks for ${msg.isolate}");
msg.port.send(1);
}
print("_slaveIsolate done for ${msg.isolate}");
msg.port.send(0);
}
final _dart_ffi_wait_for_callbacks = dylib.lookupFunction<Int32 Function(Int32), int Function(int)>('dart_ffi_wait_for_callbacks'); The |
Issue: #37022 (comment) Change-Id: If30d168e6666131b6d96d5885a0dbe32291b1ef9 Cq-Include-Trybots: luci.dart.try:vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64-try,app-kernel-linux-debug-x64-try,vm-kernel-linux-debug-ia32-try,vm-kernel-win-debug-x64-try,vm-kernel-win-debug-ia32-try,vm-kernel-precomp-linux-debug-x64-try,vm-dartkb-linux-release-x64-abi-try,vm-kernel-precomp-android-release-arm64-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-linux-release-simarm-try,vm-kernel-linux-release-simarm64-try,vm-kernel-precomp-android-release-arm_x64-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,dart-sdk-linux-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,front-end-linux-release-x64-try,vm-kernel-precomp-win-release-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/134704 Reviewed-by: Martin Kustermann <[email protected]>
Issue: #37022 (comment) Change-Id: I774befa1d9843c043883038e59c0f8b629bf3c77 Cq-Include-Trybots: luci.dart.try:vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64-try,app-kernel-linux-debug-x64-try,vm-kernel-linux-debug-ia32-try,vm-kernel-win-debug-x64-try,vm-kernel-win-debug-ia32-try,vm-kernel-precomp-linux-debug-x64-try,vm-dartkb-linux-release-x64-abi-try,vm-kernel-precomp-android-release-arm64-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-linux-release-simarm-try,vm-kernel-linux-release-simarm64-try,vm-kernel-precomp-android-release-arm_x64-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,dart-sdk-linux-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,front-end-linux-release-x64-try,vm-kernel-precomp-win-release-x64-try,vm-kernel-mac-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/134822 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
The samples have landed, see referenced commits. |
Some status of when this will be available, I saw that the Realm team is just waiting for this feature to be able to launch a Realm for Flutter |
Asynchronous callbacks can be today done through the native ports mechanism as illustrated in samples/ffi/async. In the future we would like to add a more concise syntax that requires less boilerplate code. |
As I understand we still depends from APIs declared by dart_native_api.h on the native side so this technique still does not usable with flutter. |
The @katyo Would that work for you? |
@mkustermann Can you give a small example with the method you're mentioning? I need this for my app, because it's basically a deal breaker and I don't exactly understand how your method should work in code. |
That would be very interesting, and I think it has a lot to do with what I requested here. dart-lang/language#1758 I think having a strict mode in dart is somewhat close to C# unsafe which allows running parts of code outside the garbage collector and doing raw pointer operations with a better syntax https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/unsafe I think it would be very positive to have this strict mode in dart to be able to optimize parts of a code and maybe it would also open up the possibility of having multithreading dart-lang/language#333 in this strict mode, that would be fantastic to not need to leave dart to do certain things, without needing the complexity having to rely on another language or cross-platform software development stack with rust or c++ |
How would I keep track of which Dart object my callback was bound to? Can I use a closure? |
@Keithcat1 At the moment you can only create FFI callbacks from static functions, but we're planning to add closure support soon. The infrastructure we needed to build for async callbacks will also let us support closures: #52689 |
@dcharkes As shown in the diagram above, |
Asynchronous callbacks with void returns can now be done with |
It seems these aren't usable via |
You seem to be asking the question in the opposite direction. So just to clarify you have a Dart function that returns a |
Exactly what you suggested - calling an async Dart function from native code and either treating the result as synchronous, or at least some other way of propagating a result back to native code that's compatible with wasm. My first attempt was using native Dart ports, but these seem not to be supported under wasm. e.g. something like the following: native_code.c (compiled to wasm)
Dart
|
@nmfisher well, that's not going to work. Consider the following: to call asynchronous function synchronously you need to pause Wasm execution until async function completes then resume it later. What you can do is to restructure your native code to use callbacks and thread some additional context through them manually:
|
Thanks @mraleph - that's the approach I've been using so far, I was just having issues with concurrency on the native side when using dart2js and then function pointers when using dart2wasm, so I was wondering if there was a Dart-first approach that was better. I'll keep plugging away and see if I can get it to work as expected. |
Actually I suppose this is a good a place as any to ask - is it even currently possible to use e.g.
fails with:
|
This issue is currently tracking all kinds of asynchronous use cases. The basic use case of an asynchronous call is now supported via https://api.dart.dev/stable/3.2.4/dart-ffi/NativeCallable/NativeCallable.listener.html. Further work for other use cases is tracked in more specific issues:
(Side note: all of the above use cases are synchronous from the native code point of view, but asynchronous from the spawning Dart isolate point of view.) I'll close this issue. Please open a new issue if your use case is not covered by one of the remaining open issues. Thanks @liamappelbe for implementing |
Can you use NativeCallable for system audio API callbacks or miniaudio libraries like PortAudio? |
I doubt it. AFAIK NativeCallable won't wait for Dart to run your callback on the main thread, so from Miniaudio's point of view, you it assumes that since the callback has already finished, you aren't using the data anymore and it is free to do something else with it. NativeCallable will give you pointers to Miniaudio buffers, but not whatever data was in those buffers (the buffers can change before you get a chance to read it!). The Miniaudio device event APIs would work, though and Miniaudio Osaudio (miniaudio/extras/osaudio) can be used to record or play audio data without using threads, it'll just block until the ammount you asked for is ready. |
It's a little sad and discouraging that we still don't have a pure Dart solution for Audio API callbacks, Dart ffi won't be complete without this, the team behind Dart is very talented and I hope that a solution will be implemented in the future |
I'm assuming the callbacks (1) come from another thread than the mutator thread of the isolate, and (2) need to be blocking and return an answer. If this is the case one of the following:
The first one is being discussed. The second one is currently on hold because the first is a more general solution.
The Dart concurrency system (isolates) is designed to make isolated code easy to reason about. However, that doesn't align so well with interoperating with languages which do shared memory multi-threading. We're aware of this and trying to find a good solution. It might take a while to find a good solution, so please be patient. And if you're interested feel free to contribute to the discussion about shared memory multithreading. (P.S. With shared memory multi threading we're going to have things like ConcurrentModificationException etc. 😅 There's a reason why Dart doesn't have shared memory multi-threading until now.) |
Thanks @dcharkes for the pointer to the new proposal in the Shared Memory Multithreading PR, very exciting to see it being worked on! And yes you are correct, most audio APIs seem to follow the model of application code needing to register a callback with the audio system, that then gets called back by a thread (which thread is determined by the audio system) which passes in a pointer to a buffer which it expects to be filled in with required number of audio samples by the time that callback function returns. So from that proposal PRs doc, this:
is exactly the case for audio APIs. @dcharkes is commenting on that markdown doc in the PR the best way to contribute to the discussion? |
Yes! |
Hi can you please help me to resolved this issue "cannot invoke a native callback outside of an isolate" Now what happened in the C++ DLL file. Internally, they started a separate thread with a callback. so how to handle this issue.https://stackoverflow.com/questions/78404125/cannot-invoke-a-native-callback-outside-of-an-isolate?noredirect=1#comment138225228_78404125 |
@sanjaygholap That error message means you're using a sync callback (either |
same issue "Cannot invoke a native callback outside of an isolate".Within the C++ DLL file. A separate thread with a callback was started internally. |
@sanjaygholap It's not possible to hit that error message unless you're using a sync callback. Async callbacks follow this code path instead, which always returns before that error. Async callbacks are specifically designed to work from any thread, so they should work even if your DLL spawned a new thread. Double check that your callback is a If you're sure that you have a |
@liamappelbe sir, we get same issue "Cannot invoke a native callback outside of an isolate".Within the C++ DLL file. A separate thread with a callback was started internally. |
Update Sept 6, 2023: Asynchronous callbacks with void returns can now be done with
NativeCallable.listener
from Dart 3.1.Update March 12, 2020: Asynchronous callbacks can be done through the native ports mechanism. See the samples in samples/ffi/async and corresponding c code. In the future we would like to add a more concise syntax that requires less boilerplate code.
An asynchronous callback can be invoked outside the parent isolate and schedules a microtask there.
The text was updated successfully, but these errors were encountered: