浅谈如何使用 github.com/yuin/gopher-lua

news/2025/1/11 4:16:42/

1、 gopher-lua 基础介绍

我们先开看看官方是如何介绍自己的:

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是一个Lua5.1(Lua5.2中的+goto语句)虚拟机和用Go编写的编译器。GopherLua与Lua有着相同的目标:成为一种具有可扩展语义的脚本语言。它提供了Go API,允许您轻松地将脚本语言嵌入到Go主机程序中。

看上面的翻译还是有点抽象,说说自己的理解。 github.com/yuin/gopher-lua 是一个纯 Golang 实现的 Lua 虚拟机,它能够很轻松的在 go 写的程序中调用 lua 脚本。另外提一嘴,使用插件后,也能够在 lua 脚本中调用 go 写好的代码。挺秀的!

接下来我们看一看, github.com/yuin/gopher-lua 的性能如何,这里就直接引用官方自己做的测试来介绍。详情见 wiki page 链接。点进链接过后,发现性能还不错,执行效率和性能仅比 C 实现的 bindings 差点。

官方测试例子是生成斐波那契数列,测试执行结果如下:

progtime
anko182.73s
otto173.32s
go-lua8.13s
Python3.45.84s
GopherLua5.40s
lua5.1.41.71s

2、 gopher-lua 基础介绍

下面的介绍,都是基于 v1.1.0 版本进行的。

go get github.com/yuin/gopher-lua@v1.1.0

Go的版本需要 >= 1.9

2.1 gopher-lua 中的 hello world

这里写一个简单的程序,了解 gopher-lua 是如何使用的。

package mainimport (lua "github.com/yuin/gopher-lua"
)func main() {// 1、创建 lua 的虚拟机L := lua.NewState()// 执行完毕后关闭虚拟机defer L.Close()// 2、加载fib.luaif err := L.DoString(`print("hello world")`); err != nil {panic(err)}}

执行结果:

hello world

看到这里,感觉没啥特别的地方,接下来,我们看一看 gopher-lua 如何调用事先写好的 lua脚本

fib.lua 脚本内容:

function fib(n)if n < 2 then return n endreturn fib(n-1) + fib(n-2)
end

main.go

package mainimport ("fmt"lua "github.com/yuin/gopher-lua"
)func main() {// 1、创建 lua 的虚拟机L := lua.NewState()defer L.Close()// 加载fib.luaif 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)) // 传递输入参数nif 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")}
}

执行结果:

55

从上面我们已经能够感受到部分 gopher-lua 的魅力了。接下来,我们就一起详细的学习学习 gopher-lua 。

2.2 gopher-lua 中的数据类型

All data in a GopherLua program is an LValue . LValue is an interface type that has following methods.

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

  • String() string
  • Type() LValueType
// value.go:29type LValue interface {String() stringType() 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)
}

上面来自官方的介绍,接下来我们看看 gopher-lua 支持那些数据类型。

Type nameGo typeType() valueConstants
LNilType(constants)LTNilLNil
LBool(constants)LTBoolLTrueLFalse
LNumberfloat64LTNumber-
LStringstringLTString-
LFunctionstruct pointerLTFunction-
LUserDatastruct pointerLTUserData-
LStatestruct pointerLTThread-
LTablestruct pointerLTTable-
LChannelchan LValueLTChannel-

具体的实现,大家有兴趣,可以自己去看看源码,这里就不做分析了。

那我们是如何知道 go 调用 lua 函数后,得到结果的类型呢?我们可以通过以下方式来知道:

