dtolnay / cxx

Safe interop between Rust and C++
https://cxx.rs
Apache License 2.0
5.82k stars 330 forks source link

Support async functions #138

Open dtolnay opened 4 years ago

dtolnay commented 4 years ago

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 commented 4 years ago

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);
      });
}
ErikWittern commented 4 years ago

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

dtolnay commented 4 years ago
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());
        }
      });
}
ErikWittern commented 4 years ago

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

capickett commented 3 years ago

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

semirix commented 3 years ago

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

pcwalton commented 3 years ago

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