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

[vm/ffi] Support asynchronous callbacks #37022

Open
sjindel-google opened this issue May 20, 2019 · 98 comments
Open

[vm/ffi] Support asynchronous callbacks #37022

sjindel-google opened this issue May 20, 2019 · 98 comments

Comments

@sjindel-google
Copy link
Contributor

sjindel-google commented May 20, 2019

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.

@sjindel-google sjindel-google added this to 1.0 in Dart VM FFI via automation May 20, 2019
@sjindel-google sjindel-google moved this from 1.0 to 1.1 in Dart VM FFI Jun 11, 2019
@dcharkes
Copy link
Contributor

dcharkes commented Sep 20, 2019

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.

@dcharkes
Copy link
Contributor

dcharkes commented Sep 20, 2019

For inspiration, Kotlin requires the C library to be aware of Kotlin when doing non-main-thread-callbacks:

If the callback doesn't run in the main thread, it is mandatory to init the Kotlin/Native runtime by calling kotlin.native.initRuntimeIfNeeded().

Source: https://github.com/JetBrains/kotlin-native/blob/master/INTEROP.md#callbacks

@sjindel-google
Copy link
Contributor Author

sjindel-google commented Sep 20, 2019

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 Dart_EnterIsolate.

@dcharkes
Copy link
Contributor

dcharkes commented Sep 20, 2019

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 Dart_EnterIsolate.

I presume Dart_EnterIsolate also (could) prevent isolates from being shutdown or killed.

@sjindel-google
Copy link
Contributor Author

sjindel-google commented Sep 23, 2019

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 Isolate the callback should run on. In AOT we have only one trampoline per target method, and there may be indefinitely many isolates from the same source at runtime.

@derolf
Copy link

derolf commented Dec 11, 2019

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?

@dcharkes
Copy link
Contributor

dcharkes commented Dec 11, 2019

We have not made a design for this feature yet, we'll explore the possibilities when creating a design.

@derolf
Copy link

derolf commented Dec 11, 2019

Okay, we have a workaround for it:

  • Native: Thread X wants to run callback
  • Native: Thread X adds the callback to a global list of pending callbacks
  • Native: Thread X send sigusr1 to the process
  • Dart: Main Isolate watches sigusr1 (using ProcessSignal.watch) and gets woken up
  • Dart: Main Isolate calls via FFI into a function that executes all pending callbacks

@dcharkes
Copy link
Contributor

dcharkes commented Dec 11, 2019

@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?

@derolf
Copy link

derolf commented Dec 12, 2019

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?

@dcharkes
Copy link
Contributor

dcharkes commented Dec 13, 2019

@derolf, to my understanding the NativePort solution only works if you own the Dart embedder yourself.

cc @mkustermann

Linking flutter/flutter#46887.

@derolf
Copy link

derolf commented Dec 18, 2019

Okay, I implemented a way that works reliable and clean.

The queue:

On the native side, you need a threadsafe queue that allows to:
-- Enqueue callbacks from some thread that you want to execute in the main isolate (enqueue)
-- Have a function exposed through ffi that blocks until there are callbacks in the queue (wait)
-- Have a function exposed through ffi that executes pending callbacks (execute)

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.

@mkustermann
Copy link
Member

mkustermann commented Dec 18, 2019

Have a function exposed through ffi that blocks until there are callbacks in the queue (wait)

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 dart_native_api.h and ports.

@derolf
Copy link

derolf commented Dec 18, 2019

@mkustermann The problem is that none of the dart_native_api.h functions are available/exported in Flutter, so I can't use them. Happy to see your example how you make that work!

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 isolate int used to distinguish different native callback queues as we plan to have more than one "main" isolate.

dart-bot pushed a commit that referenced this issue Feb 18, 2020
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 <kustermann@google.com>
dart-bot pushed a commit that referenced this issue Feb 18, 2020
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 <dacoharkes@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
@dcharkes
Copy link
Contributor

dcharkes commented Feb 19, 2020

The samples have landed, see referenced commits.

@insinfo
Copy link

insinfo commented Mar 12, 2020

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

@dcharkes
Copy link
Contributor

dcharkes commented Mar 12, 2020

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.

@katyo
Copy link

katyo commented Apr 11, 2020

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.

@mkustermann
Copy link
Member

mkustermann commented Apr 11, 2020

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 dart:ffi library exposes the native api symbols now to Dart code via NativeApi.postCObject (as well as NativeApi.closeNativePort, NativeApi.newNativePort). Dart code can make a ReceivePort, obtain it's native port via receivePort.sendPort.nativePort, give that native port as well as the NativeApi.postCObject function pointer to C code. The C code can then post messages on the port, which Dart code can react on.

@katyo Would that work for you?

@jld3103
Copy link

jld3103 commented Apr 12, 2020

@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.
Thanks in advance

@vladvoina
Copy link

vladvoina commented Oct 25, 2021

Can someone provide some help on how to include and link dart_api_dl.h and dart_api_dl.c? Do I only need to worry that my library can see dart_api_dl.h (maybe added as header search paths in my C++ library project)?

@blaugold
Copy link
Contributor

blaugold commented Oct 25, 2021

@vladvoina The Dart SDK contains a folder include. Add that to the header search paths. You also need to add include/dart_api_dl.c to the list of files to compile and link into your app.

@Sunbreak
Copy link

Sunbreak commented Oct 25, 2021

Can someone provide some help on how to include and link dart_api_dl.h and dart_api_dl.c? Do I only need to worry that my library can see dart_api_dl.h (maybe added as header search paths in my C++ library project)?

For simplicity on Flutter Android/iOS: https://github.com/Sunbreak/native_interop.tour/blob/3d59a430c4b2f3caf5988486e01b7fa062666859/android/CMakeLists.txt#L3

add_library(native_interop
            # Sets the library as a shared library.
            SHARED
            # Provides a relative path to your source file(s).
            ../ios/Classes/dart_api/dart_api.h
            ../ios/Classes/dart_api/dart_api_dl.h
            ../ios/Classes/dart_api/dart_native_api.h
            ../ios/Classes/dart_api/dart_version.h
            ../ios/Classes/dart_api/internal/dart_api_dl_impl.h
            ../ios/Classes/dart_api/dart_api_dl.c
            ../ios/Classes/native_add.c
            ../ios/Classes/native_sync_callback.cpp
            ../ios/Classes/native_async_callback.cpp)

For cross-platform on Flutter Moblie/Desktop: https://github.com/Sunbreak/cronet_flutter/blob/master/android/CMakeLists.txt#L10-L22

add_library(${PLUGIN_NAME} SHARED
  "../common/dart_api/dart_api_dl.c"
  "../common/native_interop.cpp"
  "../common/sample_url_request_callback.cc"
  "../common/sample_executor.cc"
)

target_include_directories(${PLUGIN_NAME} INTERFACE
  "../common"
  "../common/dart_api"
  "../common/dart_api/internal"
  "../common/cronet"
)

Official package: https://github.com/google/cronet.dart
Official article: https://medium.com/dartlang/google-summer-of-code-2021-results-e514cce50fc
Author article: https://unsuitable001.medium.com/package-cronet-an-http-dart-flutter-package-with-dart-ffi-84f9b69c8a24

@vladvoina
Copy link

vladvoina commented Oct 26, 2021

Thanks, that worked! (although I decided to keep a copy of the flutter sdk into the repo to avoid having to surgically copy the dart_api files)

That said, I am getting This function declaration in not a prototype warnings when compiling the app from XCode. Seems to come from function signatures with no params like DART_EXPORT void Dart_ShutdownIsolate();. Is it only happening to me?

@Sunbreak
Copy link

Sunbreak commented Oct 26, 2021

Thanks, that worked! (although I decided to keep a copy of the flutter sdk into the repo to avoid having to surgically copy the dart_api files)

That said, I am getting 'This function declaration in not a prototypewarnings when compiling the app from XCode. Seems to come from function signatures with no params like DART_EXPORT void Dart_ShutdownIsolate();`. Is it only happening to me?

File another issue please. You could try https://github.com/google/cronet.dart or https://github.com/Sunbreak/cronet_flutter

