Skip to content
jonathanperisPublic

About

A C# front-end for LLVM: compiles C# source to small, fast native executables via Roslyn + LLVM — like clang, rustc and gollvm. Written in C#.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Repository files navigation

csharpc

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.

CI .NET 11 LLVM 18 C# 14 license MIT

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"])
Loading

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

Highlights

  • 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 by tools/checksec.sh on every Linux CI run; --hardening=max adds CET/BTI/PAC control-flow protection. Bounds, cast and array-covariance checks behave as .NET's (docs/17-security.md).
  • Drop-in. Compile a .csproj directly, or keep using dotnet build / dotnet publish with -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 of dotnet publish -p:PublishAot=true itself, with NativeAOT's parameters (ADR 0016). The finish line: that dotnet build beating NativeAOT on the ASP.NET Core TechEmpower benchmarks (roadmap M7 with the csharpc CLI; M9, the finish line, through the fork of M8).
  • Debuggable. -g emits 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.

Language and library support

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.

Install

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.json

Every 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-alpha

Build from source

Requirements: 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 && ./hello

Release 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.

WebAssembly (WASI)

# 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.wasm

The 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).

Building real projects

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 tool

Command line

csharpc [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

Why LLVM?

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)

Why LLVM is a Game Changer for Compilers — The Coding Gopher

How it works

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
Loading
  1. 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.
  2. Middle — Roslyn's ControlFlowGraph as MIR. Loops, &&/||, ?:, ??, ?., switch and 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-block allocas that mem2reg promotes (the clang strategy). Generics are monomorphized; closures, iterators, async (LLVM coroutines), exceptions (invoke/landingpad), records and the rest are lowered by csharpc itself.
  3. 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.
  4. Back-end — LLVM. The module, with the runtime's bitcode linked in, goes through LLVM's optimization pipeline and the target backend; clang links a hardened executable.

The C# corelib: your code binds to .NET, runs on csharpc

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"]
Loading

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.

Repository layout

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

Testing

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"])
Loading
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 NativeAOT

See 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.

Documentation

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

Project status

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).

License

MIT

About

A C# front-end for LLVM: compiles C# source to small, fast native executables via Roslyn + LLVM — like clang, rustc and gollvm. Written in C#.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages