github.com/yuin/gopher-lua 踩坑日记
本文主要记录下在日常开发过程中, 使用 github.com/yuin/gopher-lua 过程中需要注意的地方。
后续遇到其他的需要注意的事项再补充。
1、加载LUA_PATH环境变量
在实际开发中,我们会将一些公共的、可重复使用的代码封装起来,假如我们只是一些简单的处理,全部写在一个文件是没有问题的,维护起来也并不是很麻烦。但是当我们的需求变得复杂起来,或者需求调整的时候,我们还是将所有功能都写在同一个文件的时候,就变得不合理起来,后期的维护更是灾难性的,这个时候就需要将一些公共的、重复使用的代码,抽离出来,根据功能的不同分类,当做一个个模块,在使用的时候使用 require
导入进来,这样才合理。
既然我们需要将自定义的 module 导入进来,那么我们肯定是需要设计环境变量的。
最重要的地方就是下面这点:
// 重点就是这句, 先设置环境变量, 再创建 lua 的 虚拟机, 这样环境变量才生效!!!
setLuaLibPath()
lvm := newLuaVm(pluginPath, context.Background())
接下来再详细看演示的demo。
先看看代码目录分层,有一个整体认识。
--go_call_lua_test
|---go.mod
|---main.go
|---plugin.lua
|---lib/lua/test.lua
main.go 代码
package main
import (
"context"
"errors"
"fmt"
lua "github.com/yuin/gopher-lua"
"go.uber.org/atomic"
luar "layeh.com/gopher-luar"
"os"
"path/filepath"
)
func getExeDir() (string, string) {
exePath, _ := os.Executable()
exeDir, exeName := filepath.Split(exePath)
return exeDir, exeName
}
func absPath(fp string) string {
exeAbsDir, _ := getExeDir()
//绝对路径
if !filepath.IsAbs(fp) {
return filepath.Join(exeAbsDir, fp)
} else {
return fp
}
}
func setLuaLibPath() {
//设置lua环境变量
pathStr := ""
LuaLibPath := []string{"./lib/lua/?.lua"}
for _, path := range LuaLibPath {
pathStr += ";" + absPath(path)
}
pathStr += ";;"
os.Setenv("LUA_PATH", pathStr)
}
type LuaVm struct {
*lua.LState
path string
Loaded atomic.Bool
}
func newLuaVm(path string, ctx context.Context) *LuaVm {
lv := &LuaVm{
path: path,
LState: lua.NewState(),
}
lv.SetContext(ctx)
if err := lv.load(); err != nil {
panic(fmt.Errorf("lvm error=%s", err))
}
return lv
}
func (lv *LuaVm) load() error {
//加载工具 下面是将 go 提供的功能当做模块 给 lua 使用
//lv.SetGlobal("FILE", luar.New(lv.LState, &test.FileTool{}))
if len(lv.path) == 0 {
return fmt.Errorf("plugin file empty")
}
if err := lv.DoFile(lv.path); err != nil {
return err
}
lv.Loaded.Store(true)
return nil
}
func (lv *LuaVm) CallLua() (result string, err error) {
if err := lv.CallByParam(lua.P{
Fn: lv.GetGlobal("PluginTest"),
NRet: 2,
Protect: true,
}, luar.New(lv.LState, "I'm coming go!")); err != nil {
fmt.Printf("PrimaryIn error=%s", err)
}
retCode, _ := lv.Get(-1).(lua.LNumber)
lv.Pop(1)
retPkt, _ := lv.Get(-1).(lua.LString)
lv.Pop(1)
if retCode != 0 {
return "", errors.New("untreated")
}
return string(retPkt), nil
}
func main() {
pluginPath := "./plugin.lua"
// 重点就是这句, 先设置环境变量, 再创建 lua 的 虚拟机, 这样环境变量才生效!!!
setLuaLibPath()
lvm := newLuaVm(pluginPath, context.Background())
ret, err := lvm.CallLua()
fmt.Println("ret --> ", ret)
fmt.Println("err --> ", err)
}
plugin.lua 代码
local TestLib = require("test")
function PluginTest(param)
print(param)
print("coming PluginTest")
TestLib.CallTestFunc()
return "I'm coming lua!"
end
test.lua 代码
justTest = {}
function justTest.CallTestFunc()
print("coming justTest.CallTestFunc")
end
-- 一定要 return, 否则不生效
return justTest
编译后执行结果:
I'm coming go!
coming PluginTest
coming justTest.CallTestFunc
ret --> I'm coming lua!
err --> <nil>
2、并发调用 lua 代码时报错
最近在对项目中,使用到 go 调用 lua 代码的地方,进行压测,发现经常出现错误,报错信息如下:
接下来,讲讲如何解决上面的问题,
解决办法:
- 加锁执行
- lvm.NewThread
2.1、加锁执行
我们以第一个示例中的代码为准,其他的地方都不用动,主要更改的地方就是执行 lua 脚本的地方。
type LuaVm struct {
*lua.LState
path string
Loaded atomic.Bool
lock sync.Mutex
}
func (lv *LuaVm) CallLua() (result string, err error) {
//加锁执行,并发安全
lock.Lock()
defer lock.Unlock()
if err := lv.CallByParam(lua.P{
Fn: lv.GetGlobal("PluginTest"),
NRet: 2,
Protect: true,
}, luar.New(lv.LState, "I'm coming go!")); err != nil {
fmt.Printf("PrimaryIn error=%s", err)
}
retCode, _ := lv.Get(-1).(lua.LNumber)
lv.Pop(1)
retPkt, _ := lv.Get(-1).(lua.LString)
lv.Pop(1)
if retCode != 0 {
return "", errors.New("untreated")
}
return string(retPkt), nil
}
2.2、lvm.NewThread (推荐)
下面的解决方案,本质上是给执行这段 lua 代码隔离出一段内存空间,这样在并发执行时,就不会相互影响。
相对加锁来说,占用的内存会多一些,但是并发量会相对高一点
func (lv *LuaVm) CallLua() (result string, err error) {
//并发安全
state, _ := lv.NewThread()
if err := state.CallByParam(lua.P{
Fn: state.GetGlobal("PluginTest"),
NRet: 2,
Protect: true,
}, luar.New(state, "I'm coming go!")); err != nil {
fmt.Printf("PrimaryIn error=%s", err)
}
retCode, _ := state.Get(-1).(lua.LNumber)
state.Pop(1)
retPkt, _ := state.Get(-1).(lua.LString)
state.Pop(1)
if retCode != 0 {
return "", errors.New("untreated")
}
return string(retPkt), nil
}