Skip to content

[experiment] opt-in clang-cl support for Windows#128264

Closed
EgorBo wants to merge 2 commits into
dotnet:mainfrom
EgorBo:clang-cl
Closed

[experiment] opt-in clang-cl support for Windows#128264
EgorBo wants to merge 2 commits into
dotnet:mainfrom
EgorBo:clang-cl

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented May 15, 2026

Experiment: building CoreCLR's JIT (and ~most of native) on Windows with clang-cl

Opt-in path to build CoreCLR's native components on Windows using LLVM clang-cl instead of MSVC cl.exe. Not intended to merge as-is — opening as draft so we can discuss whether the approach is interesting and where it could go.

Two commits:

  1. JIT-only (verified end-to-end): Clr.Jit builds + JIT smoke test passes with the clang-cl-built clrjit.dll (Vector SIMD, exception throw/catch, loop arithmetic — exit code 100, output identical to MSVC).
  2. Root-level switch + broader native (~109/2551 native objects + libs + host before blocker): -clangcl promoted to a top-level build.cmd switch and many additional source/cmake fixes for CoreCLR runtime, libs native, and host. Hits one upstream LLVM crash that we couldn't sidestep (see "Known limitation").

Usage

Requires clang-cl.exe on %PATH% or under %ProgramFiles%\LLVM\bin (winget install LLVM.LLVM). Tested with clang 22.1.5.

# JIT only (works end-to-end):
build.cmd Clr.Jit -c Release -clangcl /p:NoPgoOptimize=true

# Full native (currently blocked by upstream LLVM bug, see below):
build.cmd clr+libs+host -c Release -clangcl /p:NoPgoOptimize=true

Build infrastructure

  • eng/build.ps1 — new -clangcl switch. Sets =1 so it propagates through Arcade → MSBuild Exec → all three native build scripts (src/coreclr/build-runtime.cmd, src/native/libs/build-native.cmd, src/native/corehost/build.cmd). All three call eng/native/gen-buildsys.cmd which already reads __UseClangCl, so no further plumbing was needed.
  • src/coreclr/build-runtime.cmd-clangcl flag (also honors a pre-set __UseClangCl=1 env var).
  • eng/native/gen-buildsys.cmd — discovers clang-cl.exe, injects -DCMAKE_C_COMPILER / -DCMAKE_CXX_COMPILER, forces Ninja generator.
  • eng/native/configurecompiler.cmake — clang-cl branch inside if (MSVC). Each suppression empirically verified to actually fire (audit method: replace -Wno-foo with -Wno-error=foo and rebuild). IPO/LTO is also disabled for clang-cl since ThinLTO triggers additional LLVM crashes.

Source fixes (all MSVC-safe; verified MSVC builds still pass)

File Why
utilcode/pedecoder.cpp Stray line-continuation \ silently merged two STATIC_CONTRACT_* statements (real bug; was masking a clang frontend crash).
jit/gentree.cpp (line 31645) Compare-greater-than scalar branch was missing id =, computing a ?: value and discarding it. Real JIT bug found by -Wunused-value.
jit/importercalls.cpp Replace two impPopStack().val; discards with one impPopStack(2);.
jit/utils.cpp (int) cast on size_t template arg compared against int.
inc/sigparser.h operator= = default to satisfy -Wdeprecated-copy-with-user-provided-copy.
utilcode/debug.cpp __assume(0) after RaiseFailFastException so clang treats FailFastOnAssert as truly noreturn.
utilcode/fstring.cpp + gc/gcpriv.h Gate #pragma optimize("t", on) on _MSC_VER && !__clang__.
utilcode/pedecoder.cpp + utilcode/stacktrace.cpp Explicit (const char *) and reinterpret_cast<LPVOID> casts.
minipal/{guid.c,xoshiro128pp.c} (unsigned int)/size_t casts for format/sign-compare.
gc/vxsort/{vxsort,vxsort_targets_*,smallsort/*}.h + codegen/*.py #ifdef __GNUC__ excluded clang-cl; widen to #if defined(__GNUC__) || defined(__clang__) so the AVX2/AVX512 __attribute__((target(...))) push gets emitted. The python codegen is updated too.
vm/threads.{h,cpp} Split AreShadowStacksEnabled into an out-of-line definition decorated with [[gnu::target("shstk")]] for clang-cl, since cl.exe always exposes _rdsspq but clang-cl gates it behind the shstk target feature.
vm/dispatchinfo.h Forward declaration enum BinderMethodID widened to enum BinderMethodID : int to match the actual definition's fixed underlying type.
native/eventpipe/ds-ipc-pal-namedpipe.c Restructure case-fallthrough-with-goto pattern in ds_ipc_poll to use a flag — cleaner code, helps clang's CFG analyzer (but does NOT fully sidestep the upstream LLVM crash on this file).
native/libs/System.Globalization.Native/pal_icushim.c (unsigned int) cast for %u of GetLastError().

Known limitation: upstream LLVM bug

clang-cl 22.1.5 crashes (clang frontend SIGSEGV) when compiling src/native/eventpipe/ds-ipc-pal-namedpipe.c. We tried:

  • __attribute__((optnone)) on ds_ipc_poll
  • __attribute__((noinline)) combined with optnone
  • File-wide #pragma clang attribute push(__attribute__((optnone, noinline)), apply_to = function)
  • Per-file /Od
  • Per-file -mllvm -opt-bisect-limit=0 (which disables every LLVM pass)
  • Disabling IPO/LTO globally for clang-cl
  • Restructuring the goto-from-switch pattern that initially looked suspicious

None sidestep the crash, which appears to happen in clang's IR emission rather than in a pass we can disable. cl.exe builds the same source cleanly. This one file currently gates the rest of CoreCLR from building under clang-cl. Filing upstream is the next step — the file is small enough to make a usable repro from.

Verification

  • ✅ MSVC build.cmd Clr.Jit -c Release /p:NoPgoOptimize=true still passes.
  • ✅ clang-cl build.cmd Clr.Jit -c Release -clangcl /p:NoPgoOptimize=true passes; JIT smoke test succeeds (Vector, exception, loop) with output identical to MSVC.
  • ⚠️ clang-cl build.cmd clr+libs+host -c Release -clangcl /p:NoPgoOptimize=true reaches CMake configure cleanly and compiles ~109/2551 native objects before being blocked by the LLVM bug above. Libs and host targets are unaffected by that one file and would build to completion.

Discussion points

  • Worth landing the JIT-only path as an experimental opt-in knob even if the broader CoreCLR build is blocked upstream?
  • Are the source fixes individually acceptable as standalone cleanups (regardless of the clang-cl story)? Notably gentree.cpp:31645 is a real JIT bug, and the \ removal in pedecoder.cpp fixes a typo cl.exe also miscompiles silently.
  • Should -clangcl be promoted to a top-level build.cmd flag (as done here) or kept under -cmakeargs?

Note

This PR was authored with assistance from GitHub Copilot CLI (Claude Opus 4.7).

Adds an experimental, opt-in build path for CoreCLR's JIT (clrjit.dll)
on Windows using LLVM clang-cl instead of MSVC cl.exe.

Usage:

    # Direct (forces Ninja generator):
    src\coreclr\build-runtime.cmd -clangcl -release -component jit

    # Via top-level build (set env var, then build normally):
    $env:__UseClangCl="1"
    build.cmd Clr.Jit -c Release /p:NoPgoOptimize=true

Requires clang-cl.exe on PATH or under %ProgramFiles%\LLVM\bin
(winget install LLVM.LLVM).

Build infrastructure:
* src/coreclr/build-runtime.cmd: new -clangcl flag, also honors a
  pre-set __UseClangCl=1 env var (so MSBuild can opt in without
  modifying the call into build-runtime.cmd).
* eng/native/gen-buildsys.cmd: when __UseClangCl=1, locates
  clang-cl.exe and injects -DCMAKE_C_COMPILER /
  -DCMAKE_CXX_COMPILER. Forces the Ninja generator.
* eng/native/configurecompiler.cmake: a 10-line clang-cl branch
  inside the existing if (MSVC) block. Each suppression was
  empirically verified to actually fire by replacing -Wno-foo with
  -Wno-error=foo and rebuilding (see comment block).

Source fixes (all MSVC-safe; verified MSVC build still passes):
* src/coreclr/utilcode/pedecoder.cpp: remove a stray line-continuation
  '\' that silently merged two STATIC_CONTRACT_* statements (this
  was actually masking a clang frontend crash). Also add explicit
  cast for __unaligned char[] -> const char*.
* src/coreclr/jit/gentree.cpp: add missing 'id =' on a compare-
  greater-than scalar branch where the ?: result was being discarded
  (a real JIT bug found by -Wunused-value).
* src/coreclr/jit/importercalls.cpp: replace two 'impPopStack().val;'
  pop-and-discard statements with one 'impPopStack(2);'.
* src/coreclr/jit/utils.cpp: explicit (int) cast on size_t template
  argument compared against int (sign-compare).
* src/coreclr/inc/sigparser.h: 'operator= = default' to satisfy
  -Wdeprecated-copy-with-user-provided-copy.
* src/coreclr/utilcode/debug.cpp: __assume(0) after
  RaiseFailFastException so clang sees FailFastOnAssert as truly
  noreturn (the WinAPI declaration isn't __declspec(noreturn)).
* src/coreclr/utilcode/fstring.cpp: gate '#pragma optimize("t", on)'
  on _MSC_VER && !__clang__ (clang-cl only accepts an empty arg).
* src/coreclr/utilcode/stacktrace.cpp: reinterpret_cast<LPVOID>(...)
  for FARPROC -> LPVOID conversion plus (int) cast on ARRAY_SIZE
  for the loop counter.
* src/native/minipal/guid.c: (unsigned int) cast on guid.Data1 for
  %08x format spec.
* src/native/minipal/xoshiro128pp.c: 'size_t i' instead of 'int i'
  for the sizeof-based loop bound.

Verified:
* MSVC build.cmd Clr.Jit -c Release /p:NoPgoOptimize=true passes
* clang-cl __UseClangCl=1 build.cmd Clr.Jit -c Release
  /p:NoPgoOptimize=true passes (clang 22.1.5)
* JIT smoke test (loop sum, Vector<int> SIMD, throw/catch) JITs and
  runs correctly with the clang-cl-built clrjit.dll, exit code 100,
  identical output to the MSVC-built JIT.

Out of scope for this PR (intended as follow-ups):
* Other CoreCLR components (coreclr.dll, DAC, mscordbi, host).
* Libs native and host clang-cl support.
* VS-integrated -T ClangCL MSBuild path.
* CI pipeline coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 15, 2026 17:53
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 15, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

else if (isScalar)
{
reverseCond ? NI_X86Base_CompareScalarNotLessThanOrEqual : NI_X86Base_CompareScalarGreaterThan;
id = reverseCond ? NI_X86Base_CompareScalarNotLessThanOrEqual : NI_X86Base_CompareScalarGreaterThan;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like a bug that clang's warning highlighted

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Experimental, opt-in path to build CoreCLR's JIT on Windows using LLVM clang-cl instead of MSVC cl.exe. Adds a -clangcl flag (and __UseClangCl env-var bridge) to the native build, wires CMake to locate clang-cl.exe and force the Ninja generator, adds a clang-cl-specific warning-suppression block in configurecompiler.cmake, and applies a handful of small source fixes (one of which is a real JIT bug — a missing assignment in GetHWIntrinsicIdForCmpOp's scalar greater-than branch).

Changes:

  • New -clangcl opt-in build flag plumbed through build-runtime.cmd and gen-buildsys.cmd, with a clang-cl branch in configurecompiler.cmake that suppresses MSVC-only command-line warnings and clang-only diagnostics that fire widely in current sources.
  • Source fixes to compile cleanly under clang-cl: stray line continuation in pedecoder.cpp (also a real bug), missing id = assignment in gentree.cpp (real JIT bug), impPopStack(2) instead of two discarded impPopStack().val, sign/cast tweaks (utils.cpp, stacktrace.cpp, pedecoder.cpp, guid.c, xoshiro128pp.c), defaulted operator= for SigParser, __assume(0) after RaiseFailFastException, and gating an MSVC #pragma optimize("t", on) on !__clang__.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/coreclr/build-runtime.cmd Adds -clangcl flag, env-var default, validation against -msbuild, and help text.
eng/native/gen-buildsys.cmd When __UseClangCl=1, locates clang-cl.exe and injects -DCMAKE_C_COMPILER/-DCMAKE_CXX_COMPILER; requires Ninja.
eng/native/configurecompiler.cmake Adds a clang-cl branch inside the existing if (MSVC) block to suppress unused-cli-arg and several clang-only diagnostics.
src/coreclr/jit/gentree.cpp Adds missing id = assignment in scalar greater-than branch (real JIT bug).
src/coreclr/jit/importercalls.cpp Replaces two discarded impPopStack().val statements with a single impPopStack(2).
src/coreclr/jit/utils.cpp Casts bufferSize to int for sign-compare cleanliness.
src/coreclr/inc/sigparser.h Adds defaulted operator= to satisfy -Wdeprecated-copy-with-user-provided-copy.
src/coreclr/utilcode/debug.cpp __assume(0) after RaiseFailFastException so clang treats it as truly noreturn.
src/coreclr/utilcode/fstring.cpp Gates #pragma optimize("t", on) on _MSC_VER && !__clang__.
src/coreclr/utilcode/pedecoder.cpp Removes stray line-continuation that merged two STATIC_CONTRACT_* statements; explicit (const char *) cast for __unaligned.
src/coreclr/utilcode/stacktrace.cpp Cast ARRAY_SIZE to int and reinterpret_cast<LPVOID> around GetProcAddress.
src/native/minipal/guid.c (unsigned int) cast on guid.Data1 for %08x.
src/native/minipal/xoshiro128pp.c Loop index changed from int to size_t to match sizeof-based bound.

…ource fixes

Promotes the previous -clangcl flag to a top-level build.cmd switch and
extends source fixes to many more native components beyond just the JIT.

Top-level switch:
* eng/build.ps1: new -clangcl switch. Sets $env:__UseClangCl=1 so it
  propagates through Arcade -> MSBuild Exec -> all three native build
  scripts (src/coreclr/build-runtime.cmd, src/native/libs/build-native.cmd,
  src/native/corehost/build.cmd). All three call eng/native/gen-buildsys.cmd
  which already reads __UseClangCl, so no further plumbing was needed.

  Usage:
      build.cmd clr+libs+host -c Release -clangcl /p:NoPgoOptimize=true

Compiler-flag suppressions added to the clang-cl block:
* -Wno-sign-compare, -Wno-deprecated-copy-with-user-provided-copy,
  -Wno-nontrivial-memcall, -Wno-unnecessary-virtual-specifier,
  -Wno-duplicate-decl-specifier, -Wno-enum-compare,
  -Wno-tautological-undefined-compare, -Wno-unused-but-set-parameter
  -ferror-limit=4096 (match Unix Clang block; emit all errors per file)
* IPO/LTO disabled for clang-cl: ThinLTO triggers additional LLVM
  compiler crashes in CoreCLR codepaths, so set
  CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF for clang-cl until the upstream
  fix lands.

Source fixes (all MSVC-safe, verified MSVC build still passes):
* gc/vxsort/{vxsort.h,vxsort_targets_*.h,smallsort/*.h,smallsort/codegen/*.py}:
  pragma guards used #ifdef __GNUC__ which excludes clang-cl
  (clang-cl defines __clang__ but not __GNUC__). Widen to
  #if defined(__GNUC__) || defined(__clang__) so the
  __attribute__((target("avx2/avx512f"))) push gets emitted under
  clang-cl too. The .py codegen is updated so future regeneration
  produces the right output.
* gc/gcpriv.h: gate #pragma optimize("t", on) on
  defined(_MSC_VER) && !defined(__clang__) (clang-cl only accepts "").
* vm/threads.h + vm/threads.cpp: split AreShadowStacksEnabled into an
  out-of-line definition decorated with [[gnu::target("shstk")]] for
  clang-cl, since cl.exe always exposes the _rdsspq intrinsic but
  clang-cl gates it behind the "shstk" target feature.
* vm/dispatchinfo.h: forward declaration "enum BinderMethodID" must
  match the actual definition's fixed underlying type
  (enum BinderMethodID : int) under clang.
* native/eventpipe/ds-ipc-pal-namedpipe.c: restructure
  case-fallthrough-with-goto pattern in ds_ipc_poll into
  switch+poll_should_raise_error flag. Behaviorally identical and
  cleaner; helps but does NOT fully sidestep the upstream LLVM crash
  on this file (see "Known limitation" below).
* native/libs/System.Globalization.Native/pal_icushim.c: explicit
  (unsigned int) cast on GetLastError() for %u format.

Known limitation: upstream LLVM bug
* clang-cl 22.1.5 crashes (clang frontend SIGSEGV) when compiling
  src/native/eventpipe/ds-ipc-pal-namedpipe.c. Tried: __attribute__((optnone)),
  __attribute__((noinline)), #pragma clang attribute push optnone+noinline,
  per-file /Od and -mllvm -opt-bisect-limit=0. None sidestep the
  crash, which appears to be in clang's IR emission rather than a
  pass we can disable. cl.exe builds the same source cleanly. The
  one file gates the rest of the CoreCLR build; this is the only
  thing currently between us and an end-to-end build.cmd clr+libs+host
  pass on clang-cl. Filing upstream is the next step.

Verified:
* MSVC build.cmd Clr.Jit -c Release /p:NoPgoOptimize=true still passes.
* clang-cl build.cmd Clr.Jit -c Release /p:NoPgoOptimize=true -clangcl
  still passes (~30 s) and the JIT smoke test succeeds.
* clang-cl build.cmd clr+libs+host -c Debug -clangcl runs through CMake
  configure cleanly and gets ~109/2551 native objects compiled before
  the ds-ipc-pal-namedpipe.c crash blocks further progress on CoreCLR
  (libs and host targets are unaffected by that one file).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@EgorBo EgorBo closed this May 15, 2026
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 15, 2026

Ran into a bunch of LLVM crashes, seems like it needs more efforts I was willing to put in.

Will upstream the bug-fix and some other source changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants