[RFC] Clang with GCC2 C++ ABI

Overview

I am working on a Clang backend that compiles C++ code with an ABI compatible to GCC 2.95 (referred to as GCC2).

This would be enabled by the -fc++-abi=gcc2 option.

Features

The changes would include:

  • Name mangling.
  • C++ runtime support function names.
  • RTTI.
  • Vtable layouts.
  • DWARF2 exception handling information.
  • Other GCC2-specific ABI quirks.

Scope

Unlike Clang’s MSVC compatibility feature, this effort aims to provide only a reasonable level of binary compatibility only. Source compatibility is not included. This means:

  1. Changes will strictly be in CodeGen only. No Sema changes will be submitted.
    • This means, to achieve the same result, slightly different code may need to be used for modern Clang than GCC2.
  2. The majority of changes to the shared CodeGen layer will come in the form of ABI/Layout/Vtable hooks and dispatchers/factories that call these hooks.
    • Most changes will be done by overriding existing virtual functions. These implementations will be in separate files.
    • A limited number of additional hooks will allow the GCC2 backend to provide additional customizations to CodeGen. These hooks will have trivial default implementations that work for both MSVC/Itanium ABIs.

Motivation

GCC2 uses an ABI that is fundamentally incompatible with GCC4+ and Clang, which use the Itanium ABI.

This forces projects relying on ABI-compatibility with GCC2-era binaries, such as the Haiku Operating System, to stick to legacy codebases and legacy C++ versions.

Clang’s modular design, especially after its support for the MSVC ABI, makes it easier to develop a legacy ABI backend while unlocking the latest C++ features provided by the frontend parser.

Impact

Codebase

The implementation should be designed as a Peripheral Tier feature. If not possible, it should aim to follow the tier’s guidelines of sub-community maintenance and minimizing breakages and disturbance as far as practically possible for a tightly integrated project like Clang.

Most of the ABI implementation should be isolated into GCC2-specific files Outside of these files, changes should only be related to command parsing (e.g. supporting -fc++-abi=gcc2) and dispatching to the relevant GCC2 counterparts (as described in the #Scope).

Maintainability Costs

Newer C++ versions may introduce runtime features entirely unknown to GCC2. The most likely affected components is mangling and RTTI, while others like Vtables will more likely incur a one-time cost.

For these features, we will let the compiler fail only when GCC2 is specified, until any interested volunteer handles them. This behavior is already visible in other less-supported targets. For example, with --target=armv7-windows-msvc:

  • Until very recently, SEH __try/__except/__finally will simply return a compile error.
  • C++ try/catch, an important language feature, still crashes the compiler.

Since new C++ features are unknown to GCC2 and never used in legacy binaries, there will be room for implementations that are more lax/creative.

Review Costs

We will try to minimize review costs to the LLVM maintainers.

Changes will be small (aiming for < 100 lines of diff for non-GCC2-specific files) with C++98 CodeGen tests acting both as a quick feature demonstration and protection against regression.

While AI tools will be used for exploration, all upstreamed patches will be hand-crafted, following the AI Tool Use Policy.

To prevent wasteful code review for stubs that never get implemented, PRs will only be opened once feasibility has been demonstrated downstream. For this purpose, a software package already supported by the Haiku operating system’s official repository (HaikuPorts) will be chosen and ported to the GCC2 ABI runtime environment. The software must be sufficiently complex (with 10+ C++ source files and a non-trivial build system).

Testing

CodeGen tests will be provided, allowing the project to:

  • Safeguard against regressions in new pull requests.
  • Provide some form of documentation for the expected behavior in GCC2 binaries, aiding the code review process.

However, these tests may:

  • Slow down development speed for newer language features.
  • Add overhead to the CI time.

Therefore, the effort aims to provide tests that:

  • Are restricted to standard C++98, preventing interference with modern C++ features.
  • Are restricted to “non-buggy” behavior reproducible in GCC2. “Non-buggy” should mean cases where GCC2 is already standards-conforming or agrees with modern Clang in how to interpret the relevant source code.
  • Are simple (the entire GCC2 CodeGen suite should take <1s total CI time).

:cross_mark: This RFC was rejected in this message.

I am working with an exploratory branch here: GitHub - trungnt2910/llvm-project at dev/trungnt2910/llvm-gcc2 · GitHub.

Right now, it is a proof-of-concept that supports mangling, RTTI, Vtables, and exception handling. It can compile and cross-link with GCC2 for “Hello, World!” binaries, but there are still edge cases that have not been fuzzed and handled correctly.

I do not think it is reasonable to try to make that work. I’m not opposed to try accommodate the haiku use case is there is some signal this is valuable to a significant amount of users, but in a restricted fashion.

i.e, GCC 2.95 existed in the C++98 time frame, so it’s reasonable to not accept that ABI for C++11 or a version of C greater than C99. And I would like these combinations of ABI flags and language modes to be simply ill-formed at the driver level

i.e I’m okay to support an existing ABI (is there any documentation?) - but not to start inventing things for folks who want to mix 20 years old binary with C++26 features.

The haiku documentation - which was updated 5 years ago mentions this only affects 32 bits x86 (which in itself is not where most of our users are going to be) Haiku compilers — Haiku internals documentation

4 Likes

The intention of this was not to, say, load a BeOS library and have the host app use std::meta or coroutines.

It is more like, for example, adding some consteval helper functions to Haiku headers, or using nullptr in system code - which are both helpful and incur 0 binary footprint.

It is true that the GCC2 ABI is only used for 32-bit x86. But to ensure x86_gcc2 compatibility, every Haiku system library and header is still stuck in C++98.

Thank you for the RFC! I think there are a few challenges worth thinking about:

  • GCC 2.95 was C++98 and the Haiku headers are stuck there, but WG21 has resolved a lot of DRs which could impact C++98 support. So “What is C++98?” is a different answer between what GCC 2.95 thought and what Clang 22 thinks. It’s possible ABI compatibility for the features of GCC 2.95 isn’t feasible.
  • Even though you propose it for peripheral tier support, Clang doesn’t really have that notion because of the language support is a core component of the compiler. e.g., we still have to do something to handle newer language features like coroutines, etc. It’s not really clear from the RFC what the expectation is there – should those constructs be rejected in this mode?
  • We’d still expect sufficient test coverage to ensure we don’t regress behavior, but “test coverage” in this case is basically the full languages we support. So will there be a build bot configured to run tests in this mode to ensure we don’t crash, etc? Or what is the testing plan?

(I’ll likely have some other questions while thinking about this, just some early ones that came to mind.)

2 Likes

If I understand correctly that what you’re after is more modern language frontend that matches GCC 2.95 ABI for the features that antiquated compiler supports, I don’t have objections in principle. However:

  1. Is there a commitment to maintain your new code upstream for a prolonged period of time?
  2. What kind of testing and CI will be provided?
  3. Expanding on Aaron’s point, even if you implement all you have in mind, are you sure you’ll get what you’re after? Our C++98 mode is actually C++03 mode, which implements almost all 92 defect reports that were resolved in the meantime between those Standard publications. For instance, CWG33 expanded the set of associated entities of ADL when address of an overload set is involved. Or CWG108 made classes nested within class templates dependent even when named within the enclosing class template. Search for “TC1” in Clang - C++ Defect Report Status. Every time the table says anything but “Unknown”, there is a test in clang/test/CXX/drs that you can use to compare the behavior of modern Clang and GCC 2.95.

If it turns out that you need some non-trivial changes in semantic analysis to match non-conforming behavior of GCC 2.95, I’m likely to raise objections to the entire effort.

3 Likes

Thanks for the reply! Please let me know if any of the following resolutions work and if I should include them in the RFC description:

  • On the first point:
    • The C++98 used by Haiku is the union of the C++98 in GCC2 and the one in GCC 13, which should be much closer to what Clang 22 supports.
    • Changes will be restricted to the code generator to ensure that the generated functions and structures are compatible with GCC2. Any potential workaround will only be from the backend looking at the symbol information to generate the expected output. The effort will not guarantee that Clang should parse like (or attempt to add Clang frontend support for) GCC2 parsing behavior.
  • On the second point:
    • I would imagine simply letting the compiler fail: reject the construct and emit an error message whenever we can, or just crash. Crashing sounds undesirable, but it is already the case for some less well-supported targets, such as C++ EH for armv7-windows-msvc.
    • To minimize community impact, when upstream, we can guard the support under a language flag at the driver level or guard the GCC2 module under a configuration-time flag.
  • On the third point:
    • The test plan would be adding an isolated set of Clang CodeGen tests to verify code generation.
    • All tests run strictly under --std-c++98 or --std-gnu++98 (and the newly proposed -fc++-abi=gcc2). No tests under CI will touch new language features to prevent slowing down development.
    • Tests will only be added for constructs that can be reproduced under GCC2 for clearer verification.

Gcc didn’t implement ADL for templates correctly until at least gcc 4.2.0. Even for c++98/03.

As a big example of what 2.95.3 compatibility will just go wrong.

Namespaces for std was also disabled too …

  1. Yes, I have been involved in Haiku-related ports for years (see this repo for an example). If the support can get more mainstream, it may attract even more contributors.
  2. As mentioned above, testing will cover code generation behavior proven in GCC2. It will be restricted to C++98 to avoid blocking mainline CI on new language features. If dedicated coverage for the feature is required, I can set up infrastructure on GitHub Actions, like I did with this repo.
  3. As covered in my response to Aaron’s point, we will only support C++98 in its modern form. This will not be a drop-in replacement to GCC2, only an ABI-compatible port (and so we may, for example, require the source code to be slightly different to have the same ABI output). The frontend parser will remain untouched.
1 Like

To further address the commitment concerns in 1:

If this RFC gets approved, I will only upstream patches once the proof-of-concept reaches a stage where it can compile sufficiently complex projects (for this purpose, I will select a real software package ported on HaikuPorts with multiple (let’s say 10+) files and a non-trivial build system).

This is to prevent half-baked patches to be upstreamed without serving any useful functionality in the foreseeable future.

1 Like

Tangentially related: I/we (Carbon folks) are looking at what it might be like to have custom ABI support in clang for an external language like Carbon - so if there’s other ABI support work happening, especially with a desire to limit invasive integration with Clang - that sounds interesting.

Ie: if some of this proposal moves forward, perhaps it could be an opportunity to localize more ABI knowledge into the ABI hierarchy (eg: I think currently relative vtables aren’t implemented purely in the ABI - nothing outside the CXXABI derived classes should know about/query for “is itanium” or “is relative/absolute vtable layout”, etc, we should add hooks into the ABI that expresses the functionality in all those places.

(& then eventually - I haven’t looked at the current API usage - but a clang-as-API user like Carbon could inject a custom ABI without touching any Clang code)

2 Likes

I have updated the RFC to address the initial round of concerns.
Most notable changes:

  • A “Scope” subsection is added, explicitly ruling out changes to the frontend and drop-in GCC2 source compatibility.
  • Clarify what should be done for unsupported features.
  • Add a “Testing” section with my proposal to balance coverage and CI cost/developer velocity impact.

An interesting example for the third point that I just discovered:

GCC 2.95 somehow supported using a double as a non-type template parameter (it lets the user off with a warning) and had their own mangling rules.

Clang rejects this behavior unless set to C++20 mode.

If this behavior were to be seen in MSVC, a patch would be sent to relax this behavior under -fms-compatibility. However, under this RFC, for the GCC2 support effort:

  • We will still follow Clang’s frontend and reject this construct, unless C++20 is specified. No patches to the frontend relaxing this behavior will be sent.
  • If C++20 is specified, the GCC2 mangler (which should not know anything about the C++ version) will still emit GCC2-compatible mangling for the double non-type template parameter.

This seems wrong. Maybe gcc2-compatibility should be the option.

The context was a hypothetical situation where MSVC had such a behavior and contrast it with this RFC’s scope.

There will be no such flag under this RFC.

IIUC (Please correct if wrong!), the underlying goal here is the following:

Haiku OS wishes to continue to support running existing binary-only applications built for BeOS way-back-when. (Aside: Are there really any of those which matter? And why not just use a full-system emulator running a legacy copy of the entire BeOS?)

Anyhow, currently, in order to keep compatibility with these apps, Haiku builds the current versions of its system libraries twice: once with modern GCC, for modern apps to link against, and once with GCC 2.95, for legacy apps to link against. Since it’s not currently possible to generate code compatible with the GCC 2.95 C++ ABI using anything but GCC 2.95, the current versions of these system libraries must continue to be written in old C++, so it remains possible to build with GCC 2.95.

Haiku would like to find a solution where it’s able to use modern C++ features in the internals of its system libraries, while still exposing the same ABI.

If that’s correct, then the problem is a lot more limited than “GCC 2.95 ABI compatibility”. Instead, it’s “Compatibility with the fixed, legacy, BeOS ABI, as invoked by actual applications”. There is no need to support arbitrary object or function ABIs, only exactly those objects and functions which were exposed by the legacy BeOS libraries and used by real applications that someone wants to run.

This proble,m seems like it’d be more easily solved with an ABI adaptation layer: write a set of shim libraries exposing the required ABIs, and have them call the modern library to do the real work. Once the shim is written, it never needs to be extended to support any new APIs, because new apps should use modern ABIs.

The shim could either be:

  1. C++, compiled with GCC 2.95, which calls through to the underlying actual library implementation through a set of C wrapper functions (which are at-least-mostly ABI compatible), or
  2. Custom-written C structs and C functions (and possibly some asm), which, when built with a modern compiler, simulate the GCC 2.95 C++ object layouts and C++ function-name manglings.
2 Likes

This sounds like a great idea!

However, to build a shim that bridges between the old and new ABIs, one would need to implement all the features mentioned in the RFC (mangling, RTTI, Vtables, EH etc.).

This would require at least a similar amount of code than writing as a Clang backend if written from scratch as a standalone code generator (maybe even more without the Clang shared abstractions). On the other hand, I believe the C++ API is vast enough to making hand-written wrappers impractical.

You would also need to marshal objects that are fundamentally different in terms of layout to and from function calls. Adding exception marshalling on top of that, a Clang backend does not sound like too much work in comparison while offering much more potential benefit.

I actually have a pretty big concern: The Clang-IR effort (and MLIR in general) is moving towards using a new ABI engine created during a GSOC (@nikic , @makslevental were mentors, I don’t see Narayan on discourse?).

So whatever ABI work is done will likely be lost when we transition to CIR unless this effort is coordinated. Please make sure you work with that effort/clangiR to make sure this isn’t something that gets lost along the way.

3 Likes

Thanks for the heads-up!

From my research, the project you are referring to is this: GSoC 2025: Introducing an ABI Lowering Library - The LLVM Project Blog, whose implementation is here: [WIP] ABI Lowering Library by vortex73 · Pull Request #140112 · llvm/llvm-project · GitHub.

Based on my understanding, the ABI library project is mainly focused on the C ABI (e.g. calling conventions for different architectures). It does touch some aspects of the C++ ABI such as record layout, but it seems to fetch such information from an abstraction (instead of being aware of, for example, Itanium vs. MSVC differences).

The GCC2 backend effort under this RFC does not touch C ABI or the IR representation. Theoretically, with the GCC2 support addition, one could target i686-w64-mingw32 with -fc++-abi=gcc2 and get valid objects without knowing anything about PE, MinGW, or Windows.

From what I’ve seen in review, it is changing a lot of how register selection/in-reg/etc(basically everything about how arguments are passed), which is very much the C-ABI of course.