package mainimport ("fmt"lua "github.com/yuin/gopher-lua"
)func main() {// 1、创建 lua 的虚拟机L := lua.NewState()defer L.Close()// 加载fib.luaif err := L.DoFile(`fib.lua`); err != nil {panic(err)}TestString(L)
}func TestString(L *lua.LState) {err := L.CallByParam(lua.P{Fn:      L.GetGlobal("TestLString"), // 获取函数引用NRet:    1,                          // 指定返回值数量Protect: true,                       // 如果出现异常,是panic还是返回err})if err != nil {panic(err)}lv := L.Get(-1) // get the value at the top of the stack// 从堆栈中扔掉返回结果L.Pop(1)if str, ok := lv.(lua.LString); ok {// lv is LStringfmt.Println(string(str))}if lv.Type() != lua.LTString {panic("string required.")}
}

fib.lua中的代码:

function TestLString()return "this is test"
end

接下来看看指针类型是如何判断的:

lv := L.Get(-1) // get the value at the top of the stack
if tbl, ok := lv.(*lua.LTable); ok {// lv is LTablefmt.Println(L.ObjLen(tbl))
}

特别注意:

  • LBool , LNumber , LString 这三类不是指针类型,其他的都属于指针类型。
  • LNilType and LBool 这里没看懂官方在说什么,知道的可以告知下,谢谢。
  • lua 中,nil和false都是认为是错误的情况。nil表示一个无效值(在条件表达式中相当于false)。

大家有不明白的地方,推荐去看看官方怎么说的。

2.3 gopher-lua 中的调用堆栈和注册表大小

官方还介绍了性能优化这块的内容,我就不介绍了,大家感兴趣可以去看官方。

主要是对于我这种非科班出生的菜鸟来说,还是有点难度的,这里就不瞎说了,免得误导大家。哈哈......

一般来说,使用默认的方式,性能不会太差。对性能没有特别高的要求,也没有必要去折腾这个。

3、gopher-lua 中常用的API

3.1 lua 调用 Go 中的代码

test.lua 脚本内容:

print(double(100))

main.go 中的内容:

package mainimport ("fmt"lua "github.com/yuin/gopher-lua"
)func main() {L := lua.NewState()defer L.Close()L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */L.DoFile("test.lua")
}func Double(L *lua.LState) int {fmt.Println("coming go code.............")lv := L.ToInt(1)            /* get argument */L.Push(lua.LNumber(lv * 2)) /* push result */return 1                    /* number of results */
}

执行结果:

coming go code.............
200

上面我们已经实现了一个简单的 lua 脚本中调用 go 代码的功能。

3.2 使用Go创建模块给lua使用

上面介绍了 lua 中调用 Go中的代码,Go提供的功能不多还好,直接使用即可,但是实际项目中,既然使用到了Go和lua结合的模式,必然会存在Go提供基础功能,lua来编写业务的方式,这个时候如果还是使用上面的方式,使用起来将非常不方便。这里提供了一种方式,将Go中的功能封装成一个模块,提供给 lua 使用,这样就方便许多。

接下来我们一起看看怎么做。

mymodule.go 的内容:

package mainimport ("fmt"lua "github.com/yuin/gopher-lua"
)func Loader(L *lua.LState) int {// register functions to the tablemod := L.SetFuncs(L.NewTable(), exports)// register other stuffL.SetField(mod, "name", lua.LString("testName"))// returns the moduleL.Push(mod)return 1
}var exports = map[string]lua.LGFunction{"MyAdd": MyAdd,
}func MyAdd(L *lua.LState) int {fmt.Println("coming custom MyAdd")x, y := L.ToInt(1), L.ToInt(2)// 原谅我还不知道怎么把计算结果返回给 lua ,太菜了啦// 不过用上另外一个包后,我知道,具体看实战篇。fmt.Println(x)fmt.Println(y)return 1
}

main.go 的内容:

package mainimport lua "github.com/yuin/gopher-lua"func main() {L := lua.NewState()defer L.Close()L.PreloadModule("myModule", Loader)if err := L.DoFile("main.lua"); err != nil {panic(err)}
}

main.lua 的内容:

local m = require("myModule")
m.MyAdd(10, 20)
print(m.name)

运行 main.go 得到执行结果:

coming custom MyAdd
10      
20      
testName

3.3 Go 调用 lua 中的代码

lua 中的代码

function TestGoCallLua(x, y)return x+y, x*y
end

go 中的代码

func TestTestGoCallLua(L *lua.LState) {err := L.CallByParam(lua.P{Fn:      L.GetGlobal("TestGoCallLua"), // 获取函数引用NRet:    2,                            // 指定返回值数量,注意这里的值是 2Protect: true,                         // 如果出现异常,是panic还是返回err}, lua.LNumber(10), lua.LNumber(20))if err != nil {panic(err)}multiplicationRet := L.Get(-1)addRet := L.Get(-2)if str, ok := multiplicationRet.(lua.LNumber); ok {fmt.Println("multiplicationRet is: ", int(str))}if str, ok := addRet.(lua.LNumber); ok {fmt.Println("addRet is: ", int(str))}}

具体的可以看 xxx 中的 TestTestGoCallLua 函数。

执行结果:

multiplicationRet is:  200
addRet is:  30

3.4 lua中使用go中定义好的类型

这里我们直接使用官方的例子:

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 attributesL.SetField(mt, "new", L.NewFunction(newPerson))// methodsL.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 = personL.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)}
}

官方还讲解了如何使用 go 中的context 来结束lua代码的执行,这里我就不演示了,大家自行研究。

3.5 gopher_lua 中goroutine的说明

这里直接放官方的文档,大家自行理解

The LState is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.

LState不是goroutine安全的。建议每个goroutine使用一个LState,并通过使用通道在goroutine之间进行通信。

Channels are represented by channel objects in GopherLua. And a channel table provides functions for performing channel operations.

在GopherLua中,通道由通道对象表示。通道表提供了执行通道操作的函数。这意味着,我们可以使用通道对象来创建、发送和接收消息,并使用通道表中的函数来控制通道的行为。通道是一种非常有用的并发编程工具,可以帮助我们在不同的goroutine之间进行通信和同步。通过使用GopherLua中的通道对象和通道表,我们可以轻松地在Lua代码中实现并发编程。

Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.

某些对象无法通过通道发送,因为其内部有非goroutine安全的对象。

  • a thread(state)
  • a function
  • an userdata
  • a table with a metatable

上面这四种类型就不支持往通道中发送。

package mainimport (lua "github.com/yuin/gopher-lua""time"
)func receiver(ch, quit chan lua.LValue) {L := lua.NewState()defer L.Close()L.SetGlobal("ch", lua.LChannel(ch))L.SetGlobal("quit", lua.LChannel(quit))if err := L.DoString(`local exit = falsewhile not exit do-- 这个 channel 的写法是固定的 ??channel.select({"|<-", ch, function(ok, v)if not ok thenprint("channel closed")exit = trueelseprint("received:", v)endend},{"|<-", quit, function(ok, v)print("quit")exit = trueend})end`); err != nil {panic(err)}
}func sender(ch, quit chan lua.LValue) {L := lua.NewState()defer L.Close()L.SetGlobal("ch", lua.LChannel(ch))L.SetGlobal("quit", lua.LChannel(quit))if err := L.DoString(`ch:send("1")ch:send("2")`); err != nil {panic(err)}ch <- lua.LString("3")quit <- lua.LTrue
}func main() {ch := make(chan lua.LValue)quit := make(chan lua.LValue)go receiver(ch, quit)go sender(ch, quit)time.Sleep(3 * time.Second)
}

执行结果:

received:       1
received:       2
received:       3
quit

4、gopher_lua 性能优化

下面这些内容,主要来自参考的文章,大家可以点击当 Go 遇上了 Lua 查看原文。

如果侵权,请联系删除,谢谢。

4.1 提前编译

在查看上述 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), "<string>")
}func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {chunk, err := parse.Parse(reader, name)// ...proto, err := Compile(chunk, name)// ...
}

从这一点考虑,在同份 Lua 代码将被执行多次(如在 http server 中,每次请求将执行相同 Lua 代码)的场景下,如果我们能够对代码进行提前编译,那么应该能够减少 parse 和 compile 的开销(如果这属于 hotpath 代码)。根据 Benchmark 结果,提前编译确实能够减少不必要的开销。

package glua_testimport ("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

4.2 虚拟机实例池

看到这里的需要注意,官方提醒我们,在每个 goroutine

在同份 Lua 代码被执行的场景下,除了可使用提前编译优化性能外,我们还可以引入虚拟机实例池。

因为新建一个 Lua 虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。

func BenchmarkRunWithoutPool(b *testing.B) {for i := 0; i < b.N; i++ {l := lua.NewState()_ = l.DoString(`a = 1 + 1`)l.Close()}
}func BenchmarkRunWithPool(b *testing.B) {pool := newVMPool(nil, 100)for i := 0; i < b.N; i++ {l := pool.get()_ = l.DoString(`a = 1 + 1`)pool.put(l)}
}// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPool-8          10000            129557 ns/op          262599 B/op        826 allocs/op
// BenchmarkRunWithPool-8            100000             19320 ns/op           85626 B/op         67 allocs/op
// PASS
// ok      glua    3.467s

Benchmark 结果显示,虚拟机实例池的确能够减少很多内存分配操作。

下面给出了 README 提供的实例池实现,但注意到该实现在初始状态时,并未创建足够多的虚拟机实例(初始时,实例数为 0),以及存在 slice 的动态扩容问题,这都是值得改进的地方。

type lStatePool struct {m     sync.Mutexsaved []*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),
}

http://www.ppmy.cn/news/809584.html

相关文章

Go分享好的github插件和项目

插件 QQ、微信&#xff08;WeChat&#xff09;、支付宝&#xff08;AliPay&#xff09;的Go版本SDK http://github.com/go-pay/gopay发送邮件库 https://github.com/go-gomail/gomail读写Microsoft Excel https://github.com/360EntSecGroup-Skylar/excelize生成uuid https://…

前后端分离项目问题总结

这里记录一下&#xff0c;我在写一个自己设计的项目时遇到的几个问题&#xff01;&#xff01;&#xff01; 1、Session获取为null 需求&#xff1a;session保存验证码&#xff0c;在session中验证验证码是否正确 解释&#xff1a; 前端跨域访问后端接口, 在浏览器的安全策略…

申请和注销设备号的方法

一、Linux内核对设备的分类 linux的文件种类&#xff1a; -&#xff1a;普通文件 d&#xff1a;目录文件 p&#xff1a;管道文件 s&#xff1a;本地socket文件 l&#xff1a;链接文件 c&#xff1a;字符设备 b&#xff1a;块设备 Linux内核按驱动程序实现模型框架的不…

S3C2440 ARM920T CPU

S3C2440 ARM920T CPU 支持大端/小端模式。共同有8个存储器BANK&#xff0c;每个BANK为128M。BANK0~BANK6为固定起始地址。BANK7可编程BANK起始地址和大小&#xff0c;其开始地址是BANK6的结束地址&#xff0c;灵活可变。BANK0~BANK5用于ROM或者SRAM&#xff0c;BANK6、BANK7用…

linux驱动38:后备高速缓存

设备驱动程序常常会反复地分配同一大小的内存块&#xff0c;为这些反复使用的内存块增加某些特殊的内存池——后备高速缓存。 linux内核的高速缓存管理称为slab分配器&#xff0c;头文件<linux/slab.h>&#xff0c;类型struct kmem_cache。 一、api接口&#xff1a; 1…

socket简介及java实例

网络上的两个程序通过一个双向的通信连接实现数据的交换&#xff0c;这个连接的一端称为一个socket。 建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API)&#xff0c;对TCP/IP的封装&#xff0c;TCP/IP也要提供可供程序员做网络开发所用的接口&#xff0c;…

三星ARM9以上芯片(244X,6410等)和cortex-a8芯片(S5PV210)比较PK

贴2张图&#xff0c;一看便明了&#xff1a;&#xff08;s5pv210开发技术交流群&#xff1a;8334819&#xff09; KEY FEATURES OF S5PV210 The key features of S5PV210 include: •ARM CortexTM-A8 based CPU Subsystem with NEON −32/ 32 KB I/D Cache, 512 KB L2 Cache −…

关于S3C6400 DM9000a 驱动

平台&#xff1a;S3C6400Ethernet :DM9000a由于是A公司的BSP&#xff0c;改了网卡的片选&#xff0c;发现可以找到ID&#xff0c;但是就是ping失败。厂家的帮忙 &#xff0c;加上了一些debug信息&#xff0c;最后发现应该是SROM的设置不对&#xff0c;发的数据高位都没有了。A公…