cxx-async

repository·main·Indexed 42 days ago

https://github.com/pcwalton/cxx-async

A Rust crate providing interoperability between Rust async/await and C++20 coroutines. It enables the conversion of Rust futures and streams into C++ coroutines, supporting libraries such as cppcoro and Folly. Version 0.1.3.

Tokens
2.3K
Snippets
7
Records
11
Agent score
53%

What's inside cxx-async

  1. Overview of cxx-async

    main

    cxx-async is a Rust crate that enables interoperability between asynchronous Rust (async/await) and C++20 coroutines (co_await). It allows you to convert between C++ coroutines and Rust futures or streams.

    Key distinctions:

    • It is not a C++ binding to Rust I/O libraries (like Tokio).
    • It is not a Rust binding to C++ I/O libraries (like boost::asio).
    • If using it in server code, ensure that the Rust and C++ sides run on separate I/O executors.

    Supported C++ coroutine libraries:

    • cppcoro
    • Folly
  2. How to bridge Rust futures and C++ coroutines

    main

    To enable async interoperability, follow these steps to configure the cxx bridge and the cxx-async macros.

    1. Define the Bridge

    In your #[cxx::bridge] module, declare type aliases for your future types and define the async methods. For Rust-to-C++ calls, the C++ method must return the declared future type. For C++-to-Rust calls, the Rust method must return a boxed version of that future type.

    #[cxx::bridge]
    mod ffi {
        unsafe extern "C++" {
            type RustFutureString = crate::RustFutureString;
            fn hello_from_cpp() -> RustFutureString;
        }
    
        extern "Rust" {
            fn hello_from_rust() -> Box<RustFutureString>;
        }
    }

    2. Implement the Future Bridge in Rust

    Use the #[cxx_async::bridge] attribute to implement Future for your custom type. The Output type should be the type the future yields.

    #[cxx_async::bridge]
    unsafe impl Future for RustFutureString {
        type Output = String;
    }

    3. Configure the C++ side

    Include the appropriate headers (rust/cxx_async.h and either rust/cxx_async_cppcoro.h or rust/cxx_async_folly.h). Use the CXXASYNC_DEFINE_FUTURE macro at the top level (not in a namespace) to define the future.

    Macro Syntax: CXXASYNC_DEFINE_FUTURE(cpp_type, namespace_parts...)

    • cpp_type: The C++ type that cxx maps your Rust type to (e.g., rust::String for String).
    • namespace_parts: The fully-qualified name of the future, with :: replaced by commas.
    #include "rust/cxx.h"
    #include "rust/cxx_async.h"
    #include "rust/cxx_async_cppcoro.h"
    
    CXXASYNC_DEFINE_FUTURE(rust::String, RustFutureString);
    
    RustFutureString hello_from_cpp() {
        co_return std::string("Hello world!");
    }

    4. Calling Async functions

    Rust calling C++:

    async fn call_cpp() -> String {
        // Returns a Result; unwrap to get the value or catch C++ exceptions
        ffi::hello_from_cpp().await.unwrap()
    }

    C++ calling Rust: In Rust, use CxxAsyncResult::infallible for standard async blocks, or fallible if your block returns a Result.

    // Rust side
    fn hello_from_rust() -> RustFutureString {
        RustFutureString::infallible(async { "Hello world!".to_owned() })
    }
    // C++ side
    cppcoro::task<rust::String> call_rust() {
        co_return hello_from_rust();
    }
    #[cxx::bridge]
    mod ffi {
        unsafe extern "C++" {
            type RustFutureString = crate::RustFutureString;
            fn hello_from_cpp() -> RustFutureString;
        }
        extern "Rust" {
            fn hello_from_rust() -> Box<RustFutureString>;
        }
    }
    
    #[cxx_async::bridge]
    unsafe impl Future for RustFutureString {
        type Output = String;
    }
  3. Requirements and Folly installation

    main

    Compiler Requirements

    • A C++ compiler implementing the Coroutines TS (C++20 support).
    • It is recommended to use libc++ instead of libstdc++ for better coroutine support.
    • Note: Some compilers (e.g., Apple clang 13.0.0) may crash when compiling Folly.

    Installing Folly with Coroutine Support

    If you use Folly, it must be built with coroutine support. If you encounter a linker error for folly::resumeCoroutineWithNewAsyncStackRoot, your Folly installation is likely missing coroutine support.

    To build Folly with coroutine support on macOS:

    $ git clone https://github.com/facebook/folly.git
    $ cd folly
    $ ./build.sh --install-dir=/usr/local --extra-cmake-defines \
        '{"CXX_STD": "c++20", "CMAKE_CXX_FLAGS": "-fcoroutines-ts"}'
  4. Handle C++ exceptions in Rust with CxxAsyncException

    main

    Any exception thrown by a C++ coroutine is caught and converted into a CxxAsyncException. This type wraps the value returned by std::exception::what().

    Use CxxAsyncResult<T> (a type alias for Result<T, CxxAsyncException>) when awaiting C++ functions in Rust to handle these errors.

    use cxx_async::CxxAsyncResult;
    
    // CxxAsyncResult is a shorthand for Result<T, CxxAsyncException>
    type MyResult<T> = CxxAsyncResult<T>; 
  5. How bridged Futures and Streams work

    main

    The #[cxx_async::bridge] and #[cxx_async::bridge_stream] macros automate the boilerplate required to share asynchronous types across the C++/Rust boundary.

    When you apply these macros, they generate:

    1. A #[repr(transparent)] wrapper struct around a boxed Rust future or stream.
    2. Implementations of the standard Future or Stream traits.
    3. Convenience methods .infallible() and .fallible() to wrap concrete futures/streams.
    4. A C-compatible vtable function (e.g., cxxasync_..._vtable) that allows C++ to interact with the Rust-side asynchronous state machine.

    Safety Note: The programmer is responsible for ensuring the Output or Item type matches the C++ type. Mismatched types will result in undefined behavior.

  6. Convert Rust Futures to C++ using IntoCxxAsyncFuture

    main

    The IntoCxxAsyncFuture trait (automatically implemented by the bridge macro) allows you to wrap Rust futures so they can be returned to C++.

    • Use .infallible(future) when the future returns the output type directly and you do not need to propagate Rust errors as C++ exceptions.
    • Use .fallible(future) when the future returns a Result<T, CxxAsyncException>, allowing you to turn Rust error values into C++ exceptions.
    // Example of wrapping an infallible future
    fn hello_from_rust() -> RustFutureString {
        RustFutureString::infallible(async { "Hello world!".to_owned() })
    }
    
    // Example of wrapping a fallible future
    fn fallible_rust() -> RustFutureString {
        RustFutureString::fallible(async {
            let res: CxxAsyncResult<String> = Ok("Success".to_string());
            res
        })
    }
  7. Convert Rust Streams to C++ using IntoCxxAsyncStream

    main

    The IntoCxxAsyncStream trait (automatically implemented by the bridge_stream macro) allows you to wrap Rust streams so they can be returned to C++.

    • Use .infallible(stream) for streams yielding items directly.
    • Use .fallible(stream) for streams yielding CxxAsyncResult<Item>, allowing error propagation to C++ as exceptions.
  8. Define a bridged Future with #[cxx_async::bridge]

    main

    Use the #[cxx_async::bridge] attribute to define a Future type that can be awaited from both Rust and C++. This is applied to an unsafe impl Future block for a type that was declared in your #[cxx::bridge] module.

    Usage Requirements

    • The impl block must be marked unsafe.
    • The impl block must not be marked default.
    • The implementation must contain exactly one associated type: type Output = <Type>;.
    • The type being implemented must not have generics.
    • The Output type must correctly reflect the C++ type it resolves to (refer to cxx documentation for mapping rules).

    Namespacing

    If the future type resides within a C++ namespace, you must specify it in the attribute: #[cxx_async::bridge(namespace = mycompany::myproject)].

    Error Handling

    Rust Err returns are automatically translated into C++ exceptions.

    #[cxx_async::bridge]
    unsafe impl Future for RustFutureString {
        type Output = String;
    }
  9. Define a bridged Stream with #[cxx_async::bridge_stream]

    main

    Use the #[cxx_async::bridge_stream] attribute to define a Stream type that can be consumed from both Rust and C++. This is applied to an unsafe impl Stream block for a type declared in your #[cxx::bridge] module.

    Usage Requirements

    • The impl block must be marked unsafe.
    • The impl block must not be marked default.
    • The implementation must contain exactly one associated type: type Item = <Type>;.
    • The type being implemented must not have generics.
    • The Item type must correctly reflect the C++ type it resolves to.

    Namespacing

    If the stream type resides within a C++ namespace, specify it via the namespace attribute: #[cxx_async::bridge_stream(namespace = mycompany::myproject)].

    Error Handling

    Rust Err returns are automatically translated into C++ exceptions.

    #[cxx_async::bridge_stream]
    unsafe impl Stream for RustStreamString {
        type Item = String;
    }