5.1 KiB
LuaJIT Go Wrapper
This is a Go wrapper for LuaJIT that makes it easy to embed Lua in your Go applications. We've focused on making it both performant and developer-friendly, with an API that feels natural to use.
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/Sky/LuaJIT-to-Go
You'll need LuaJIT's development files, but don't worry - we include libraries for Windows and Linux in the vendor directory.
Here's the simplest thing you can do:
L := luajit.New()
defer L.Close()
defer L.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")
When to Use Bytecode
Bytecode execution is consistently faster than direct execution:
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
Use bytecode when you:
- Have code that runs frequently
- Need maximum performance
- Want to precompile your Lua code
- Are distributing Lua code to many instances
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
Working with Tables
Lua tables are pretty powerful - they're like a mix of Go's maps and slices. We make it easy to work with them:
// Go → Lua
stuff := map[string]interface{}{
"name": "Arthur Dent",
"age": 30,
"items": []float64{1, 2, 3},
}
L.PushTable(stuff)
// Lua → Go
L.GetGlobal("some_table")
result, err := L.ToTable(-1)
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: %s\n", luaErr.Message)
}
}
Memory Management
The wrapper uses a custom table pooling system to reduce GC pressure when handling many tables:
// Tables are pooled and reused internally for better performance
for i := 0; i < 1000; i++ {
L.GetGlobal("table")
table, _ := L.ToTable(-1)
// Use table...
L.Pop(1)
// Table is automatically returned to pool
}
Best Practices
- Always use
defer L.Close()
anddefer L.Cleanup()
to prevent memory leaks - Each Lua state should stick to one goroutine
- For concurrent operations, create multiple states
- You can share functions between states safely
- Keep an eye on your stack management - pop as many items as you push
- Use bytecode for frequently executed code paths
- Consider compiling critical Lua code to bytecode at startup
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...
L.GetGlobal("increment")
L.Call(0, 1) // Returns 1
L.Pop(1)
L.GetGlobal("increment")
L.Call(0, 1) // Returns 2
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!