neither gcc nor clang are compliant with standard c++
25 points by raymii
25 points by raymii
I have encountered a compiler that enforces this distinction. It was inconvenient, and “extern static” or “extern” in anonymous namespaces (so you could mention the C linkage so you could pass the function pointer to pthread_create say) tended to confuse tooling and people.
Not to worry, EDG has you covered: https://godbolt.org/z/vxEPb713e
https://www.edg.com/faq/customers#:~:text=but%20Intellisense%20uses%20our%20front%20end%2E is where I would insert that Police Squad gif of all the people slapping their foreheads
But, given 2026 is half-way over I guess Microsoft (and allegedly Intel) are about to save themselves $250,000/yr and here's hoping EDG picks a clang and/or gcc friendly license on the way out (the blog post cited by the footnote didn't offer a license choice)
With regards to C and C++ calling conventions, the one famous? infamous? "difference" is of course C++ classes! Let's say you're a C absolutist and will never write a single line in any other language, but want to interact with a C++-only library, maybe a dylib: that C++ library will almost certainly expose some functions that return non-POD class objects by-value:
// LibString has a copy constructor and destructor - it is non-POD.
class LibString;
class Foo {
public:
LibString toString() const;
}
What do you do? Here's your first attempt of binding Foo::toString:
// Let's say the LibString class internals look like this:
struct LibString {
void *data_;
}
// Foo::toString
LibString _ZNK3lib3Foo8toStringE(Foo *this);
int main(int argc, char *argv[])
{
Foo *this = newFoo(); // we had some constructor binding somewhere
LibString result = _ZNK3lib3Foo8toStringE(this);
}
You run this and it segfaults. Damn! What's up with that? Well, you forgot about non-POD classes always being pass-by-reference. Here's what you should do instead:
// Foo::toString
LibString *_ZNK3lib3Foo8toStringE(LibString *out, Foo *this);
int main(int argc, char *argv[])
{
Foo *this = newFoo(); // we had some constructor binding somewhere
LibString result;
LibString *result_ptr = _ZNK3lib3Foo8toStringE(&result, this);
}
Now things work, horrah!
I am not aware of any way that C make the struct LibString be automatically passed and returned by reference/pointer. Maybe that's for the best, since copy and move constructor and destructor questions will follow very soon after you've done this.
P.S. I've never really written a line of C in my life, so the above snippets are probably all wrong.
Well yes, if you're calling a function from C with an incorrect calling convention it will go wrong. You would get the same calling any function in any other language that uses a different calling convention.
That's not "infamous": that's a basic feature of different languages having different calling conventions. This is not a matter of struct vs class difference either: it's a matter of "is this type a POD type or not?".
The fact that you can construct the mangled name of the function is not relevant: you could construct the mangled name of a function from any other language (swift, rust, D, whatever), and get the same problem from calling with the incorrect calling convention.
You created a type that has the same in memory structure: so what? it's not the same type of object, the C++ object is not defined by just the memory layout, the constructor is part of its ABI. Your C "binding" is not the same object, it does not have the same ABI. Again: looking the same in memory is not relevant: the existence of a constructor is part of the type definition, if your definition of the type does not include that, your type is not the same.
Acting like this is some C++ specific problem is nonsense: If you want to call code that is using a different calling convention that means FFI.
The original post ends with the following sentence:
the calling conventions for c and c++ are identical on, as far as i can tell, pretty much every platform
I was making a point that this is not exactly true.
I interpreted that statement as saying that the calling conventions in this case are identical.
What you are showing as a "problem" is a function being called, and expecting the wrong type, and so it is calling with the wrong calling convention. e..g in your example LibString case both C and C++ have the same calling convention because it's a pod type, and because out of necessity C++ has the same calling convention for pod types that C does. But the moment you have a non-pod type the memory layout is irrelevant, the calling convention of a pod type is not the same as a pod type.
Now, it remains annoying to me (from a perf PoV) that most (all?) C++ objects with default copy constructors still take the non-pod path, but that's a perf issue, not a weird ABI difference. e.g clang has the trivial_abi attribute that will move C++ objects as if pod types, but the constructors and destructors still have to be run as part of the calling convention, and it causes interesting interaction consequences.
in your example LibString case both C and C++ have the same calling convention because it's a pod type
I did purposefully make the C++ forward declare LibString and added a comment mentioning that it is non-POD. Nitpicking, but I also never used the word "problem" - this isn't a problem, it's just a difference in calling conventions, and a thing one lives with when doing FFI.
the moment you have a non-pod type the memory layout is irrelevant it's not the same type of object, the C++ object is not defined by just the memory layout, the constructor is part of its ABI
I would push back on this a little: the ABI changes, but the bits in the memory layout of course do not magically become special bits with pixie dust on them. Or in other words, a struct definition in C matching the memory layout of a class in C++ can be used to represent an object of that class. (Admittedly I think this isn't true when using CFI, so we'll have to assume I'll never use that.) If you manually write out the correct ABI and uphold other requirements of the class, then you've successfully used a C++ (or any other language) 's object from C or whatever language you're holding. (As an example, I've had a lot of fun binding directly to a C++ dylib from Deno using its FFI ABI. Sharp corners abound!)
FFI is terrible and treacherous waters where we usually think of the C ABI as the only relatively safe passageway, but it is not immediate UB to cross those waters outside that route either.
Now, it remains annoying to me (from a perf PoV) that most (all?) C++ objects with default copy constructors still take the non-pod path
I haven't really kept up with the trivially_copyable / trivially_relocatable things at all (I don't really write C++ at work or at home so I have little reason to), but it would definitely be nice if it became possible to avoid the non-POD path for simple classes (and most/many classes are simple after all).
Seems like the winning strategy would be to make a small C++ file that wraps what you need
extern "C" {
void* foo_new() { return (void*) new Foo(); }
char* foo_to_string(void* foo) { return foo->toString().c_str(); }
}
then your C can remain readable and you don't have to worry about name mangling rules changing across platforms (I don't know if they do, just being defensive)
worry about name mangling rules changing across platforms
MSVC does indeed have its own name mangling scheme.
small C++ file
For a C absolutist this would be an insurmountable barrier: I believe their choice would instead be to write a preprocessor macro for doing mangling automatically:
LibString *CPP_MANGLE(lib::LibString lib::Foo::toString())(LibString *out, Foo *this);
For this strawman C absolutist, they would rewrite the C++ code in C, rather than depending on C++.
Fair! :D
Admittedly... I'm sort of the absolutist (way to tell on myself, eh?) except that the language is Rust instead of C. I've been doing this sort of C++ dylib binding crimes lately, just for the fun of it:
#[link_name = "\0_ZNK3lib3Foo8toStringE"]
extern "C" fn lib__Foo__toString(out: *mut LibString, this: *mut Foo) -> *mut LibString;
I'm only up to perhaps 10-20% mental readiness for the rewrite :D
this "solution" causes me great anxiety o_o please use an intermediary C++ file and C header with extern+bindgen, at least!
Heh, but this way it's more fun! :) As long as I'm just playing around anyway.
I myself have put custom re-mangling into production, so I secretly have zero room to complain. It is... shockingly fragile.
Ooh, cool!
I have probably hundreds of C++ functions that I now bind to, so writing a manual listing would be too much work to bother. A smart person would still somehow create or generate a C++ glue file of some sort, but that just doesn't speak to me. I don't want a second compiler into the build process :) (although I am using g++ for compiling oracle files that I use as layout and vtable layout tests)
So I'm honestly thinking that I might write a beautiful little macro that would mangle a copied off C++ function definition into the symbol name directly. Now that'd be a fun thing!
Does complying with a C++ standard benefit me in any way?
Do I get any "write once, run anywhere" points from doing so?
You usually do get "write once, run anywhere" points for doing so - assuming you're okay compiling once for each platform, which is pretty easy to do nowadays.
It's like anything else, though... if you think about cross-platform support up front and be proactive then you'll have an easier time than if you don't think about it and just hope it works.
This sounds like a bug the gcc and clang people may fix. Do you want to fix your code when they do?