GopherLua is a Lua5.1(+ goto statement in Lua5.2) VM and compiler written in Go. GopherLua has a same goal
with Lua: Be a scripting language with extensible semantics . It provides
Go APIs that allow you to easily embed a scripting language to your Go host
programs.
GopherLua is not fast but not too slow, I think.
GopherLua has almost equivalent ( or little bit better ) performance as Python3 on micro benchmarks.
There are some benchmarks on the wiki page .
$ go get github.com/yuin/gopher-lua
GopherLua supports >= Go1.9.
GopherLua APIs perform in much the same way as Lua, but the stack is used only for passing arguments and receiving returned values.
GopherLua supports channel operations. See "Goroutines" section.
Import a package.
import (
"github.com/yuin/gopher-lua"
)
Run scripts in the VM.
L := lua.NewState()
defer L.Close()
if err := L.DoString(`print("hello")`); err != nil {
panic(err)
}
L := lua.NewState()
defer L.Close()
if err := L.DoFile("hello.lua"); err != nil {
panic(err)
}
Refer to Lua Reference Manual and Go doc for further information.
Note that elements that are not commented in Go doc equivalent to Lua Reference Manual , except GopherLua uses objects instead of Lua stack indices.
All data in a GopherLua program is an LValue . LValue is an interface
type that has following methods.
String() stringType() LValueTypeObjects implement an LValue interface are
| Type name | Go type | Type() value | Constants |
|---|---|---|---|
LNilType |
(constants) | LTNil |
LNil |
LBool |
(constants) | LTBool |
LTrue, LFalse |
LNumber |
float64 | LTNumber |
- |
LString |
string | LTString |
- |
LFunction |
struct pointer | LTFunction |
- |
LUserData |
struct pointer | LTUserData |
- |
LState |
struct pointer | LTThread |
- |
LTable |
struct pointer | LTTable |
- |
LChannel |
chan LValue | LTChannel |
- |
You can test an object type in Go way(type assertion) or using a Type() value.
lv := L.Get(-1) // get the value at the top of the stack
if str, ok := lv.(lua.LString); ok {
// lv is LString
fmt.Println(string(str))
}
if lv.Type() != lua.LTString {
panic("string required.")
}
lv := L.Get(-1) // get the value at the top of the stack
if tbl, ok := lv.(*lua.LTable); ok {
// lv is LTable
fmt.Println(L.ObjLen(tbl))
}
Note that LBool , LNumber , LString is not a pointer.
To test LNilType and LBool, You must use pre-defined constants.
lv := L.Get(-1) // get the value at the top of the stack
if lv == lua.LTrue { // correct
}
if bl, ok := lv.(lua.LBool); ok && bool(bl) { // wrong
}
In Lua, both nil and false make a condition false. LVIsFalse and LVAsBool implement this specification.
lv := L.Get(-1) // get the value at the top of the stack
if lua.LVIsFalse(lv) { // lv is nil or false
}
if lua.LVAsBool(lv) { // lv is neither nil nor false
}
Objects that based on go structs(LFunction. LUserData, LTable)
have some public methods and fields. You can use these methods and fields for
performance and debugging, but there are some limitations.
The size of an LState's callstack controls the maximum call depth for Lua functions within a script (Go function calls do not count).
The registry of an LState implements stack storage for calling functions (both Lua and Go functions) and also for temporary variables in expressions. Its storage requirements will increase with callstack usage and also with code complexity.
Both the registry and the callstack can be set to either a fixed size or to auto size.
When you have a large number of LStates instantiated in a process, it's worth taking the time to tune the registry and callstack options.
The registry can have an initial size, a maximum size and a step size configured on a per LState basis. This will allow the registry to grow as needed. It will not shrink again after growing.
L := lua.NewState(lua.Options{
RegistrySize: 1024 * 20, // this is the initial size of the registry
RegistryMaxSize: 1024 * 80, // this is the maximum size that the registry can grow to. If set to `0` (the default) then the registry will not auto grow
RegistryGrowStep: 32, // this is how much to step up the registry by each time it runs out of space. The default is `32`.
})
defer L.Close()
A registry which is too small for a given script will ultimately result in a panic. A registry which is too big will waste memory (which can be significant if many LStates are instantiated).
Auto growing registries incur a small performance hit at the point they are resized but will not otherwise affect performance.
The callstack can operate in two different modes, fixed or auto size.
A fixed size callstack has the highest performance and has a fixed memory overhead.
An auto sizing callstack will allocate and release callstack pages on demand which will ensure the minimum amount of memory is in use at any time. The downside is it will incur a small performance impact every time a new page of callframes is allocated.
By default an LState will allocate and free callstack frames in pages of 8, so the allocation overhead is not incurred on every function call. It is very likely that the performance impact of an auto resizing callstack will be negligible for most use cases.
L := lua.NewState(lua.Options{
CallStackSize: 120, // this is the maximum callstack size of this LState
MinimizeStackMemory: true, // Defaults to `false` if not specified. If set, the callstack will auto grow and shrink as needed up to a max of `CallStackSize`. If not set, the callstack will be fixed at `CallStackSize`.
})
defer L.Close()
The above examples show how to customize the callstack and registry size on a per LState basis. You can also adjust some defaults for when options are not specified by altering the values of lua.RegistrySize, lua.RegistryGrowStep and lua.CallStackSize.
An LState object that has been created by *LState#NewThread() inherits the callstack & registry size from the parent LState object.
true .OpenXXX(L *LState) int functions you can open only those libraries that you require, for an example see below.true .Refer to Lua Reference Manual and Go doc(LState methods) for further information.
func Double(L *lua.LState) int {
lv := L.ToInt(1) /* get argument */
L.Push(lua.LNumber(lv * 2)) /* push result */
return 1 /* number of results */
}
func main() {
L := lua.NewState()
defer L.Close()
L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */
}
print(double(20)) -- > "40"
Any function registered with GopherLua is a lua.LGFunction, defined in value.go
type LGFunction func(*LState) int
Working with coroutines.
co, _ := L.NewThread() /* create a new thread */
fn := L.GetGlobal("coro").(*lua.LFunction) /* get function from lua */
for {
st, err, values := L.Resume(co, fn)
if st == lua.ResumeError {
fmt.Println("yield break(error)")
fmt.Println(err.Error())
break
}
for i, lv := range values {
fmt.Printf("%v : %v\n", i, lv)
}
if st == lua.ResumeOK {
fmt.Println("yield break(ok)")
break
}
}
The following demonstrates how to open a subset of the built-in modules in Lua, say for example to avoid enabling modules with access to local files or system calls.
main.go
func main() {
L := lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close()
for _, pair := range []struct {
n string
f lua.LGFunction
}{
{lua.LoadLibName, lua.OpenPackage}, // Must be first
{lua.BaseLibName, lua.OpenBase},
{lua.TabLibName, lua.OpenTable},
} {
if err := L.CallByParam(lua.P{
Fn: L.NewFunction(pair.f),
NRet: 0,
Protect: true,
}, lua.LString(pair.n)); err != nil {
panic(err)
}
}
if err := L.DoFile("main.lua"); err != nil {
panic(err)
}
}
mymodule.go
package mymodule
import (
"github.com/yuin/gopher-lua"
)
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// register other stuff
L.SetField(mod, "name", lua.LString("value"))
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"myfunc": myfunc,
}
func myfunc(L *lua.LState) int {
return 0
}
mymain.go
package main
import (
"./mymodule"
"github.com/yuin/gopher-lua"
)
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("mymodule", mymodule.Loader)
if err := L.DoFile("main.lua"); err != nil {
panic(err)
}
}
main.lua
local m = require("mymodule")
m.myfunc()
print(m.name)
L := lua.NewState()
defer L.Close()
if err := L.DoFile("double.lua"); err != nil {
panic(err)
}
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal("double"),
NRet: 1,
Protect: true,
}, lua.LNumber(10)); err != nil {
panic(err)
}
ret := L.Get(-1) // returned value
L.Pop(1) // remove received value
If Protect is false, GopherLua will panic instead of returning an error value.
You can extend GopherLua with new types written in Go.
LUserData is provided for this purpose.
type Person struct {
Name string
}
const luaPersonTypeName = "person"
// Registers my person type to given L.
func registerPersonType(L *lua.LState) {
mt := L.NewTypeMetatable(luaPersonTypeName)
L.SetGlobal("person", mt)
// static attributes
L.SetField(mt, "new", L.NewFunction(newPerson))
// methods
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
}
// Constructor
func newPerson(L *lua.LState) int {
person := &Person{L.CheckString(1)}
ud := L.NewUserData()
ud.Value = person
L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
L.Push(ud)
return 1
}
// Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
func checkPerson(L *lua.LState) *Person {
ud := L.CheckUserData(1)
if v, ok := ud.Value.(*Person); ok {
return v
}
L.ArgError(1, "person expected")
return nil
}
var personMethods = map[string]lua.LGFunction{
"name": personGetSetName,
}
// Getter and setter for the Person#Name
func personGetSetName(L *lua.LState) int {
p := checkPerson(L)
if L.GetTop() == 2 {
p.Name = L.CheckString(2)
return 0
}
L.Push(lua.LString(p.Name))
return 1
}
func main() {
L := lua.NewState()
defer L.Close()
registerPersonType(L)
if err := L.DoString(`
p = person.new("Steeve")
print(p:name()) -- "Steeve"
p:name("Alice")
print(p:name()) -- "Alice"
`); err != nil {
panic(err)
}
}
GopherLua supports the [Go Concurrency Patterns:
$ claude mcp add gopher-lua \
-- python -m otcore.mcp_server <graph>