当Golang 遇到 gopher-lua ….
最近在搞服务端框架需要一个支持热更新的插件系统..找了一下能够内嵌go的脚本
结果嘛…可谓是五花八门 品类齐全….
各种各样 主流的 小众的…
嗯 想了下需求 能够在脚本中调用宿主程序函数,同时也能从宿主程序中调用脚本的函数就差不多了
语法也尽量不要太复杂 能够简单上手就ok…
就是你了 LUA..虽然以前没用过 但是都说很简单嘛..
就找到了:gopher-lua
功能支持:
1.支持Lua5.1特性.
2.且支持LuaJit模式执行 Ps:这点很重要..
3.将golang 的 channel引入了脚本中
4.完全go实现 无cgo参与 Ps:节省了cgo调用的开销.
5.当然最主要是简单简单….
项目地址:https://github.com/yuin/gopher-lua
开一个lua虚拟机实例也是很简单
import (
lua "github.com/yuin/gopher-lua"
)
func main(){
L:=lua.NewState()
L.DoString(`print("2333")`)
L.Close()
}
//先把require 引用环境做一下 这种直接在go中设置最好了…
// 获取指定目录下所有子目录
func (u *Utils) GetAllChildDirFromPath(path string) []string {
result := make([]string, 0)
//遍历文件
files, err := ioutil.ReadDir(path)
if err == nil {
for _, file := range files {
if file.IsDir() {
//遍历文件夹 后将结果添加到返回集合
nP := path + "/" + file.Name()
result = append(result, nP)
//将该目录下的子目录加入返回值
result = append(result, u.GetAllChildDirFormPath(nP)...)
}
}
}
return result
}
// 设置Lua虚拟机package.path路径
//[Lua虚拟机实例]
//[模块目录] 如.\dir\?.lua格式
//[模块目录] \dir 用于遍历模块目录下所有子目录
func (p *PluginSystem) PreloadPackagePath(L *lua.LState, modulesPackagePath string, modulesPath string) {
//设置Lua package.path
luaPackage := L.GetGlobal("package")
if luaPackage != lua.LNil {
if luaPackage.Type() == lua.LTTable {
vPackage := luaPackage.(*lua.LTable)
vPath := vPackage.RawGet(lua.LString("path"))
if vPath != lua.LNil {
//默认路径
var NewPathString = modulesPackagePath
//获取模块目录下所有子目录
ChildDirArray := GetAllChildDirFormPath(modulesPath)
//处理字符路径
for i := 0; i < len(ChildDirArray); i++ {
//替换斜杠 貌似只支持这样的...
NewChildDirPath := strings.Replace(ChildDirArray[i], "/", "\\", -1)
//添加限定符
NewChildDirPath = ".\\" + NewChildDirPath + "\\?.lua" + ";"
//追加
NewPathString = NewPathString + NewChildDirPath
}
//设置新的package.path
vPackage.RawSet(lua.LString("path"), lua.LString(NewPathString))
}
}
}
}
如果涉及到虚拟机的复用 比如将它们做成池 且执行不同的脚本 那么可能需要清理虚拟机实例的环境 以及加载的模块 研究了一下作者的源代码 实现了一下
首先是清理虚拟机污染
这里最好是在你将宿主程序所需要的模块或者其他导入后在进行快照备份
恢复快照在放回池的时候进行..
//实例化
type StoreLuaState struct {
...
BackupEnv *lua.LTable //用于备份的Env
...
}
// 快照虚拟机状态
func (s *StoreLuaState) SnapshotLuaVmState() {
if s.IsInPool() {
//创建新的env
s.BackupEnv = s.L.NewTable()
//保存当前Env环境
s.L.Env.ForEach(func(k lua.LValue, v lua.LValue) {
s.BackupEnv.RawSet(k, v)
})
}
}
// 恢复虚拟机状态
func (s *StoreLuaState) ResotreLuaVmState() {
if s.IsInPool() {
//弹出虚拟机堆栈
s.L.Pop(s.L.GetTop())
//创建新的Env环境
s.L.Env = s.L.NewTable()
//恢复原始Env数据
s.BackupEnv.ForEach(func(k lua.LValue, v lua.LValue) {
s.L.Env.RawSet(k, v)
})
//同步环境
s.L.G.Global = s.L.Env
}
}
以上代码又会带来另外一个问题 那就是lua脚本中如果有require(“xxxx”) 那么再次执行同脚本 就不会再进行加载了.
经过查找资料
首次require 模块后 会在 package.loaded 表中 记录 该模块 表示已经加载了
也就是需要将package.loaded 将脚本中加载的模块卸载掉 下次复用同脚本时才不会出现问题 这也是热更新需要用到的 PS:没有做值的保存 我认为脚本中不需要涉及存放什么数据的.
// 使指定虚拟机重新加载自身已加载的模块
func (p *PluginSystem) UninstallAll3rdPartyModules(L *lua.LState) error {
luaPackage := L.GetGlobal("package")
if luaPackage != lua.LNil {
if luaPackage.Type() == lua.LTTable {
vPackage := luaPackage.(*lua.LTable)
vLoaded := vPackage.RawGet(lua.LString("loaded"))
if vLoaded != lua.LNil {
if vLoaded.Type() == lua.LTTable {
vLoaded.(*lua.LTable).ForEach(func(moduleName lua.LValue, moduleTable lua.LValue) {
//是模块目录中的模块 就卸载掉
if p.IsValidModuleByName(moduleName.String()) {
//清空
vLoaded.(*lua.LTable).RawSet(moduleName, lua.LNil)
}
})
return nil
} else {
return errors.New("package.loaded is not ltable object.")
}
} else {
return errors.New("Not Found package.loaded")
}
} else {
return errors.New("package is not ltable object.")
}
} else {
return errors.New("Not Found package.")
}
return nil
}
关于在脚本中调用Go函数方法很多. 但是最方便的还是使用gopher-luar 这是该项目的例子 一眼就知道有多方便了..
import (
"fmt"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
)
type User struct {
Name string
token string
}
func (u *User) SetToken(t string) {
u.token = t
}
func (u *User) Token() string {
return u.token
}
const script = `
print("Hello from Lua, " .. u.Name .. "!")
u:SetToken("12345")
`
func Example_basic() {
L := lua.NewState()
defer L.Close()
u := &User{
Name: "Tim",
}
L.SetGlobal("u", luar.New(L, u))
if err := L.DoString(script); err != nil {
panic(err)
}
fmt.Println("Lua set your token to:", u.Token())
// Output:
// Hello from Lua, Tim!
// Lua set your token to: 12345
}
Go中调用脚本中的函数
// 调用Lua虚拟机中指定的函数
func (p *PluginSystem) Call(L *lua.LState, luaModuleName string, luaFuncName string, luaFuncRetValueNum int, Args ...interface{}) (error, []lua.LValue) {
luaFunc := lua.LNil
//从指定模块中查找
if luaModuleName != "" {
luaPackAge := L.GetGlobal(luaModuleName)
if luaPackAge != lua.LNil {
if luaPackAge.Type() == lua.LTTable {
luaFunc = luaPackAge.(*lua.LTable).RawGet(lua.LString(luaFuncName))
} else {
return errors.New(`object [` + `"` + luaModuleName + `"` + `] is not a luaTable`), nil
}
} else {
return errors.New(`not found lua module [` + `"` + luaModuleName + `"` + `] in scripts.`), nil
}
} else {
//直接查找目标函数
luaFunc = L.GetGlobal(luaFuncName)
}
//没有查找到函数
if luaFunc == lua.LNil {
return errors.New(`not found lua function [` + `"` + luaFuncName + `"` + `] in scripts.`), nil
}
//目标不是函数
if luaFunc.Type() != lua.LTFunction {
return errors.New(`object [` + `"` + luaFuncName + `"` + `] is not a function`), nil
}
//将目标函数压入
L.Push(luaFunc)
// 将go参转换为Lua参后 压入
for _, arg := range Args {
L.Push(luar.New(L, arg))
}
//进入虚拟机
err := L.PCall(len(Args), luaFuncRetValueNum, L.NewFunction(p.Error))
//是否出错
if err != nil {
return err, nil
}
//没有返回值
if luaFuncRetValueNum == 0 {
return nil, nil
}
//有返回值
result := make([]lua.LValue, luaFuncRetValueNum)
//将所有返回值抽出 因为按照虚拟机栈的先入后出原则 so我们也要倒着来
for i := luaFuncRetValueNum - 1; i >= 0; i-- {
result[i] = L.Get(L.GetTop())
L.Pop(1)
}
return nil, result
}