2

Consider a library that uses stdatomic.h header. This one will not compile in C++ project, especially if it is required to use structure with atomic components.

How to properly implement library with atomic components, to build with C or C++ compiler? Small demo that builds well - but it seems very shady and holds potential undefined behavior.

lib.h header file

#ifndef LIB_HDR_H
#define LIB_HDR_H

#include <stdint.h>
#include <stddef.h>

#if defined(__cplusplus)
/* C++ atomic header */
#include <atomic>
#define lib_size_t std::atomic<size_t>
#else
/* STDATOMIC C header */
#include <stdatomic.h>
typedef atomic_size_t lib_size_t;
#endif

#if defined(__cplusplus)
extern "C" {
#endif /* defined(__cplusplus) */

/* Tricky part here... */
typedef struct {
  lib_size_t a;
  lib_size_t b;
} lib_t;

void lib_init(lib_t *l);

#if defined(__cplusplus)
}
#endif /* defined(__cplusplus) */

#endif /* LIB_HDR_H */

lib.c implementation file

#include "lib.h"

/* Not much to do here right now... */
void lib_init(lib_t* l) {
    /* These operations shall be atomic */
    l->a = 5;
    l->b = 10;
}

C++ application file that uses the library

#include <iostream>
#include "lib.h"

lib_t my_lib;

int main() {
    lib_init(&my_lib);
}
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
unalignedmemoryaccess
  • 7,246
  • 2
  • 25
  • 40
  • 1
    These operations `l->a = 5; l->b = 10;` are not atomic. – 273K Mar 28 '23 at 16:24
  • 2
    And why do you need atomic access when initializing a new struct instance, anyway? – Remy Lebeau Mar 28 '23 at 16:26
  • @273K could you please elaborate on this one? Godbolt shows sync/protect instructions. Thanks – unalignedmemoryaccess Mar 28 '23 at 16:28
  • 1
    On a side note: in a C++ compilation, you should be using `` and `` instead of `` and ``. Also, when defining `lib_size_t`, why are you using `#define`? You should be using `typedef std::atomic lib_size_t;` or `using lib_size_t = std::atomic;` instead. – Remy Lebeau Mar 28 '23 at 16:28
  • 1
    It's unlikely `atomic_size_t` and `std::atomic` are equal types, don't mix them. – 273K Mar 28 '23 at 16:30
  • @RemyLebeau atomic is needed in later stage, when multiple threads access the variables. If CPU cannot transfer full variable size in one cycle, you need some sort of atomicity. Lib is for pc or embedded app. – unalignedmemoryaccess Mar 28 '23 at 16:30
  • 1
    "Godbolt shows sync/protect instructions" Each of the assignments is atomic, but both are not. – 273K Mar 28 '23 at 16:31
  • @273K thats OK - until read/write to one of them separately is atomic, it is all good. Indeed you need atomic read operations too (acquire & release) – unalignedmemoryaccess Mar 28 '23 at 16:31
  • 1
    *If CPU cannot transfer full variable size in one cycle, you need some sort of atomicity.* - At the C++ level, you always need to tell the compiler when you need atomicity- [Which types on a 64-bit computer are naturally atomic in gnu C and gnu C++? -- meaning they have atomic reads, and atomic writes](https://stackoverflow.com/q/71866535) - none. But yes, in the compiler-generated asm, `relaxed` load/store (or acq/rel on x86) don't have to use any special instructions for register-width or narrower on most ISAs. (The default memory-order is `seq_cst`; even x86 needs special insns for stores) – Peter Cordes Mar 28 '23 at 17:01
  • 1
    @273K: ISO C++ requires that `std::atomic_size_t` is a typedef for `std::atomic` (https://en.cppreference.com/w/cpp/atomic/atomic). (So `using std::atomic_size_t` is preferable to `#define`). C11 guarantees that `atomic_size_t` is a typedef for `_Atomic size_t` (https://en.cppreference.com/w/c/thread#Atomic_operations). ISO C and C++ intend their atomics to be interoperable with each other, so even in a mixed-language project it's safe to mix them with existing mainstream implementations, especially for lock-free atomics. – Peter Cordes Mar 28 '23 at 17:07
  • @PeterCordes What you have described seems to be valid only since C++23. I don't see any guaranty of `std::atomic_size_t` to be `_Atomic size_t` in C until C++23. – 273K Mar 28 '23 at 17:21
  • 1
    You mean a guarantee of interoperability between C++ and C? That's been de-facto true with mainstream implementations like GCC from the start of its C11 / C++11 support I think, and has always(?) been a goal of the committees; I haven't kept track of what's officially guaranteed. Within each language separately, those typedefs date back to C++11 and C11, according to cppref. – Peter Cordes Mar 28 '23 at 17:24
  • @PeterCordes So, this is up to a compiler, therefore is implementation detail. As for the first answer, even GCC makes it differently on ARM. Unfortunately, until a goal of the committees is not hardcopied, it's just a private goal. – 273K Mar 28 '23 at 17:33
  • @273K: The alignment difference on some 32-bit targets was only for 64-bit objects, as discussed in https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71660 (C++ fixed it by adding alignas to the `std::atomic` template), vs. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4 (C11 `_Atomic` needs the compiler internals to do that, and went unfixed for years until recently). But `size_t` is 32-bit and `alignof(size_t) == 4` which is sufficient for `atomic` as well; that's why I didn't mention the under-alignment bug in my earlier comments, because it doesn't affect `atomic_size_t`. – Peter Cordes Mar 28 '23 at 17:41
  • @273K Show me where the semantics of any mix of PL is defined, or could possibly defined. Show me how separate compilation can even be defined, when neither C nor C++ std recognize or define separate compilation! – curiousguy Mar 28 '23 at 19:27

1 Answers1

3

In C++ std::atomic_size_t is an alias for std::atomic<size_t> and in C atomic_size_t is an alias for _Atomic size_t.

With a using std::atomic_size_t in the C++ side of the #if, you can simply use typedef atomic_size_t lib_size_t; for both languages and starting with C++23 you can include <stdatomic.h> and use typedef atomic_size_t lib_size_t; directly in both C and C++.

The two types are not the same in the same way that size_t is supposed to be the same type in both C and C++, because C++ doesn't have any equivalent to the _Atomic modifier. So it is not obvious that the ABI handles the two types in the same way and that what you are doing is allowed. (In practice, most real implementations did handle them the same way.)

However, the C++23 standard draft contains an implementation recommendation that same representation and compatibility of memory ordering mechanism between _Atmomic T and std::atomic<T> should be assured. Of course this can only be a recommendation and the details will be on the particular implementation to decide.

So, assuming your compiler supports both C atomics and C++ atomics, it should make an effort to support the approach you are using here, although that is not a requirement for conformance. However, it doesn't seem to be a given. See the proposal introducing the implementation recommendation P0943 for some details. For example it mentions that GCC had an alignment difference between 64-bit atomic C vs. C++ types on 32-bit targets like ARMv7 and x86.

This was probably a GCC bug, like x86 GCC bug 65146 resulting in lack of atomicity for _Atomic long long inside structs in some C programs, which was fixed in GCC11. It went unfixed for a long time partly because GCC already chose to align 8-byte objects by 8 when they were outside structs so it was allowed to. (g++ originally had an equivalent bug, but it was fixed with alignas() in the std::atomic<> class definition in library headers, vs. _Atomic requiring the compiler internals to do that.) We don't know for sure what ARMv7 detail they were talking about, but it's likely similar and probably only affects 64-bit types.

So you probably need to verify that this is properly supported for your specific compiler/architecture.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
user17732522
  • 53,019
  • 2
  • 56
  • 105
  • The alignment difference on some 32-bit targets was only for 64-bit objects, as discussed in https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71660 (C++ fixed it by adding alignas to the `std::atomic` template), vs. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4 (C11 `_Atomic` needs the compiler internals to do that, and went unfixed for years until recently). But `size_t` is 32-bit and `alignof(size_t) == 4` which is sufficient for `atomic` as well; C `atomic_size_t` and C++ `std::atomic_size_t` have always been compatible on GCC. – Peter Cordes Mar 28 '23 at 17:43
  • The alignment thing could maybe matter for a whole struct, like `_Atomic lib_t` vs. `std::atomic`, if you needed atomicity for both members together as a transaction, with older versions of GCC perhaps only having `alignof(_Atomic lib_t) == 4`, thus potentially allowing them to split across cache lines or something. – Peter Cordes Mar 28 '23 at 17:47
  • @PeterCordes I have not verified the claims of the linked proposal, but it refers to ARMv7, which is not actually mentioned in the linked bugs, nor seems to be considered in the patch in the latter bug. Do you know how this relates? – user17732522 Mar 28 '23 at 17:55
  • Hrm, I was thinking that bug 65146 was more general for 32-bit architectures, but the proposed fix for it seems only to be in `config/i386/i386.c`, not a more general file. Hopefully similar changes were made to other ISAs if that's where it belongs; I guess that makes sense, not all ISAs can do 2-register atomic load / store / RMW. Anyway, I think ARMv7 can, so any less alignment for `_Atomic int64_t` on ARMv7 would be the same bug. WG21/P0943R6 was dated a couple months after that patch hit GCC's CVS repo, so maybe 32-bit x86 was fixed but ARMv7 wasn't, on nightly builds. – Peter Cordes Mar 28 '23 at 18:07
  • 1
    @PeterCordes The first revision of the proposal is from 2018 and the relevant section seems to be unchanged. It is also possible the authors didn't keep track of GCC changes (they mention 4.9 only). But since I don't see the change in the linked bug and didn't test myself, I am not sure the claim "This _was_ a GCC bug" in the answer is now correct for both ARM and x86. – user17732522 Mar 28 '23 at 18:13
  • Yeah, I'm not sure my edit is fully correct either. My test case from the x86 GCC bug doesn't reproduce the problem on ARM: https://godbolt.org/z/veP8qKb3f shows (at the bottom) `offsetof(struct AtomicStruct, x)` was `64` even in ARM GCC 5.4, vs. `60` in x86 gcc `-m32` up until GCC11. So maybe it was a different bug for ARM, or fixed even earlier? Godbolt doesn't have GCC4.9 for ARM, just 4.6 and 5.4. There is a bug in GCC10 and earlier for C. But whatever alignment difference there was on ARMv7, it's probably only for 64-bit types, and probably is a bug unless it's non-lock-free. – Peter Cordes Mar 28 '23 at 18:27