cxx icon indicating copy to clipboard operation
cxx copied to clipboard

Support async functions

Open dtolnay opened this issue 4 years ago • 7 comments

The fine details of this need to be worked out.

The dream would be that this:

mod ffi {
    extern "C++" {
        async fn doThing(arg: Arg) -> Ret;
    }
}

knows how to call this:

Future<Ret> doThing(Arg arg) noexcept {
    ...
    co_return ret;
}

where Future is any type with a suitable API to support co_await/co_return (std::future, folly::coro::Task, folly::Future, folly::SemiFuture, etc).

dtolnay avatar Apr 23 '20 02:04 dtolnay

The current workaround for async calls is to implement them using a callback function and oneshot channel. In my work codebase that ends up looking like:

use futures::channel::oneshot;

#[cxx::bridge]
mod ffi {
    extern "Rust" {
        type DoThingAsync;
    }

    extern "C" {
        fn doThing(
            arg: Arg,
            done: fn(Box<DoThingAsync>, ret: Ret),
            ctx: Box<DoThingAsync>,
        );
    }
}

type DoThingAsync = oneshot::Sender<Ret>;

pub async fn do_thing(arg: Arg) -> Ret {
    let (tx, rx) = oneshot::channel();
    let context = Box::new(tx);

    ffi::doThing(
        arg,
        |tx, ret| { let _ = tx.send(ret); },
        context,
    );

    rx.await.unwrap()
}
void doThing(
    Arg arg,
    rust::Fn<void(rust::Box<DoThingAsync> ctx, Ret ret)> done,
    rust::Box<DoThingAsync> ctx) noexcept {
  doThingImpl(arg)
      .then([done, ctx(std::move(ctx))](auto &&ret) mutable {
        (*done)(std::move(ctx), ret);
      });
}

dtolnay avatar Apr 23 '20 02:04 dtolnay

@dtolnay Cxx is a fantastic library, thank you for it! I am interested in async support, and have a few questions about the example above:

  • Is it correct that DoThingContext should be DoThingAsync?
  • Should the C++ signature not rather be:
    void doThing(
      Arg arg,
      rust::Fn<void(rust::Box<DoThingAsync>, Ret)> done,
      rust::Box<DoThingAsync> ctx) noexcept {
    ...
    
    (I.e., without ctx and ret in the function pointer signature.)
  • Finally, your C++ example code uses .then, which I assume is non-blocking, but does seem to be an experimental feature at this point (https://en.cppreference.com/w/cpp/experimental/future/then). I tried replicating your code with std::async using a lambda like so:
    std::async(std::launch::async, [done, ctx(std::move(ctx))]() mutable {
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
        (*done)(std::move(ctx), "Some result");
     });
    
    ...but this approach does not yield control back to Rust while sleeping in the C++ lambda. Do you have advise how to write the C++ part in a non-blocking way?

ErikWittern avatar Aug 20 '20 14:08 ErikWittern

  • Fixed.
  • No, it's fine with argument names. Maybe a style difference. We do it as written.
  • We're using folly, not std::experimental. Here's a more typical usage for the case of fallible functions, based on folly::Future<T>::thenTry and folly::Try<T>:
void doThing(
    Arg arg,
    rust::Fn<void(rust::Box<DoThingAsync> ctx, Ret ret)> ok,
    rust::Fn<void(rust::Box<DoThingAsync> ctx, const std::string &exn)> fail,
    rust::Box<DoThingAsync> ctx) noexcept {
  doThingImpl(arg)
      .thenTry([ok, fail, ctx(std::move(ctx))](auto &&res) mutable {
        if (res.hasValue()) {
          (*ok)(std::move(ctx), *res);
        } else {
          (*fail)(std::move(ctx), res.exception().what().toStdString());
        }
      });
}

dtolnay avatar Aug 20 '20 17:08 dtolnay

@dtolnay Thank you very much for the prompt response! I will look into folly.

ErikWittern avatar Aug 20 '20 17:08 ErikWittern

@dtolnay I've started to prototype a very rough implementation of async support in https://github.com/capickett/cxx/tree/cpp-futures.

I'm still getting a lay of the cxx land, but my high-level thinking is the following.

Constraints

  1. Only the Rust calling C++ direction will be supported.
  2. A cxx-specific Future type would be exposed -- i.e. will not take a dependency on boost or folly.
  3. The initial version will have some overhead for mutual exclusion + reference counting. In other words, I won't try to optimize for cases such as immediately available futures, or promises that resolve without jumping threads.
  4. No support for coroutines right away. I think coroutines could address some of the overhead concerns of (3), but will require more effort to bridge between Rust's Future::poll and C++'s std::coroutine_handle<>::resume and Awaiter types.

Approach

  1. Introduce a cxx::Future and cxx::Promise type to cxx.h These types would primarily support C++11 - C++17 in non-coroutines codebases. The types would follow a similar "shared core state machine" approach that folly::Future takes. The public API would look like:
template <typename T>
struct Future {
  // Immediately available futures
  explicit Future(const T &);
  explicit Future(T &&);

  Future(Future &&) noexcept = default;
  Future &operator=(Future &&) noexcept = default;

  bool ready() const noexcept;
  // Either throw if Future contains error, or wrap T in a Result type
  T result() noexcept;
};

template <typename T>
struct Promise {
  Promise() noexcept;

  Promise(Promise &&) noexcept = default;
  Promise &operator=(Promise &&) noexcept = default;

  Future<T> getFuture() const noexcept;

  void setValue(const T &value) noexcept;
  void setValue(T &&value) noexcept;
};
  1. Introduce a CxxFuture type in Rust that is repr(C) and allows for receiving the future by value. This type with implement Future<Output = Result<T, Exception> and will require that T is a type that can be returned from a non-async C++ function. E.g. SharedType, UniquePtr<OpaqueType>, SharedPtr<OpaqueType>, etc. Primitives should likely be supported as well.
  2. With (1) and (2), and some parser changes we should be able to immediately support C++ functions that return CxxFuture.
  3. Introduce changes to allow for parsing async functions, and perform the necessary sugaring to treat them as Future returning functions, same as (3).

What do you think of that approach? Feel free to look at the top two changes in my fork, they're not the cleanest, but show an example of calling an async function and awaiting it. I'm also doing some strange packing/unpacking (CxxFuture<T> impls Future<Output = Result<UniquePtr<T, Exception>>) of the type that I hope to clean up.

capickett avatar Jan 10 '21 20:01 capickett

I'm curious to know if there's been more progress on this.

semirix avatar May 14 '21 05:05 semirix

This is implemented in https://github.com/pcwalton/cxx-async

pcwalton avatar Sep 30 '21 03:09 pcwalton