Go wrapper for LuaJIT.
Go to file
2025-03-27 22:05:09 -05:00
bench move sandbox benches 2025-03-27 21:51:31 -05:00
example interface{} to any 2025-03-07 07:25:34 -06:00
tests move sandbox benches 2025-03-27 21:51:31 -05:00
vendor/luajit Update libraries to use statically-linked amalgamated LuaJIT libraries. 2025-02-02 16:02:45 -06:00
.gitignore Update libraries to use statically-linked amalgamated LuaJIT libraries. 2025-02-02 16:02:45 -06:00
bytecode.go optimize bytecode 2025-03-27 21:41:06 -05:00
DOCS.md update docs 2025-03-27 22:05:09 -05:00
functions.go optimize functions 2025-03-27 21:45:39 -05:00
go.mod Add generate to the go.mod 2025-01-25 22:10:49 -06:00
LICENSE Wrapper rewrite 2025-02-26 07:00:01 -06:00
README.md update docs 2025-03-27 22:05:09 -05:00
sandbox.go optimize sandbox 2025-03-27 21:58:56 -05:00
stack.go Wrapper rewrite 2025-02-26 07:00:01 -06:00
table.go optimize table 2025-03-27 21:42:58 -05:00
types.go Wrapper rewrite 2025-02-26 07:00:01 -06:00
wrapper.go optimize wrapper 2025-03-27 21:46:23 -05:00

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

Sandboxed Execution

For more controlled execution, the sandbox provides isolation and environment management:

// 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:

// 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]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:

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
}

The sandbox also manages its environment efficiently:

// 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:

// 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

Sandbox Environment Customization

You can tailor the sandbox environment for specific needs:

// 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!