Go&lua——github.com/yuin/gopher-lua

目录

go调用lua

这里比较下两个比较有名的go-lua包:

github.com/Shopify/go-luagithub.com/yuin/gopher-lua是两个Go语言库,允许Go程序与Lua脚本进行交互。

以下是这两个库之间的主要区别:

  1. Shopify/go-lua:

    • Shopify/go-lua是一个用Go编写的Lua解释器。
    • 它旨在提供一个轻量级、易于使用的Go和Lua之间的接口。
    • 该项目主要关注简单性和易集成性。
    • 它提供了调用Go函数和Lua函数之间的绑定。
  2. yuin/gopher-lua:

    • yuin/gopher-lua是一个更丰富功能且活跃维护的Go语言Lua虚拟机实现。
    • 它支持广泛的Lua功能,并包含一个标准库,涵盖许多常见的Lua功能。
    • 这个库允许Go代码执行Lua脚本,访问Lua变量,并注册Go函数供Lua脚本使用。
    • 它提供了更全面的文档和更大的社区,相比之下优于github.com/Shopify/go-lua
    • 根据我上次更新的信息,yuin/gopher-lua已经有了更近期的更新和改进,这表明它的维护活跃性更高。

在选择这些库时,请考虑项目的具体要求。如果您需要与Lua进行简单轻量级集成,github.com/Shopify/go-lua可能已经足够。另一方面,如果您需要一个功能更丰富且持续维护的库,支持更广泛的Lua功能,github.com/yuin/gopher-lua会是一个更好的选择。

安装

GopherLua supports >= Go1.9.

bash 复制代码
go get github.com/yuin/gopher-lua

使用

GopherLua的API与Lua的运行方式非常相似,但是堆栈仅用于传递参数和接收返回值。

Run scripts in the VM.

go 复制代码
L := lua.NewState()
defer L.Close()
// 直接执行lua代码
if err := L.DoString(`print("hello")`); err != nil {
    panic(err)
}
go 复制代码
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的基础上配置初始大小、最大大小和步长大小。这将允许注册表根据需要进行扩展。一旦扩展,它将不会再缩小。

go 复制代码
 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个调用帧的方式分配和释放调用栈帧,因此不会在每个函数调用时产生额外的分配开销。对于大多数用例,自动调整大小的调用栈的性能影响可能是可以忽略的。

go 复制代码
 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()

Data model

在GopherLua程序中,所有的数据都是LValue。LValue是一个接口类型,具有以下方法:

go 复制代码
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 -

Go中调用lua

go 复制代码
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 脚本内容:

lua 复制代码
function fib(n)
    if n < 2 then return n end
    return fib(n-1) + fib(n-2)
end
go 复制代码
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")
	}
}

API

Lua调用go

LGFunction类型:type LGFunction func(*LState) int

go 复制代码
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中的内容:

lua 复制代码
print(double(100))

再来一个案例:

go 复制代码
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内容:

lua 复制代码
print(add(10,20))

打开Lua内置模块的子集

打开Lua内置模块的子集可以通过以下方式实现,例如,可以避免启用具有访问本地文件或系统调用权限的模块。

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)
    }
}

使用Go创建模块

GopherLua除了可以满足基本的lua需要,还将Go语言特有的高级设计直接移植到lua环境中,使得内嵌的脚本也具备了一些高级的特性

可以使用context.WithTimeout对执行的lua脚本进行超时

可以使用context.WithCancel打断正在执行的lua脚本

多个lua解释器实例之间还可以通过channel共享数据

支持多路复用选择器select

使用Lua作为内嵌脚本的另外一个重要优势在于Lua非常轻量级,占用内存极小。

示例1(官方)

mymodule.go:

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:

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:

go 复制代码
local m = require("mymodule")
m.myfunc()
print(m.name)

示例2

在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代码中可能并没有特别的实际意义。

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
}

关闭一个运行的lua虚拟机

go 复制代码
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"

虚拟机之间共享lua字节码

调用DoFile会加载Lua脚本,将其编译为字节码并在LState中运行该字节码。

如果您有多个LState,都需要运行相同的脚本,您可以在它们之间共享字节码,这将节省内存。共享字节码是安全的,因为它是只读的,不会被Lua脚本更改。

go 复制代码
// 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)
}

go-lua调优

预编译

在查看上述 DoString(...) 方法的调用链后,发现每执行一次 DoString(...) 或 DoFile(...) ,都会各执行一次 parse 和 compile 。

go 复制代码
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), "<string>")
}

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 的开销。

go 复制代码
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 虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。

go 复制代码
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 程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。

go 复制代码
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
相关推荐
AI理性派思考者2 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
LeonNo115 小时前
golang , chan学习
开发语言·学习·golang
龙门吹雪5 小时前
GO语言基础面试题
golang·面试题·map·channel·
zyh_0305215 小时前
GIN中间件
后端·golang·gin
wy02_6 小时前
Linux下载RabbitMQ,并解决Github拒绝访问443的问题
linux·rabbitmq·github
Domain-zhuo7 小时前
Git常用命令
前端·git·gitee·github·gitea·gitcode
aPurpleBerry7 小时前
解决 fatal: detected dubious ownership in repository at ‘XXXX‘ 问题
github
vvw&11 小时前
如何在 Ubuntu 22.04 上安装 Graylog 开源日志管理平台
linux·运维·服务器·ubuntu·开源·github·graylog
HelloGitHub13 小时前
跟着 8.6k Star 的开源数据库,搞 RAG!
开源·github
007php0071 天前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程