Expand description
cxx-async
is a Rust crate that extends the cxx
library to provide
seamless interoperability between asynchronous Rust code using async
/await
and C++20
coroutines using co_await
. If your C++ code is asynchronous, cxx-async
can provide a more
convenient, and potentially more efficient, alternative to callbacks. You can freely convert
between C++ coroutines and Rust futures and await one from the other.
It’s important to emphasize what cxx-async
isn’t: it isn’t a C++ binding to Tokio or any
other Rust I/O library. Nor is it a Rust binding to boost::asio
or similar. Such bindings
could in principle be layered on top of cxx-async
if desired, but this crate doesn’t provide
them out of the box. (Note that this is a tricky problem even in theory, since Rust async I/O
code is generally tightly coupled to a single library such as Tokio, in much the same way C++
async I/O code tends to be tightly coupled to libraries like boost::asio
.) If you’re writing
server code, you can still use cxx-async
, but you will need to ensure that both the Rust and
C++ sides run separate I/O executors.
cxx-async
aims for compatibility with popular C++ coroutine support libraries. Right now,
both the lightweight cppcoro
and the more
comprehensive Folly are supported. Pull requests are
welcome to support others.
§Quick tutorial
To use cxx-async
, first start by adding cxx
to your project. Then add the following to your
Cargo.toml
:
[dependencies]
cxx-async = "0.1"
Now, inside your #[cxx::bridge]
module, declare a future type and some methods like so:
#[cxx::bridge]
mod ffi {
// Declare type aliases for each of the future types you wish to use here. Then declare
// async C++ methods that you wish Rust to call. Make sure they return one of the future
// types you declared.
unsafe extern "C++" {
type RustFutureString = crate::RustFutureString;
fn hello_from_cpp() -> RustFutureString;
}
// Async Rust methods that you wish C++ to call go here. Again, make sure they return one of
// the future types you declared above.
extern "Rust" {
fn hello_from_rust() -> RustFutureString;
}
}
After the #[cxx::bridge]
block, define the future types using the
#[cxx_async::bridge]
attribute:
// The `Output` type is the Rust type that this future yields.
#[cxx_async::bridge]
unsafe impl Future for RustFutureString {
type Output = String;
}
Note that it’s your responsibility to ensure that the type you specify for Output actually matches the type of the value that your future resolves to. Otherwise, undefined behavior can result.
Next, in your C++ header, make sure to #include
the right headers:
#include "rust/cxx.h"
#include "rust/cxx_async.h"
#include "rust/cxx_async_cppcoro.h" // Or cxx_async_folly.h, as appropriate.
And add a call to the CXXASYNC_DEFINE_FUTURE
macro in your headers to define the C++ side of
the future:
// The first argument is the C++ type that the future yields, and the second argument is the
// fully-qualified name of the future, with `::` namespace separators replaced with commas. (For
// instance, if your future is named `mycompany::myproject::RustFutureString`, you might write
// `CXXASYNC_DEFINE_FUTURE(rust::String, mycompany, myproject, RustFutureString);`. The first
// argument is the C++ type that `cxx` maps your Rust type to: in this case, `String` maps to
// `rust::String`, so we supply `rust::String` here.
//
// Note that, because the C preprocessor doesn't know about the `<` and `>` brackets that
// surround template arguments, a template type that contains multiple arguments (e.g.
// `std::pair<int, std::string>`) will need to be factored out into a `typedef` to be used
// inside `CXXASYNC_DEFINE_FUTURE`. Otherwise, the C preprocessor won't parse it properly.
//
// This macro must be invoked at the top level, not in a namespace.
CXXASYNC_DEFINE_FUTURE(rust::String, RustFutureString);
You’re done! Now you can define asynchronous C++ code that Rust can call:
RustFutureString hello_from_cpp() {
co_return std::string("Hello world!");
}
On the Rust side:
async fn call_cpp() -> String {
// This returns a Result (with the error variant populated if C++ threw an exception), so
// you need to unwrap it:
ffi::hello_from_cpp().await.unwrap()
}
And likewise, define some asynchronous Rust code that C++ can call:
use cxx_async::CxxAsyncResult;
fn hello_from_rust() -> RustFutureString {
// You can instead use `fallible` if your async block returns a Result.
RustFutureString::infallible(async { "Hello world!".to_owned() })
}
Over on the C++ side:
cppcoro::task<rust::String> call_rust() {
co_return hello_from_rust();
}
In this way, you should now be able to freely await futures on either side.
Structs§
- CxxAsync
Exception - Any exception that a C++ coroutine throws is automatically caught and converted into this error type.
Traits§
- Into
CxxAsync Future - Wraps an arbitrary Rust Future in a boxed
cxx-async
future so that it can be returned to C++. - Into
CxxAsync Stream - Wraps an arbitrary Rust Stream in a boxed
cxx-async
stream so that it can be returned to C++.
Type Aliases§
- CxxAsync
Result - A convenient shorthand for
Result<T, CxxAsyncException>
.
Attribute Macros§
- bridge
- Defines a future or stream type that can be awaited from both Rust and C++.