LuaJIT to Go wrapper! Use LuaJIT 2.1 in Go apps.
  • Go 96.9%
  • Shell 2.1%
  • Lua 1%
Find a file
2026-01-13 19:18:14 -06:00
bench Update README, update go mod link 2026-01-13 19:18:14 -06:00
example Update README, update go mod link 2026-01-13 19:18:14 -06:00
tests Update README, update go mod link 2026-01-13 19:18:14 -06:00
.gitignore add bench profiling 2025-04-04 21:32:17 -05:00
builder.go massive rewrite 2025-05-31 17:42:58 -05:00
bytecode.go massive rewrite 2025-05-31 17:42:58 -05:00
DOCS.md Clean up DOCS 2026-01-13 18:22:45 -06:00
functions.go Major improvements: correctness, performance, and API enhancements 2026-01-13 18:03:20 -06:00
go.mod Update README, update go mod link 2026-01-13 19:18:14 -06:00
go.sum Update README, update go mod link 2026-01-13 19:18:14 -06:00
LICENSE Wrapper rewrite 2025-02-26 07:00:01 -06:00
README.md Update README, update go mod link 2026-01-13 19:18:14 -06:00
stack.go Major improvements: correctness, performance, and API enhancements 2026-01-13 18:03:20 -06:00
types.go massive rewrite 2025-05-31 17:42:58 -05:00
validation.go massive rewrite 2025-05-31 17:42:58 -05:00
wrapper.go Major improvements: correctness, performance, and API enhancements 2026-01-13 18:03:20 -06:00

LuaJIT Go Wrapper

This is a wrapper that makes it easy to embed LuaJIT 2.1 into Go apps.

What's This For?

This wrapper lets you run Lua code from Go and easily pass data back and forth between the two languages. You might want this if you're:

  • Adding scripting support to your application
  • Building a game engine
  • Creating a configuration system
  • Writing an embedded rules engine
  • Building test automation tools

Get Started

First, grab the package:

go get git.sharkk.net/go/ljtg

Here's the simplest thing you can do:

L := luajit.New() // pass false to not load standard libs
defer L.Close()  // Automatically handles all cleanup

err := L.DoString(`print("Hey from Lua!")`)

Working with Bytecode

Need even more performance? You can compile your Lua code to bytecode and reuse it:

// Compile once
bytecode, err := L.CompileBytecode(`
	local function calculate(x)
		return x * x + x + 1
	end
	return calculate(10)
`, "calc")

// Execute many times
for i := 0; i < 1000; i++ {
	err := L.LoadAndRunBytecode(bytecode, "calc")
}

// Or do both at once
err := L.CompileAndRun(`return "hello"`, "greeting")
Benchmark                           Ops/sec         Comparison
----------------------------------------------------------------------------
BenchmarkSimpleDoString             2,561,012        Base
BenchmarkSimplePrecompiledBytecode  3,828,841        +49.5% faster
BenchmarkFunctionCallDoString       2,021,098        Base
BenchmarkFunctionCallPrecompiled    3,482,074        +72.3% faster
BenchmarkLoopDoString                 188,119        Base
BenchmarkLoopPrecompiled              211,081        +12.2% faster
BenchmarkTableOperationsDoString       84,086        Base
BenchmarkTableOperationsPrecompiled    93,655        +11.4% faster
BenchmarkComplexScript                 33,133        Base
BenchmarkComplexScriptPrecompiled      41,044        +23.9% faster

Registering Go Functions

Want to call Go code from Lua? It's straightforward:

// This function adds two numbers and returns the result
adder := func(s *luajit.State) int {
	sum := s.ToNumber(1) + s.ToNumber(2)
	s.PushNumber(sum)
	return 1  // we're returning one value
}

L.RegisterGoFunction("add", adder)

Now in Lua:

result = add(40, 2)  -- result = 42

Error Handling in Go Functions

Handle errors clearly with ReturnError:

calculator := func(s *luajit.State) int {
	// Validate arguments
	if err := s.CheckArgs(
		luajit.ArgSpec{Name: "x", Type: "number", Required: true, Check: luajit.CheckNumber},
		luajit.ArgSpec{Name: "y", Type: "number", Required: true, Check: luajit.CheckNumber},
	); err != nil {
		return s.ReturnError("validation failed: %v", err)
	}

	result := s.ToNumber(1) + s.ToNumber(2)
	s.PushNumber(result)
	return 1
}

Use the LUA_ERROR constant for clarity:

func divide(s *luajit.State) int {
	a, b := s.ToNumber(1), s.ToNumber(2)
	if b == 0 {
		return s.ReturnError("division by zero")  // Returns LUA_ERROR
	}
	s.PushNumber(a / b)
	return 1
}

Convenient API Methods

The wrapper provides intuitive methods for common operations:

Quick Function Calls

// Call a function and get the first result directly
result, err := L.CallGlobalSingle("myFunc", arg1, arg2)

// Evaluate Lua code concisely
answer, err := L.Eval("return 2 + 2")  // Returns 4

// Get a Lua function for repeated calls
fn, err := L.GetFunction("calculate")
defer fn.Release()
result, _ := fn.Call(10, 20)

Stack Validation

// Ensure stack has space before bulk operations
if err := L.CheckStack(1000); err != nil {
	// Handle stack overflow
}

// Validate stack indices
if L.IsValidIndex(-1) {
	value := L.ToValue(-1)
}

Working with Tables

Lua tables are powerful - they're like a mix of Go's maps and slices. We make it easy to work with them:

// Go → Lua (optimized bulk array push)
stuff := map[string]any{
	"name": "Arthur Dent",
	"age": 30,
	"items": []float64{1, 2, 3},  // Fast bulk push!
}
L.PushValue(stuff)  // Handles all Go types automatically

// Lua → Go with automatic type detection
L.GetGlobal("some_table")
result, err := L.ToTable(-1)  // Returns optimal Go type
// For maps: map[string]string, map[string]int, or map[string]any
// For arrays: []int, []float64, []string, []bool, or []any

Table Builder

Build tables fluently:

L.NewTableBuilder().
	SetString("name", "John").
	SetNumber("age", 30).
	SetBool("active", true).
	SetArray("scores", []any{95, 87, 92}).
	Build()

Table Field Access

Get fields with defaults:

L.GetGlobal("config")
host := L.GetFieldString(-1, "host", "localhost")
port := L.GetFieldNumber(-1, "port", 8080)
debug := L.GetFieldBool(-1, "debug", false)

Error Handling

We provide useful errors instead of mysterious panics:

if err := L.DoString("this isn't valid Lua!"); err != nil {
	if luaErr, ok := err.(*luajit.LuaError); ok {
		fmt.Printf("Error in %s:%d - %s\n", luaErr.File, luaErr.Line, luaErr.Message)
		fmt.Printf("Stack trace:\n%s\n", luaErr.StackTrace)
	}
}

Memory Management

Memory management is now automatic - just call Close():

L := luajit.New()
defer L.Close()  // Automatically cleans up:
                 // - Lua state
                 // - Registered Go functions
                 // - String buffers
                 // - All allocations

// No need for separate Cleanup() anymore!

The wrapper uses smart optimizations:

// String buffer reuse for large strings (zero allocations)
for i := 0; i < 1000; i++ {
	L.PushString(largeString)  // Reuses buffer after first allocation
	L.Pop(1)
}

// Bytecode buffer pooling
bytecode, _ := L.CompileBytecode(code, "test")  // Buffers are pooled internally

// Per-state function storage (no malloc!)
L.RegisterGoFunction("myFunc", myGoFunc)  // Uses integer IDs, not pointers

Best Practices

State Management

  • Always use defer L.Close() (handles all cleanup automatically)
  • Each Lua state should stick to one goroutine for safety
  • Create multiple states for concurrent operations - they're thread-safe!
  • Use CheckStack() before bulk operations to prevent overflows
  • Use IsValidIndex() to validate stack indices before access

Performance Optimization

  • Arrays are automatically bulk-marshaled (300x fewer CGO calls!)
  • Use CallGlobalSingle() instead of CallGlobal()[0] for single results
  • Use Eval() for quick code evaluation
  • Compile frequently-executed code to bytecode
  • For small scripts (<1024 bytes), direct execution may be faster

Type Safety

  • ToTable() returns typed arrays and maps when possible:
    • []int, []string, []bool, []float64 for homogeneous arrays
    • map[string]string, map[string]int, map[string]bool for homogeneous maps
    • Falls back to []any or map[string]any for mixed types
  • Use typed field accessors (GetFieldString, GetFieldNumber) for configs
  • Leverage PushValue() for automatic Go-to-Lua conversion

Error Handling

  • Use ReturnError() in Go functions for clear error signaling
  • Use LUA_ERROR constant instead of magic -1
  • Check errors from all Lua operations

Advanced Features

Bytecode Serialization

You can serialize bytecode for distribution or caching:

// Compile once
bytecode, _ := L.CompileBytecode(complexScript, "module")

// Save to file
ioutil.WriteFile("module.luac", bytecode, 0644)

// Later, load from file
bytecode, _ := ioutil.ReadFile("module.luac")
L.LoadAndRunBytecode(bytecode, "module")

Closures and Upvalues

Bytecode properly preserves closures and upvalues:

code := `
	local counter = 0
	return function()
		counter = counter + 1
		return counter
	end
`

bytecode, _ := L.CompileBytecode(code, "counter")
L.LoadAndRunBytecodeWithResults(bytecode, "counter", 1)
L.SetGlobal("increment")

// Later...
results, _ := L.CallGlobal("increment")  // Returns []any{1}
results, _ = L.CallGlobal("increment")   // Returns []any{2}

Batch Execution

Execute multiple statements efficiently:

statements := []string{
	"x = 10",
	"y = 20",
	"result = x + y",
}
err := L.BatchExecute(statements)

Package Path Management

Manage Lua module paths:

L.SetPackagePath("./?.lua;./modules/?.lua")
L.AddPackagePath("./vendor/?.lua")

Type Conversion System

The wrapper includes a comprehensive type conversion system:

// Get typed values with automatic conversion
value, ok := luajit.GetTypedValue[int](L, -1)
global, ok := luajit.GetGlobalTyped[[]string](L, "myArray")

// Convert between compatible types
result, ok := luajit.ConvertValue[map[string]int](someMap)

Performance Benchmarks

Recent improvements deliver significant performance gains:

Operation                  Performance       Notes
----------------------------------------------------------------------
Array push (100 ints)      740.5 ns/op      300x fewer CGO calls
Array extract (100 ints)   1384 ns/op       400x fewer CGO calls
String push (<256 bytes)   91.86 ns/op      Stack allocation
String push (>512 bytes)   69.44 ns/op      0 allocs (buffer reuse!)
Function calls             648.0 ns/op      15-25% faster
State creation             31316 ns/op      Auto-cleanup included

Key Performance Tips:

  • Arrays of 100+ elements see 40-60% performance improvement
  • String operations are GC-safe with zero allocations for large strings
  • Use CallGlobalSingle() for ~10% better performance vs CallGlobal()[0]
  • Bytecode compilation worthwhile for scripts executed 10+ times
  • Create multiple states for true parallelism (thread-safe!)

Need Help?

Check out the tests in the repository - they're full of examples. If you're stuck, open an issue! We're here to help.

License

MIT Licensed - do whatever you want with it!