-
Notifications
You must be signed in to change notification settings - Fork 50
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
[ffigen] Blocking blocks #1796
base: main
Are you sure you want to change the base?
[ffigen] Blocking blocks #1796
Conversation
There is a
This gets added to the native bindings as: typedef Duration = SInt32; You probably need to add a new header to your generated code like:
and then generate:
|
Apparently if you do this, you have to prefix all the built in types too: I'll just add |
Some high level questions:
Could we implement this as
Since this depends on
https://api.dart.dev/dart-ffi/NativeCallable/NativeCallable.listener.html |
Yes, but that's a more complicated change to make. It would also mean waiting on a Dart release before I can make the corresponding ffigen changes and unblock cupertino_http. I'm also not aware of any other use cases that need it (other than jnigen, which already has a solution like this one).
Is your concern that it might do something unexpected, like crash? IIRC, that's technically possible, but in practice I think it pretty much always just does nothing. Or is your point that we shouldn't worry about deadlocking in this case, since it's UB anyway, and the timeout is unnecessary? |
We had users upvote it, I'm fine with landing a custom solution here first. However, I'd like to avoid building the custom solution here in a way that would be incompatible with with dart-lang/sdk#54554.
Yes, it's UB. So providing an API gives the false security that it's a covered case. Also, I think it would make the API imcompatible with basing it on
If we have use cases where users have callbacks outliving isolates and we can guarantee that it's not UB, we should upgrade what we guarantee in the Dart SDK before adding guarantees in FFIgen/JNIgen. |
@brianquinlan For Option (2) cannot be implemented on top of |
@dcharkes I didn't understand the framing of your question. There are Apple APIs where it is important that the callback execute the Dart code before exiting. The one API that I'm currently using that his this property is for file downloads, where the API works like:
So, if the logic for |
@brianquinlan I think the distinction Daco is making is between the approach in this PR, where the caller thread is blocked while the Dart code is executed on a VM managed thread, vs one of the other approaches from my old design docs, where the caller thread tries to enter the target isolate and execute the Dart code on the caller thread. |
What is the semantic different from the user's POV? |
The only differences I can think of is that the current approach marshals args into a message (whereas the other approach could invoke the Dart function directly), and the fact that any thread-local storage would have different data (only relevant if your Dart code calls back into native code that uses TLS). |
Ok, I'll remove the timeout. |
Yes, thread local storage. And also if the calling native thread has acquired a lock with reentrancy support, and the Dart code calls back into native code and tries to acquire that lock again. However, we first need to solve thread pinning (dart-lang/sdk#46943 (comment)) for that to work. So, I don't think we can promise to semantics (2) any time soon. We can probably switch from semantics (1) to semantics (2) later without breaking people. It would be very weird if people would rely what the Dart mutator thread is and use its TLS and thread-identity for locking. (And it would be broken, because the mutator thread can change.) |
@liamappelbe You can run the dart code on the same thread as the ObjC block was called and therefore make it similar to actual ObjC callbacks, make TLS work ... Not too familiar with the bindings generator, but it may not be hard to do, you can see the pattern the main thread does e.g. here. You basically
|
Add a new
.blocking()
constructor to all ObjC blocks that returnvoid
. This constructor returns a block that can be invoked from any thread, and blocks the caller until the Dart callback is complete.Under the hood it has two modes, depending on whether it's invoked from the same thread as the isolate that created the block, or a different thread. If it's on the same thread, it uses a
NativeCallable.isolateLocal
, and doesn't need anything special to get blocking behavior. If it's on a different thread, it uses aNativeCallable.listener
, and uses an ObjC condition variable,NSCondition
, to wait for the callback to complete. This difference avoids deadlocks in the on-thread case.Async callbacks aren't supported in either case. It would be trivial to support them in the off-thread case, but difficult/impossible in the on-thread case. So to avoid semantic differences I'm just not supporting them at all.
There are still edge cases where it's possible to deadlock. If the target isolate has shut down, and the block is invoked from a different thread, then it will send a message to a non-existent isolate and wait forever to be signaled. To mitigate this case we use a timeout when waiting on the condition variable. That timeout can be set by the user.
The different behavior on-thread vs off-thread means we need two Dart-side trampolines, and two blocks passed to the native block wrapper. You can see how it all works by looking at the new ObjC-side trampolines. For example:
Part of #1647. Non-void callbacks and blocking protocol methods are left as follow up work.
Details
newWaiter
,signalWaiter
, andawaitWaiter
.DOBJCWaiter
, which just wraps anNSCondition
and abool
flag. The flag is needed because whenever you wait on a condition variable, there can be spurious wake ups.newWaiter
returns an object with a +2 ref count, and the other two each decrement the ref count by 1.newWaiter
andawaitWaiter
functions from package:objective_c. So instead I'm passing them down as function pointers. That's what package:objective_c's newwrapBlockingBlock
function is for.