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

[ffigen] Blocking blocks #1796

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

[ffigen] Blocking blocks #1796

wants to merge 11 commits into from

Conversation

liamappelbe
Copy link
Contributor

@liamappelbe liamappelbe commented Dec 10, 2024

Add a new .blocking() constructor to all ObjC blocks that return void. 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 a NativeCallable.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:

_ListenerTrampoline _ObjectiveCBindings_wrapBlockingBlock_1j2nt86(
    _BlockingTrampoline block, _BlockingTrampoline listenerBlock, double timeoutSeconds,
    void* (*newWaiter)(), void (*awaitWaiter)(void*, double))  NS_RETURNS_RETAINED {
  NSThread *targetThread = [NSThread currentThread];
  return ^void(id arg0, id arg1, id arg2) {
    if ([NSThread currentThread] == targetThread) {
      objc_retainBlock(block);
      block(nil, objc_retainBlock(arg0), objc_retain(arg1), objc_retain(arg2));
    } else {
      void* waiter = newWaiter();
      objc_retainBlock(listenerBlock);
      listenerBlock(waiter, objc_retainBlock(arg0), objc_retain(arg1), objc_retain(arg2));
      awaitWaiter(waiter, timeoutSeconds);
    }
  };
}

Part of #1647. Non-void callbacks and blocking protocol methods are left as follow up work.

Details

  • The condition variable is created and managed by 3 new functions in package:objective_c: newWaiter, signalWaiter, and awaitWaiter.
    • Under the hood these manage a DOBJCWaiter, which just wraps an NSCondition and a bool flag. The flag is needed because whenever you wait on a condition variable, there can be spurious wake ups.
    • In the normal flow of the off-thread case, each of these functions should be called once per invocation.
    • newWaiter returns an object with a +2 ref count, and the other two each decrement the ref count by 1.
  • Due to linker restrictions in google3, I can't tell users to use the clang linker flags that would allow the ffigen generated .m file to dynamically link the newWaiter and awaitWaiter functions from package:objective_c. So instead I'm passing them down as function pointers. That's what package:objective_c's new wrapBlockingBlock function is for.

@liamappelbe liamappelbe changed the title WIP: [ffigen] Blocking blocks [ffigen] Blocking blocks Dec 12, 2024
@coveralls
Copy link

coveralls commented Dec 12, 2024

Coverage Status

coverage: 87.991% (-0.04%) from 88.032%
when pulling c2de14b on blockblock
into 4d81ce6 on main.

@liamappelbe liamappelbe marked this pull request as ready for review December 12, 2024 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants