libcoro

repository·main·Indexed 21 days ago

https://github.com/jbaldwin/libcoro

A C++20 coroutine library providing low-level, high-performance constructs and networking primitives for asynchronous applications. It includes execution control primitives like sync_wait, when_all, and when_any; coroutine types such as task<T> and generator<T>; and synchronization tools including mutex, shared_mutex, semaphore, event, and latch. The library also features executors like thread_pool and scheduler, as well as asynchronous networking support for DNS, TCP, UDP, and TLS.

Tokens
15.6K
Snippets
40
Records
48
Agent score
74%

What's inside libcoro

  1. Overview of libcoro coroutine constructs

    main
    libcoro is a C++20 coroutine library providing low-level constructs for building larger applications. It offers a modern, safe API with several high-level primitives for managing asynchronous execution and synchronization.
  2. Handle thread switching and thread_local with co_await

    main

    In libcoro, any co_await operation has the potential to switch the underlying thread executing the coroutine if the scheduler has more than one thread.

    Warning: Do not use thread_local across a co_await boundary. This will lead to bugs due to thread switching and work stealing in libcoro schedulers.

    Safe usage: Only use thread_local safely if you are using a coro::thread_pool with exactly 1 thread or an inline scheduler with 1 thread.

  3. How coro::shared_mutex handles scoped locking

    main

    The coro::shared_mutex provides asynchronous shared (reader) and exclusive (writer) locking.

    Important Note on Scoped Locks: Because coro::shared_mutex.unlock() and unlock_shared() are coroutines, they cannot be called inside a standard RAII destructor. Therefore, you cannot use a standard RAII-style scoped lock. Instead, you must use the scoped_lock or scoped_lock_shared methods, which take a lambda (a coroutine) that defines the work to be done while the lock is held. The mutex is automatically released when the provided coroutine completes.

    // Exclusive lock usage
    co_await mutex.scoped_lock([](auto& tp) -> coro::task<void> {
        // Do exclusive work here
        co_await tp->yield();
    }(tp));
    
    // Shared lock usage
    co_await mutex.scoped_lock_shared([](auto& tp, uint64_t i) -> coro::task<void> {
        // Do shared work here
        co_await tp->yield();
    }(tp, i));
  4. Executors and Schedulers in libcoro

    main

    libcoro uses executors to drive coroutine execution and I/O events:

    • coro::thread_pool: Used for cooperative multitasking of coroutines. Ideal for latency-sensitive or long-lived tasks.
    • coro::scheduler: Used for driving I/O events. It can use inline task processing for thread-per-core or short-lived tasks. Note that using the scheduler requires LIBCORO_FEATURE_NETWORKING to be supported.
    • coro::task_group: Used for grouping tasks with dynamic lifetimes.
  5. Avoid use-after-free with lambda captures in coroutines

    main

    Following C++ Core Guidelines (CP.51), it is recommended not to use capturing lambdas that are coroutines. Captures are destroyed at the coroutine's first suspension point, which can lead to use-after-free bugs if accessed after suspension.

    Best Practices:

    1. Pass data into coroutines via function arguments by value to guarantee lifetime.
    2. If you must use lambda captures, use coro::invoke to create a stable coroutine frame that holds the captures for the duration of the coroutine.
  6. Coroutine Networking in libcoro

    main

    libcoro provides asynchronous networking primitives. Some features require specific dependencies:

    • DNS: coro::net::dns::resolver provides asynchronous DNS resolution using libc-ares.
    • TCP: coro::net::tcp::client and coro::net::tcp::server.
    • TLS: coro::net::tls::client and coro::net::tls::server (requires OpenSSL).
    • UDP: coro::net::udp::peer.
  7. High-level coroutine primitives in libcoro

    main

    libcoro provides the following core coroutine and synchronization primitives:

    Execution Control

    • coro::sync_wait(awaitable): Blocks the current thread until the provided awaitable completes.
    • coro::when_all(awaitable...): Returns an awaitable that completes when all provided awaitables have completed.
    • coro::when_any(awaitable...): Returns an awaitable that completes when any of the provided awaitables completes.
    • coro::invoke(functor, args...): Returns an awaitable that invokes the provided functor with the given arguments.

    Coroutine Types

    • coro::task<T>: A core coroutine task type.
    • coro::generator<T>: A coroutine type used for producing a sequence of values.

    Synchronization and Data Structures

    • coro::event: A synchronization primitive for signaling.
    • coro::latch: A synchronization point for multiple threads/coroutines.
    • coro::mutex / coro::shared_mutex: Mutual exclusion primitives.
    • coro::semaphore: A semaphore for controlling access to a resource.
    • coro::ring_buffer<element, num_elements>: A fixed-size circular buffer.
    • coro::queue: A queue for asynchronous communication.
    • coro::condition_variable: A condition variable for coroutine synchronization.
  8. Avoid blocking coroutines with yield()

    main

    When working within a coroutine, never use std::this_thread::sleep_for(). Doing so blocks the entire thread, preventing other ready coroutines from executing. Instead, use cooperative yielding to allow other tasks to run:

    1. Use co_await tp->yield() if you have access to a thread pool.
    2. Use co_await scheduler->yield() to place the current task at the back of the scheduler's queue.
    3. Use co_await scheduler->yield_for(duration) to yield for a specific amount of time.
    4. Use co_await scheduler->yield_until(timepoint) to yield until a specific time point.
    // Instead of std::this_thread::sleep_for(ms),
    // use yield to let other coroutines run
    if (condition) {
        co_await tp->yield();
    }
  9. Integrate libcoro into an Android CMake project

    main

    To use libcoro in an Android native application, add it as a subdirectory in your CMake configuration and link against the libcoro target. If you require networking or TLS features, you must explicitly define the corresponding macros. For TLS, ensure you provide OpenSSL for the target ABI and configure OPENSSL_ROOT_DIR and OPENSSL_USE_STATIC_LIBS.

    # Add libcoro as a subdirectory in your native CMake
    add_subdirectory(${CMAKE_SOURCE_DIR}/path/to/libcoro libcoro_build)
    
    # Link to your library target
    target_link_libraries(your-lib PRIVATE libcoro log)
    
    # Enable networking and TLS features
    target_compile_definitions(your-lib PRIVATE LIBCORO_FEATURE_NETWORKING LIBCORO_FEATURE_TLS)
  10. Configure Android test execution via properties file

    main

    When running the libcoro test app on an Android emulator, you can pass test filters and timeouts by placing a properties file at /data/local/tmp/coro_test_config.properties or within the app sandbox at files/coro_test_config.properties.

    filter=~[benchmark] ~[bench] ~[semaphore] ~[scheduler]
    timeout=600
  11. Build the Android test APK locally

    main

    To build the Android test harness for validating coroutine primitives and networking/TLS on Android devices or emulators, follow these steps.

    Prerequisites:

    • Android SDK + NDK r29
    • CMake 3.22.1
    • JDK 17

    Steps:

    1. Clone the repository with submodules: git clone --recurse-submodules <libcoro-url>
    2. Navigate to the Android directory: cd test/android
    3. (Optional) Build OpenSSL for required ABIs: bash scripts/build_openssl.sh --abis arm64-v8a,armeabi-v7a,x86_64,x86 --api 24
    4. Build a single-ABI debug version: gradle assembleDebug -PciAbi=x86_64 -PcustomBuildDir=build-x86_64
    5. Build a multi-ABI fat APK: gradle assembleDebug
    cd test/android
    # Optionally build OpenSSL for required ABIs
    bash scripts/build_openssl.sh --abis arm64-v8a,armeabi-v7a,x86_64,x86 --api 24
    
    # Single-ABI debug build
    gradle assembleDebug -PciAbi=x86_64 -PcustomBuildDir=build-x86_64
    
    # Multi-ABI build
    gradle assembleDebug
  12. Configure libcoro build options

    main

    When building libcoro from source using CMake, you can use the following options to customize the build:

    | Name | Default | Description |
    |:---|:---|:---|
    | LIBCORO_EXTERNAL_DEPENDENCIES | OFF | Use CMake find_package to resolve dependencies instead of embedded libraries. |
    | LIBCORO_BUILD_TESTS | ON | Should the tests be built? Note this is only default ON if libcoro is the root CMakeLists.txt |
    | LIBCORO_CODE_COVERAGE | OFF | Should code coverage be enabled? Requires tests to be enabled. |
    | LIBCORO_BUILD_EXAMPLES | ON | Should the examples be built? This is only default ON if libcoro is the root CMakeLists.txt |
    | LIBCORO_FEATURE_NETWORKING | ON | Include networking features. MSVC not currently supported |
    | LIBCORO_FEATURE_TLS | ON | Include TLS features. Requires networking to be enabled. MSVC not currently supported. |