@dvc94ch
Copy link

dvc94ch commented Nov 26, 2021

@Sunbreak: is the libwrapper.so something that could be abstracted into a reusable component? So you register your dart closure with libwrapper which returns a function pointer which is passed to the native library?

@Sunbreak
Copy link

Sunbreak commented Nov 26, 2021

@Sunbreak: is the libwrapper.so something that could be abstracted into a reusable component? So you register your dart closure with libwrapper which returns a function pointer which is passed to the native library?

It is possible I think. According to https://cloud.tencent.com/developer/article/1881387:

为了解决以上这些问题,我们希望能够更加方便地调用c++的方法,因此参考grpc/trpc 实现了一套dart::ffi的简单的rpc。在引入这套rpc工具后,对开发效率有显著的提升。在proto上定义dart调用c++的接口,数据结构统一为proto,c++层引入rpc的部分能力,dart层也引入相应的stub,我们去掉rpc的通信机制,改为dart::ffi来进行client和server的通信,只在c++层引入至关重要的服务发现与服务调用。整体的架构如下:

Another way is compile Dart within C++/ObjC: https://github.com/dart-native/dart_native

@Keithcat1
Copy link

Keithcat1 commented Dec 18, 2021

Why is it a problem that an asynchronous Dart FFI callback could mutate global state? Any C FFI is either unsafe or rather limited. Go allows C to invoke callbacks on another thread, even though Go has a multithreaded garbage collector. I have a feeling that trying to wrap low latency audio libraries like Miniaudio with a C callback that uses a send port to let the main Dart isolate would add some latency. Having to ship extra dynamic libraries in order to wrap FFI callbacks along with my application is also something I'd like to avoid if at all possible.

@blaugold
Copy link
Contributor

blaugold commented Dec 19, 2021

Here’s a proposal for an API for async callbacks, that I think is generally useful.

/// A callback, that allows native code to call a static Dart function
/// asynchronously.
///
/// Any callback, for which [close] has not been called, will keep the
/// [Isolate] that created it alive.
abstract class AsyncCallback<T extends Function> {
  /// Creates a callback, that allows native code to call a Dart function
  /// asynchronously and blocks the native caller until the Dart function
  /// returns.
  ///
  /// If an exception is thrown while calling [f], the native function will
  /// return [exceptionalReturn], which must be assignable to return type of
  /// [f].
  AsyncCallback(
    @DartRepresentationOf("T") Function f, [
    Object? exceptionalReturn,
  ]);

  /// Creates a callback, that allows native code to call a Dart function
  /// asynchronously and in a non-blocking way.
  ///
  /// The native caller needs to ensure, that the arguments it passes to
  /// [nativeFunction] stay valid until the Dart function returns, and must
  /// not rely on the side effects of the Dart function.
  ///
  /// The [nativeFunction] always returns [nonBlockingReturn], which must be
  /// assignable to the return type of [f]. The return value of the Dart
  /// function is ignored.
  AsyncCallback.nonBlocking(
    @DartRepresentationOf("T") Function f, [
    Object? nonBlockingReturn,
  ]);

  /// C function pointer to a C function, that will call the Dart function.
  ///
  /// The C function can be called from any thread.
  ///
  /// Calling the C function is undefined behavior, after [close] has been
  /// called or the [Isolate] that created the callback has died.
  Pointer<NativeFunction<T>> get nativeFunction;

  /// Closes this callback.
  ///
  /// After this method has been called, [nativeFunction] must not be called
  /// anymore.
  ///
  /// A callback, that has not been closed, will keep the [Isolate] that created
  /// it alive.
  void close();
}

Non-blocking AsyncCallbacks can be useful, if the native caller just notifies of some event or delivers a message and blocking it is detrimental to performance.

It would be useful for non-blocking AsyncCallbacks to allow CObjects in the arguments of the callback.

With this proposal, it would be the responsibility of the Dart code, that created an AsyncCallback, to close it and ensure native code stops calling it, before that point.

It does not specify what happens when multiple AsyncCallbacks are created with the same static Dart function.

@dcharkes
Copy link
Contributor

