Windows on Arm can run native Arm and emulated x64 code in the same app, something Apple never let Macs do

Windows on Arm can run native Arm and emulated x64 code in the same app, something Apple never let Macs do

Published Nov 2, 2026, 8:00 PM EST I’m Adam Conway, an Irish technology fanatic with a BSc in Computer Science and I'm XDA’s Lead Technical Editor. My Bachelor’s thesis was conducted on the viability of benchmarking the non-functional elements of Android apps and smartphones such as performance, and I’ve been working in the tech industry in some way or another since 2017. In my spare time, you’ll probably find me playing Counter-Strike or VALORANT, and you can reach out to me at adam@xda-developers.com, on Twitter as @AdamConwayIE, on Instagram as AdamConwayIE, or u/AdamConwayIE on Reddit. Sign in to your XDA account When Apple made the transition to Apple Silicon, apps could either be compiled and run natively for Arm, or they could be compiled and run as x86_64 applications through Rosetta. However, it's a distinct choice; pick one or the other, you can't do both. Windows on Arm does a similar thing to emulate x64 apps too, except Microsoft went a step further: a single Windows process can run native Arm code alongside emulated x64 code. That's thanks to Arm64EC. Arm64EC is an application binary interface (ABI) announced by Microsoft in 2021 alongside Windows 11 on Arm. Arm64EC makes it possible to do things like run old x64 add-ins with Office on Arm, and Windows itself can change how much of itself runs natively when an x64 app calls into it. On the HP OmniBook Ultra 16 that I've been testing, Nvidia's driver package for the RTX Spark packs most of its user-mode components twice: once as plain Arm64 and once as Arm64EC. It's got a bit of a trade-off, though. Arm64EC code has fewer CPU registers available while indirect calls also have to pass through an extra check to determine whether they're calling Arm or x64 code. What's more, it's not an Arm-defined standard, as it's a Microsoft-specific design, but Wine has implemented support for it, too. Arm64EC is Arm code that follows x64's rules EC stands for Emulation Compatible An ABI defines the set of rules that compiled code, allowing separately built pieces to work together. This can be things like which registers carry a function's arguments, how structures are laid out in memory, where the return value goes, that kind of thing. x64 Windows has one set of rules and normal Arm64 Windows has another, which is why an Arm64 program can't load an x64 DLL. Arm64EC, though, introduces a third set, where Its instructions are real Arm64 instructions running natively on the CPU, but it follows x64-compatible software conventions for things like data structures and stack layout. It still retains much of Arm64's calling conventions. What this means is that, in practice, code that gets compiled as Arm64EC identifies itself as x64 to any existing architecture checks. If you compile an application for Arm64EC, the compiler will define an _M_AMD64 macro used by x64 builds, and any application asking if it's built for x64 will be told that it is. Data structures also follow x64's layout rules, and compiled Arm64EC executables contain an x64 machine type in their header. The system knows, though, and Task Manager will show these apps as "ARM64 (x64 compatible)." This only applies to x64, though, as 32-bit x86 apps still use the older WOW64 emulation layer. The biggest reason for all of this, though, is plugin support. Before Microsoft introduced Arm64EC, porting an app to Arm was "all-or-nothing," in Microsoft's words. If your app loads third-party DLLs into its own process that you don't control, such as an Office add-in or an audio plugin for a DAW, you would need to wait for the original developer to also port their code. Arm64EC makes this a lot easier, as you can recompile your own code as native Arm while loading x64 plugins under Prism emulation in the same process. This is how Microsoft built its 64-bit Office for Arm, allowing "existing x64 plugins will work seamlessly." The company says that most of the operating system code loaded by an x64 app on Arm is compiled as Arm64EC, which means much of the Windows code an emulated app calls into runs natively. The concept has roots in Microsoft's older Compiled Hybrid PE, or CHPE, first demonstrated at Build 2017, which similarly combined Arm64 code with x86 compatibility. Arm64EC gives up some registers to stay compatible with x64 They're incompatible, and there's nowhere to save them Arm64 has 31 general-purpose registers and 32 vector registers, whereas x64 has 16-general-purpose registers and, without AVX-512, 16 vector registers. That's especially a problem for Windows, as it represents a thread's CPU state using a structure called CONTEXT, which exception handling, debuggers, and APIs like GetThreadContext rely on. Because Windows has been built on x86_64 assumptions, x64 code expects that structure to have an x64 layout. Arm64EC gets around this by retaining the x64 structure, making it so the Arm registers that are used all fit into the x64 CONTEXT structure. X0 maps to RCX, X1 maps to RDX, the stack pointer maps to RSP, the program counter maps to RIP, and so on. Some of the mappings are a bit odd, as a handful of Arm registers (X6, X7, X9 through X12, X15, and the link register) get stashed in the slots for x87 floating-point registers. Whatever doesn't fit is disallowed entirely, which includes X13, X14, X23, X24, or X28, and it can't touch vector registers V16 through V31. Long-time Microsoft engineer Raymond Chen explained that their values "are not preserved across context switches or asynchronous exceptions. (There's nowhere to save them!)" Microsoft chose the mapping in a way that still keeps all the registers used by normal Arm64 calling conventions available, making it so that Arm64EC functions can call eachother much like they do in native Arm64 code. There are some exceptions, namely variadic functions like printf. These use x64-like argument-passing rules, with only the first four argument positions eligible for registers. This restriction means that the compiler has fewer registers to keep values in, which can introduce overhead. Thunks and call checkers handle every jump between the two A layer has to act as a translator Even when both systems have matching data layouts, Arm64 and x64 disagree on where a function's arguments belong. Microsoft's own example is a function that takes an int, a double, another int, and another double. On Arm64, those will go in X0, D0, X1, and D1, while on x64 they go in in RCX, XMM1, R8, and XMM3. A small piece of glue code, called a "thunk", handles the conversion depending on the type and position of each argument. An entry thunk handles x64 code that calls into Arm64EC, rearranging the arguments from x64's layout into Arm's. There's also an exit thunk which does the same thing in reverse when Arm64EC code calls into something that could be x64. The problem is that the compiler itself often can't know what architecture it's calling, as a function pointer could point at native Arm code or emulated x64 code, which might only become clear at runtime. Microsoft dealt with this by having indirect calls in Arm64EC go through a "call checker" that's provided by Windows. If the target is Arm64EC, the call proceeds natively. If it's x64, the checker routes it through the exit thunk and into the emulator. In order to make that work, Arm64EC binaries have a table built in that says which of their code pages contain Arm64EC instructions and which contain x64 instructions. Functions in Arm64EC also reserve four bytes immediately before their first instruction to store a relative offset to their entry thunk, so that when the emulator encounters x64 code calling an Arm64EC function, it uses that offset to find the correct thunk. Finally, code generated at runtime by a JIT compiler is treated as x64 by default, so .NET, Java, and other unmodified x64 runtimes keep working. For Windows applications that hook system functions by overwriting their first few instructions, those programs expect to find x64 instructions. By default, Arm64EC DLLs will provide a tiny x64 stub for each exported functions, which Microsoft calls a "fast-forward sequence." This redirects execution to the real Arm64EC code, and if nothing modified it, Windows skips the the stub entirely. If something has hooked it, the emulator will execute the modified version in order to keep the hook intact. This is why there's overhead associated with Arm64EC; every indirect Arm64EC call requires a check, even when both sides are native, and calls crossing into x64 code need a thunk and a transition into the emulator. Direct calls between Arm64EC functions, though, are just normal Arm calls. Arm64X is another piece of Microsoft's solution here, as it's a file format capable of packing both normal Arm64 code and Arm64EC code into a single file. Microsoft's analogy compares it to old red-and-blue 3D images, where "an x64 app sees the DLL as though it's an x64 DLL, while an Arm64 app sees the same DLL as an Arm64 DLL." Arm64X is used by Windows for the system DLLs that both x64 and Arm64 apps need from System32, which allows the company to use a single folder of x64 system files unlike the SysWOW64 folder used for 32-bit x86 apps. It's also useful for developers who need to support both, as normal Arm64 and Arm64EC processes can't directly load each other's DLLs. They can work together, just not in the same place Apple's approach to Rosetta and Apple silicon is entirely different, with the company's official documentation stating that "The system prevents you from mixing arm64 code and x86_64 code in the same process. Rosetta translation applies to an entire process, including all code modules that the process loads dynamically." If a native Arm app tries to load an Intel-only plugin into the same process, the system will throw an error instead. This doesn't mean that Intel and Arm code can't work together on a Mac; they just can't share a process. Apple's own suggestion is to run plugins in separate processes, allowing them to communicate over XPC. This means one service for arm64 plugins and another for x86_64 ones, and Audio Units are some of the best-known examples. macOS has a built-in AUHostingCompatibilityService that runs Intel-only AU plugins outside of the host, which programs like Ableton use to support Intel-based plugins. Intel-only VST2 and VST3 plugins still won't work this way, and the only solution Ableton is to run the entire DAW through Rosetta instead. Apple never wanted to keep Intel code around forever, which it clearly signalled when it chose to make the C++ ABI on Apple silicon match "the ABI for iOS devices, and not the ABI for Intel-based Mac computers." Rosetta even has a shelf-life, as it will only continue to exist as a general-purpose tool "through macOS 27." Microsoft has no equivalent for x64 emulation on Windows, and Arm64EC is very much a product of a platform where old x64 code is expected to keep running. Arm64EC is a Microsoft design, but Microsoft isn't the only one using it Wine implemented it too Arm64EC isn't a formally standardised ABI, with Microsoft saying that the normal Arm64 ABI on Windows "usually follows ARM's standard AArch64 EABI." Arm's own ABI specifications for AArch64 make no mention of Arm64EC, either. This is a Windows-specific design built around Microsoft's x64 emulation system, which Microsoft documents rather than Arm. Arm64EC is also a lot of work for anyone else to support. In 2024, the maintainer of the llvm-mingw toolchain, Martin Storsjö, once said that "ARM64EC is a totally different thing though; that's an entirely separate ABI on almost every single level" in the GCC mailing list. GCC was only getting support for normal Windows on Arm at the time, and it seems that to this day, Arm64EC isn't supported in GCC. LLVM has supported it since version 18.1, and Rust offers it as a Tier 2 target, though the maintainer listed for Rust's Arm64EC target is a Microsoft developer. Wine has one of the few independent implementaions, and support arrived with Wine 10.0 in January 2025. It was one of the main highlights of the release, and the same release also added an x86-64 emulation interface that "takes advantage of the ARM64EC support to run all of the Wine code as native, with only the application's x86-64 code requiring emulation." Wine doesn't have its own x86-64 emulator, but FEX, an x86 emulator for Arm Linux, implements that interface when built as Arm64EC. Arm apps can be run on Linux with the same techniques used by Windows, with one caveat: Wine 10.0 requires a 4K memory page size, "since that is what the Windows ABI specifies." At the end of the day, Apple has a significantly simpler approach, but that's helped by the fact that it owns the entire macOS ecosystem. Windows has decades of assumptions of compatibility built in, and Arm64EC is what you get when that old software still needs to work. It may not be perfect, but Microsoft's approach to Arm is leaps and bounds ahead of what it was just a few years ago.

Original Source

Read the full article at Xda-developers →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.