Swift on Windows: A year of refinement

18 min read Original article ↗

A year ago, Swift on Windows had proven itself in production. Teams were shipping applications across AMD64 and ARM64—from UI-heavy clients to command-line applications exercising substantial portions of the stack. Some deployments used C++ for networking, while others relied directly on Foundation’s URLSession. These real-world workloads surfaced subtle issues that helped drive stability improvements, though a few rough edges remained.

But ‘working’ isn’t the same as being predictable — and predictability is what makes a system usable at scale. The past twelve months have not been about adding flashy new features but about refinement: reducing fragility, improving the developer experience, and lowering the cost of adoption.

This post walks through the year’s major improvements: rebuilding the runtime build system to establish a reliable foundation, creating the Experimental Swift SDK with multiple distribution models, implementing static linking for self-contained deployments, and validating the approach with production tools.

The year’s work tackled three interconnected problems: runtime selection surprises, fragile build configurations, and linkage mismatches. These had to be addressed in dependency order, making the toolchain’s build, packaging, and linking assumptions explicit at each step.

We started by rebuilding the runtime build system around modern CMake, because the SDK packaging and linking enhancements only makes sense if the runtime build is reproducible across variants.

On that foundation, we defined the Experimental Swift SDK as a single, well-specified deliverable so the distribution and linkage models were encoded, not environment-dependent. With the SDK’s layout in place, we corrected Windows DLL entry-point and import/export semantics. A dynamic-only configuration could still work despite mismatches, but it paid for that convenience with extra thunks and noisy linker output. Static linking forces the toolchain to model those decisions explicitly.

Finally, we validated this approach by shipping and running real tools in multi-toolchain environments, where runtime mix-ups happen under normal developer/CI setups.

Before this work, Swift on Windows was production-ready but costly. The gaps were in ergonomics and platform-specific surprises that raised the cost of adoption. A concrete example: tools built with toolchain X would bind to runtime Y at execution time because DLL discovery relied on Path ordering. Here, “runtime” includes the standard library, the core runtime DLLs, and corelibs (Foundation, Dispatch). In multi-toolchain environments, this was not hypothetical—it was routine.

The runtime and compiler are deeply intertwined, built using a bespoke system that accumulated platform-specific logic over a decade. It worked, but it did not cleanly express how the runtime should be built.

Early last spring, engineers from The Browser Company began collaborating with Apple and the broader community to rewrite the build system using modern CMake. With proper Swift language support now available in CMake, we could build the runtime in a standalone manner using the target-based modern CMake model.

Build configuration matters here: dropping a linker or compiler flag can change optimization behavior (including LTO) in ways that only show up in some configurations. Because the migration is still ongoing across platforms, we keep the legacy and CMake build semantically aligned as runtime changes land.

We hit this directly during the transition: one configuration accidentally left a memory-clobbering debug option enabled, and runtimes immediately became slower—slow enough that we did not need benchmarks to know something was wrong. That experience reinforced why the build system rewrite matters: it is not just about “can we build it,” it is about making the runtime’s configuration inspectable so subtle changes do not silently ship.

The rewrite also made previously awkward configurations practical, including cross-compilation. The CMake build now supports Darwin and Windows hosts, and we use it to build the Android runtime on Windows. The result is a build that’s easier to audit and less likely to hide configuration drift.

Building on this foundation, we created the Experimental Swift SDK for Windows—a new incarnation of the Windows Swift SDK that packages the runtime, core libraries, and integration metadata as a single artifact supporting multiple distribution models: static distribution, side-by-side installation, or bundling a private copy of the shared runtime.

The SDK is the Swift layer that augments the Windows SDK with everything needed to build and run Swift code:

  • Core language libraries: Swift, _Concurrency, and _StringProcessing

  • Higher-level facilities: The corelibs implementations of Foundation and Dispatch

  • Testing libraries: XCTest and Swift Testing

  • Integration glue: APINotes, module maps, and other metadata

