| 中文 | English |
FakeLua is an embeddable Lua-subset runtime for high-performance hosts: it compiles scripts to a bytecode VM and optionally to native code via GCC/TCC JIT, ships a large C++ native standard library (net/http/db/crypto/…), and uses an arena allocator with frame reset so there is no GC pause.
FakeLua was designed to address the throughput jitter and memory bloat caused by garbage collection in traditional scripting languages (standard Lua/LuaJIT) when used in high-performance game servers or similar real-time systems.
In a typical real-time high-performance server architecture:
To support this positioning, FakeLua does not implement a complex dynamic garbage collector (tri-color marking, generational GC, etc.). Instead, it uses an extremely efficient Arena memory pool (Bump Allocator):
malloc fragmentation or overhead.State::Reset() is called. It invokes destructors in reverse order, but instead of freeing individual blocks, the pool offset pointer is simply reset to zero. There is no complex object graph traversal, no system-level free cost or defragmentation — cleanup is instantaneous.This design allows FakeLua to fully eliminate GC pause impact on frame rates while maintaining JIT native execution speed, keeping memory overhead at a completely predictable, extremely low level.
FakeLua is not a blind clone of standard Lua’s overly permissive dynamic features. Instead, it makes conscious, intentional trade-offs for execution performance, large-scale engineering collaboration, and long-term maintainability:
nil, and extra arguments are silently discarded. In large-scale, multi-developer projects, this looseness easily conceals bugs caused by signature changes, omitted arguments, or parameter mismatches. FakeLua deliberately tightens this rule:
...). Strict arity keeps interfaces explicit, prevents silent runtime nil pollution, and enables the JIT compiler to emit direct, zero-overhead native C calls without redundant runtime nil padding or truncation boilerplate.local; undeclared assignments trigger compile errors, preventing accidental global pollution from typos."10" + 1 errors), keeping types predictable and enabling aggressive native numeric specialization.The same Call API can target any registered backend:
-O3) to generate high-quality native code. Primary backend for production.src/interp/). No external C compiler required; useful for portability, tooling, and mixed JIT↔interp closures.int ret = 0;
Call(s, JIT_GCC, "add", ret, 10, 20); // Production: GCC (-O3)
Call(s, JIT_TCC, "add", ret, 10, 20); // Dev/test: TCC (fast compile)
Call(s, JIT_INTERP, "add", ret, 10, 20); // Bytecode VM (no host C compiler)
The compiler automatically performs type inference and specialization for function math parameters:
int64_t / double combinations) plus a runtime entry dispatcher that routes to the appropriate specialization based on actual argument types.int64_t/double) for arithmetic and generate native C bool for comparisons, completely eliminating boxing overhead on hot paths.-- Example Lua function: recursive Fibonacci
function fib(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end
Auto-generated specialized C code:
// 1. Numeric specialization: params/return promoted to native int64_t, no boxing
static int64_t fib_spec_0(int64_t n) {
if (n <= 1) {
return n;
}
return fib_spec_0(n - 1) + fib_spec_0(n - 2);
}
// 2. Generic entry dispatcher: fast type check, zero-overhead routing
static CVar fib_dispatcher(CVar n_var) {
if (LIKELY(n_var.type_ == VAR_INT)) {
return (CVar){.type_ = VAR_INT, .data_.i = fib_spec_0(n_var.data_.i)};
}
// ... dynamic dispatch to double specialization or generic CVar path
}
With recursive Fibonacci (n=32) as an example, the GCC backend is 36.6x faster than Lua 5.4, and the TCC backend is 11.2x faster (see benchmark/README.md / 中文).
If a Table constructor can statically infer all its keys at compile time (string literals, explicit/implicit integer indices, booleans, floats), the compiler specializes it as a C struct:
FL_SPEC/FL_SET_SPEC) are used directly, completely avoiding hash lookups and key comparisons.spec_get / spec_set function pointers.-- Example Lua code: defining and accessing Table fields
local point = { x = 10, y = 20 }
point.x = point.x + 5
Auto-generated specialized C struct and pointer-offset access:
// 1. Compile-time key layout inference, auto-generate C struct definition
typedef struct Table_Spec_1 {
CVar x;
CVar y;
} Table_Spec_1;
// 2. On Table initialization, bind specialized struct layout and spec accessors
SET_TABLE_SPEC(point, Table_Spec_1, spec_get_fn, spec_set_fn, 2);
FL_SET_SPEC(Table_Spec_1, point, x, 0, (CVar){.type_ = VAR_INT, .data_.i = 10});
FL_SET_SPEC(Table_Spec_1, point, y, 1, (CVar){.type_ = VAR_INT, .data_.i = 20});
// 3. Field access converted to ultra-fast pointer member offsets (no hash table lookup)
FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CVar){.type_ = VAR_INT, .data_.i = 5});
CVar *), shared across closures in the same scope.return a, b; C++ side receives via std::tie(a, b, c). Vararg functions with ... fully supported.function(args) body end as values, arbitrary callee calls like tbl[key]() or (fn)().obj:method(args) sugar with implicit self parameter.for in iterators: Stateless iterators, closure generators, and pairs/ipairs with native C struct-optimized loops.package "Name" for namespace isolation, zero-require cross-module calls.__fakelua_init().RegisterMethod, colon-syntax calls from Lua.string.find/match/gmatch/gsub use a self-contained Lua-pattern engine (%d/%w classes, custom sets, lazy -, captures, frontier %f[set], balanced %bxy), fully compatible with standard Lua.string.trim/trim_left/trim_right/split/join/replace/starts_with/ends_with/contains/iequals/icontains/istarts_with/iends_with via Boost.Algorithm.... for variadic arguments).coroutine.create/resume/yield support.__index, __newindex, metamethods, or operator overloading.require/module: No standard module system (replaced by package "Name" mechanism).rawequal/rawget/rawset/rawlen: Meaningless without metatables.debug.* standard library."10" + 1 errors).local; implicit global declarations are rejected.json.encode rejects array holes: integer keys mean array intent, so a table whose keys are positive integers but not contiguous from 1 (t[1]="a"; t[3]="c", or even {[1001]=...}) is an error instead of an object with "1"/"3" keys — the wire shape must not change silently. Keep protocol arrays gap-free (use "" or {} placeholders, not nil), and use string keys (or container.map) for id-keyed maps. #t still stops at the first nil, like standard Lua; serialize.encode/decode keeps non-contiguous keys.attempt to perform arithmetic on non-numeric value at main.lua:4 on all three backends, including errors raised inside native functions.FakeLua provides 30+ independent C++ native modules under src/native/ (registered automatically on each State), covering math, string, table, IO, networking, timers, events, random, containers, compression, cryptography, serialization, databases, protobuf, config formats, logging, and subprocesses.
Full API reference: src/native/README.md / 中文
| Category | Modules |
|---|---|
| Core Lua | basic, math, table, string, os, utf8, io, random |
| Runtime / I/O | runtime (runtime.tick()), net (TCP/UDP), http (HTTP/1.1), url, timer, event |
| Data | json, csv, serialize, protobuf, container (Boost.Container deque/vector/list/map/set) |
| Config | yaml, toml, xml, ini |
| Database | mysql (async + pool), redis (async), sqlite (synchronous) |
| Crypto / compress | compress (LZ4/zlib/gzip/Zstd), crypto (OpenSSL digests/ciphers, UUID, CRC-32, xxHash) |
| Process | process (process.run; does not replace os.execute) |
| Logging | log (levels, tagged output, file rotation) |
| Object | object (NativeObject Lua-side API) |
Pattern note: string.find/match/gmatch/gsub use Lua 5.4 patterns (a self-contained byte-pattern engine in src/native/string/lua_pattern.*), not ECMAScript/POSIX regex. See Lua Pattern Matching below.
string.find/match/gmatch/gsub follow PUC-Rio Lua 5.4 semantics exactly, including:
%. %( %) %% %+ … for punctuation; . matches any byte.%a %c %d %g %l %p %s %u %w %x and their uppercase complements; %z matches the zero byte.[set], [^set], [a-z], classes inside sets ([%d_]), leading ]/- as literals.* + - ? — including Lua’s lazy - (a.-b).^ at pattern start, $ at pattern end.(...), position captures (), back-references %1… in patterns and %0…%9 + %% in gsub replacement strings.%f[set] and balanced match %bxy.gsub: string/function/table replacements; a function/table result of nil/false keeps the original match; plain=true on find bypasses the pattern engine.pcall) instead of silently returning no match.string.match("limit=15", "%d+") --> "15"
string.gsub("hello world", "(%w+) (%w+)", "%2 %1") --> "world hello", 1
string.match("a(b(c)d)e", "%b()") --> "(b(c)d)"
Lua patterns are not regular expressions: there is no alternation (
a|b), and the escape prefix is%, not\. Scripts written for the old ECMAScript behavior (e.g."\\d+",$1replacements) must be updated to Lua form ("%d+","%1").
cmake -S . -B build
cmake --build build --parallel
On macOS, first
brew install lua cmakeand add-DCMAKE_PREFIX_PATH="$(brew --prefix)"to the cmake command.
Build only core library and CLI tools (no tests/benchmarks):
cmake --build build --target fakelua flua --parallel
Dependencies come through CPM (CPM_SOURCE_CACHE, default ~/.cache/CPM). Boost is a
superbuild whose libraries are git submodules, so a cache entry can hold the repository
but have empty submodule worktrees — configure then reports one missing Boost::* target
after another. Configure stops with the repair command when it detects this; it is:
cd ~/.cache/CPM/boost/<hash>
git submodule update --init --depth 1 --jobs 8
git submodule foreach --recursive 'if [ -e .git ]; then git checkout HEAD -- .; fi'
cmake -S . -B build -G Ninja
cmake --build build --parallel
ctest --test-dir build -V
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build --parallel
ctest --test-dir build -V
./build/bin/bench_mark
Unit tests and benchmarks require the Lua development package (header
lua.hand library files).
- Linux:
sudo apt-get install liblua5.4-devorliblua5.3-dev- macOS:
brew install lua- Windows MSYS2:
pacman -S mingw-w64-x86_64-lua
flua./build/bin/flua <script.lua | dir/> [--once] [--jit_type=<0|1|2>]
Entry points are fixed and always live in package main — there are no flags to rename
them, so every app looks the same:
package "main"
function init() ... end -- called once at startup
function update() ... end -- called every frame
function fini() ... end -- called once before exit
<script.lua | dir/>: a single script, or a directory whose .lua files are all compiled (sorted by name)--once: run init + fini only, without the update loop (one-shot script / self-check)--jit_type: 0=TCC, 1=GCC, 2=INTERP (bytecode VM). Only the backend you pick gets
compiled: an INTERP run skips the GCC pass (~0.6 s per file) instead of paying for code it
will never execute.--log_level: minimum log level, 3=Warn by default (0=Trace … 6=Off). A long-running
host does not want the per-file INFO compile lines drowning out real errors; pass
--log_level=2 to get them back.--debug: Enable debug mode (default false; when true, outputs generated C source / richer diagnostics)update is called back-to-back; a script that wants a fixed cadence paces itself, e.g.
os.sleep(1000) at the end of update (os.sleep takes milliseconds).
Graceful shutdown (both run fini first): return a non-zero code from update, or call
flua.set_exit(code) from Lua. Note that os.exit still exits immediately and skips
fini. Signals are not handled by flua – installing a handler for SIGINT / SIGTERM is
the application’s own decision.
Each lifecycle phase is a separate top-level Call. That matters because fakelua has no GC:
its frame arena is only reset when a top-level Call starts (reentrant count back to zero),
so a while true loop inside a single Lua Call would make the arena grow without bound.
Keep update to one frame of work (pump IO/timers, dispatch requests).
# Build
cmake -S . -B build
cmake --build build --parallel
# Install (default prefix: /usr/local)
sudo cmake --install build
# or: cd build && sudo make install
flua is installed as its own component (cli) and is marked OPTIONAL, so building only
the library no longer makes the install step fail:
cmake --build build --target fakelua --parallel
sudo cmake --install build # installs the library, skips the missing flua
sudo cmake --install build --component cli # or: only the CLI
After installing to your system, other CMake projects can discover and link FakeLua using standard find_package:
cmake_minimum_required(VERSION 3.20)
project(my_project CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Discover FakeLua package
find_package(fakelua REQUIRED)
add_executable(my_project main.cpp)
# Link against fakelua (automatically sets up include directories and link flags)
target_link_libraries(my_project PRIVATE fakelua::fakelua)
# Alternatively, the unqualified alias is also supported:
# target_link_libraries(my_project PRIVATE fakelua)
In your C++ code:
#include "fakelua.h"
// or
#include <fakelua/fakelua.h>
FakeLua also installs fakelua.pc for pkg-config consumers.
Comparing Lua 5.4, FakeLua TCC, FakeLua GCC across 11 algorithms (Release -O3 mode):
| Algorithm (typical params) | Lua 5.4 | FakeLua TCC | FakeLua GCC |
|---|---|---|---|
| Fibonacci n=32 | 297.9 ms | 26.7 ms (11.2x↑) | 6.8 ms (36.6x↑) |
| Sum n=5000000 | 33.9 ms | 18.4 ms (1.8x↑) | 1.1 ms (30.4x↑) |
| Popcount n=100000 | 18.2 ms | 3.1 ms (5.9x↑) | 488.0 μs (37.3x↑) |
| BubbleSort n=200 | 1.5 ms | 3.3 ms (0.45x) | 738.8 μs (1.9x↑) |
| Sieve n=5000 | 353.4 μs | 1.0 ms (0.34x) | 219.3 μs (1.8x↑) |
| FloatPoly n=1000000 | — | — | 34.9x↑ (浮点特化,GCC 2x 快于 C++) |
TCC is generally faster than Lua for pure computation; in Table-operation-heavy scenarios, Table struct specialization gives both GCC and TCC a significant boost. Full data available in benchmark/README.md / 中文.
FakeluaStateGuard guard;
State* s = guard.GetState();
CompileFile(s, "script.lua", CompileConfig{.debug_mode = false});
int sum = 0;
Call(s, JIT_GCC, "add", sum, 10, 20); // embed-call a Lua function
// Manual management (not recommended — easy to leak)
State* s = FakeluaNewState(StateConfig{});
// ... use s ...
FakeluaDeleteState(s);
// Or RAII style (recommended)
FakeluaStateGuard guard(StateConfig{});
State* s = guard.GetState();
// ... use s ...
// automatically freed
| Function | Description |
|---|---|
FakeluaNewState() |
Create FakeLua state |
FakeluaDeleteState() |
Free FakeLua state |
CompileFile() |
Compile a Lua file |
CompileString() |
Compile a Lua code string |
Call() |
Invoke a compiled function |
GetLastRecordedCCode() |
Get the most recently compiled C code |
SetVarInterfaceNewFunc() |
Set custom VarInterface factory |
SetDebugLogLevel(s, level) |
Set this State’s log level (LogLevel::Warn … LogLevel::Trace; Lua: log.set_level) |
Host code should use the public helpers; the inter:: converters are the interpreter and
JIT internals and hosts are not supposed to depend on them.
// Inside a native callback: read arguments (missing/nil argument -> default value)
int64_t id = GetNativeIntArg(s, args, n, 0, 0);
std::string name = GetNativeStringArg(s, args, n, 1, "anon");
bool flag = GetNativeBoolArg(s, args, n, 2, false);
double ratio = GetNativeFloatArg(s, args, n, 3, 1.0);
// Return values
return NativeString(s, name); // also NativeInt / NativeBool / NativeFloat / NativeNil
Two forms. The callback form takes the raw CVar array and declares its arity
(arg_count + is_vararg):
RegisterNativeFunction(s, "greet", 1, false,
[](State *s, CVar *args, int n) -> CVar {
return NativeString(s, "hello " + GetNativeStringArg(s, args, n, 0));
});
The typed form wraps a std::function whose first parameter is State *. The template
parameters are <Ret, Args...>, so the signature has to be spelled out inside
std::function<> — that is the only form that compiles:
RegisterNativeFunction(s, "hp_of", false,
std::function<int64_t(State *, int64_t)>([](State *, int64_t id) { return id * 2; }));
Writing it as RegisterNativeFunction<std::string(State *, int64_t)>(...) passes the whole
function type as Ret; the header catches that with a static_assert instead of the wall
of “function cannot return function type” errors it used to produce.
class CustomVar : public VarInterface { /* ... */ };
SetVarInterfaceNewFunc(s, []() { return new CustomVar(); });
// Table-type arguments in Call automatically construct CustomVar instances
Lua source
↓
[Lexing] → tokens (flexer)
↓
[Parsing] → AST (bison + syntax_tree)
↓
[File-level stmt check] → reject non-declaration statements (semantic_analysis)
↓
[Preprocessing] → normalized AST (preprocessor)
↓
[Semantic analysis] → analysis result (semantic_analysis)
↓
[Type inference] → type hints (type_inferencer)
↓
┌─────────────────────────────┬──────────────────────────────┐
↓ ↓ ↓
[C code generation] [Bytecode codegen] (shared AST)
(c_gen) (interp/codegen)
↓ ↓
[JIT TCC / GCC] [Interpreter VM]
native code (interp/interpreter)
└───────────── Call(s, JIT_*, …) ─────────────┘
| Module | Responsibility |
|---|---|
lexer/parser |
Lua lexing and parsing |
syntax_tree |
AST representation and traversal |
preprocessor |
Lua syntax normalization (e.g., functiondef hoisting) |
semantic_analysis |
Semantic and control flow analysis |
type_inferencer |
Static type inference and specialization decisions |
c_gen |
C code generation and type-driven optimization |
interp/* |
Bytecode codegen, opcodes, and interpreter VM |
compile_common |
Common type inference and codegen utilities |
jit/* |
TCC/GCC backends plus Vm function registry |
native/* |
Built-in standard libraries (net, http, db, crypto, …) |
state |
FakeLua runtime state management |
var |
Dynamic value CVar and conversion utilities |
A: Certain dynamic features of full Lua (e.g., metatables) are difficult to compile efficiently. The subset focuses on statically analyzable common patterns, achieving near-C performance through type inference and JIT compilation.
A: GCC is the primary production backend (-O3). TCC compiles extremely fast for development and CI. INTERP runs bytecode without a host C compiler — good for constrained environments, tooling, and validating script semantics; hot paths can still call JIT closures when mixed.
A: Yes — use JIT_INTERP (no GCC/TCC required at runtime) or the small TCC backend. Native modules that need OpenSSL/MySQL/etc. are optional at the dependency level for your build.
A: Enable CompileConfig::debug_mode to inspect logs and C code; use GetLastRecordedCCode() to export C code for analysis.
A: Each State is currently thread-local; in multithreaded environments, create an independent State per thread.