dcharkes commented Dec 21, 2021

I was thinking of just adding named arguments bool async, bool blocking to fromFunction rather than introducing a new API.

Can you elaborate on your reasoning for a separate API?

It would be useful for non-blocking AsyncCallbacks to allow CObjects in the arguments of the callback.

Why use CObjects instead of Dart_Handle in native and Object in Dart? That is how we do it for all the other FFI calls and callbacks. (The fact that it it could use native ports as an implementation mechanism should not leak out.)

With this proposal, it would be the responsibility of the Dart code, that created an AsyncCallback, to close it and ensure native code stops calling it, before that point.

I would give async callbacks the same semantics as synchronous callbacks for now: once allocated they stay alive until the isolate terminates. We should indeed also add an API at some point that can reclaim the memory used for the callbacks. It would indeed be the responsibility of the native code not to call those function pointers anymore after that.

@blaugold
Copy link
Contributor

blaugold commented Dec 21, 2021

Can you elaborate on your reasoning for a separate API?

I was thinking of an AsyncCallback as something similar to a ReceivePort, which prevents the isolate from exiting while it is not closed. If an isolate does nothing else but create an async callback, pass it to native code and wait for the callback to be called, what would stop the isolate from exiting? The isolate could schedule a timer far in the future and cancel it when it's done receiving callbacks, but that seems a bit like a hack.

Why use CObjects instead of Dart_Handle in native and Object in Dart? That is how we do it for all the other FFI calls and callbacks. (The fact that it it could use native ports as an implementation mechanism should not leak out.)

I was thinking about how a non-blocking caller would be able to pass data that needs to be allocated to an async callback.

Let's take a log callback as an example. It doesn't have to be blocking because the caller just wants to hand a message and maybe a few other values over to Dart code. When native code passes a Dart_CObject to Dart_PostCObject, it is deeply copied and does not need to remain allocated, even though Dart code receives that value asynchronously.

The native Dart API that is currently available through dart_api_dl.h, doesn't allow native code to build a Dart_Handle to a String or List, for example.


BTW, looking forward to acdf82d being available in one of the release channels.
Looks great.

@dcharkes
Copy link
Contributor

dcharkes commented Dec 21, 2021

which prevents the isolate from exiting while it is not closed

It would by default not prevents isolates from exiting. Because without a cancel API the isolates would never exit. Also, it's easy to miss.

Instead, I would suggest opening another port from your code, and sending an exit message to that on the last callback from native code, to prevent the isolate from exiting until native code is done with it.

