这里比较下两个比较有名的go-lua包:
github.com/Shopify/go-lua
和github.com/yuin/gopher-lua
是两个Go语言库,允许Go程序与Lua脚本进行交互。
以下是这两个库之间的主要区别:
Shopify/go-lua:
yuin/gopher-lua:
github.com/Shopify/go-lua
。在选择这些库时,请考虑项目的具体要求。如果您需要与Lua进行简单轻量级集成,github.com/Shopify/go-lua
可能已经足够。另一方面,如果您需要一个功能更丰富且持续维护的库,支持更广泛的Lua功能,github.com/yuin/gopher-lua
会是一个更好的选择。
GopherLua supports >= Go1.9.
go get github.com/yuin/gopher-lua
GopherLua的API与Lua的运行方式非常相似,但是堆栈仅用于传递参数和接收返回值。
Run scripts in the VM.
L := lua.NewState()
defer L.Close()
// 直接执行lua代码
if err := L.DoString(`print("hello")`); err != nil {
panic(err)
}
L := lua.NewState()
defer L.Close()
// 执行lua脚本文件
if err := L.DoFile("hello.lua"); err != nil {
panic(err)
}
调用栈和注册表大小:
LState的调用栈大小控制脚本中Lua函数的最大调用深度(Go函数调用不计入其中)。
LState的注册表实现对调用函数(包括Lua和Go函数)的栈式存储,并用于表达式中的临时变量。其存储需求会随着调用栈的使用和代码复杂性的增加而增加。
注册表和调用栈都可以设置为固定大小或自动大小。
当您在进程中实例化大量的LState时,值得花时间来调整注册表和调用栈的选项。
注册表可以在每个LState的基础上配置初始大小、最大大小和步长大小。这将允许注册表根据需要进行扩展。一旦扩展,它将不会再缩小。
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()
如果注册表对于给定的脚本来说太小,最终可能会导致程序崩溃。而如果注册表太大,将会浪费内存(如果实例化了许多LState,这可能是一个显著的问题)。自动增长的注册表在调整大小时会带来一点点性能损耗,但不会影响其他方面的性能。
调用栈可以以两种不同的模式运行,即固定大小或自动大小。固定大小的调用栈具有最高的性能,并且具有固定的内存开销。自动大小的调用栈将根据需要分配和释放调用栈页面,从而确保任何时候使用的内存量最小。缺点是,每次分配新的调用帧页面时都会带来一点小的性能影响。默认情况下,一个LState会以每页8个调用帧的方式分配和释放调用栈帧,因此不会在每个函数调用时产生额外的分配开销。对于大多数用例,自动调整大小的调用栈的性能影响可能是可以忽略的。
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()
在GopherLua程序中,所有的数据都是LValue。LValue是一个接口类型,具有以下方法:
type LValue interface {
String() string
Type() LValueType
// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
assertFloat64() (float64, bool)
// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
assertString() (string, bool)
// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
assertFunction() (*LFunction, bool)
}
实现LValue接口的对象是:
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 |
- |
L := lua.NewState()
defer L.Close()
// 加载double.lua
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
Lua支持多个参数和多个返回值,参数好办,用lua.LNumber(123),返回值个数也可以是多个,调用CallByParam的时候,NRet就是返回参数个数,Fn是要调用的全局函数名,Protect为true时,如果没找到函数或者出错不会panic,只会返回err。
**GopherLua的函数调用是通过堆栈来进行的,调用前将参数压栈,完事后将结果放入堆栈中,调用方在堆栈顶部拿结果。**调用完成后,要以压栈的方式,一个一个取回返回值ret := L.Get(-1)。
fib.lua 脚本内容:
function fib(n)
if n < 2 then return n end
return fib(n-1) + fib(n-2)
end
package main
import (
"fmt"
lua "github.com/yuin/gopher-lua"
)
func main() {
// 1、创建 lua 的虚拟机
L := lua.NewState()
defer L.Close()
// 加载fib.lua
// Calling DoFile will load a Lua script, compile it to byte code and run the byte code in a LState.
// 所以DoFile既加载了文件又执行了字节码
if err := L.DoFile(`fib.lua`); err != nil {
panic(err)
}
// 调用fib(n)
err := L.CallByParam(lua.P{
Fn: L.GetGlobal("fib"), // 获取fib函数引用
NRet: 1, // 指定返回值数量
Protect: true, // 如果出现异常,是panic还是返回err
}, lua.LNumber(10)) // 传递输入参数n
if err != nil {
panic(err)
}
// 获取返回结果
ret := L.Get(-1)
// 从堆栈中扔掉返回结果
// 这里一定要注意,不调用此方法,后续再调用 L.Get(-1) 获取的还是上一次执行的结果
// 这里大家可以自己测试下
L.Pop(1)
// 打印结果
res, ok := ret.(lua.LNumber)
if ok {
fmt.Println(int(res))
} else {
fmt.Println("unexpected result")
}
}
LGFunction
类型:type LGFunction func(*LState) int
func main() {
L := lua.NewState()
defer L.Close()
L.SetGlobal("double", L.NewFunction(func(state *lua.LState) int {
lv := state.ToInt(1) /* get argument */
L.Push(lua.LNumber(lv * 2)) /* push result */
// 返回值个数
return 1
}))
// 加载编译执行hello.lua
L.DoFile("./hello.lua")
}
hello.lua中的内容:
print(double(100))
再来一个案例:
package main
import (
"fmt"
lua "github.com/yuin/gopher-lua"
)
func Add(L *lua.LState) int {
// 获取参数
arg1 := L.ToInt(1)
arg2 := L.ToInt(2)
ret := arg1 + arg2
// 返回值
L.Push(lua.LNumber(ret))
// 返回值的个数
return 1
}
func main() {
L := lua.NewState()
defer L.Close()
// 注册全局函数
L.SetGlobal("add", L.NewFunction(Add))
// go
err := L.DoFile("main.lua")
if err != nil {
fmt.Print(err.Error())
return
}
}
main.lua内容:
print(add(10,20))
打开Lua内置模块的子集可以通过以下方式实现,例如,可以避免启用具有访问本地文件或系统调用权限的模块。
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)
}
}
GopherLua除了可以满足基本的lua需要,还将Go语言特有的高级设计直接移植到lua环境中,使得内嵌的脚本也具备了一些高级的特性
可以使用context.WithTimeout对执行的lua脚本进行超时
可以使用context.WithCancel打断正在执行的lua脚本
多个lua解释器实例之间还可以通过channel共享数据
支持多路复用选择器select
使用Lua作为内嵌脚本的另外一个重要优势在于Lua非常轻量级,占用内存极小。
mymodule.go:
package main
import lua "github.com/yuin/gopher-lua"
var exports = map[string]lua.LGFunction{
"myfunc": myfunc,
}
func myfunc(L *lua.LState) int {
return 0
}
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// 注册name属性到module
L.SetField(mod, "name", lua.LString("value"))
// returns the module
L.Push(mod)
return 1
}
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)
在Go语言的实现中,当我们将一个函数注册为Lua函数(通过L.SetFuncs等方法),它会被转换为LGFunction类型,这样可以确保与Lua C API兼容。返回的整数值用于指示函数的返回值数量(通常用0表示成功,1表示出错)。
实际上,这个整数值在Go的GopherLua实现中没有特别的意义,因为Go语言不需要遵循C API规范。但为了与标准的Lua C API保持一致,Go的GopherLua库仍然要求注册给Lua的函数遵循这个规范,即返回一个整数值。
因此,在Go的GopherLua中,LGFunction类型一定要有返回值,以满足与Lua C API的兼容性需求,即使这个返回值在Go代码中可能并没有特别的实际意义。
package main
import (
lua "github.com/yuin/gopher-lua"
)
var exports = map[string]lua.LGFunction{
"add": add,
}
func add(L *lua.LState) int {
// 第一个参数
firstArg := L.Get(1).(lua.LNumber)
// 第二个参数
secondArg := L.Get(2).(lua.LNumber)
// 返回值
L.Push(firstArg + secondArg)
// 返回值个数
return 1
}
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// returns the module
L.Push(mod)
return 1
}
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// set the context to our LState
L.SetContext(ctx)
err := L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
// err.Error() contains "context deadline exceeded"
调用DoFile
会加载Lua脚本,将其编译为字节码并在LState中运行该字节码。
如果您有多个LState,都需要运行相同的脚本,您可以在它们之间共享字节码,这将节省内存。共享字节码是安全的,因为它是只读的,不会被Lua脚本更改。
// CompileLua reads the passed lua file from disk and compiles it.
func CompileLua(filePath string) (*lua.FunctionProto, error) {
file, err := os.Open(filePath)
defer file.Close()
if err != nil {
return nil, err
}
reader := bufio.NewReader(file)
chunk, err := parse.Parse(reader, filePath)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, filePath)
if err != nil {
return nil, err
}
return proto, nil
}
// DoCompiledFile takes a FunctionProto, as returned by CompileLua, and runs it in the LState. It is equivalent
// to calling DoFile on the LState with the original source file.
func DoCompiledFile(L *lua.LState, proto *lua.FunctionProto) error {
lfunc := L.NewFunctionFromProto(proto)
L.Push(lfunc)
return L.PCall(0, lua.MultRet, nil)
}
// Example shows how to share the compiled byte code from a lua script between multiple VMs.
func Example() {
codeToShare := CompileLua("mylua.lua")
a := lua.NewState()
b := lua.NewState()
c := lua.NewState()
DoCompiledFile(a, codeToShare)
DoCompiledFile(b, codeToShare)
DoCompiledFile(c, codeToShare)
}
在查看上述 DoString(…) 方法的调用链后,发现每执行一次 DoString(…) 或 DoFile(…) ,都会各执行一次 parse 和 compile 。
func (ls *LState) DoString(source string) error {
if fn, err := ls.LoadString(source); err != nil {
return err
} else {
ls.Push(fn)
return ls.PCall(0, MultRet, nil)
}
}
func (ls *LState) LoadString(source string) (*LFunction, error) {
return ls.Load(strings.NewReader(source), "" )
}
func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {
chunk, err := parse.Parse(reader, name)
// ...
proto, err := Compile(chunk, name)
// ...
}
从这一点考虑,在同份 Lua 代码将被执行多次的场景下,如果我们能够对代码进行提前编译,那么应该能够减少 parse 和 compile 的开销。
package glua_test
import (
"bufio"
"os"
"strings"
lua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"
)
// 编译 lua 代码字段
func CompileString(source string) (*lua.FunctionProto, error) {
reader := strings.NewReader(source)
chunk, err := parse.Parse(reader, source)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, source)
if err != nil {
return nil, err
}
return proto, nil
}
// 编译 lua 代码文件
func CompileFile(filePath string) (*lua.FunctionProto, error) {
file, err := os.Open(filePath)
defer file.Close()
if err != nil {
return nil, err
}
reader := bufio.NewReader(file)
chunk, err := parse.Parse(reader, filePath)
if err != nil {
return nil, err
}
proto, err := lua.Compile(chunk, filePath)
if err != nil {
return nil, err
}
return proto, nil
}
func BenchmarkRunWithoutPreCompiling(b *testing.B) {
l := lua.NewState()
for i := 0; i < b.N; i++ {
_ = l.DoString(`a = 1 + 1`)
}
l.Close()
}
func BenchmarkRunWithPreCompiling(b *testing.B) {
l := lua.NewState()
proto, _ := CompileString(`a = 1 + 1`)
lfunc := l.NewFunctionFromProto(proto)
for i := 0; i < b.N; i++ {
l.Push(lfunc)
_ = l.PCall(0, lua.MultRet, nil)
}
l.Close()
}
// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPreCompiling-8 100000 19392 ns/op 85626 B/op 67 allocs/op
// BenchmarkRunWithPreCompiling-8 1000000 1162 ns/op 2752 B/op 8 allocs/op
// PASS
// ok glua 3.328s
新建一个 Lua 虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。
type lStatePool struct {
m sync.Mutex
saved []*lua.LState
}
func (pl *lStatePool) Get() *lua.LState {
pl.m.Lock()
defer pl.m.Unlock()
n := len(pl.saved)
if n == 0 {
return pl.New()
}
x := pl.saved[n-1]
pl.saved = pl.saved[0 : n-1]
return x
}
func (pl *lStatePool) New() *lua.LState {
L := lua.NewState()
// setting the L up here.
// load scripts, set global variables, share channels, etc...
return L
}
func (pl *lStatePool) Put(L *lua.LState) {
pl.m.Lock()
defer pl.m.Unlock()
pl.saved = append(pl.saved, L)
}
func (pl *lStatePool) Shutdown() {
for _, L := range pl.saved {
L.Close()
}
}
// Global LState pool
var luaPool = &lStatePool{
saved: make([]*lua.LState, 0, 4),
}
README 提供的实例池实现,但注意到该实现在初始状态时,并未创建足够多的虚拟机实例(初始时,实例数为 0),以及存在 slice 的动态扩容问题,这都是值得改进的地方(这是一个可以提交pr的点)。
gopher-lua 支持 Lua 调用 Go 模块,在 Golang 程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。
package main
import (
"fmt"
lua "github.com/yuin/gopher-lua"
)
const source = `
local m = require("gomodule")
m.goFunc()
print(m.name)
`
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("gomodule", load)
if err := L.DoString(source); err != nil {
panic(err)
}
}
func load(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
L.SetField(mod, "name", lua.LString("gomodule"))
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"goFunc": goFunc,
}
func goFunc(L *lua.LState) int {
fmt.Println("golang")
return 0
}
// golang
// gomodule