GitHub - mmcdole/lunar: A Lua 5.1 runtime for Go.

GitHub

5 min read Original article ↗

Lunar gopher orbiting the Moon

A fast, memory-efficient Lua 5.1 runtime for Go.

Go Reference MIT License

Lunar is a complete implementation of Lua 5.1 (plus 5.2-style goto and labels) in pure Go. It passes the official Lua 5.1 test suite.

Quick start

package main

import (
	"fmt"

	"github.com/mmcdole/lunar"
)

func main() {
	state, err := lua.New(lua.Options{Libraries: lua.CoreLibraries()})
	if err != nil {
		panic(err)
	}
	defer state.Close()

	results, err := state.DoString("@demo.lua", `return ("lunar"):upper()`)
	if err != nil {
		panic(err)
	}

	text, _ := results[0].AsString()
	fmt.Println(text) // LUNAR
}

Libraries and file access are opt-in. lua.CoreLibraries() installs everything that can't touch the host, and lua.FullLibraries() adds IO, OS, and debug for scripts you trust. Loading scripts from disk is disabled until you configure a ScriptLoader.

Calling Go from Lua

NewNativeFunction wraps a Go function as a Lua value. Inside the callback, everything goes through the Frame: read arguments with its typed accessors, then return a result or throw a Lua error:

// Callable from Lua as greet(name).
greet, err := state.NewNativeFunction(func(frame lua.Frame) lua.Outcome {
	name, ok := frame.String(0) // first argument, must be a string
	if !ok {
		frame.ThrowArgTypeError(0, lua.StringKind)
	}
	return frame.ReturnString("hello, " + name)
})
if err != nil {
	panic(err)
}
if err := state.SetGlobal("greet", greet.Value()); err != nil {
	panic(err)
}

// results[0] is "hello, moon"
results, err := state.DoString("@hello.lua", `return greet("moon")`)

See Embedding Lunar for the full guide: calls, callbacks, tables, errors, cancellation, coroutines, and lifecycle.

Performance

Medians from 15 runs on an Apple M3 Pro with Go 1.25.1; each linked result set records its exact source revision. Lower is better.

Established Lua program Lunar GopherLua go-lua
binary-trees 162.2 ms 172.4 ms 179.8 ms
fannkuch-redux 23.63 ms 33.17 ms 40.14 ms
n-body 59.03 ms 193.39 ms 197.77 ms
spectral-norm 52.82 ms 160.14 ms 153.66 ms
Embedding operation Lunar GopherLua go-lua
Go calls Lua with scalar arguments 61.21 ns 61.51 ns 146.80 ns
Lua calls Go 1,000 times 62.49 µs 99.58 µs 84.37 µs
Lua echoes a 128-byte Go string 86.20 ns 81.41 ns 144.00 ns
Lua checksums a reused Go-built table 312.4 ns 578.3 ns 972.9 ns
Build a table in Go, then checksum it in Lua 2.230 µs 1.405 µs 1.645 µs
Live heap added after loading and GC Lunar GopherLua Ratio
9 MB CBOR graph: 183,513 tables, 938,452 entries 72.2 MiB 542.3 MiB 7.5×
25,000 four-field tables, repeated 16 B keys 7.3 MiB 72.0 MiB 9.9×
25,000 four-field tables, repeated 80 B keys 14.9 MiB 78.1 MiB 5.3×
One table, 100,000 unique 16 B keys 6.5 MiB 14.7 MiB 2.26×
One table, 100,000 unique 256 B keys 29.4 MiB 37.6 MiB 1.28×
One table, 100,000 unique 1 KiB keys 102.7 MiB 110.9 MiB 1.08×

The ratio depends on workload shape: Lunar wins on per-table overhead and on reusing repeated strings up to 64 bytes, while raw string bytes cost both runtimes the same, so the gap narrows toward 1× as string payload dominates. Loading the CBOR graph also allocates 7.3× less transient memory (107.5 MB versus 784.6 MB).

The full results include confidence intervals, allocation counts, and raw output; the benchmark protocol lists the commands, inputs, and runtime versions.

Compatibility

Lunar GopherLua Shopify go-lua
Lua version Lua 5.1 with Lua 5.2-style goto Lua 5.1 with Lua 5.2-style goto Lua 5.2
Go API Functions return typed values; callbacks use typed Frame accessors Values are LValue objects; callbacks pass arguments and results through an LState stack Mirrors the Lua C API; values are addressed by numeric stack position
Libraries in a new state None by default; select any subset at construction or open one later All standard libraries None; call OpenLibraries or open them individually
Script-file loading Denied by default; select host files, an fs.FS, or a host function Ambient OS file access Ambient OS file access when the applicable libraries are open
Coroutines Supported from Lua and Go Supported from Lua and Go Not implemented
Cancellation One installed context covers execution, loading, and coroutines One context on the state, execution only No context-based cancellation
os.exit Returns an *lua.ExitRequest to Go Exits the entire Go process Exits the entire Go process
Binary chunks Reads and writes Lua 5.1 bytecode when byte order and type sizes match Cannot read or write standard Lua bytecode files Reads and writes Lua 5.2 bytecode through the Go API

Scope

The compiler, VM, standard libraries, coroutines, binary chunks, weak tables, and finalizers are implemented. Current intentional limits:

  • no C ABI, native C-module loading, or light userdata;
  • no debug.sethook or debug.gethook;
  • garbage collection runs synchronously rather than incrementally;
  • no deterministic VM-instruction budget; and
  • no table-iteration helpers beyond the Table.Next primitive.

A State serves one goroutine at a time; separate States can run concurrently.

The public embedding API is still stabilizing.

Documentation

  • Embedding: setup, calls, callbacks, values, contexts, errors, and lifecycle
  • Architecture: compiler, VM, runtime representation, and API boundaries
  • Language compatibility: the goto extension and intentional Lua 5.1/5.2/LuaJIT choices
  • Collection: Lua reachability, weak tables, and finalization
  • Third-party notices: adapted algorithms, artwork, and benchmark sources

Lunar is available under the MIT License.