19 Jan 2024

Windows support in Crystal 1.11

It has been 6 months since we last reported on the status of Windows support in Crystal 1.9. Although there aren’t as many changes in 1.10 and 1.11, we have nonetheless made some significant breakthroughs which will be described below.

Playground support

The playground now works on Windows. For a long time, Process#wait was synchronous on Windows, which means other fibers would not get the chance to run until the process exits. The playground server communicates with the compiled program via multiple WebSocket messages, and a synchronous Process#wait would effectively block the server. This is no longer the case since #13908, and crystal play works just like on other systems.

Finishing DLL coverage

Currently, the compiler is statically linked on Windows for two reasons: the C function LibC.snprintf is not available in the C runtime DLLs, and the compiler depends on LLVM’s C++ API, which requires a static LLVM build tree.

The main remaining use of LibC.snprintf was implementing the floating-point format specifiers for Crystal’s sprintf and String#%, such as %.2f and %g. By porting the Ryu Printf algorithm to native Crystal (#14067, #14084, #14102, #14123, #14132), this dependency is now gone from Windows and Unix-like systems, with the added benefit that float printing is unaffected by the active C locale.

There is nothing Crystal could do if the used LLVM version does not expose all the needed functionality via LLVM’s C API, so instead the Crystal team now upstreams those necessary APIs to LLVM itself. After #14082 and #14101, the LLVM-C.dll library from LLVM 18 or above plus a small configuration file is sufficient; the compiler will eventually become dynamically linked, and still be able to rebuild itself without an LLVM build tree.

Crystal originally used a technique called DLL delay-loading to load DLLs from non-default locations, but it was revealed that not all DLLs work that way, so Crystal is now trying a different strategy to simplify dynamic builds. The @[Link] annotation now accepts a dll parameter which lets you specify non-system DLL dependencies: (#14131)

@[Link("z", dll: "zlib1.dll")]
lib LibYAML
end

The compiler will look up the DLLs in a set of search paths, and copy them to the same directory as the built program, including the temporary executables built for commands like crystal run. Refer to the documentation for more details.

Interpreter support

With everything combined, the interpreter will now more or less run on Windows. There are still some rough edges pending resolution before the interpreter is released on Windows, but you could try rebuilding the compiler itself to see the REPL session in action.

What’s left

As before, the list of outstanding issues on Windows can be found in this GitHub project, and we hope to squash the “Todo” and “In progress” columns by the time an official Windows release is available. The main remaining issues, in order of importance, are:

  • Finalizing the details for dynamic linking (#11575)
  • crystal i (#12396)
  • Behavior of Process.new(shell: true) (#9030)
  • Channels do not behave correctly under -Dpreview_mt (#14222)
  • Support for case-insensitive Dir.glob (#13510)
  • Support for long paths in file APIs (#13420)
  • Making /SUBSYSTEM:WINDOWS more usable (#13330)

Regarding dynamic linking, if everything goes as intended, a future Crystal version will make the following breaking changes:

  • The compiler itself will be dynamically linked, as described above, once LLVM 18 is generally available.
  • All builds will assume dynamic linking by default; -Dpreview_dll will have no effect, and --static becomes mandatory to preserve the current behavior.
  • The delay-load helper and Crystal::LIBRARY_RPATH will be removed.