A C# front-end for LLVM.
csharpc compiles C# source code straight to LLVM IR and then to small, fast native executables —
what clang is to C, rustc to Rust and gollvm to Go.
flowchart LR
src["C# source<br/>.cs / .csproj"] --> roslyn["Roslyn<br/>parse · bind · diagnose"]
roslyn --> cfg["ControlFlowGraph<br/>csharpc's MIR"]
cfg --> lower["csharpc lowering<br/>generics · closures · async · EH"]
corelib["C# corelib<br/>collections · LINQ · BCL"] --> lower
lower --> ir["LLVM IR"]
ir --> opt["LLVM optimizer<br/>whole-program LTO"]
rt["csrt runtime<br/>GC · threads · unwinding"] --> opt
opt --> link["codegen + clang link"] --> exe(["native executable"])
csharpc is written in C# (.NET 11). It uses Roslyn as its parser and type checker — so every diagnostic, overload resolution and language rule is exactly the official compiler's — and the LLVM C API as its backend. Everything in between is csharpc: lowering Roslyn's control-flow graph to LLVM IR, the object model, a garbage-collected runtime, a C# corelib, and the driver. No IL, no JIT and no CoreCLR at run time: the output is an ordinary native executable.
Use it standalone, the way rustc is used (csharpc app.cs -o app, or on a .csproj), or as .NET's
AOT layer in NativeAOT's place, behind dotnet publish -p:PublishAot=true (the end state,
ADR 0016). Priorities, in order: speed, security,
size — each measured in CI against NativeAOT.
$ csharpc hello.cs -o hello && ./hello
Hello from C# compiled by LLVM!
$ ls -lh hello
-rwxr-xr-x 1 dev dev 32K hello- Real C#, verified against real .NET. Every test program runs under
dotnet run(the oracle) and under csharpc, and the outputs must be byte-identical. 100 % of the 195-program conformance corpus passes, with zero silent miscompilations. - Fast. On 20 benchmark programs, measured by CI on clean x86-64 and arm64 runners, csharpc beats the .NET JIT on every one and beats or ties NativeAOT on 16 of 20 on x86-64 (CPU time), with binaries 8–120× smaller (11–161 KiB vs 1.3–1.6 MiB on x86-64; hello world is 27 KiB with default hardening), faster start-up and a fraction of the memory (size, start-up and memory).
- Secure by default. Hardened executables by default (full RELRO + BIND_NOW, non-executable stack,
separate code segment, stack-clash probes; the C runtime with stack protector and
_FORTIFY_SOURCE=3), checked bytools/checksec.shon every Linux CI run;--hardening=maxadds CET/BTI/PAC control-flow protection. Bounds, cast and array-covariance checks behave as .NET's (docs/17-security.md). - Drop-in. Compile a
.csprojdirectly, or keep usingdotnet build/dotnet publishwith-p:UseCsharpc=true/-p:PublishCsharpc=true(docs/08-drop-in.md). The target is 100 % parity with .NET NativeAOT — reached: all 249 programs of the compatibility corpus that NativeAOT runs like the JIT, with no wrong outputs — and the end state is csharpc as the AOT backend ofdotnet publish -p:PublishAot=trueitself, with NativeAOT's parameters (ADR 0016). The finish line: that dotnet build beating NativeAOT on the ASP.NET Core TechEmpower benchmarks (roadmap M7 with thecsharpcCLI; M9, the finish line, through the fork of M8). - Debuggable.
-gemits DWARF: breakpoints on C# lines, C# names in backtraces, locals in gdb/lldb. - Whole-program optimization. Closed-world compilation: internal linkage everywhere, class-hierarchy devirtualization, generics monomorphized, and the runtime itself LTO-linked into your program.
| Types | classes, structs, records (class & struct), enums, interfaces (default & static abstract members), generics (monomorphized, constraints, variance, generic math), tuples, Nullable<T>, anonymous types, delegates & events, arrays (incl. multi-dimensional), Span<T>/Memory<T>, pointers & unsafe, decimal, BigInteger |
| Code | lambdas & closures, local functions, all statements, switch expressions, every pattern kind (type, property, positional, list, relational, logical), try/catch/finally/filters, checked, using, lock, iterators, async/await, await foreach, LINQ (method and query syntax), C# 14 extension members, primary constructors, required/init/field |
| Runtime | conservative garbage collector (thread-safe, stop-the-world), real threads (Thread, Monitor, Interlocked, Parallel), Itanium-ABI exceptions with .NET's exact messages, async scheduler on LLVM coroutines |
| Library | Console, Math, strings & StringBuilder, number formatting/parsing (all standard and custom format strings), DateTime/TimeSpan/Stopwatch, Type objects, System.Collections.Generic and System.Linq (a corelib written in C# and compiled with your program), tasks, cancellation |
Not supported, by design (the same limits NativeAOT has): run-time code generation and loading code
outside the closed world. Reflection works on the members [DynamicallyAccessedMembers] keeps, as in
NativeAOT (ADR 0018). Not supported yet: dynamic and expression
trees (NativeAOT runs these, so they're on the roadmap), parts of networking. Libraries that ship only IL
(-r X.dll, <Reference>, NuGet packages) are compiled from their IL by the IL front-end
(ADR 0010), as far as the BCL surface they use is implemented; so is a
whole program given as its assembly (csharpc app.dll, what the dotnet pipeline hands csharpc). The full matrix is in docs/06-roadmap.md.
Prebuilt NativeAOT bundles (no .NET SDK required) ship on
GitHub Releases. Each archive includes the
csharpc binary, corelib/, runtime/, a bundled libLLVM 18 under lib/ (Apache-2.0 with LLVM
exceptions), and ref/net11.0/ (the Microsoft.NETCore.App reference assemblies used to bind). You
still need clang on the host (and lld when available) to compile the C runtime and link
programs; on Windows that means
LLVM-MinGW. WebAssembly (--target wasm32-wasip1) still
needs the WASI SDK 24 and wasmtime separately.
# Linux / macOS: pick the right archive, verify, extract
curl -fsSL -o install.sh https://raw.githubusercontent.com/jonathanperis/csharpc/main/install.sh
bash install.sh # newest v* release, including prereleases
# or manually:
# curl -fsSL -O https://github.com/jonathanperis/csharpc/releases/download/v0.1.0-alpha/csharpc-0.1.0-alpha-linux-x64.tar.gz
# curl -fsSL -O https://github.com/jonathanperis/csharpc/releases/download/v0.1.0-alpha/SHA256SUMS
# sha256sum -c --ignore-missing SHA256SUMS
# tar xzf csharpc-0.1.0-alpha-linux-x64.tar.gz && export PATH="$PWD/csharpc-0.1.0-alpha-linux-x64:$PATH"
# clang (Ubuntu 24.04); lld optional but used when present
sudo apt install clang-18 lld-18
# macOS: brew install llvm@18 && export PATH="$(brew --prefix llvm@18)/bin:$PATH"# Windows (PowerShell): install.ps1 extracts to %LOCALAPPDATA%\csharpc and updates the user PATH
irm https://raw.githubusercontent.com/jonathanperis/csharpc/main/install.ps1 | iex
# Then put LLVM-MinGW's bin\ on PATH (clang + lld). libLLVM-18.dll is already in the archive.dotnet tool (RID-specific NativeAOT packages — same binaries as the release archives, including
bundled libLLVM; still needs host clang/lld). nuget.org has the tagged releases; GitHub
Packages also has every green main build:
# nuget.org (v* tags only)
dotnet tool install -g csharpc --prerelease
# or GitHub Packages
dotnet nuget add source https://nuget.pkg.github.com/jonathanperis/index.json \
--name github-jonathanperis --username USERNAME --password GITHUB_TOKEN --store-password-in-clear-text
dotnet tool install -g csharpc --prerelease --add-source https://nuget.pkg.github.com/jonathanperis/index.jsonEvery green main CI publishes a rolling GitHub prerelease (latest-main), a container image and a
versioned tool package 0.1.0-alpha.<ci_run_number> on GitHub Packages. A v* tag on a commit
whose main CI is green publishes a named release and that version to GitHub Packages and nuget.org,
whose packages can't be deleted (.github/workflows/release.yml).
Versions. csharpc is pre-release: 0.MINOR.PATCH-alpha, starting at 0.1.0-alpha (the version in
src/Csharpc/Csharpc.csproj). A tag publishes it (v0.1.0-alpha); the next tag bumps PATCH for fixes
and MINOR for a milestone or a breaking change in options or output. main builds are
<that version>.<ci_run_number> on GitHub Packages only. The earlier 0.1.0-spike* packages are
unlisted on nuget.org.
Container (clang and libLLVM included; linux/amd64):
docker run --rm -v "$PWD":/work -w /work ghcr.io/jonathanperis/csharpc:latest-main app.cs -o app
# or a release tag / version: ghcr.io/jonathanperis/csharpc:v0.1.0-alphaRequirements: Linux (x86-64 or ARM64), macOS on Apple silicon or Windows x64 (WebAssembly is a
cross target, see below), the .NET 11 SDK,
LLVM 18 (libLLVM-18.so, Homebrew's llvm@18 on macOS, or LLVM-MinGW
on Windows; 19/20 are also probed) and clang (linker driver and runtime compiler).
# .NET 11 (side by side in ~/.dotnet11)
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 11.0 --quality preview --install-dir ~/.dotnet11
export PATH=$HOME/.dotnet11:$PATH
# LLVM 18 + clang (Ubuntu 24.04)
sudo apt install llvm-18 clang-18
# ...or on macOS (Apple silicon; Xcode Command Line Tools provide the SDK and the linker)
brew install llvm@18 && export PATH="$(brew --prefix llvm@18)/bin:$PATH"
# ...or on Windows (Git Bash): LLVM-MinGW 20240619 (LLVM 18, UCRT), unzipped, with its bin/ on PATH
export PATH="/c/llvm-mingw-20240619-ucrt-x86_64/bin:$PATH"
# Build csharpc
git clone https://github.com/jonathanperis/csharpc && cd csharpc
dotnet build src/Csharpc -c Release
alias csharpc="dotnet $PWD/src/Csharpc/bin/Release/net11.0/csharpc.dll"
# Compile and run
cat > hello.cs <<'EOF'
var primes = Enumerable.Range(2, 50).Where(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(d => n % d != 0));
Console.WriteLine($"Hello from C# compiled by LLVM! Primes: {string.Join(", ", primes)}");
EOF
csharpc hello.cs -o hello && ./helloRelease builds of csharpc are precompiled (ReadyToRun) and a compilation uses all cores (-j N or
CSHARPC_THREADS to limit it; the output is the same either way), see
ADR 0014. For the fastest start-up, build csharpc itself as a native
executable: tools/publish-native.sh, then use artifacts/csharpc-native/csharpc. Platform release
archives are produced the same way by tools/pack-release.sh (used by the Release workflow).
If libLLVM isn't on the default library path set CSHARPC_LIBLLVM=/path/to/libLLVM.so; to use another
C compiler or linker driver pass --cc or set CSHARPC_CC.
# the WASI SDK 24 (LLVM 18, like csharpc) and wasmtime
export WASI_SDK_PATH=/opt/wasi-sdk
csharpc --target wasm32-wasip1 hello.cs -o hello.wasm && wasmtime run --dir=. hello.wasmThe same programs run as WebAssembly modules, and the CI checks them against .NET under wasmtime.
WebAssembly has no threads (Thread.Start throws PlatformNotSupportedException, as in .NET's
single-threaded WebAssembly builds), no sockets and no dynamic libraries
(ADR 0011, docs/04-runtime.md).
csharpc path/to/App.csproj # evaluates the project with MSBuild; ProjectReferences included
csharpc bin/Debug/net11.0/App.dll # the program as csc built it: everything lowered from IL
dotnet build -p:UseCsharpc=true # with the Csharpc.Sdk package: native exe replaces the apphost
dotnet publish -p:PublishCsharpc=true # one native executable, like PublishAot
dotnet pack src/Csharpc && dotnet tool install -g csharpc --add-source src/Csharpc/bin/Release # as a dotnet toolcsharpc [options] <file.cs>...
csharpc [options] [<project.csproj> | <directory>]
csharpc [options] <app.dll | app.exe> # a program built by csc, compiled from its IL
-o, --output <path> Output file, or directory for <AssemblyName>
-c, --configuration <c> Debug (default) | Release, like dotnet: #if symbols and project properties
-p:<Name>=<Value> MSBuild property used when evaluating a project
--emit <kind> exe (default) | obj | asm | llvm-ir | llvm-bc
-O0 | -O1 | -O2 | -O3 Optimization level (default -O2)
-Os | -Oz Optimize for size (-Oz: smallest)
--static Fully static executable (static-pie, no shared libraries)
--strip | --no-strip Strip symbols (default: strip unless -g)
-g, --debug DWARF debug info (C# source-level debugging)
--target <triple> Target triple (default: host)
--cpu <name> native, x86-64-v3, znver4, ... (default: best x86-64-vN of the host)
--dump-cfg Print Roslyn's control-flow graphs (csharpc's "MIR")
--intrinsics List the library members implemented by the runtime
-v, --verbose Pipeline phases and timings
LLVM separates a language from its machine code: a front-end only has to translate the language to LLVM IR, and gets a world-class optimizer, every CPU target and the surrounding tooling (linkers, debuggers, sanitizers) for free. That is how clang, rustc, Swift and Zig work — and the idea behind csharpc: Roslyn already knows C# perfectly, LLVM already knows machines perfectly, and csharpc is the bridge between them. For an introduction to why that design is so effective:
Why LLVM is a Game Changer for Compilers — The Coding Gopher
flowchart TB
subgraph frontend["Front-end — the official C# compiler"]
direction LR
parse["Roslyn parser"] --> bind["binding · overload resolution<br/>against real .NET reference assemblies"] --> diag["diagnostics"]
end
subgraph middle["csharpc"]
direction LR
cfg["ControlFlowGraph per method"] --> mono["monomorphize generics<br/>closure conversion"] --> lowerops["lower operations to LLVM IR<br/>objects · vtables · interfaces<br/>exceptions · coroutines"]
end
subgraph library["Library compiled with your program"]
direction LR
corelib["corelib in C#<br/>src/Csharpc/corelib"]
csrt["csrt in C<br/>src/runtime"]
end
subgraph backend["Back-end — LLVM"]
direction LR
opt["default#lt;O2#gt; pipeline<br/>inlining · devirtualization · vectorization"] --> code["x86-64 codegen"] --> link["clang link<br/>hardened ELF"]
end
frontend --> middle --> backend
corelib --> middle
csrt -- "bitcode, LTO-linked" --> backend
- Front-end — Roslyn. Parsing, binding, overload resolution and diagnostics come from the official C# compiler, bound against the real .NET reference assemblies, so language semantics are exactly the official compiler's.
- Middle — Roslyn's
ControlFlowGraphas MIR. Loops,&&/||,?:,??,?.,switchand patterns arrive already lowered to basic blocks, much like rustc's MIR. csharpc maps blocks 1:1 to LLVM blocks and lowers each operation, with entry-blockallocas thatmem2regpromotes (the clang strategy). Generics are monomorphized; closures, iterators,async(LLVM coroutines), exceptions (invoke/landingpad), records and the rest are lowered by csharpc itself. - Library — corelib + csrt. The .NET library surface is implemented in C# wherever possible
(
src/Csharpc/corelib) and compiled together with your program; a small C runtime (src/runtime) provides the GC, stack unwinding, threads and the OS boundary. - Back-end — LLVM. The module, with the runtime's bitcode linked in, goes through LLVM's
optimization pipeline and the target backend;
clanglinks a hardened executable.
flowchart LR
code["your code<br/>new List#lt;int#gt;()<br/>xs.Where(...).ToList()"] -- "Roslyn binds to" --> ref["System.Collections.Generic.List#lt;T#gt;<br/>System.Linq.Enumerable<br/>(real .NET reference assemblies)"]
ref -- "csharpc redirects" --> shadow["[Shadows] implementations<br/>written in C#, src/Csharpc/corelib"]
shadow -- "monomorphized, inlined,<br/>optimized with your code" --> ir["one LLVM module"]
Overload resolution, IDE experience and diagnostics stay exactly .NET's; only the implementation behind the symbols is csharpc's — closed-world, specialized for your program, and dead-stripped.
Read docs/02-architecture.md, docs/03-lowering.md and docs/12-corelib.md for the details, and docs/01-prior-art.md for how this compares to Mono's LLVM backend, LLILC, NativeAOT(-LLVM), IL2CPP and bflat.
src/
Csharpc/ the compiler: Frontend (Roslyn), CodeGen (CFG → LLVM IR), Llvm (C API bindings), Driver
Csharpc/corelib/ the BCL in C#: collections, LINQ, strings, StringBuilder, number formatting/parsing,
decimal, BigInteger, DateTime; compiled with every program
Csharpc.Sdk/ MSBuild integration (UseCsharpc / PublishCsharpc)
runtime/ csrt — the C runtime: GC, exceptions, threads, async scheduler, console/OS calls
tests/
samples/ differential tests (each .cs has .expected output recorded from real .NET)
conformance/ the 195-program C# conformance corpus
projects/ multi-project solutions for the drop-in / MSBuild tests
run.sh, conformance.sh, projects.sh, debuginfo.sh
benchmarks/ benchmark programs, harness (run.sh) and RESULTS.md
docs/ architecture, lowering, runtime, roadmap, ADRs, spike log
tools/ gdb and lldb pretty-printers for csharpc binaries
Real .NET is the oracle: every test program runs under dotnet run once to record its output, and
csharpc's binaries must reproduce it byte for byte, at every optimization level, on every commit.
flowchart LR
prog["test program<br/>tests/**/*.cs"] --> net["dotnet run<br/>real .NET"] --> exp[".expected<br/>+ exit code"]
prog --> cs["csharpc -O0 / -O2"] --> bin["native binary"] --> out["stdout<br/>+ exit code"]
exp --> cmp{"byte-identical?"}
out --> cmp
cmp -- yes --> pass(["pass"])
cmp -- no --> fix(["fix csharpc —<br/>never the expectation"])
tests/run.sh --all-opts # differential suite at -O0 and -O2 (compares against recorded .NET output)
tests/run.sh --regen 42 # re-record expectations for samples matching "42" with real .NET
tests/conformance.sh -j 4 # conformance corpus, per-area coverage report
tests/programs.sh # larger real-world programs (interpreter, JSON, ray tracer, ...)
tests/projects.sh # csproj input, dotnet build/run/publish integration
tests/debuginfo.sh # gdb-driven debug-info checks
benchmarks/run.sh # csharpc vs .NET JIT vs NativeAOTSee docs/05-testing.md. CI shards that differential suite and keeps the five
test (<platform>) jobs as the merge bar; a docs-only change can skip the suite, and a targeted
run (CI-Only: on a ci/** branch, or Actions → CI (targeted)) does not count as the bar.
| 00 Vision · 01 Prior art · 02 Architecture · 03 Lowering | how csharpc is built and why |
| 04 Runtime · 10 Async · 11 BCL value types · 12 Corelib · 14 Threading | the runtime and library |
| 05 Testing · 09 Conformance · 13 Debugging · 08 Drop-in | using and verifying it |
| 06 Roadmap · 07 Spike log · ADRs | where it's going and how it got here |
csharpc is pre-1.0 and under active development. What that means in practice:
| Guarantee | A compiled program behaves exactly like it does on .NET — same output, same exceptions and messages, same exit code. Anything csharpc can't compile faithfully is rejected at compile time with a CSLLVM0001: not supported by csharpc yet diagnostic (in libraries compiled from IL, a method the program cannot reach compiles to a stub that stops the program with that diagnostic, warning CSLLVM0007); a silent behavioral difference is treated as the highest-priority bug. |
| Verified surface | The language and library features in the table above, checked on every commit by CI: the differential samples at -O0 and -O2, the conformance corpus, and a set of larger real-world programs. |
| Platforms | Linux x86-64, Linux ARM64, macOS ARM64 (Apple silicon) and Windows x64 (MinGW-w64 toolchain, x86_64-w64-windows-gnu), each tested in CI against real .NET on that machine. WebAssembly is on the roadmap. |
| Not supported | run-time code generation and loading code outside the closed world (NativeAOT's limits); not yet: dynamic, expression trees, member reflection; compiled async methods in IL-only libraries (source async works); parts of the BCL such as networking and JSON, which grow with the corelib. |
| Stability | Whole-program compilation with no stable ABI between csharpc versions: rebuild everything when you upgrade. Command-line options may still change. |
Found a gap? Open an issue with a minimal program and what dotnet run prints — the differential
test format turns every report directly into a regression test. Contributions are welcome; see
docs/CONTRIBUTING.md (and AGENTS.md for automated coding agents).
