cxx
cxx copied to clipboard
Support async functions
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).
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 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 beDoThingAsync
? - Should the C++ signature not rather be:
(I.e., withoutvoid doThing( Arg arg, rust::Fn<void(rust::Box<DoThingAsync>, Ret)> done, rust::Box<DoThingAsync> ctx) noexcept { ...
ctx
andret
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 withstd::async
using a lambda like so:
...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?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"); });
- 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 Thank you very much for the prompt response! I will look into folly.
@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
- Only the Rust calling C++ direction will be supported.
- A
cxx
-specificFuture
type would be exposed -- i.e. will not take a dependency on boost or folly. - 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.
- 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++'sstd::coroutine_handle<>::resume
andAwaiter
types.
Approach
- Introduce a
cxx::Future
andcxx::Promise
type tocxx.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 thatfolly::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;
};
- Introduce a
CxxFuture
type in Rust that isrepr(C)
and allows for receiving the future by value. This type with implementFuture<Output = Result<T, Exception>
and will require thatT
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. - With (1) and (2), and some parser changes we should be able to immediately support C++ functions that return
CxxFuture
. - 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.
I'm curious to know if there's been more progress on this.
This is implemented in https://github.com/pcwalton/cxx-async