[RFC] Multi-architecture COFF object files for ARM64X

I would like to propose adding support for multi-architecture object files that contain both ARM64EC and AArch64 object files as the result of compilation with -marm64x.

Together with ARM64EC, Windows on ARM introduced ARM64X image files, which are binaries containing both ARM64EC and AArch64 code. In MSVC, this is implemented entirely as a linker feature. Users are expected to compile sources separately for both targets and then pass both sets of object files to the linker ( Build Arm64X Files | Microsoft Learn ). We already support this workflow in both Clang and LLD. The build process typically looks like this:

clang -target aarch64-windows -c foo.c -o foo-aarch64.o
clang -target arm64ec-windows -c foo.c -o foo-arm64ec.o
clang -marm64x -o bar.exe foo-aarch64.o foo-arm64ec.o

This differs significantly from a typical build process. The requirement to produce two object files for every source file is challenging for build systems such as CMake, autoconf or Meson. I believe this can be significantly simplified by moving more of the logic into the toolchain itself. Ideally, the above example could instead become:

clang -marm64x -c foo.c -o foo.o
clang -marm64x -o bar.exe foo.o

This is straightforward for build systems to understand and is effectively equivalent to CFLAGS=-marm64x LDFLAGS=-marm64x.

This requires foo.o to contain both AArch64 and ARM64EC object files. The concept is not entirely new; Mach-O has supported multi-architecture binaries for years. With Mach-O, users typically have two options: build separate images and merge them afterward or build each source file into a multi-architecture object file. The former approach is not feasible for ARM64X due to format constraints, so I would like to provide an experience similar to Mach-O multi-architecture object files.

For the format of such multi-architecture object files, I propose using an archive file. A regular archive would be sufficient for most cases, so existing linkers would understand such files. To avoid behavioral differences between linking an object file and linking an archive, a small archive extension is needed. I created a separate RFC for that: [RFC] Introduce a -wholearchive marker embedded in archives themselves .

The -marm64x argument already exists in Clang, but it is currently used only during linking to pass -machine:arm64x to the linker. With the changes I am proposing, it would also affect compilation. The Clang driver would construct two intermediate assembly jobs and merge their outputs by invoking llvm-ar.

PR implementing driver changes: [clang][ARM64X] Support compiling both native and EC objects with -marm64x by cjacek · Pull Request #195836 · llvm/llvm-project · GitHub

Please prefer the term “multi-architecture” over “fat” as the latter can have negative connotations.

2 Likes

I edited this and the other RFC, thanks.

1 Like

This is probably out of scope for this specific issue, but I’d like to hear thoughts on multi-architecture BMI files for C++ modules as well at some point. Since these are compiler-specific, there’s not much to coordinate, but these also likely need to support being multi-architecture. Not sure if Clang’s existing PCH format already considers it or not (it may, given the Apple ecosystem).

1 Like

Does llvm-lib need to know how to compute the symbol table for these files? If you stick one of these files into a static library, you end up with a nested archive, I think, and I wouldn’t expect that to naturally work.

I’m tempted to say it’s simpler to just introduce a new file format: the header is a fixed string followed by the offset of the start of the arm64ec slice, and then you just concatenate the arm64 and arm64ec objects. I sort of see the appeal of leveraging existing infrastructure for archives, but I think you end up with enough weird differences that you’re not really benefiting that much.

Looking more closely, the static library side sort of works naturally because llvm-lib will detect the “.o” file as an archive, and insert the contained object files instead of the archive itself. That’s very subtle, and sort of fragile.

I thought that lib.exe was the standard way to merge static libraries with MSVC tools.

I will revisit the format. It could indeed be as simple as you suggested. I was also initially considering something like an extra discardable COFF section with a magic name that would store another COFF object file as its contents.

I will also take a look at the PCH format. Thanks for the feedback!

The most confusing part of depending on the lib.exe behavior is that Unix “ar” doesn’t work like that.

I created #202740 with an alternative implementation that uses a new .llvm.arm64x section to embed the ARM64EC object file directly within the native object file. This approach is analogous to how .llvm.lto handles embedded LTO objects: the section is marked as discardable, making it completely harmless to unaware tools while allowing lld-link to easily utilize it.

The archive writer needs to handle these hybrid files so that, at a bare minimum, their symbols are properly reflected in the archive map. To achieve this, I split the object file within the archive writer, ensuring that the resulting static library does not depend on .llvm.arm64x section support.

Currently, multi-architecture BMI files are unsupported in universal Mach-O targets, where an attempt to compile them results in an error. My ARM64X draft has a similar limitation (though it currently emits a less descriptive warning). For ARM64X, I think we could force the BMI inside a COFF container (like in -fmodule-format=obj) and leverage this exact same .llvm.arm64x section mechanism to embed the EC module.

I can’t edit the topic anymore, so I created a new one for the new approach: Multi-architecture COFF object files for ARM64X using an extra section