311 lines
8.1 KiB
Markdown
311 lines
8.1 KiB
Markdown
# 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:
|
|
```bash
|
|
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:
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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
|
|
|
|
## Sandboxed Execution
|
|
|
|
For more controlled execution, the sandbox provides isolation and environment management:
|
|
|
|
```go
|
|
// Create a sandbox
|
|
sandbox := luajit.NewSandbox()
|
|
defer sandbox.Close()
|
|
|
|
// Initialize the environment
|
|
err := sandbox.Initialize()
|
|
|
|
// Register a function
|
|
adder := func(s *luajit.State) int {
|
|
sum := s.ToNumber(1) + s.ToNumber(2)
|
|
s.PushNumber(sum)
|
|
return 1
|
|
}
|
|
sandbox.RegisterFunction("add", adder)
|
|
|
|
// Run code in the sandbox
|
|
result, err := sandbox.Run(`
|
|
local result = add(40, 2)
|
|
return "The answer is: " .. result
|
|
`)
|
|
fmt.Println(result) // "The answer is: 42"
|
|
|
|
// Add permanent functions to the environment
|
|
sandbox.AddPermanentLua(`
|
|
function square(x)
|
|
return x * x
|
|
end
|
|
`)
|
|
|
|
// Run bytecode for better performance
|
|
bytecode, _ := sandbox.Compile(`return square(10)`)
|
|
result, _ = sandbox.RunBytecode(bytecode)
|
|
```
|
|
|
|
### When to Use the Sandbox
|
|
|
|
Use the sandbox when you need:
|
|
- Isolated execution environments
|
|
- Persistent state between executions
|
|
- Control over available libraries and functions
|
|
- Enhanced security for untrusted code
|
|
- Better resource management for long-running applications
|
|
|
|
## Registering Go Functions
|
|
|
|
Want to call Go code from Lua? It's straightforward:
|
|
```go
|
|
// 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:
|
|
```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
|
|
// Go → Lua
|
|
stuff := map[string]any{
|
|
"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:
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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
|
|
}
|
|
```
|
|
|
|
The sandbox also manages its environment efficiently:
|
|
|
|
```go
|
|
// Environment objects are pooled and reused
|
|
for i := 0; i < 1000; i++ {
|
|
result, _ := sandbox.Run("return i + 1")
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### State Management
|
|
- Always use `defer L.Close()` and `defer 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
|
|
|
|
### Bytecode Optimization
|
|
- Use bytecode for frequently executed code paths
|
|
- Consider compiling critical Lua code to bytecode at startup
|
|
- For small scripts (< 1024 bytes), direct execution might be faster
|
|
|
|
### Sandbox Usage
|
|
- Call `Initialize()` before first use of a sandbox
|
|
- Use `defer sandbox.Close()` to ensure proper cleanup
|
|
- For persistent objects, add them with `SetGlobal()`
|
|
- Control available libraries with `AddModule()`
|
|
- For frequently used functions, use `RegisterFunction()`
|
|
- Reset the environment with `ResetEnvironment()` if state gets corrupted
|
|
- For maximum performance, compile scripts to bytecode first, then run with `RunBytecode()`
|
|
|
|
## Advanced Features
|
|
|
|
### Bytecode Serialization
|
|
|
|
You can serialize bytecode for distribution or caching:
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
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
|
|
```
|
|
|
|
### Sandbox Environment Customization
|
|
|
|
You can tailor the sandbox environment for specific needs:
|
|
|
|
```go
|
|
// Add a custom module
|
|
myModule := map[string]any{
|
|
"version": "1.0",
|
|
"config": map[string]any{
|
|
"debug": true,
|
|
"maxItems": 100,
|
|
},
|
|
}
|
|
sandbox.AddModule("myapp", myModule)
|
|
|
|
// Add permanent Lua code
|
|
sandbox.AddPermanentLua(`
|
|
-- Utility functions available to all scripts
|
|
function myapp.formatItem(item)
|
|
return string.format("%s: %d", item.name, item.value)
|
|
end
|
|
`)
|
|
|
|
// Use in script
|
|
result, _ := sandbox.Run(`
|
|
local item = {name="test", value=42}
|
|
return myapp.formatItem(item)
|
|
`)
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
- The sandbox adds a small overhead to execution but provides better isolation
|
|
- For maximum performance with the sandbox, use bytecode compilation
|
|
- The wrapper optimizes for common operations with specialized C helpers
|
|
- For small scripts (<1KB), direct execution with `DoString` is fastest
|
|
- For larger scripts or repeated execution, bytecode provides better performance
|
|
- Table operations are optimized for both array-like and map-like tables
|
|
|
|
## 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! |