Writing a JIT Compiler Is Easy. Making Your OS Accept It Is Hell.

You wrote a JIT compiler. The machine code generation works. The optimizations are clean. You run it on Linux — perfect. You run the exact same binary on Windows — crash. No error message that makes sense. No stack trace you can read. Just a process that dies silently, as if the OS looked at your dynamically generated code and said: I don’t know you. You don’t exist.

And that’s exactly what happened.

Here’s the truth nobody tells you when you start building a JIT: the hard part was never generating the code. The hard part is making the host operating system believe your code belongs there.

The OS doesn’t care how fast your JITted code runs. It cares whether it can unwind the stack when everything goes wrong.

And that’s where the war begins.

See, operating systems were built on a fundamental assumption: code is static. Functions are compiled ahead of time. Stack frames follow predictable, well-documented ABI conventions. When an exception fires — whether it’s a C++ throw, a Rust panic, or a segfault — the runtime walks the stack frame by frame, using metadata that was generated at compile time, to figure out who called whom and where to resume execution.

Your JIT just blew that assumption to pieces.

The code you generated at runtime? It has no debug info. No unwind tables. No entry in the OS’s exception directory. As far as the stack unwinder is concerned, your JITted frames are invisible — or worse, they’re garbage that corrupts the unwind chain and takes the whole process down.

One developer described this perfectly: they wrote a JIT for a toy language in C++, using C++ exceptions. On Windows with MinGW, they enabled the longjmp/setjmp backend, which happily skipped right over JIT-generated frames. It worked. Then they ran it on Linux — crash. Same compiler. Same code. Different OS, different unwind mechanism, different definition of what a valid stack frame even looks like.

Every operating system has its own idea of what a stack frame should look like, and none of them were designed with you in mind.

On Linux, you’re fighting DWARF unwind tables and the .eh_frame section. On Windows, you’re fighting SEH (Structured Exception Handling) and RUNTIME_FUNCTION entries that must be registered with RtlAddFunctionTable. On macOS, it’s compact unwind info. Each platform demands that you generate metadata in a different format, register it through a different API, and pray that the unwinder respects it when the moment comes.

And here’s the twist that catches everyone off guard: it’s not enough to generate correct unwind info. You have to generate it at runtime, for code that didn’t exist seconds ago, in a format that was designed to be produced by a compiler at build time. The entire toolchain ecosystem — debuggers, profilers, crash reporters, sanitizers — assumes that code and its metadata are born together, statically, forever. Your JIT violates that contract by existing.

Writing a JIT compiler is an act of rebellion against an entire ecosystem that decided, decades ago, that code doesn’t change after it’s compiled.

So what do you do? You register your code. On Windows, you call RtlAddFunctionTable with RUNTIME_FUNCTION entries pointing to your JITted code. On Linux, you use __register_frame to add your .eh_frame data. On some platforms, you fall back to setjmp/longjmp and just skip the whole mess — accepting that exceptions won’t propagate through your JITted frames cleanly. Every approach is a compromise. Every platform is a different battlefield.

The developers who ship working JIT compilers aren’t the ones who write the best code generators. They’re the ones who’ve spent weeks in OS documentation, reading source code for libunwind, staring at DWARF specs at 2 AM, and learning that the gap between your beautiful dynamic code and the OS’s rigid static world is where JIT projects go to die.

The next time someone tells you JIT compilation is just about generating fast machine code, ask them what happens when an exception fires inside their generated code. If they don’t have an answer, they haven’t shipped one yet.

FAQ

Q: Can't you just avoid exceptions entirely in JITted code?

A: You can — and many JITs do. But then you lose structured error handling, can't interop with C++ exceptions, and debuggers/profilers still can't unwind through your frames. You're not solving the problem; you're hiding from it.

Q: Does this mean every JIT compiler needs platform-specific unwind code?

A: Yes. There is no portable solution. If you want exceptions, debugging, and crash reports to work through JITted frames, you must implement OS-specific registration on every platform you target. This is non-negotiable.

Q: Is this really worth it? Why not just interpret everything?

A: Because the performance gap between interpretation and JIT is often 10-100x. For languages like JavaScript, LuaJIT, or Julia, that gap is the difference between a usable product and a dead one. The OS integration pain is the price of admission for speed.

📎 Source: View Source