Hi, I work on the LLVM libc project and I have a weird question about how the clang frontend handles implicit functions in C++ headers: For a struct defined in a header, can its implicit constructor or destructor ever be put in a COMDAT section?
Here is the explanation of why I need to know this:
One of our users (Fuchsia) wants to be able to avoid generating COMDAT when building our library. Fuchsia has had issues where the same header is built in multiple different ways as part of the same program. If the functions in those headers aren’t properly inlined, they generate COMDAT entries with different properties but the same name. At link time, one of the COMDAT entries is picked for the whole program, meaning some calls get the wrong version of the function. To avoid this in LLVM’s libc, we mark all functions in headers as “LIBC_INLINE”, which Fuchsia defines as “[[clang::internal_linkage]] inline” to force internal linkage. The important question is whether an implicit constructor or destructor would ever be not inlined, thereby causing this problem. If it can happen, then I need to make all the constructors/destructors for structs explicit so they can be marked with “LIBC_INLINE”. Aside from being a pain, this also changes the initialization semantics of the structs, which would require rewriting everywhere the structs are used. Needless to say, I’d like to avoid this if at all possible, so I’m hoping that these implicit constructors/destructors are trivial enough they will always be inlined.
AFAICT inlining the ctor/dtor is something that LLVM decides, not Clang; so, the ctor/dtor would always be COMDAT (unless the struct is in an anonymous namespace, which I assume it wouldn’t be in your headers). The only reason you would not see COMDAT in the object file is if all calls are inlined; in that case, the out-of-line definition has no uses and won’t be emitted to the object file. Triviality (in the C++ standard’s sense) has nothing to do with it, it’s all dependent on LLVM’s inlining decision-making.
As for “would an implicit ctor/dtor ever not be inlined” it’s easy to see an implicit ctor not being inlined, just use -O0.
The only way I know of to guarantee inlining is with the always_inline attribute, which obviously means (in your case) that the ctors/dtors would have to be explicit.
For more details on what Fuchsia is trying to do, here is a section of the relevant doc (shared with permission, clarifications in [brackets]):
“[W]hen code is compiled for a special environment with special ABI requirements, those requirements extend to all the functions it calls. If any [header] library functions it called weren’t inlined, then it generates COMDAT definitions for them that meet those requirements. However, when linked with other translation units that made their own COMDAT definitions of the same [header] library functions, it might actually wind up calling one of theirs instead of its own. This is bad if that other translation unit wasn’t compiled to meet the same special ABI requirements. For example, code in the C library’s startup path is responsible for initializing the thread pointer and setting up the Fuchsia Compiler ABI, so its own code cannot rely on that ABI.”
The specific example I was given is compiling memcpy, since it may be needed for both startup code and other library code.
Hopefully this is enough detail, and thank you for your help.
Does llvm-libc make the distinction between hosted and unhosted? I remember that being a thing, with different requirements, back when I had to deal with library stuff. It sounds kind of like the startup code wants the unhosted version/subset of libc, which ought to be built in a way that (as you’ve pointed out) doesn’t end up using COMDAT. Maybe the anonymous namespace trick would be helpful in that situation?
Yes, that is a classic ODR violation. To handle this, you have to go outside of what the C++ standard provides.
I think you should be able to address this using the abi_tag attribute. See ABI tags — Clang 18.0.0git documentation. This allows for there to be only one visible definition per TU, but for each ABI-dependent variant to be given a different mangled name.
If you want to make a static library with a well-defined interface, I would look into -fvisibility=hidden, ld -r, and objcopy --localize-hidden. I think there are some gaps here, but those are the existing tools for producing a static archive that has a well-defined interface.
If all the libc-internal functions are correctly in a private namespace, and only defined in libc-private headers (which I assume they would be, given that the public interface is only C), then how could any other object file ever have a definition which might conflict, in the first place?
There’s some distinction made between hosted and unhosted in our build system, specifically the startup code is treated as a “startup” object, and built with special compile options (-fno-omit-frame-pointer -ffreestanding). This is (as far as I understand) the source of the problems for Fuchsia. They are building their startup code in a similar manner, then trying to link it with the main code, which is causing the issue.
I’ll have to look into ABI tags, those seem like a good way to avoid tagging implicit functions by instead tagging the entire class.
From what I remember, the reason we have these macros in the first place is to avoid an additional objcopy step. It works, but is more difficult to work with in most cases than building it in a way that doesn’t require that step in most cases.
The problem comes when two different parts of the libc itself are built in different ways and linked together, specifically the startup section. Both startup and the rest of the libc use internal_memcpy, and ours is implemented in headers.