C++20 coroutine frame layout mismatch when linking object files from different compilers
0 reputation · 06 Jul 2023, 13:04 UTC
0 reputation · 06 Jul 2023, 13:04 UTC
The C++20 standard defines coroutine semantics and the promise_type concept but leaves coroutine frame layout, handle representation, and intrinsic calling conventions as implementation-defined. GCC, Clang, and MSVC each emit distinct frame structures and symbol mangling for coroutine state machines, so translation units built with different toolchains cannot be linked together. Even within a single compiler family, major releases have changed the lowering strategy, breaking binary compatibility with previously compiled object files.
A library author who exposes coroutine-returning functions across a shared-library boundary must either restrict consumers to a single compiler version or provide a stable ABI wrapper. Compiler flags that affect exception handling, optimization level, or sanitizer instrumentation can also alter frame layout without diagnostic warnings.
What is the minimal set of compiler and linker flags that guarantees a consistent coroutine frame layout across translation units built with the same major compiler version? Are there any standardized or de-facto ABI specifications (such as Itanium C++ ABI extensions) that document the expected frame layout for a given toolchain? How can a build system detect and reject accidental mixing of object files with incompatible coroutine ABIs at link time?
34275 reputation · 06 Jul 2023, 14:37 UTC
To guarantee a consistent coroutine frame layout across translation units built with the same major compiler version, you must unify the flags that influence the lowering of the coroutine state machine into a physical memory layout. Because the C++20 standard leaves the frame layout implementation-defined, any flag that alters spill slots, padding, or exception handling can create a binary mismatch.
The following settings must be identical across all translation units to avoid ABI breakage:
-fexceptions or -fno-exceptions (GCC/Clang) or /EHsc (MSVC). Mismatched exception models change how the frame handles cleanup and unwinding.-O2 or -Os) across all units. Different levels can alter the placement of variables within the frame or trigger different elision strategies.-fsanitize=undefined or -fsanitize=thread are applied globally or not at all. Sanitizers often insert additional padding or metadata into the frame layout./Zc:await settings are identical; mismatched flags result in incompatible resumption struct layouts.While the Itanium C++ ABI (used by GCC/Clang on Linux) and the Microsoft x64 ABI provide nominal requirements for promise_type placement and suspension tokens, they do not guarantee binary compatibility across major compiler releases. The lowering strategy—how the compiler transforms the coroutine into a state machine—is subject to change between versions, meaning object files from different major versions are generally incompatible.
Standard linkers cannot natively detect coroutine frame mismatches, as they typically manifest as runtime memory corruption rather than unresolved symbols. To detect these incompatibilities in a build system, use the following verification method:
-fdump-coroutines-all (GCC/Clang) or /coroutine:dump (MSVC).__coro_frame (or equivalent internal struct).One diagnostic detail that would change this recommendation is the specific compiler version and target architecture; some specific minor releases may introduce breaking changes to the lowering strategy that require a full rebuild of all dependencies regardless of flags.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.