You could implement AsyncCallback as a user like this. (Well, except the fact that we're not supporting generics in FFI callback signatures, so not fully.)

The lack of support for String (#39787) and Lists is a separate issue in my opinion. We would also like to support these for FFI calls and synchronous FFI callbacks, they are not async callback specific. (Side note: For creating a list in native code, we can do callbacks with handles to build the list. Both the dart_api and FFI callbacks do transitions from native code in the VM, so the performance should be better with FFI as it is not reflective. Side note 2: Same for creating a Dart string, we can do a callback that passes in a Pointer, and that gets the string as a return value in a handle.)

@blaugold
Copy link
Contributor

blaugold commented Dec 21, 2021

Thanks for the explanation. Makes sense to keep the API simple and consistent.

@mraleph
Copy link
Member

mraleph commented Jan 3, 2022

I have come to realise that #47778 might also provide a way to tackle this issue to a degree. Isolate independent functions can be invoked on any thread and then could communicate back to the (isolate dependent) Dart world via ports. I will incorporate these considerations on the initial design.

@dvc94ch
Copy link

dvc94ch commented Jan 3, 2022

Cool. Does this mean the flutter engine can be just a regular dynamic library once this issue gets resolved?

@maks
Copy link

maks commented May 5, 2022

@mraleph Having any kind of better support for callbacks in FFI would be a big win. I find that dealing with callbacks from C by far the most difficult thing and roadblock to using FFI with many C libraries. I completely understand that there is the restriction due to the 1 thread per Isolate at a time rule, but that doesn't make life any easier when using FFI.

Looking at the Finalizer proposal doc, it looks great for non-Isolate code, but could it for instance be expanded to something that would work with Isolates? I was thinking maybe it could be expanded by having functionality that creates a new Isolate per each native callback into Dart, thus ensuring that it meets the re-entrancy restriction? With the recent improvements with Isoalate Groups when it comes to Isolate creation performance and mem overhead, could this be a way forward? I guess the main difficulty with "create a new Isolate per callback" approach would be how to wire up ports to existing Isolates to make these automatically created Isolates actually useful, but perhaps thats a more tractable problem and I would think is better than that current workaround of needing us Dart developers to deal with ports/Isolates in the native side code.

@mraleph
Copy link
Member

mraleph commented May 5, 2022

@maks If the APIs you are finding challenging to bound to are public please share some details of what you are trying to achieve and where it is failing.

I was thinking maybe it could be expanded by having functionality that creates a new Isolate per each native callback into Dart, thus ensuring that it meets the re-entrancy restriction?

I have thought about this approach before but it raises a question: what are you going to do in that newly created isolate? It is not going to share any memory with another isolate so callback essentially has to be stateless. And if it is stateless then isolate independent code is probably a good enough answer. Alternatively if you are planning to use that freshly created isolate just as a jumping point to another isolate - by sending some data through SendPort then having automatic way to marshal asynchronous invocations to another isolate is good enough answer as well.

@Keithcat1
Copy link

Keithcat1 commented May 5, 2022

The FFI finalizers proposal mentioned possibly allowing Dart code to be compiled into native callbacks so long as it only invokes C functions. This would probably allow sending messages to the main isolate to run the callback there without having to compile a separate DLL alongside your app.

@maks
Copy link

maks commented May 5, 2022

@mraleph good points! What I had in mind was really just moving the current workarounds use of SendPorts into the Dart side vs having to do it in native code. I don't quite follow what you mean by:

having automatic way to marshal asynchronous invocations to another isolate

as reading the finalizers proposal, it talked about running outside any Isolate and only allowing Dart code that could only call FFI code and not reference any other Dart code.

What I had in mind was some sort of mechanism to allow newly created Isolates to be able to get hold of a SendPort from another existing Isolate. Some sort of registry perhaps, so that other Isolates could register SendPorts there and then those SendPorts would be passed in via the spawn() that got called to create these "automatic" Isolates for native callbacks.
For example

// this could be the main Isolate
final receivePort = ReceivePort();
portRegistry.add("sendMeNativeStuff",  receivePort.sendPort);

and then when the "automatic" Isolate is created:

Isolate.spawn(SendPort registeredPort) {
  registeredPort.send(thisIsSomeDataFromNative)
}

I'm not sure if I've explained, so hope the above makes sense?

In regards to which libraries I've run into problems with: its basically any library that has a async callback api. The first one I ran into a while back was libfuse. Since then I've also found that most audio playback libraries use callbacks as well, eg. miniaudio and my initial attempt at a binding.

@mraleph
Copy link
Member

mraleph commented May 6, 2022

I don't quite follow what you mean by:

having automatic way to marshal asynchronous invocations to another isolate

I mean we could provide a way to write code like this:

// Create callback from a function [func] which supports being called 
// from a thread which does not have a Dart isolate associated with it.
// If such situation occurs it will instead send a message back to this
// isolate and wait for a response.
final cb = Pointer.fromFunction<Int32 Function(Int32)>(func, detached: true, synchronous: true);

It's not going to work well though for situations which require fast callbacks.

In this cases isolate independent code is likely a better answer. @dcharkes is working on some initial prototypes right now.

@roc2539
Copy link

roc2539 commented Jul 5, 2022

My Windows dynamic library is a third party that cannot be modified. Is there a better way to support it? thanks
Is there a better way to support it?

@dcharkes
Copy link
Contributor

dcharkes commented Jul 5, 2022

My Windows dynamic library is a third party that cannot be modified. Is there a better way to support it? thanks
Is there a better way to support it?

You can create a new dynamic library that loads the third party library.

The new dynamic library would provide the callback for the third party library and use native ports to call back to Dart.

@kenfred
Copy link

kenfred commented Aug 20, 2022

@dcharkes I have followed the code in samples/ffi/async. Calls to Dart_PostCObject_DL return true, but I never get an event on the dart side that is listening to the ReceivePort.

Do you have any tips on how to debug this?

Edit
I found my issue. Here are some details in case it may help someone in the future..

Here is the basic mechanism from the sample code referenced above:

sequenceDiagram
    participant D as Dart<br>Dart Main Thread
    participant N as Native<br>Dart Main Thread
    participant NT as Native<br> Some Thread
    
    D->>D: Lookup native symbols:<br>initDartApiDL = "InitDartApiDL"<br>executeFunc = "ExecuteFunc"
    D->>N: initDartApiDL
    N->>N: Dart_InitializeApiDL
    D->>D: Create ReceivePort and listen
    activate D
    D->>N: Send SendPort and<br>Dart callback function pointer
    N->>N: Store SendPort and callback for later
    loop Event loop while program is running
        Note over NT: Some event happens that<br>makes you want to invoke Dart callback
        NT->>NT: Create opaque "work" functor<br>(containing Dart callback)<br>Post the functor with Dart_PostCObject_DL
        NT-->>D: Asynchronous exchange of "work" from SendPort to Receive Port
        D->>N: executeFunc<br>(delegating execution of<br>callback to Native on main thread)
        N->>N: reveal "work" to get the<br>Dart callback
        N->>D: callback
    end
    deactivate D

In my case, the event was triggered from Dart and it blocked waiting for the callback to execute. This caused a deadlock and explains why the ReceivePort listen never seemed to receive the work.

sequenceDiagram
    participant D as Dart<br>Dart Main Thread
    participant N as Native<br>Dart Main Thread
    
    D->>D: Lookup native symbols:<br>initDartApiDL = "InitDartApiDL"<br>executeFunc = "ExecuteFunc"
    D->>N: initDartApiDL
    N->>N: Dart_InitializeApiDL
    D->>D: Create ReceivePort and listen
    activate D
    D->>N: Send SendPort and<br>Dart callback function pointer
    N->>N: Store SendPort and callback for later
    loop Event loop while program is running
        D->>N: Some function call (blocking)
        N->>N: Handling function call<br>wants to callback to Dart and is<br>blocking for a result
        N->>N: Create opaque "work" functor<br>(containing Dart callback)<br>Post the functor with Dart_PostCObject_DL
        N-->>D: Asynchronous exchange of "work" from SendPort to Receive Port
        Note over D: Deadlock here!<br>The original Dart function that triggered the event<br>and the native code handling the event are blocking.<br>So the main thread can't service the ReceivePort.
        D->>N: executeFunc<br>(delegating execution of<br>callback to Native on main thread)
        N->>N: reveal "work" to get the<br>Dart callback
        N->>D: callback
    end
    deactivate D

So the obvious fix is to break the interlock between Dart and Native when the event originates from Dart:

sequenceDiagram
    participant D as Dart<br>Dart Main Thread
    participant N as Native<br>Dart Main Thread
    participant NT as Native<br> Some Thread
    
    D->>D: Lookup native symbols:<br>initDartApiDL = "InitDartApiDL"<br>executeFunc = "ExecuteFunc"
    D->>N: initDartApiDL
    N->>N: Dart_InitializeApiDL
    D->>D: Create ReceivePort and listen
    activate D
    D->>N: Send SendPort and<br>Dart callback function pointer
    N->>N: Store SendPort and callback for later
    loop Event loop while program is running
        D->>N: Some function call (blocking)
        N-->>NT: Async dispatch<br>(Unblocks Dart main thread)
        NT->>NT: Handling function call<br>wants to callback to Dart and is<br>blocking for a result
        NT->>NT: Create opaque "work" functor<br>(containing Dart callback)<br>Post the functor with Dart_PostCObject_DL
        NT-->>D: Asynchronous exchange of "work" from SendPort to Receive Port
        D->>N: executeFunc<br>(delegating execution of<br>callback to Native on main thread)
        N->>N: reveal "work" to get the<br>Dart callback
        N->>D: callback
    end
    deactivate D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests