Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

luau

Idiomatic Zig bindings to Luau, Roblox's typed Lua dialect. Built against Luau 0.726 with Zig 0.16.

The public API is pure, idiomatic Zig — the raw C layer is internal and never exposed. zig build coverage reports two things honestly:

A. binding gate (C API + our shims)   1255/1255 wrapped — gated, build fails if any slip
B. Luau C++ surface                   1048 entry points across every module

The C API is complete and enforced. The C++ libraries (parser, compiler, type checker, codegen) are bound broadly via extern "C" shims — a curated subset, not parity: the common embedding and tooling paths are covered, while deeper corners (e.g. source-level type-annotation reflection in the AST) are thinner. zig build gaps prints the per-module gap map; full accounting is in coverage.md.

Highlights

  • Zero-cost state*Lua is a lua_State*; the wrapper adds no overhead.
  • Allocator-backedLua.init(gpa) routes every VM allocation through a Zig allocator; deinit() is RAII. setAllocator(gpa) routes the C++ tooling's allocations (parser/checker/…) through one too.
  • Errors, not codes — protected calls return Zig error unions.
  • Comptime marshalling — push/pull any Zig type: ints, floats, bools, enums, optionals, strings, lists/arrays ↔ tables, structs ↔ tables, tagged unions ↔ variants, tuples ↔ multiple returns.
  • Real function signatures — expose fn(a: f64, b: f64) f64 directly; args pulled from the stack, results pushed, a returned error raised as a Luau error. Plus closures (setCapture bundles captured state) and overloading (setOverload(.{f0, f1}), dispatched by arg count).
  • Sol-style usertypesregisterType(T) binds a struct's constructor, methods, fields, computed properties (read-only or read/write), operators, and destructor.
  • Table ergonomics — typed get/set, getOr(default), and nested getPath/setPath (vm.globals().setPath(&.{ "a", "b" }, x)).
  • Full language tooling — parse to an AST, type-check, lint, autocomplete, hand-emit bytecode, and JIT to native code with disassembly.

Install

zig fetch --save git+https://github.com/zigsel/luau
// build.zig
const luau = b.dependency("luau", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("luau", luau.module("luau"));

Options: -Dvector_size=3|4 (native vector lanes), -Dcodegen=true|false (JIT).

A taste

const std = @import("std");
const luau = @import("luau");

fn add(a: f64, b: f64) f64 {
    return a + b;
}

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();

    var vm = try luau.Lua.init(gpa.allocator());
    defer vm.deinit();
    vm.openLibs();

    // expose a Zig function, call it from Luau
    vm.setFn("add", add);
    try vm.doString("=demo", "print(add(40, 2))"); // 42

    // run a script and read a global back, typed
    try vm.doString("=cfg", "port = 8080");
    const port = try vm.get(u16, "port");
    std.debug.print("port = {d}\n", .{port});
}

Two tiers of API

Embedding the VM — the Lua C API, made idiomatic:

luau.Lua the state: stack, call/pcall, globals, libraries, GC, refs, coroutines, debug
luau.Table / luau.Ref / luau.Buffer ergonomic value handles
luau.compile / luau.compiler source → owned bytecode
luau.codegen native JIT: compile, stats, disassembly, native toggles

Tooling over Luau source — Luau's own additions, via extern "C" shims:

luau.ast parse → syntax errors, hot comments, full AST node walking
luau.analysis type-check, lint, autocomplete
luau.config parse .luaurc (mode, globals, lint settings)
luau.require configurable require-by-string (Resolver vtable; sandboxed FsResolver included)
luau.bytecode hand-emit bytecode (BytecodeBuilder)

Examples

Run any with zig build <name>:

command file shows
embedding examples/embedding.zig run a script, read globals typed
zig-functions examples/zig_functions.zig expose Zig functions to Luau
usertypes examples/usertypes.zig register a Zig struct (methods, fields, operators)
modules examples/modules.zig bind a Zig namespace as a library
tables examples/tables.zig the Table handle and a typed Ref
coroutines examples/coroutines.zig drive a Luau coroutine across its yields
compiling examples/compiling.zig compile to bytecode, then JIT it
bytecode examples/bytecode.zig hand-emit bytecode, then load and run it
parsing examples/parsing.zig parse and walk the AST
ast-types examples/ast_types.zig reflect type annotations (aliases, unions, params)
analysis examples/analysis.zig type-check, lint, autocomplete
config examples/config.zig parse .luaurc, resolve path aliases
require examples/require.zig install require via a Resolver over a virtual module tree
declare examples/declare.zig project Zig types into Luau type defs, type-check a mod

Architecture

your Zig code
    │  import "luau"
public API (src/root.zig)         pure idiomatic Zig — the only thing you touch
    ├─ src/lua/    embedding the VM (the Lua C API, idiomatic)
    └─ src/luau/   Luau tooling (ast, analysis, config, require, bytecode)
        │
internal `bindings` module        translate-c over the Luau headers + shims
        │
libluau.a                         Luau C++ compiled by Zig (extern "C" + longjmp)
        ├─ shim/                  extern "C" wrappers over the C++ APIs
        └─ Luau 0.726 sources     fetched & built by build.zig

The C++ tooling (parser, type checker, …) has no C entry points, so shim/ provides hand-written extern "C" wrappers that are compiled into libluau and fed through the same translate-c pipeline as the C headers.

Build commands

command what
zig build test run the test suite (tests/)
zig build coverage verify every Luau symbol is wrapped (fails if not)
zig build api inventory Luau's real public C++ surface (clang AST)
zig build gaps per-module shim coverage gap map (-- <Module> to drill in)
zig build <example> build and run an example

Scope

The C API (VM, compiler, codegen, require) is wrapped and gated at 1255/1255 by zig build coverage — the stable layer, won't change under you.

The C++ libraries (Ast, Compiler, Bytecode, CodeGen, Config, Analysis) are bound broadly via extern "C" shims — 1048 entry points spanning the common embedding & tooling paths: parse, build & compile an AST, typed AST node walking, pretty-print/lex, bytecode emit, native JIT and the full x64/a64 assemblers, type-check, lint, autocomplete, the type graph, type relations & transforms, multi-module/require, host type definitions, go-to-definition, and type visualization.

This is a curated subset, not parityzig build gaps maps what each module does and doesn't reach. What's left untouched is now overwhelmingly live-state-only: the constraint solver / type-function runtime (Analysis) and the JIT lowering pipeline (CodeGen). The everyday standalone surface — including source-level type-annotation reflection, structured diagnostics, .luaurc aliases, and signature docs — is bound.

These libraries are internal and unstable, so this layer is pinned to the vendored Luau version; tools/stability.sh is the upgrade map. Beyond the curated subset, the parts that are provably un-callable from outside the solver (e.g. occursCheck asserts; Normalizer::unionType is private) and the deep constraint-solver internals that require live solver state. Full accounting in coverage.md.

About

zig bindings for luau scripting language

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages