The Trials and Tribulations of Native Lua Compilation
I have been working with Lua for a long time, and one of the things I have always liked about the language is how little it needs. It is small, elegant, portable, and remarkably easy to embed. That last property is probably one of the reasons Lua has been so successful: you can put a Lua interpreter inside a game, an application, a tool, or almost anything else and immediately gain a powerful scripting language.
But after using Lua for years, I eventually started running into the other side of that design.
Whenever I wanted to build and distribute a Lua application, there was always a Lua VM somewhere in the picture. With LuaRT and its rtc compiler, I had already solved a large part of the deployment problem, at least on Windows. I could package a Lua application as a standalone executable instead of asking users to install Lua separately.
That worked well, but it left me with another question. Even if the interpreter was embedded in the executable, the application was still ultimately running Lua code through a Lua VM. The executable might look like a native application from the outside, but underneath it was still a Lua program being interpreted.
I wanted to see what would happen if I went one step further.
I wanted a genuinely native Lua experience, without depending on lua55.dll, liblua55.so, or an embedded Lua VM to execute the application. I wasn't trying to eliminate the runtime entirely — a compiled Lua program still needs native support for things like tables, strings, garbage collection, coroutines and the rest of the Lua semantics. What I wanted was to replace the general-purpose Lua VM with a runtime designed specifically to support compiled native Lua code.
If I could also get some additional performance out of the process, that would be a nice bonus.
That idea eventually became clx.
Packaging a VM vs compiling a program
The important difference between rtc and clx is the way they approach the problem.
A system based around the Lua VM can package an application very conveniently. The Lua source or bytecode is bundled together with the interpreter, and the interpreter takes care of executing it. From a deployment perspective, that can be perfectly reasonable.
clx takes a different approach. It parses Lua 5.5 source code, analyzes it, and generates C++ code representing the program. That generated C++ is then compiled by a regular native compiler such as Clang, GCC, or MSVC.
The resulting executable therefore contains native code generated from the Lua program, together with the clx runtime that the program needs. There is no Lua interpreter underneath it executing Lua bytecode.
That distinction is important. clx still has a runtime. It simply isn't a Lua VM.
The clx runtime provides the native implementation of the parts of Lua that compiled programs still need: tables, strings, memory management, garbage collection, coroutines, standard libraries and other runtime facilities. The difference is that the application itself has been compiled to native code and calls directly into that runtime.
This sounds fairly straightforward when described in a few paragraphs. Building it is another matter entirely.
Lua is a deceptively small language. The syntax is compact, but the semantics are not. Tables, multiple return values, closures, coroutines, metatables, environments, numeric conversions, upvalues, iteration, error handling and all the interactions between these features make compiling Lua correctly much more complicated than simply translating its syntax into C++.
The further I went, the more I realized that the interesting part of a native Lua compiler wasn't generating code for if, while or arithmetic expressions. The difficult part was preserving Lua's semantics while taking advantage of the fact that the target was now native code.
From Lua source to a native executable
The basic workflow with clx is deliberately simple.
Given a Lua program such as:
you can compile it with:
and get a native executable.
There is no special Lua code adjustements needed. You write Lua, and clx takes care of the compilation process.
The compiler can also generate the C++ source instead of immediately invoking the native compiler:
This is useful for debugging the compiler itself, as well as for people who want to integrate generated code into their own build systems.
clx can also produce object files and static libraries:
This was an important part of the design for me. I didn't want clx to be limited to the "Lua script becomes executable" use case. Native compilation opens up possibilities for using compiled Lua code as another component in a larger native application.
Lua modules still work the way you expect
One thing I didn't want to sacrifice in the process was Lua's module system.
A project can contain several Lua files:
and compile them together:
A normal Lua call such as:
continues to work.
The difference is that the modules can be compiled into the resulting native program rather than being interpreted from separate Lua source files at runtime.
This is one of the nice consequences of compiling the whole program. A require() call doesn't necessarily mean that another Lua file has to be found and interpreted while the program is running. The module can already be part of the native executable.
Native modules
One of the reasons I chose C++ as the output language is that it gives clx a natural bridge to native libraries.
A native module can be written using the clx C++ API. For example, a simple function receiving Lua arguments and returning a Lua value can look like this:
The module can then be compiled and linked with the Lua application:
and loaded normally from Lua:
This is one of the areas where the native approach becomes particularly useful. Instead of treating Lua as an isolated scripting environment, compiled Lua code can live alongside C++ code and call into native libraries directly through the clx API.
It also means that libraries written in C or C++ don't need to be translated into some special Lua-specific implementation. They can remain native libraries, with a thin clx binding layer exposing the functionality to Lua.
Compiled Lua libraries
The same idea works in the other direction.
A Lua module can be compiled as a static library:
and then exposed to a C++ application through the clx API.
The result is that Lua code can become a native component of a larger application rather than simply being a script loaded by that application.
That was one of the things I wanted from the beginning: Lua should be able to sit at the same level as the other native components of a program.
But sometimes you still need the Lua VM
There is another important distinction that became clear while developing clx.
Removing the VM from the normal execution path doesn't mean that a Lua VM is never useful.
Some applications genuinely need to execute Lua code that wasn't known when the application was compiled. Plugins, user scripts, configuration code, or applications that deliberately expose scripting capabilities are good examples.
For that reason, clx also has a --dynamic mode.
When this mode is enabled, clx embeds a Lua 5.5 VM and provides the necessary bridge between the compiled program and the interpreter. This makes runtime facilities such as load() possible while keeping the normal AOT compilation model for the rest of the application.
So there are really two different execution models.
With normal AOT compilation, your Lua code becomes native code and uses the clx runtime. With --dynamic, you can additionally have a real Lua VM available when you actually need to load and execute Lua code dynamically.
I think this is a much more useful distinction than simply saying that one approach "has a runtime" and another doesn't. A runtime is necessary either way. The interesting question is what that runtime is doing.
Performance was part of the motivation
Eliminating the Lua VM from the execution path was the main reason I started working on clx, but performance was another motivation.
Native compilation gives the compiler considerably more freedom than interpreting Lua bytecode. clx can perform analysis while compiling the program and generate C++ that can then be optimized by the underlying native compiler.
There are workloads where this makes a substantial difference, although I don't consider clx universally faster than LuaJIT or every other Lua implementation. The performance characteristics depend heavily on the program and on how effectively the compiler can optimize it.
For me, the more interesting part is that performance improvements come from compiling the program itself rather than simply trying to build a faster Lua interpreter.
That changes the optimization problem completely.
The compiler can reason about the program before it runs, specialize certain operations, eliminate unnecessary work, and leave the native compiler with ordinary C++ code to optimize.
There is still plenty of room for improvement, but this direction has already produced meaningful results on several workloads.
Size matters too
Another thing that surprised me while working on clx was how small the resulting programs could become.
A trivial program can produce a surprisingly small executable after native optimization and dead-code elimination, particularly when unnecessary parts of the clx runtime are excluded.
The same principle applies to larger applications. A complete Pong game using a native Sokol module can be compiled into a standalone executable of only a few hundred kilobytes, including the required clx runtime and native functionality.
For applications where deployment size matters, being able to compile only the functionality that is actually needed is quite useful.
Again, the important distinction is that the runtime hasn't disappeared. It has become part of the native program, and the compiler can avoid including functionality that isn't required.
The difficult part is everything in between
The compiler itself has gone through a lot of iterations to get there.
Tables have probably been one of the biggest challenges. Lua tables are simultaneously arrays, hash maps, objects, module containers, environments and the foundation of much of the language. A native representation has to handle all of those roles without making every table operation unnecessarily expensive.
Multiple return values are another example. Lua's calling convention is fundamentally different from a conventional C++ function call, and handling things such as return f(), assignments with different numbers of values, varargs, and calls nested inside expressions requires a dedicated representation.
Coroutines brought another class of problems because Lua's execution model doesn't map naturally onto ordinary C++ control flow. clx eventually had to use platform-specific mechanisms to implement the required context switching while keeping the Lua coroutine semantics intact.
Garbage collection is yet another part that cannot simply be delegated to C++. clx has its own incremental mark-and-sweep garbage collector because compiled Lua programs still need Lua's memory-management semantics.
Then there are all the less obvious cases: environments, metatables, integer semantics, string handling, closures, error propagation, iteration, module loading, and the interactions between all of these features.
Every time I thought a particular subsystem was finished, another Lua program would find an edge case I hadn't considered.
That is probably the most accurate description of developing a native Lua compiler: the individual pieces are manageable, but making all of them behave correctly together is where the real work is.
Why C++?
I considered the possibility of targeting other forms of native code, but C++ turned out to be a very practical choice.
Clang, GCC and MSVC already provide mature optimizing backends for the platforms I care about. By generating modern C++20 rather than implementing an entire machine-code backend, clx can benefit from decades of work that has already gone into native compilers.
It also makes integration with existing native software considerably easier.
If someone already has a C++ library, writing a thin clx module around it is much more straightforward than creating a completely separate foreign-function interface and code-generation backend.
There is a trade-off, of course. Generated C++ can become large, and very large generated translation units can take a significant amount of time to compile. That is one of the problems I am still working on as the compiler grows.
Native compilation doesn't make the complexity disappear. It moves some of it into a different part of the toolchain.
Where clx stands today
clx is still evolving, but it has reached the point where I no longer see it as an experiment that happens to compile a few Lua examples.
It supports Lua 5.5 syntax, can compile real multi-file projects, can integrate native modules, can produce standalone executables and libraries, and can optionally embed a Lua VM when runtime code loading is required.
There are still rough edges, and there are certainly Lua programs that expose things I haven't implemented or optimized yet. A compiler like this is difficult to declare "finished" because every new program is another opportunity to discover a corner of the language or the runtime that deserves attention.
But the fundamental idea works.
You can write a Lua program, compile it through clx, and end up with a native executable that doesn't need a separate Lua installation, lua55.dll, or liblua55.so to run. The executable still contains the clx runtime, but that runtime is there to provide native implementations of Lua's runtime semantics rather than to interpret the application.
That is what I wanted when I started the project.
Why I built clx
Looking back, clx didn't really start because I thought Lua needed another compiler.
It started because I had spent years using Lua and building tools around it, and I became increasingly curious about what Lua would feel like if it were treated as a native programming language rather than primarily as an embedded scripting language.
Lua has spent more than thirty years being one of the best languages to put inside another program. I wanted to see what it would feel like to put Lua at the center of the program instead.
That turned out to be considerably harder than I expected.
There were plenty of moments where the simplest solution would have been to fall back to the interpreter model, or to accept another VM dependency, or to stop worrying about a particular corner case because "Lua is dynamic anyway."
Instead, I kept pushing the compiler a little further.
And that's really what clx is today: the result of that process.
It isn't an attempt to replace every Lua implementation, and it isn't a claim that native compilation is always the right answer. It is simply an attempt to explore what Lua can become when the language is compiled directly into a native program and supported by a dedicated native runtime.
After spending years packaging Lua VMs into applications, there is something genuinely satisfying about being able to compile a Lua program and know that, when I hand somebody the resulting executable, I'm handing them a native program rather than an application whose execution still depends on a Lua interpreter.