Building a Native x64-to-C# Transpiler: Why I Built Alicorn
Why I Built Alicorn
Alicorn started with a question I couldn’t stop asking: what if you could take raw x64 machine code and turn it into readable, editable C#?
Not decompiling. Not reverse engineering. Something stranger and more useful: a transpiler that carries native execution into managed C#.
The Moment It Clicked
I was working on a project that needed to call into native libraries. P/Invoke worked, but it felt like a seam—something that could tear. I started wondering: what if instead of calling across the boundary, you dissolved it?
What if a DLL’s logic could become just another C# class?
That’s the core idea of Alicorn: native x64 binaries, transformed into C# that runs inside the CLR. No unmanaged calls. No marshaling overhead. Just C# that happens to do what the original binary did.
What It Actually Does
Alicorn takes x64 machine code and lifts it into C# through a few stages:
- Disassembly — using Iced, it parses raw bytes into instructions
- Control flow analysis — building a graph of blocks and branches
- Semantic lifting — translating x64 operations into C# equivalents
- Code generation — emitting valid, compileable C#

The hardest part cuts across every stage: the assumptions.
The Assumptions That Break
x64 code assumes things the CLR doesn’t allow. Direct memory access. Self-modifying code. Hardware exceptions as control flow. Every one of these is a small earthquake in the translation.
The most interesting challenge so far: flags.
x64 has a flags register (RFLAGS) that instructions implicitly read and write. CMP sets flags. JE reads them. In C#, there is no flags register. You have to make the data flow explicit:
// x64:// cmp eax, ebx// je equal
// Becomes C# (illustrative fragment — the `equal` label is generated elsewhere):var flags = Comparisons.CompareEAX_EBX();if (flags.Zero){ goto equal;}This is where the project lives: in the gap between two execution models, finding honest translations.
What I Learned About Boundaries
Building Alicorn taught me something I didn’t expect: boundaries are stories we tell ourselves about where one thing ends and another begins.
The native/managed boundary is a convention — one that looks absolute from a distance but doesn’t survive close scrutiny. The CPU doesn’t care whether the bytes it’s executing came from a JIT or a linker. What matters is whether the abstraction holds.
Alicorn is an experiment in making that abstraction porous. Understanding it — not removing it.
Current State
Alicorn is early. It handles a subset of x64 instructions. It generates C# that compiles and runs for simple cases. It breaks on anything involving system calls, floating point, or complex memory layouts.
But the path forward is clear:
- Expand instruction coverage
- Handle memory models safely
- Support more calling conventions
- Eventually: transpile real-world binaries
Why I Keep Building It
Because it’s the kind of project that asks better questions than it answers.
Every time I sit down to implement a new instruction, I have to ask: what does this mean? The manual describes behavior. The code expresses intent. Alicorn lives in the space between them.
Also, it’s fun. There’s something deeply satisfying about watching a transpiler produce C# that compiles on the first try. Even if that C# is weird. Even if it’s full of goto. It works. And it came from something that wasn’t C# at all.
Not Open Source Yet
Alicorn isn’t open source yet. I’m doing a big refactor and cleanup on the project — the kind that turns a working prototype into something other people can actually read. The plan is to publish it by the end of 2026, depending on how much time I can give it.
If that timeline slips, it slips. The transpiler itself works; what’s missing is the polish. If you’ve ever wondered what happens when you erase the line between native and managed, this is one answer — and the code will land when it’s ready to be read.
Related
- Why I Built ZaString — another C# performance experiment
- Go Pipeline Pattern — how I think about transforming data in stages
Series Navigation
This article is part of the Projects series:
- Previous: How to Merge PGN Files in F#
- Series: Projects
Related Posts
Why I Built ZaString
On zero allocations, Span<T>, and the pursuit of performance without sacrifice.
How to Merge PGN Files in F#: Streaming, Performance, and Discriminated Unions
How I built a CLI tool to merge chess PGN files using F#'s type system, streaming I/O, and functional patterns — merging gigabytes of games with 64 KB of memory.
Go Goroutines vs C# async/await: Who Carries the Cognitive Load?
Goroutines and async/await solve the same problem from opposite assumptions. A polyglot's take on primitives vs compiler-managed patterns.