15.5 LLVM, WebAssembly, and Portable Backends
Writing instruction selection, register allocation, object emission, and optimization independently for every CPU is expensive. A reusable backend lets a language frontend lower into a well-defined intermediate representation and share mature target machinery. LLVM is a prominent native-code infrastructure; WebAssembly is a portable binary instruction format and execution environment. They solve different layers of the portability problem and can be used together.
LLVM IR is typed, SSA-based, and target-aware enough to express low-level operations while remaining independent of one machine instruction set. A frontend emits functions, globals, control flow, memory operations, calls, attributes, metadata, and a target data layout. LLVM runs analyses and transformations, lowers the result through target-specific machine IR, selects instructions, allocates registers, schedules, and emits assembly or object files.
A reusable backend still needs a precise frontend contract
Consider a source-language array access. The frontend must decide whether indices are signed, how overflow behaves, whether bounds failure traps or throws, how elements are laid out, and which aliasing rules apply. LLVM cannot infer the language contract safely from an add and load. Incorrect nsw, nuw, nonnull, dereferenceable, alignment, or alias metadata can authorize transformations that miscompile valid programs.
The target data layout describes pointer sizes, integer and vector alignment, byte order, and legal address spaces. Structure layout in frontend code must match it. The target triple selects architecture, vendor, operating system, and ABI environment. Calling conventions, exception personalities, thread-local storage, atomics, and object-file formats must agree with the runtime and linker from Chapter 14.
Garbage-collected languages need additional cooperation. A moving collector must know every live managed reference at safepoints and may require statepoints, stack maps, handles, or custom lowering. Exceptions need landing pads and personality routines. Coroutines, dynamic dispatch, and deoptimization require deliberate runtime interfaces; choosing LLVM does not create them automatically.
WebAssembly defines a validated portable machine
WebAssembly (Wasm) modules contain typed functions, tables, globals, linear memories, imports, exports, and optional custom sections. Core instructions use structured control flow and stack typing. Before execution, a host validates that instruction types, branch depths, indices, feature use, and stack states are legal. Validation makes later compilation safer and supports streaming: a runtime can decode and compile function bodies as bytes arrive.
Linear memory is a bounds-checked byte array addressed by Wasm code. It is separate from the host's ordinary objects unless the embedding explicitly shares capabilities. A module cannot open a file, access the DOM, or use a network merely because it contains an instruction named for those tasks—core Wasm has no such ambient operations. The host supplies imported functions, memories, tables, or globals. This capability-oriented boundary is central to sandboxing.
WASI specifies families of system interfaces for non-browser environments, but availability and security remain host policy. Browser JavaScript APIs use different adapters. Features such as SIMD, threads, reference types, garbage collection, exceptions, tail calls, and component-model interfaces require negotiation; a producer should not assume every engine enables every proposal.
Portability includes runtime and deployment behavior
A frontend can use LLVM to emit native binaries for x86-64, AArch64, and RISC-V, and use an LLVM Wasm target for wasm32 or wasm64. That reuses many optimizations, but target differences still matter. Wasm has structured branches and virtual registers expressed through stack operations; native targets have physical register files and platform ABIs. Pointer width, atomics, SIMD lanes, exception strategy, and calling boundaries can produce distinct lowering.
Portable execution also requires a stable module interface. Raw Wasm numeric imports are low-level; strings, records, resources, and errors need an agreed ABI or interface description. Components and bindings can adapt these values across languages, but ownership and lifetime must remain explicit. JavaScript-to-Wasm calls may be more expensive than calls within one module, so fine-grained boundary traffic should be measured.
Security is defense in depth. Wasm validation and bounds checks constrain module behavior, while the host must grant minimal imports, limit memory and execution resources, validate untrusted inputs, and isolate native extensions. JIT engines still protect generated-code pages with W^X and defend their own compiler implementation. A sandbox does not make a deliberately granted write_file import harmless.
Test a portable backend through a matrix: target triples, optimization levels, byte orders where supported, pointer widths, feature combinations, runtimes, and host adapters. Use IR verification, object inspection, Wasm validation, differential execution, deterministic builds, and conformance suites. Portability is not “the same bytes everywhere”; it is preserving defined language behavior across targets whose capabilities and costs differ.