Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC/Proposal: Turning Zig target triples into quadruples #20690

Open
Tracked by #8
alexrp opened this issue Jul 20, 2024 · 23 comments
Open
Tracked by #8

RFC/Proposal: Turning Zig target triples into quadruples #20690

alexrp opened this issue Jul 20, 2024 · 23 comments
Assignees
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@alexrp
Copy link
Member

alexrp commented Jul 20, 2024

Introduction

At the moment, a Zig target triple (without versions) generally has an obvious 1:1 mapping to a GNU target triple, with only a few exceptions. In this issue, I propose that we completely break with the GNU style of target triple and, in particular, make the choice of libc and ABI two distinct components of the triple. This will enable Zig target triples (then quadruples) to communicate information that they can't today and handle a much wider range of ABI options.

Background & Motivation

Zig target triples, AFAICT, have the goal of completely replacing the -march, -mcpu, -mtune, -mabi, and -mmacosx-version-min options with a single, unified -target option to cover them all. -mcpu hasn't been integrated in the triple yet, but that work has been accepted in #4584 (and this proposal builds on that one). The only option that remains after that is -mabi which is notably missing from zig build-obj and friends, presumably on the assumption that the third component of the triple ought to cover it.

I've spent the past few weeks doing what I think is a fairly exhaustive survey of the ISA and ABI landscape. I put the cutoff point roughly around the mid-1970s; anything prior to that is for all intents and purposes super dead and unlikely to change any of my conclusions here. Having read through ISA manuals and ABI documentation for basically every architecture that I could find a manual for, my conclusion is that there is far more nuance to ABI choice than the current style of target triple allows. This isn't theoretical either; I'll demonstrate some real cases where the current approach falls short in ways that actually matter, and I'll show why the current approach can't scale in the long term.

I'll go over just a few architectures here; there are others that would also illustrate the point well (e.g. SuperH and m68k), but I hope the following are sufficient.

RISC-V

RISC-V is the hot new ISA on the block, so it's probably the most pertinent example here. It currently defines the following ABIs:

  • ilp32 (full soft float)
  • ilp32f (soft f64 and f128; hard f32)
  • ilp32d (soft f128; hard f32 and f64)
  • ilp32e (full soft float; reduced register set)
  • lp64 (full soft float)
  • lp64f (soft f64 and f128; hard f32)
  • lp64d (soft f128; hard f32 and f64)
  • lp64q (full hard float)

zig build-obj and friends simply have no way to select between these ABIs at the moment. We would have to add ABI tags for all of them, and we can already see that adding those for each ABI (plus glibc variants, plus musl variants) is going to get way out of hand. Additionally, I believe there are more ABIs to come; I've heard talk of ilp32ef, for example, and RV128 ABIs will presumably materialize at some point.

Also worth noting here is that our current strategy of adopting gnueabi and gnueabihf (and musl variants) to differentiate hard float vs soft float starts to break down once unusual float ABIs (32-bit and 128-bit) enter the picture.

(Incidentally, these ABIs also show why the current std.Target.FloatAbi definition is not nuanced enough.)

LoongArch

LoongArch has roughly the same situation as RISC-V, minus the Q and E extensions:

  • ilp32s (full soft float)
  • ilp32f (soft f64; hard f32)
  • ilp32d (full hard float)
  • lp64s (full soft float)
  • lp64f (soft f64; hard f32)
  • lp64d (full hard float)