The Swift SDK is the runtime and its immediate ecosystem, plus the connective tissue needed to sit naturally atop the system SDK.

A significant change in the Experimental Swift SDK is that we no longer build the runtime with library-evolution mode enabled. The previous system enabled library evolution unconditionally, even without ABI stability; on the Windows platform it mostly bought us indirection by constraining generated code to a compatibility model that did not match the platform, while still not providing a stable ABI contract. Disabling it binds code more tightly to the runtime version it is built against, removes some ABI-related indirection (with modest performance wins), and reflects the platform’s actual status: Windows does not currently offer Swift ABI stability.

In practice, this shifts the compatibility story from “preserve a stable ABI surface” to “bind to an explicit runtime version,” which aligns naturally with side-by-side deployment and avoids prematurely constraining the runtime to a frozen ABI surface.

The “experimental” label reflects an expectation of iteration. This SDK is intended for real use, but its layout and contents may evolve as we gain experience and as the surrounding platform changes.

With runtime builds and SDK packaging in place, we were able to address static linking of the Swift runtime on Windows. The previous model assumed everything dynamically linked the runtime. That assumption was baked into IR generation and module metadata, and it propagated through the toolchain’s linkage and auto-linking machinery. Supporting static linking meant making those assumptions explicit and configurable.

Static linking creates a self-contained binary with a known set of code, making deployment more reliable because the runtime version is fixed at build time.

Beyond deployment benefits, statically linking also enables wider whole-program optimization: link-time optimization, dead-stripping, and inlining across library boundaries. Relocation and symbol-lookup overhead at startup can be reduced as code is internalized into the module.

These benefits come with tradeoffs: binaries are larger; memory use across many processes can be higher because dynamically linked libraries can share pages; rebuilds are slower; and security patch rollout requires rebuilding and redeploying each consumer rather than updating one shared library.

For these reasons, static linking works well for standalone tools and single-binary distributions. Dynamic linking generally works better for large system libraries, plugin ecosystems, and environments that benefit from centralized patching. The key is having both options available so developers can make the choice appropriate for their deployment context.

Implementing this correctly on Windows required changes across multiple layers.

Windows makes the interface-versus-implementation split explicit, and the SDK has to ship artifacts that reflect that split cleanly. A dynamically linked library is conceptually split into two pieces:

  • The runtime component: The DLL containing the actual implementation, loaded at execution time

  • The linker component: The import library, which describes the symbols available from the DLL but contains no implementation

This split is not a Windows quirk; it is the same interface-versus-implementation separation other platforms express in different forms. Apple platforms distribute linker-facing stubs (TBD files) separately from the runtime implementations.

ELF-based platforms have traditionally not exhibited this dichotomy, but LLVM already has the infrastructure (IFS) to represent the same separation on ELF. This provides a path toward SDKs that carry ABI definitions independently of the runtime implementations that satisfy them.

On Windows, both static libraries and import libraries use the .lib file extension. To avoid collisions, we adopted a Microsoft naming convention:

  • Static libraries use a lib prefix with the .lib suffix (e.g., libswiftCore.lib)

  • Import libraries use only the .lib suffix without the prefix (e.g., swiftCore.lib)

This makes the distinction obvious when inspecting the SDK and allows both forms to coexist in the same directory. That split between implementation and interface is why the SDK needs to ship a clear, non-colliding set of import and static libraries: the linker must be able to select the correct model deterministically.

Static linking forced us to make Windows import/export provenance explicit in IR generation. We now mark runtime entry points as imported or exported based on context, so the compiler emits calls that match Windows DLL semantics, reducing link-time noise and avoiding unnecessary thunks.

On Windows, this provenance is expressed through DLL storage annotations on symbols: is a function defined in the current module, or imported from another module? When the compiler knows a symbol is imported, it can emit the correct indirect call directly.

Previously, we effectively assumed that the Swift runtime was always dynamically linked. That assumption was hard-coded in IR generation, which meant the emitted calls and the expected linkage model could diverge. In a dynamic-only configuration, the linker could often paper over mismatches but the result was implicit and fragile—especially once static linking entered the picture.

Implementing static linking required coordinated changes across several components.

The key constraint is consistency: the compiler’s linkage model, swiftmodule metadata, auto-linking directives, and the linker must all agree on static-versus-dynamic, or the failure modes become subtle and difficult to diagnose.

The compiler and runtime needed an explicit model for a statically linked Swift runtime on Windows, and that model had to be serialized into swiftmodules so IR generation, module loading, and the linker make the same static-versus-dynamic decisions.

IR generation needed to understand where runtime functions are “homed” — which module provides the definition — in order to assign correct DLL storage to cross-module calls under both linking modes.

Auto-linking needed updates to reflect the new naming scheme and linkage model. Swift’s auto-linking feature embeds library dependencies directly in swiftmodules, allowing the compiler to emit correct linker directives automatically based on import statements. Auto-linking avoids manual, error-prone link lines, but it requires an accurate model of library naming and linkage.

To support static and dynamic coexistence, we extended both auto-linking and swiftmodule metadata to reflect the naming scheme and required linkage type.

With these pieces in place, the Experimental Swift SDK can now ship a static runtime distribution. Developers who want to explore this path can specify -static-stdlib to build against the static standard library and core libraries.

On non-Darwin platforms, Swift relies on a registrar to make metadata visible to the runtime. If the registrar is wrong or missing, conformance and type discovery can fail at runtime—manifesting as conformance lookup failures, casting or reflection anomalies that are difficult to diagnose.

The registrar is a small piece of code injected into all Swift code; it registers each module (whether an executable or a dynamic library) so that types, protocols, and protocol conformances can be enumerated. This code runs extremely early in program initialization, before any language runtime is fully set up, so it must be implemented carefully to avoid depending on language support or even system calls.

This kind of early initialization mechanism is not unique to Swift. C and C++ have similar patterns. However, Swift’s registrar is only needed on non-Darwin platforms. On Darwin, the dynamic loader provides a hook that notifies the runtime as modules are loaded, allowing the runtime to scan for metadata rather than requiring each module to register itself.

The new static-linking configuration did not change the registrar’s purpose, but it did add a new linkage mode the registrar needed to support. Because the registrar is linked into every module, it must be built appropriately for the kind of output being produced—dynamic library, dynamic executable, or static executable—and for how the runtime itself is linked.

To support this, we added a new registrar build to the SDK and updated the toolchain to apply the naming scheme consistently and select the appropriate registrar variant for static versus dynamic linkage modes. This ensures metadata registration remains correct regardless of how the runtime is linked.

On Windows, Swift tools depend on the Swift runtime. Runtime DLL discovery relies on the Path environment variable. When multiple toolchains are installed, it is easy for a tool built against one runtime to accidentally pick up another at execution time, which is especially problematic in test and bootstrap environments.

Self-hosting—building the Swift toolchain using Swift itself—forces maturity because it exercises real distribution paths and runtime discovery under Release optimizations. Self-hosting also reduces reliance on special-case bootstrap environments. If the platform cannot reliably build and run its own tools, the edge cases tend to surface later—and in less diagnosable forms.

By raising the bootstrap baseline to a toolchain that understands static linking, we could build the swift-driver for Windows as a statically linked binary using the same mechanisms exposed in the Experimental Swift SDK. That makes the driver’s runtime dependency explicit and stable, and it prevents “whichever runtime happens to come first on Path“ from becoming a hidden variable in the build.

The foundational work above—build system, SDK, static linking—addressed correctness of linkage semantics and unblocked new packaging and distribution work. Next, we focused on making the toolchain more convenient to use day-to-day and enabling developers to make tradeoffs.

The installer we build for developers at The Browser Company now provides both an Asserts variant (with compiler assertions enabled) and a NoAsserts variant (roughly 20% faster in some tests, but with reduced ability to catch miscompilations early).

Packaging both enables the performance-versus-asserts tradeoff to be made at the point of use. A developer iterating rapidly can use NoAsserts for faster compile times; CI can use Asserts to catch miscompilations. Today this dual-mode installer is available through The Browser Company’s toolchain builds, with similar builds expected in future swift.org distributions.

We also rebuilt the toolchain against mimalloc, yielding roughly 4% performance improvement in compilation workloads. While 4% sounds modest, it compounds across every incremental build throughout a developer’s day.

With Windows lacking Swift ABI stability today, side-by-side deployment is the practical compatibility mechanism: version the runtime explicitly instead of constraining the implementation to a prematurely frozen ABI surface.

Another thread of work involved supporting side-by-side (SxS) deployment of the Swift runtime. Windows supports installing multiple versions of the same DLL and selecting the appropriate one at load time. Integrating Swift into this model offers three advantages:

  1. Multiple runtime versions can coexist on one machine without forcing all tools and applications to share exactly one version

  2. Runtimes can be installed system-wide and shared across users while still allowing specific tools to bind to specific versions

  3. Upgrades can be incremental. Tools can adopt new runtimes without immediate global migration

Supporting SxS requires embedding additional metadata into the runtime DLLs during the build. While this requires developer intervention to provide the data, the linker can embed it automatically when building the library.

However, injecting this metadata requires the build system to accommodate additional flags and metadata handling. The legacy build made this untenable. The CMake-based build opened up the possibility to generate and embed SxS manifests into the runtime.

The shared runtime build in the Experimental Swift SDK now has the requisite SxS manifest embedded at build time, allowing package authors to bind to the specific build they need.

Combined with disabling library-evolution mode on Windows, SxS provides versioned binding by construction. Applications bind to a specific runtime build, multiple versions can coexist, and upgrades become a controlled rollout.

These architectural changes are exercised by real software. Several tools—including the following—have served as early adopters and stress tests for the linkage and distribution model.

nv is a Ninja visualizer that turns build logs into a clearer picture of scheduling and where time is spent. vigil is a power-user tool inspired by caffeinate, designed to keep a Windows machine awake during long-running tasks.

These tools often run on machines with multiple Swift toolchains installed. In multi-toolchain or multi-runtime environments, the default dynamic-linking model creates a recurring problem. DLLs are discovered via the Path environment variable, so a tool built against one Swift runtime may bind at execution time to a different runtime present on the system.

For these tools, the requirement is straightforward: they should run against exactly the Swift version they were built with. Rebuilding nv and vigil using static linking achieves this. The tools carry their expected runtime, and behavior is determined by build configuration. The static-linking path gets exercised under realistic multi-toolchain conditions.

Many projects rely on SwiftFormat to maintain code style throughout their codebase with its extensive formatting rules. Like other tools in the Swift community, it is written in Swift and depends on the Swift runtime. On Windows, multiple toolchains plus a shared Path made it easy for a formatter built with one toolchain to run against another toolchain’s runtime.

Migrating SwiftFormat to static linking via the Experimental Swift SDK solves the version mismatch problem. The runtime version becomes part of the binary. The tool can be upgraded or rolled back independently of installed toolchains. CI and developer machines behave consistently given the same formatter build.

Although we previously distributed a Microsoft Merge Module in the installer that could package the runtime into tool distributions, it was not always ideal. If the tool was added to the path and built against a different toolchain, the Path-based lookup could potentially load the wrong runtime for either the tool or another application, including toolchain components themselves. With static linking, we can now build and distribute the tool without these concerns. Today, you can simply download and run the tool from the latest releases.

SwiftLint is now a first-class tool on Windows.

Historically, SwiftLint has been centered on macOS. Making it credible on Windows required more than just compilation:

  • It builds against the Windows SDK and the Experimental Swift SDK

  • It passes its test suite on Windows, establishing a baseline level of confidence

  • Diagnostics and reporting are identical across macOS and Windows for identical inputs

That consistency matters for modern workflows. CI should not depend on the build OS, teams should enforce the same rules across environments, and Windows support should not require forks or divergent behavior. SwiftLint on Windows both benefits from and helps validate the runtime, SDK, and toolchain decisions described above.

Improving the developer experience also meant addressing how Swift imports Windows APIs. Windows’ use of the UNICODE macro drove a substantial improvement to the Clang Importer.

Many Win32 APIs are exposed as A and W variants for ANSI and wide-character versions respectively. The UNICODE macro selects which form is visible under the unsuffixed name. In C, calling CreateFile transparently maps to CreateFileA or CreateFileW based on the macro configuration.

Historically, Swift did not mirror this behavior. Code had to call the explicit suffixed variant like CreateFileW because the importer did not replicate macro-controlled aliasing.

A new experimental importer feature addresses this. With -enable-experimental-feature ImportMacroAliases, Swift can import these macro-controlled aliases so that the unsuffixed name like CreateFile is available and resolves to the correct underlying variant, matching C’s model.

This improves API fidelity and reduces suffix management in Swift. It also makes it easier to port existing C examples and documentation. The feature is experimental and opt-in so developers can evaluate it and report edge cases.

The toolchain build process now produces a software bill of materials describing the components that make up the toolchain. This improves supply-chain security and makes provenance easier to verify. It also provides clearer information for organizations assessing toolchain trustworthiness and can make adoption easier for organizations with compliance-oriented environments.

While much of the work described above originated at The Browser Company, Swift on Windows is increasingly a collaborative effort with contributions from the broader Swift community.

The Swift Testing framework provides a notable example. Throughout the year, the Swift Testing team worked with us to improve Windows support. The framework gained better support for attachments, making it easier to include diagnostic information in test results. Image handling was improved to work naturally on Windows. The team also identified and helped improve core Windows type bridging, such as properly handling GUID types when crossing between Swift and Windows APIs.

These improvements went beyond bug fixes. The Swift Testing team treated Windows as a first-class platform, ensuring that new features worked correctly across all supported systems from the start. This kind of cross-platform mindfulness is essential for Swift on Windows to feel like an equal citizen in the Swift ecosystem rather than an afterthought.

As the Windows platform matures, we expect to see more of this pattern: teams across the Swift community contributing to Windows support not because they are Windows specialists, but because they are building tools and libraries that naturally span all Swift platforms.

Beyond the runtime and SDK work, several topics remain active:

  • Debugging: Making Swift debugging on Windows feel natural and fully capable

  • Public symbol server: Providing symbols to enable high-quality stack traces and diagnostics without manual symbol management

  • Performance: Addressing Windows-specific performance concerns across the compiler, runtime, and platform interactions

Each of these is substantial and will be discussed separately, building on the foundation described here.

Over the past year, Swift on Windows has moved from implicit behavior to specified build and deployment models. In practice, this reduces environment-dependent failures on developer machines and CI. In environments with multiple toolchains installed, tools no longer have to “get lucky” with Path ordering to run against the runtime they were built with. And when you do want a single-binary tool that carries its runtime with it—formatters, linters, developer utilities—static linking is now a supported configuration for the cases where a single-binary distribution is the right tradeoff.

These changes are already being exercised by real software and ongoing use in real tools continues to expose and drive fixes for the remaining gaps, particularly around debugging and Windows-specific performance. The year’s work helps establish Swift as a stable, reliable, and production-ready language and ecosystem on Windows.

Saleem

Saleem Abdulrasool is a member of the Swift Core team and the driving force behind cross-platform Swift. He’s been working for years to make Swift production-ready outside the Apple ecosystem.

Discussion about this post

Ready for more?