An interesting thing to note here is that LoongArch is so far the only architecture I'm aware of to have done the sane thing and made the ABI actually, ya know, part of the ABI component of the target triple. So there's -gnu for ilp32d/lp64d, -gnuf32 for ilp32f/lp64f, and -gnusf for ilp32s/lp64s. Good job! (It used to be -gnuf64 instead of -gnu, but they simplified it because it's expected to be the common case.)

PowerPC

PowerPC (or Power ISA) has had a long list of ABIs over the years, being a fairly old architecture. Some never really saw practical use (e.g. the Windows NT ABI). It's a bit hard to categorize the ones that are actually relevant, but I think they can roughly be put like this:

  • SVR4
  • EABI
  • Apple ABI
  • ELF v1
  • ELF v2
  • AIX

(Notably, Zig's current use of the bespoke powerpc-linux-gnueabi(hf) triples is quite unfortunate because it implies an association with the PowerPC EABI, when that is not actually the case.)

In addition to these broad-strokes ABIs, there are variations based around the definition of long double. musl has done the simple thing and just declared that only long double = double is supported. Unfortunately, the rest of the world runs on one of two other definitions - either IEEE binary128 or the 128-bit "double-double" format that IBM came up with. The latter is unfortunately still very common, with binary128 only seeing limited use on newer powerpc64le distros (see #20579 for details). That last point is a real problem for Zig; zig build-obj and friends default to the "double-double" format with no way to switch to binary128 for the distros where this is the default.

But wait - there's more. Some of the above ABIs also have vector variants for efficient AltiVec usage. And of course there are also soft float variations of some of them.

And it gets much worse. You can also use a plethora of options such as -malign-natural, -malign-power, -maix-struct-return, and msvr4-struct-return to explicitly override various aspects of the aforementioned ABIs.

There's a near-incomprehensible number of possible combinations here.

MIPS

MIPS sits somewhere between RISC-V and PowerPC, with a lot of ABIs and configurability within those ABIs:

  • EABI
  • O32
  • N32
  • O64
  • N64

(MIPS support in Zig currently has the same issue as PowerPC where gnueabi/gnueabihf are used to distinguish soft float and hard float, despite neither being EABI-based.)

In addition to these, there are soft float variants and single/double-precision variants. There are also the FPXX and FP64A variants. Like PowerPC, options abound for overriding various aspects of the chosen ABI.

Miscellaneous

Some extra notes that apply to various architectures on top of what I've already written above:

  • ILP32 ABI variants (i.e. 32-bit pointers on a 64-bit machine) exist for at least Arm64, Itanium, and x86-64 (in addition to the aforementioned architectures). Note how they're called different things: x32 for x86-64, gnu_ilp32 (or aarch64_32 as arch) for Arm64, and N32 for MIPS.
  • Quite a few architectures (RISC-V, Arm, SuperH, and some others I forget) have an FDPIC ABI that is meant for systems without an MMU. I believe it functions as an addition to the base ABI, but I haven't dug too deeply into it.
  • It's probably obvious at this point, but just to make it explicit: GNU triples simply cannot represent most of the ABI nuance I've described so far.
  • If we ever add more libcs than the ones we have now (ziglibc?), status quo will start to get ugly fast, even putting aside everything else I've brought up here.

Proposal

Hopefully I managed to convincingly get the point across that the current target triple format is not scalable enough for the task it's meant to achieve. Now I'll describe my idea for fixing this situation and future-proofing std.Target.

Most importantly, the triple should be replaced with a quadruple. Concretely, I'm proposing that it should now be of the form:

<arch>[.<cpu>[+~feats]]-<os>[.<ver>][-<api>[.<ver>][-<abi>[+~opts]]]

(Where api is basically libc.)

You must now specify: Neither API nor ABI, only API, or both API and ABI.

Some API tags that I anticipate us recognizing would be:

  • system: A special tag used for platforms where there is only and can only be one libc (e.g. libSystem on macOS).
  • none: A special tag for use if you don't want a libc at all (e.g. for the freestanding OS tag).
  • mingw: Uses UCRT via MinGW-w64.
  • msvcrt: Uses UCRT and VCRuntime natively (i.e. requires MSVC / Windows SDK tooling).
  • gnu and musl: glibc and musl respectively.
  • wasi: wasi-libc (modified musl).

The ABI value is more complicated. In order to represent all the nuance necessary, it really needs to be treated in the same way that CPU model + features are. That is, you specify a base ABI and optionally add or subtract options, with the same + and ~ syntax used for CPU features. The available ABIs and options are determined by the selected architecture, just like CPU model and features.

If API and/or ABI are omitted, resolution works mostly as it does today. However, importantly, target resolution is augmented to pick sensible defaults for the ABI based on architecture, OS, and API choices. For example, if you specify powerpc64-linux-musl, elfv2 will be selected as the base ABI, as opposed to elfv1 for powerpc64-linux-gnu. This is because musl by definition requires elfv2. In addition to this, the base ABI also has sensible default options. In the aforementioned example, the complete resulting ABI is actually elfv2+ldbl64 because musl also mandates long double = double. If for some reason you want to override that, you could use elfv2~ldbl64 as the ABI component, but this is expected to be a rare need.

Examples

Here's roughly how each triple in zig targets | jq -r .libc[] | sort would look post-proposal:

  • aarch64_be-linux-gnu -> aarch64_be-linux-gnu-lp64
  • aarch64_be-linux-musl -> aarch64_be-linux-musl-lp64
  • aarch64-linux-gnu -> aarch64-linux-gnu-lp64
  • aarch64-linux-musl -> aarch64-linux-musl-lp64
  • aarch64-macos-none -> aarch64-macos-system-lp64
  • aarch64-windows-gnu -> aarch64-windows-mingw-lp64 (or -msvcrt-lp64+win)
  • armeb-linux-gnueabi -> armeb-linux-gnu-eabi+sf
  • armeb-linux-gnueabihf -> armeb-linux-gnu-eabi
  • armeb-linux-musleabi -> armeb-linux-musl-eabi+sf
  • armeb-linux-musleabihf -> armeb-linux-musl-eabi
  • arm-linux-gnueabi -> arm-linux-gnu-eabi+sf
  • arm-linux-gnueabihf -> arm-linux-gnu-eabi
  • arm-linux-musleabi -> arm-linux-musl-eabi+sf
  • arm-linux-musleabihf -> arm-linux-musl-eabi
  • arm-windows-gnu -> arm-windows-mingw-eabi (or -msvcrt-eabi+win)
  • csky-linux-gnueabi -> csky-linux-gnu-abiv2+sf
  • csky-linux-gnueabihf -> csky-linux-gnu-abiv2
  • loongarch64-linux-gnu -> loongarch64-linux-gnu-lp64d
  • loongarch64-linux-musl -> loongarch64-linux-musl-lp64d
  • m68k-linux-gnu -> m68k-linux-gnu-gnu (not to be confused with -sysv which is older and different!)
  • m68k-linux-musl -> m68k-linux-musl-gnu (likewise)
  • mips64el-linux-gnuabi64 -> mips64el-linux-gnu-n64
  • mips64el-linux-gnuabin32 -> mips64el-linux-gnu-n32
  • mips64el-linux-musl -> mips64el-linux-musl-n64
  • mips64-linux-gnuabi64 -> mips64-linux-gnu-n64
  • mips64-linux-gnuabin32 -> mips64-linux-gnu-n32
  • mips64-linux-musl -> mips64-linux-musl-n64
  • mipsel-linux-gnueabi -> mipsel-linux-gnu-o32+sf
  • mipsel-linux-gnueabihf -> mipsel-linux-gnu-o32
  • mipsel-linux-musl -> mipsel-linux-musl-o32
  • mips-linux-gnueabi -> mips-linux-gnu-o32+sf (see my notes on EABI above)
  • mips-linux-gnueabihf -> mips-linux-gnu-o32 (likewise)
  • mips-linux-musl -> mips-linux-musl-o32
  • powerpc64le-linux-gnu -> powerpc64le-linux-gnu-elfv2
  • powerpc64le-linux-musl -> powerpc64le-linux-musl-elfv2+ldbl64
  • powerpc64-linux-gnu -> powerpc64-linux-gnu-elfv1
  • powerpc64-linux-musl -> powerpc64-linux-musl-elfv2+ldbl64
  • powerpc-linux-gnueabi -> powerpc-linux-gnu-svr4+sf (see my notes on EABI above)
  • powerpc-linux-gnueabihf -> powerpc-linux-gnu-svr4 (likewise)
  • powerpc-linux-musl -> powerpc-linux-musl-svr4+ldbl64+secplt
  • riscv32-linux-gnuilp32 -> riscv32-linux-gnu-ilp32d (yes, this one was confusingly named)
  • riscv32-linux-musl -> riscv32-linux-musl-ilp32d
  • riscv64-linux-gnu -> riscv64-linux-gnu-lp64d
  • riscv64-linux-musl -> riscv64-linux-musl-lp64d
  • s390x-linux-gnu -> s390x-linux-gnu-elf
  • s390x-linux-musl -> s390x-linux-musl-elf
  • sparc64-linux-gnu -> sparc64-linux-gnu-sysv
  • sparc-linux-gnu -> sparc-linux-gnu-sysv
  • thumb-linux-gnueabi -> thumb-linux-gnu-eabi+sf
  • thumb-linux-gnueabihf -> thumb-linux-gnu-eabi
  • thumb-linux-musleabi -> thumb-linux-musl-eabi+sf
  • thumb-linux-musleabihf -> thumb-linux-musl-eabi
  • wasm32-freestanding-musl -> wasm32-freestanding-none-watc (WebAssembly Tool Conventions)
  • wasm32-wasi-musl -> wasm32-wasi-wasi-watc (likewise)
  • x86_64-linux-gnu -> x86_64-linux-gnu-sysv
  • x86_64-linux-gnux32 -> x86_64-linux-gnu-x32
  • x86_64-linux-musl -> x86_64-linux-musl-sysv
  • x86_64-macos-none -> x86_64-macos-system-sysv
  • x86_64-windows-gnu -> x86_64-windows-mingw-sysv (or -msvcrt-win64)
  • x86-linux-gnu -> x86-linux-gnu-sysv
  • x86-linux-musl -> x86-linux-musl-sysv
  • x86-windows-gnu -> x86-windows-mingw-sysv (or -msvcrt-win32)

Note that many triples are still missing here; this is just intended to give a rough idea of how things will look. Also, some of these names would certainly be subject to change and/or bikeshedding during implementation.

Anticipated Concerns

I just want to address upfront some (reasonable!) concerns that I'm almost certain will be on people's minds after reading this:

  • libc and ABI choice are linked: This is true, as any given libc only supports certain ABIs. But note that I could make the exact same argument for architecture and OS, OS and combined libc + ABI, etc. This argument would be flawed for (at least) the same reason it is here: Combinatorial explosion. Additionally, note that some architecture/OS combinations also impose restrictions on the ABI. The reality is that every component of a target triple imposes semantic restrictions on the other components, even prior to this proposal. Finally, some ABI options actually are completely independent of libc.
  • This is more complicated than before: As I've hopefully demonstrated, this is only so because the status quo is unscalable and, in a case like long double on PowerPC, downright unworkable. It's easy to keep things simple if you don't account for all cases. (Quoting zig zen: Edge cases matter.) Also, writing code for real hardware is necessarily more complex than e.g. a bytecode VM. I think it's actually entirely reasonable to ask that people understand the basics of their target environment, especially in cross-compilation scenarios. The behavior of picking sensible ABI defaults based on other components should help here. For native builds, things stay simple.
  • Deviating from GNU triples this much will add a learning curve: I simply think the benefits outweigh the costs here. Andrew has stated before that Zig wants to be able to target a much wider variety of platforms than, say, LLVM - including old ones. I think this proposal (or something equivalent, at least) is a clear prerequisite for that goal. Also, just because GNU triples have become ubiquitous, it does not follow that they are good. They are in fact not good for a long list of reasons. (Quoting zig zen again: Avoid local maximums.)
@ifreund ifreund added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 20, 2024
@ifreund ifreund added this to the 0.15.0 milestone Jul 20, 2024
@mnemnion
Copy link

This is a well-written proposal.

I'm going to provide what I consider the obvious push-back on it: Some triples can have multiple ABIs, but many cannot. For the ones which do, there's often an obvious default, and furthermore, it's a default which is lowest-common-denominator: code compiled for that ABI target will run, perhaps not optimally, on systems supporting one of the more advanced/enhanced ABIs.

Given that, what's the argument for including it in the now-quadruple? I look at the example list, and I'm not even 100% sure that the remaining info needs to be an ABI tag, it looks more like a list of supported CPU "extras". Mostly we're talking about presence or absence of hard and soft floats in various widths, I'd add vector instructions to that consideration. This is something which could affect source code in a way which is independent of the triple itself (example: providing the classic integer-casting fast inverse square root for soft float platforms, because it isn't actually faster on modern ISAs).

Why does this need to be a part of every Target, rather than continuing to conflate the libc with the base ABI, and provide a list of CPU features which is as independent as possible from the triple?

To be clear, I'm not persuaded either way, I just think that the issue would benefit from a focused answer to this question.

@alexrp
Copy link
Member Author

alexrp commented Jul 20, 2024

I'm going to provide what I consider the obvious push-back on it: Some triples can have multiple ABIs, but many cannot. For the ones which do, there's often an obvious default, and furthermore, it's a default which is lowest-common-denominator: code compiled for that ABI target will run, perhaps not optimally, on systems supporting one of the more advanced/enhanced ABIs.

Can you give an example of this? I can't immediately think of a case where this is true. The ABI fundamentally affects the calling convention. In general, post-proposal, a given ABI can only be considered compatible with another ABI if the base ABI and option set both compare exactly equal. That is how an ABI works by definition.

Given that, what's the argument for including it in the now-quadruple? I look at the example list, and I'm not even 100% sure that the remaining info needs to be an ABI tag, it looks more like a list of supported CPU "extras". Mostly we're talking about presence or absence of hard and soft floats in various widths, I'd add vector instructions to that consideration. This is something which could affect source code in a way which is independent of the triple itself (example: providing the classic integer-casting fast inverse square root for soft float platforms, because it isn't actually faster on modern ISAs).

It's not independent. If you compile a function using f32 parameters for riscv32-ilp32 and then try to call it from code compiled for riscv32-ilp32f, you will get bogus results because the former passes floating point values in integer registers, while the latter passes them in floating point registers. (Well, more likely you'll get a linker error, but that's besides the point.)

@mlugg
Copy link
Member

mlugg commented Jul 20, 2024

I really like this proposal -- it deals with an awkward overloading of the term "ABI" (which can be downright confusing), and provides a lot more flexibility around how ABIs are defined. I think your arguments for it are really solid.

One question that does come to mind is whether mandating the user to always specify the ABI when they want to specify the libc is reasonable. For instance, on Linux, it feels a little silly to have to clarify the -sysv bit just because I want to use glibc rather than musl -- yes, I could technically try to use a different ABI at my own peril, but it would be completely broken, so I never reasonably would. If you take a Linux developer who happens to be aware of glibc vs musl libc, I don't think they are necessarily aware of calling conventions, particularly that the default Linux one is named after System V! So, I wonder: would it make sense to allow omitting the ABI part even when a libc is specified, and have it default to the "obvious" thing based on the architecture + OS + libc? On some targets, that might not exist, and so it would be fine to require it to be specified; but for targets like x86_64-linux, sysv is the only ABI you really ever want to use AFAIK.

@alexrp
Copy link
Member Author

alexrp commented Jul 20, 2024

One question that does come to mind is whether mandating the user to always specify the ABI when they want to specify the libc is reasonable. For instance, on Linux, it feels a little silly to have to clarify the -sysv bit just because I want to use glibc rather than musl -- yes, I could technically try to use a different ABI at my own peril, but it would be completely broken, so I never reasonably would. If you take a Linux developer who happens to be aware of glibc vs musl libc, I don't think they are necessarily aware of calling conventions, particularly that the default Linux one is named after System V! So, I wonder: would it make sense to allow omitting the ABI part even when a libc is specified, and have it default to the "obvious" thing based on the architecture + OS + libc? On some targets, that might not exist, and so it would be fine to require it to be specified; but for targets like x86_64-linux, sysv is the only ABI you really ever want to use AFAIK.

I address this exact point in the open questions. 🙂 I agree that there is merit to this. The current ordering of components in the proposal is so purely for aesthetic reasons. I'm very open to switching libc and ABI around to enable this.

@mlugg
Copy link
Member

mlugg commented Jul 20, 2024

I'm proposing it the other way around: I think changing the libc is probably desired more often than changing the ABI! That said, my dev experience is largely x86_64-centric, so perhaps I'm underestimating how common it is to need to override the ABI on other targets.

@alexrp
Copy link
Member Author

alexrp commented Jul 20, 2024

Oh, you're saying order as proposed but make the ABI optional, ok. Hmm. I'll think on that a bit. I definitely agree that this is the more common scenario on the mainstream platforms.

@alexrp
Copy link
Member Author

alexrp commented Jul 20, 2024

I think I agree with you @mlugg. Especially if we go the route of making OS/libc components imply sensible default options for the ABI component as I described in the open questions. Some examples of how this would play out:

  • powerpc-linux-glibc expands to powerpc-linux-glibc-svr4
    • This is because svr4 is the baseline ABI for powerpc on linux.
  • powerpc-linux-musl expands to powerpc-linux-musl-svr4+ldbl64
    • This is because svr4 is the baseline ABI for powerpc on linux, and because musl mandates ldbl64 for all PowerPC targets.
  • powerpc64-linux-glibc expands to powerpc64-linux-glibc-elfv1
    • This is because elfv1 is the baseline ABI for powerpc64 on linux.
  • powerpc64-linux-musl and powerpc64-linux-musl-elfv2 expand to powerpc64-linux-musl-elfv2+ldbl64
    • This is because musl mandates elfv1 for both powerpc64/powerpc64le, and mandates ldbl64 for all PowerPC targets.
  • powerpc64le-linux-glibc expands to powerpc64le-linux-glibc-elfv2
    • This is because elfv1 is the baseline ABI for powerpc64le on linux.
  • powerpc64le-linux-musl expands to powerpc64le-linux-musl-elfv2+ldbl64
    • This is because elfv1 is the baseline ABI for powerpc64le on linux, and because musl mandates ldbl64 for all PowerPC targets.
  • riscv32-linux-glibc expands to riscv32-linux-glibc-ilp32d (likewise for LoongArch)
    • This is because ilp32d is considered the "common case" default ABI in essentially all RISC-V/LoongArch tooling.
  • riscv64-linux-musl expands to riscv64-linux-musl-lp64d (likewise for LoongArch)
    • This is because lp64d is considered the "common case" default ABI in essentially all RISC-V/LoongArch tooling.
  • arm-windows-mingw and arm-windows-mingw-eabi expand to arm-windows-mingw-eabi+win
    • This is because eabi is the baseline ABI for arm, and for windows the user almost certainly wants win.
  • arm-windows-msvcrt and arm-windows-msvcrt-eabi expand to arm-windows-msvcrt-eabi+win
    • This is because eabi is the baseline ABI for arm, and because msvcrt mandates win.
  • aarch64-windows-mingw and aarch64-windows-mingw-lp64 expand to aarch64-windows-mingw-lp64+win
    • This is because lp64 is the baseline ABI for aarch64, and for windows the user almost certainly wants win.
  • aarch64-windows-msvcrt and aarch64-windows-msvcrt-lp64 expand to aarch64-windows-msvcrt-lp64+win
    • This is because lp64 is the baseline ABI for aarch64, and because msvcrt mandates win.

And so on. This will involve a fair amount of logic to handle these cases, but I think it stays within the realm of manageability.

With this, I think explicitly specifying ABI will be relatively rare. The obvious cases would be toggling between ibmldbl128 and ieeeldbl128 for powerpc64le (because different distros use either option), and switching float ABIs for Arm (32-bit), RISC-V, and LoongArch. Beyond that, I think it'll only happen in extremely specialized/embedded scenarios where you want full and explicit control over every option anyway.

Going this route also mitigates the complexity concern because most users won't have to fiddle with ABI stuff.

@alexrp
Copy link
Member Author

alexrp commented Jul 21, 2024

I have updated the proposal so that you can specify neither libc nor ABI, only libc, or both libc and ABI, as discussed above. I've also added the behavior of picking sensible ABI defaults based on the other components to the core proposal.

Would love to hear feedback on the remaining open questions.

@andrewrk
Copy link
Member

andrewrk commented Jul 26, 2024

related to #4584. edit: I see that @alexrp already linked this above.

@alexrp
Copy link
Member Author

alexrp commented Jul 26, 2024

By the way, if this does get accepted, I'm volunteering to do the work in either the 0.15.0 or 0.16.0 cycle.

alexrp added a commit to alexrp/zig that referenced this issue Aug 2, 2024
This target triple was weird on multiple levels:

* The `ilp32` ABI is the soft float ABI. This is not the main ABI we want to
  support on RISC-V; rather, we want `ilp32d`.
* `gnuilp32` is a bespoke tag that was introduced in Zig. The rest of the world
  just uses `gnu` for RISC-V target triples.
* `gnu_ilp32` is already the name of an ILP32 ABI used on AArch64. `gnuilp32` is
  too easy to confuse with this.
* We don't use this convention for `riscv64-linux-gnu`.
* Supporting all RISC-V ABIs with this convention will result in combinatorial
  explosion; see ziglang#20690.
igor84 pushed a commit to igor84/zig that referenced this issue Aug 11, 2024
This target triple was weird on multiple levels:

* The `ilp32` ABI is the soft float ABI. This is not the main ABI we want to
  support on RISC-V; rather, we want `ilp32d`.
* `gnuilp32` is a bespoke tag that was introduced in Zig. The rest of the world
  just uses `gnu` for RISC-V target triples.
* `gnu_ilp32` is already the name of an ILP32 ABI used on AArch64. `gnuilp32` is
  too easy to confuse with this.
* We don't use this convention for `riscv64-linux-gnu`.
* Supporting all RISC-V ABIs with this convention will result in combinatorial
  explosion; see ziglang#20690.
SammyJames pushed a commit to SammyJames/zig that referenced this issue Aug 13, 2024
This target triple was weird on multiple levels:

* The `ilp32` ABI is the soft float ABI. This is not the main ABI we want to
  support on RISC-V; rather, we want `ilp32d`.
* `gnuilp32` is a bespoke tag that was introduced in Zig. The rest of the world
  just uses `gnu` for RISC-V target triples.
* `gnu_ilp32` is already the name of an ILP32 ABI used on AArch64. `gnuilp32` is
  too easy to confuse with this.
* We don't use this convention for `riscv64-linux-gnu`.
* Supporting all RISC-V ABIs with this convention will result in combinatorial
  explosion; see ziglang#20690.
Rexicon226 pushed a commit to Rexicon226/zig that referenced this issue Aug 13, 2024
This target triple was weird on multiple levels:

* The `ilp32` ABI is the soft float ABI. This is not the main ABI we want to
  support on RISC-V; rather, we want `ilp32d`.
* `gnuilp32` is a bespoke tag that was introduced in Zig. The rest of the world
  just uses `gnu` for RISC-V target triples.
* `gnu_ilp32` is already the name of an ILP32 ABI used on AArch64. `gnuilp32` is
  too easy to confuse with this.
* We don't use this convention for `riscv64-linux-gnu`.
* Supporting all RISC-V ABIs with this convention will result in combinatorial
  explosion; see ziglang#20690.
@andrewrk
Copy link
Member

I finally took the time to digest this. Thanks for the excellent writeup and background research, @alexrp.

I think you've pretty clearly established that the ABI part of the target string needs to support a wider range of ABIs.

One point where I want to diverge from this proposal is that I plan to replace all libcs with zig code at some point. That includes musl, mingw, and wasi-libc, and the static bits of glibc. This will happen invisibly, with the same target string, with no breaking changes (related: #2879).

The idea of introducing a new "ziglibc" would only really affect the C headers. In other words the API. Furthermore, I'm not sure how valuable that would be as opposed to reusing e.g. musl's API. So, my counter-proposal to you is that the target string has "api" instead of "libc". It's a bit of a theoretical difference because it doesn't really affect the implications of this proposal. However, it's important to communicate that Zig is not guaranteeing that you literally are getting musl libc, mingw-w64 libc, etc., but that you are getting a compatible API.

This also solves another open issue which I was unable to find, where someone wanted to include the libc headers but not actually link against libc. This use case would be represented with providing an API in the target string, but omitting -lc.

I am marking this as accepted, along with establishing some issue close criteria:

  • move -mcpu to be part of the target triple #4584
  • Common cases must be trivial to remember, or discover via mistakes. For example if users pass -target x86_64-linux-musl which used to work before, it should either work the same, or the error message should include a hint of how to get the previous behavior.
  • Defaults must be good. If there is a most common ABI for a given API it should be the default. If there is a most common API for a given OS (e.g. macos) it should be the default. Users should be able to pass -target aarch64-macos alone and get the default API and ABI. On RISC-V, the default ABI should be determined based on CPU features.

Related:

@andrewrk andrewrk added the accepted This proposal is planned. label Aug 23, 2024
@alexrp
Copy link
Member Author

alexrp commented Aug 23, 2024

The idea of introducing a new "ziglibc" would only really affect the C headers. In other words the API. Furthermore, I'm not sure how valuable that would be as opposed to reusing e.g. musl's API. So, my counter-proposal to you is that the target string has "api" instead of "libc". It's a bit of a theoretical difference because it doesn't really affect the implications of this proposal. However, it's important to communicate that Zig is not guaranteeing that you literally are getting musl libc, mingw-w64 libc, etc., but that you are getting a compatible API.

This seems entirely reasonable to me; I'm not married to the term "libc". I actually like the term "api" more even without your good arguments here.

This also solves another open issue which I was unable to find, where someone wanted to include the libc headers but not actually link against libc. This use case would be represented with providing an API in the target string, but omitting -lc.

Agree on this as well.

  • Common cases must be trivial to remember, or discover via mistakes. For example if users pass -target x86_64-linux-musl which used to work before, it should either work the same, or the error message should include a hint of how to get the previous behavior.

Some musl targets, such as that one, should actually continue to work as-is, but I take your point in regards to all other APIs, as the spelling for these will change. Should be fairly trivial to detect incorrect input like mips64-linux-gnuabi64 and suggest mips64-linux-glibc or mips64-linux-glibc-n64 instead.

  • Defaults must be good. If there is a most common ABI for a given API it should be the default. If there is a most common API for a given OS (e.g. macos) it should be the default. Users should be able to pass -target aarch64-macos alone and get the default API and ABI.

Strongly agree, and this is what I touched on in my interaction with @mlugg above. My ideal here is that 95% of users should be completely oblivious to the fact that there exists a 4th target component because we just do the right thing (or as close to it as possible) by default.

On RISC-V, the default ABI should be determined based on CPU features.

I think we can extend this to other architectures like Arm and LoongArch as well.


Any thoughts on the remaining open questions?:

  • libc as the API name for platforms with only one libc, vs a more specific name like libsystem for Apple platforms
  • musl (status quo) vs wasilibc as the API name for wasi-libc

@andrewrk
Copy link
Member

libc as the API name for platforms with only one libc, vs a more specific name like libsystem for Apple platforms

How about "system"? I think that name would apply just as well to e.g. FreeBSD as it would to macOS.

musl (status quo) vs wasilibc as the API name for wasi-libc

hmm. I know they forked their headers from musl. If they have not patched those headers, then I think "musl" is still appropriate, and furthermore we should delete our copy of those headers and use the musl headers. However if they have patches, which I am guessing they do, then think the API should be named after that project, with the redundant part of the name stripped (the "libc"). So, "wasi". An example target would look like -target wasm32-wasi-wasi. I'm not concerned about the redundancy because it sounds fine if you also say the field name after each part of the target string: "wasm32 CPU architecture, wasi operating system, wasi libc API". Using this same mnemonic, I think it makes sense to use "gnu" for API name rather than "glibc": "x86_64 CPU architecture, linux operating system, gnu libc API".

@alexrp
Copy link
Member Author

alexrp commented Aug 23, 2024

Ok, that makes sense to me. What do you think about mingw and msvcrt in regards to naming then? It feels like msvcrt isn't quite right then. OTOH, I don't really like msvc (status quo) for describing an "API" either...

@rootbeer
Copy link
Contributor

However if they have patches, which I am guessing they do, then think the API should be named after that project ...

There are a lot of wasi-specific patches to the Musl headers forked for wasi-libc. They've tried to use __wasilibc_unmodified_upstream to tag the changes made. So you can see the areas changed: https://github.com/search?q=repo%3Aziglang%2Fzig+__wasilibc_unmodified_upstream+path%3A%2F%5Elib%5C%2Flibc%5C%2Finclude%5C%2Fwasm-wasi-musl%5C%2F%2F&type=code

@polarathene
Copy link

One point where I want to diverge from this proposal is that I plan to replace all libcs with zig code at some point. That includes musl, mingw, and wasi-libc, and the static bits of glibc. This will happen invisibly, with the same target string

This also solves another open issue which I was unable to find, where someone wanted to include the libc headers but not actually link against libc. This use case would be represented with providing an API in the target string, but omitting -lc.

Just curious if this will make it even less likely for Zig to support eyra?

Eyra is rust-based libc implementation that is built with -gnu linux targets. It only supports static linking and does so via the equivalent glibc APIs, without the caveats that static linking glibc has.

Presently it's not usable with Zig as zig cc refuses to permit static linking for glibc targets while it's the opposite situation for musl targets (static enforced). Even when the build would have no libraries to dynamically link it'll still add the interpreter, and in the case of Eyra IIRC duplicate _start entries despite -C link-arg=-nostartfiles.

I haven't yet opened an issue with more details, but I was hoping that zig cc could support static linking here so that builds with eyra (via cargo zigbuild) would be possible.


The idea of introducing a new "ziglibc" would only really affect the C headers. In other words the API. Furthermore, I'm not sure how valuable that would be as opposed to reusing e.g. musl's API.

I suppose this is similar to what eyra is doing? Which may make the support request redundant, but I think zig cc still can't express static vs dynamic linking, so what would happen in this scenario?

With Rust, cargo defaults musl to static linking as well. Alpine and related musl based distros instead patch that to dynamic linking since that makes more sense for software built and packaged for those distros. So there's a case for musl to support dynamic linking too AFAIK.

@xdBronch
Copy link
Contributor

while it's the opposite situation for musl targets (static enforced).

that doesnt seem right, im on alpine and zig dynamically links musl by default. am i misunderstanding what you mean?

@alexrp
Copy link
Member Author

alexrp commented Oct 10, 2024

Eyra is rust-based libc implementation that is built with -gnu linux targets. It only supports static linking and does so via the equivalent glibc APIs, without the caveats that static linking glibc has.

Is Eyra fully (as in literally 100%) API/ABI compatible with glibc?

but I think zig cc still can't express static vs dynamic linking, so what would happen in this scenario?

That's a known bug that still needs to be resolved. It's not an intentional design choice or anything.

#11909

@polarathene
Copy link

polarathene commented Oct 10, 2024

am i misunderstanding what you mean?

I think my concern is specific to zig cc / zig c++. I haven't tried writing any Zig code myself yet, but from what I understand it's more flexible with linking there.


Is Eyra fully (as in literally 100%) API/ABI compatible with glibc?

It's still in development, but viable for projects with the API surface that it covers. The developer AFAIK has no need for dynamic linking support so they're only focused on static linking, particularly for use with WASM IIRC. So no dl_open or interpreter.

It is intended to match glibc AFAIK, and is built by using -gnu targets that would normally link to glibc, I'm not sure of how it does this specifically at compile time other than replacing _start with it's own.

I don't believe it has any notion of glibc versioned symbols if you were referring to that. This isn't my area of expertise, I was just interested in it as a static glibc instead of musl.

I know that Zig is better for the dynamic linking scenario with glibc where versioning is more important, which has been improving with each Zig release (I troubleshooted a symbols issue that was resolved from 0.12.0, I'm also aware of other improvements for 0.14.0, one with not linking pthread from glibc 2.34 onwards, but glibc itself also versioned a pthread symbol at 2.32 which Zig 0.14.0 dev doesn't do yet, I just haven't raised a report about that, or know if it's that important to match since it'd only apply to 2.32 and 2.33, at 2.34 Zig versions it correctly to 2.32).


That's a known bug that still needs to be resolved. It's not an intentional design choice or anything.

I'm aware of the issue, although I recall there being at least another one about static linking with glibc (not even a basic hello world can be static linked, it build successfully when given the .a static libs but even though nothing is dynamically linked, still segfaults with the interpreter enforced?).

I was planning to open a new issue that referenced both target specific issues with a bit of additional context, is that still worthwhile?

@alexrp
Copy link
Member Author

alexrp commented Oct 10, 2024

It's still in development, but viable for projects with the API surface that it covers. The developer AFAIK has no need for dynamic linking support so they're only focused on static linking, particularly for use with WASM IIRC. So no dl_open or interpreter.

Note that, above, we discussed that wasi-libc should use wasi as its API tag rather than musl because it has some slight incompatibilities with upstream musl. So if Eyra has any incompatibilities with glibc at all, it should probably be its own eyra tag.

I'm also aware of other improvements for 0.14.0, one with not linking pthread from glibc 2.34 onwards, but glibc itself also versioned a pthread symbol at 2.32 which Zig 0.14.0 dev doesn't do yet, I just haven't raised a report about that, or know if it's that important to match since it'd only apply to 2.32 and 2.33, at 2.34 Zig versions it correctly to 2.32).

We very much care about getting the versioning right. Please do file an issue.

I'm aware of the issue, although I recall there being at least another one about static linking with glibc (not even a basic hello world can be static linked, it build successfully when given the .a static libs but even though nothing is dynamically linked, still segfaults with the interpreter enforced?).

#17430

@polarathene
Copy link

So if Eyra has any incompatibilities with glibc at all, it should probably be its own eyra tag.

I believe the project author has a related project mustang that implements it's own target for rust/cargo.

There's no incompatibility to my knowledge with what is implemented, just what is not yet implemented. That said it does differ where glibc would not fair well in static linking, such as with NSS, but since glibc isn't really intended for static linking I'm not sure if that's an incompatibility vs static support fix 🤔

cargo zigbuild takes a target quadruple and adapts it to Zig triple. With this change it'd still need to adapt since the mapping would be different.

The mustang custom target wouldn't exist in Zig though, and AFAIK implements the same ABI (?) as glibc / gnu, but the implementation that it shares with eyra is as mentioned still in development (for example mimalloc fails to compile as getrusage is not provided yet, similarly snmalloc is reported to not build for musl IIRC due to a missing implementation there with atexit but Zig seems to build it successfully 🤷‍♂️ ).

Since it mirrors compatibility with the gnu target, eyra is a convenience over mustang by not requiring a custom target. I would imagine it's like building a project with an outdated glibc when a symbol is missing? However since zig cc doesn't yet allow to opt-out of dynamic linking, I cannot yet verify if Zig is compatible for building with eyra, it does appear to build projects with C / C++ dependencies via cargo without issue though.


We very much care about getting the versioning right. Please do file an issue.

👍 Alright I'll add it to my backlog, should be up within a week or two. Few other tasks to tackle first.


#17430

I was referring to: #4986 (comment)

There's a few other issues on the tracker I'm aware of too, but activity on them seems stale (fair given how busy you must all be), so I thought it might be best to open a new issue that references those with a reproduction on current dev (or whenever 0.14.0 is released) if that would help progress towards a fix.

I'm not sure what you wanted to point out with the issue you linked to, but for me it was that I could compile a hello world with:

  • gcc / clang and glibc static, as well as with rust equivalent and those all run correctly with no interpreter since nothing is dynamically linked.
  • Only zig cc was forcing the dynamic linking with LLD via a flag when using the gnu target.

I am aware of mold implementing a linker feature that prevents adding the interpreter if no libraries are dynamically linked. The maintainer wasn't too fond of it, but it resolved an issue some one was facing. Zig doesn't allow me to configure a different linker (I've heard of ZLD, but not sure if I can use that with zig cc or if it would really prevent the explicit dynamic linking flag?), otherwise I'd have tried with mold (I had tried with LD_PRELOAD / mold -run, but even with Fedora's dynamically linked Zig package this didn't seem to work IIRC).

alexrp added a commit to alexrp/zig that referenced this issue Nov 3, 2024
Once ziglang#20690 is implemented, these will go away in favor of a simple api field.
But for now, they're convenient to have.
alexrp added a commit to alexrp/zig that referenced this issue Nov 4, 2024
Once ziglang#20690 is implemented, these will go away in favor of a simple api field.
But for now, they're convenient to have.
@alexrp
Copy link
Member Author

alexrp commented Nov 26, 2024

@polarathene (sorry, completely forgot to reply to this)

I'm not sure what you wanted to point out with the issue you linked to

Just that programs compiled by Zig for *-linux-gnu* with -fno-PIC are currently broken, which would presumably affect static linking.


Regarding eyra: As I understand it, the main benefit of this project is the static linking while being mostly-glibc-compatible, right? If so, it seems like Zig should easily be able to accommodate that use case by way of #2879 (eventually). We'd basically just be doing the same thing that eyra is doing.

I understand that that's quite a ways off, though. I think we could consider some kind of temporary solution to allow eyra to work in the meantime. For example, having a separate std.Target.Abi.eyra tag (std.Target.Api.eyra after this proposal is implemented) until Zig can satisfy the same use case with gnu seems acceptable to me. I would suggest filing a separate issue to track this.


👍 Alright I'll add it to my backlog, should be up within a week or two. Few other tasks to tackle first.

(Also, just a reminder on this point.)

@polarathene
Copy link

Regarding eyra: As I understand it, the main benefit of this project is the static linking while being mostly-glibc-compatible, right?

Yes I think so, I pointed out barriers to using it with Zig here: #17268 (comment)

I know the project README states that it's libc ABI compatible, and that it uses the glibc target (it's not compatible with the musl target). The technical details beyond that information is a bit beyond my own expertise sorry 😅

There are some drawbacks with musl as a target, while the main drawback with glibc is caveats it has with static linking. Eyra still has quite a bit to implement AFAIK, it can build some common software but mimalloc for example uses libc calls that aren't yet implemented in eyra.

As per my link to #17268 there are some compatibility issues with Zig to support eyra. I was hoping if those were resolved that it could leverage Zig to build for other glibc platforms similar to musl support.


If so, it seems like Zig should easily be able to accommodate that use case by way of #2879 (eventually). We'd basically just be doing the same thing that eyra is doing.

If that's the case that'd be great!

I think we could consider some kind of temporary solution to allow eyra to work in the meantime. For example, having a separate std.Target.Abi.eyra tag (std.Target.Api.eyra after this proposal is implemented) until Zig can satisfy the same use case with gnu seems acceptable to me.

I'm not sure how compatible that would be from the Rust side with cargo-zigbuild and eyra. I think some crates may also be conditional on targets for -gnu / -musl, but perhaps that's a non-issue when bridged via cargo-zigbuild 🤔

The author of Eyra also has a similar project that has it's own custom target Mustang, the two share the same approach under the hood AFAIK, just different ways to integrate. It sounds like your temporary solution with Zig would be similar to Mustang.

I would suggest filing a separate issue to track this.

I don't know how beneficial Eyra is to Zig outside of Rust projects. It should work once Zig can support glibc static linked builds + equivalent -nostartfiles linker flag.


👍 Alright I'll add it to my backlog, should be up within a week or two. Few other tasks to tackle first.

(Also, just a reminder on this point.)

That got set back unfortunately as I encountered an OOM that took out my running containers related to the report 😅

I've got it on my backlog but there are higher priority items for me to tackle until I can revisit that to acquire all the relevant reproduction info.

alexrp added a commit to alexrp/zig that referenced this issue Dec 15, 2024
This is, at least today, a very broken target: It doesn't actually build either
musl or wasi-libc even if you use -lc. It does give you musl headers, but that's
it. Those headers are not terribly useful, however, without any implementation
code. You can sort of call some math functions because they just so happen to
have implementations in compiler-rt. But that's only true for a small subset,
and I don't think users should be relying on the ABI surface of a library that
is an implementation detail of the compiler.

Clearly, a freestanding-capable libc of sorts is a useful thing as evidenced by
newlib, picolibc, etc existing. However, calling it "musl" is misleading when it
isn't actually musl-compatible, nor can it ever be because the musl API surface
is inextricably tied to the Linux kernel. In the discussion on ziglang#20690, there was
agreement that once we split up the API and ABI components in the target string,
the API component should be about compatibility, not whether you literally get a
particular implementation of it. Also, we decided that Linux musl and wasi-libc
musl shouldn't use the same API tag precisely because they're not actually
compatible.

(And besides, how would any syscall even be implemented in freestanding? Who or
what would we be calling?)

So I think we should remove this triple for now. If we decide to reintroduce
something like this, especially once ziglang#2879 gets going, we should come up with a
bespoke name for it rather than using "musl".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

8 participants