在 GitHub 玩耍时,偶然发现了 gopher-lua ,这是一个纯 Golang 实现的 Lua 虚拟机。我们知道 Golang 是静态语言,而 Lua 是动态语言,Golang 的性能和效率各语言中表现得非常不错,但在动态能力上,肯定是无法与 Lua 相比。那么如果我们能够将二者结合起来,就能综合二者各自的长处了(手动滑稽。

在项目 Wiki 中,我们可以知道 gopher-lua 的执行效率和性能仅比 C 实现的 bindings 差。因此从性能方面考虑,这应该是一款非常不错的虚拟机方案。

Hello World

这里给出了一个简单的 Hello World 程序。我们先是新建了一个虚拟机,随后对其进行了 DoString(...) 解释执行 lua 代码的操作,最后将虚拟机关闭。执行程序,我们将在命令行看到 “Hello World” 的字符串。

package mainimport ("github.com/yuin/gopher-lua"
)func main() {l := lua.NewState()defer l.Close()if err := l.DoString(`print("Hello World")`); err != nil {panic(err)}
}// Hello World

提前编译

在查看上述 DoString(...) 方法的调用链后,我们发现每执行一次 DoString(...)DoFile(...) ,都会各执行一次 parsecompile

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 代码将被执行多次(如在 http server 中,每次请求将执行相同 Lua 代码)的场景下,如果我们能够对代码进行提前编译,那么应该能够减少 parsecompile 的开销(如果这属于 hotpath 代码)。根据 Benchmark 结果,提前编译确实能够减少不必要的开销。

package glua_testimport ("bufio""testing""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 代码被执行的场景下,除了可使用提前编译优化性能外,我们还可以引入虚拟机实例池。

因为新建一个 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),
}

模块调用

gopher-lua 支持 Lua 调用 Go 模块,个人觉得,这是一个非常令人振奋的功能点,因为在 Golang 程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。

当然,除此之外,也存在 Go 调用 Lua 模块,但个人感觉后者是没啥必要的,所以在这里并没有涉及后者的内容。

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

变量污染

当我们使用实例池减少开销时,会引入另一个棘手的问题:由于同一个虚拟机可能会被多次执行同样的 Lua 代码,进而变动了其中的全局变量。如果代码逻辑依赖于全局变量,那么可能会出现难以预测的运行结果(这有点数据库隔离性中的“不可重复读”的味道)。

全局变量

如果我们需要限制 Lua 代码只能使用局部变量,那么站在这个出发点上,我们需要对全局变量做出限制。那问题来了,该如何实现呢?

我们知道,Lua 是编译成字节码,再被解释执行的。那么,我们可以在编译字节码的阶段中,对全局变量的使用作出限制。在查阅完 Lua 虚拟机指令后,发现涉及到全局变量的指令有两条:GETGLOBAL(Opcode 5)SETGLOBAL(Opcode 7)

到这里,已经有了大致的思路:我们可通过判断字节码是否含有GETGLOBALSETGLOBAL 进而限制代码的全局变量的使用。至于字节码的获取,可通过调用 CompileString(...)CompileFile(...),得到 Lua 代码的 FunctionProto ,而其中的 Code 属性即为字节码 slice,类型为 []uint32

在虚拟机实现代码中,我们可以找到一个根据字节码输出对应 OpCode 的工具函数。

// 获取对应指令的 OpCode
func opGetOpCode(inst uint32) int {return int(inst >> 26)
}

有了这个工具函数,我们即可实现对全局变量的检查。

package mainimport "testing"// ...
func CheckGlobal(proto *lua.FunctionProto) error {for _, code := range proto.Code {switch opGetOpCode(code) {case lua.OP_GETGLOBAL:return errors.New("not allow to access global")case lua.OP_SETGLOBAL:return errors.New("not allow to set global")}}// 对嵌套函数进行全局变量的检查for _, nestedProto := range proto.FunctionPrototypes {if err := CheckGlobal(nestedProto); err != nil {return err}}return nil
}func TestCheckGetGlobal(t *testing.T) {l := lua.NewState()proto, _ := CompileString(`print(_G)`)if err := CheckGlobal(proto); err == nil {t.Fail()}l.Close()
}func TestCheckSetGlobal(t *testing.T) {l := lua.NewState()proto, _ := CompileString(`_G = {}`)if err := CheckGlobal(proto); err == nil {t.Fail()}l.Close()
}

模块

除变量可能被污染外,导入的 Go 模块也有可能在运行期间被篡改。因此,我们需要一种机制,确保导入到虚拟机的模块不被篡改,即导入的对象是只读的。

在查阅相关博客后,我们可以对 Table__newindex 方法的修改,将模块设置为只读模式。

package mainimport ("fmt""github.com/yuin/gopher-lua"
)// 设置表为只读
func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData {ud := l.NewUserData()mt := l.NewTable()// 设置表中域的指向为 tablel.SetField(mt, "__index", table)// 限制对表的更新操作l.SetField(mt, "__newindex", l.NewFunction(func(state *lua.LState) int {state.RaiseError("not allow to modify table")return 0}))ud.Metatable = mtreturn ud
}func load(l *lua.LState) int {mod := l.SetFuncs(l.NewTable(), exports)l.SetField(mod, "name", lua.LString("gomodule"))// 设置只读l.Push(SetReadOnly(l, mod))return 1
}var exports = map[string]lua.LGFunction{"goFunc": goFunc,
}func goFunc(l *lua.LState) int {fmt.Println("golang")return 0
}func main() {l := lua.NewState()l.PreloadModule("gomodule", load)// 尝试修改导入的模块if err := l.DoString(`local m = require("gomodule");m.name = "hello world"`); err != nil {fmt.Println(err)}l.Close()
}// :1: not allow to modify table

写在最后

Golang 和 Lua 的融合,开阔了我的视野:原来静态语言和动态语言还能这么融合,静态语言的运行高效率,配合动态语言的开发高效率,想想都兴奋(逃。

在网上找了很久,发现并没有关于 Go-Lua 的技术分享,只找到了一篇稍微有点联系的文章(京东三级列表页持续架构优化 — Golang + Lua (OpenResty) 最佳实践),且在这篇文章中, Lua 还是跑在 C 上的。由于信息的缺乏以及本人(学生党)开发经验不足的原因,并不能很好地评价该方案在实际生产中的可行性。因此,本篇文章也只能当作“闲文”了,哈哈。

Golang 和 lua 相遇会擦出什么火花?相关推荐

  1. c语言修仙受控可看吗,强推三本神奇到爆的小说,c语言修仙,程序员与修真会擦出什么火花...

    大家好,我是小龙.今天我给大家推荐三本神奇到爆的小说,c语言修仙,程序员与修真会擦出什么火花! 一<c语言修仙>[一十四洲] [简介]: 林浔是一个程序员,通宵编代码后发现自己身体内多了一 ...

  2. 光明日报:当教育遇上区块链,会擦出什么火花

    说起现在最火的新兴技术,区块链必是处在风口上的答案之一.日前,京津冀大数据教育区块链试验区成立,为"区块链+教育"的融合发展之路,提供了一个新的窗口. 当传统的教育行业与区块链相遇 ...

  3. 5G,上天了!卫星和基站擦出了火花?

    大家好,我是无线深海,我们好久不见. 本期我们来聊聊卫星通信,以及卫星通信和地面通信的融合:非地面网络的故事. 对于5G来说,这可能只是后半场的锦上添花:但对仍处于畅想中的6G来说,空天地海一体化通信 ...

  4. 机器学习 + NFT,跨界联合可以擦出什么火花?

    前几天在Github上看到一个用SN-GAN技术生成punk的项目,项目地址:https://github.com/teddykoker/cryptopunks-gan,跑了一遍感觉很有趣,所以就研究 ...

  5. 安搭Share:当色彩与文物碰撞会擦出什么火花

    每年流行色的发布,都会引发新一轮设计风尚,那么当色彩与文物碰撞会有怎样的呈现呢,如果你有这样的好奇心,不妨一起来看看色彩微妙变化与文物的碰撞. 中国传统正五色主要有青.赤.黄.白.黑,"五色 ...

  6. 当量子计算和机器学习相遇,会碰撞出什么火花?

    https://3w.huanqiu.com/a/564394/7LHofkwRmxi?agt=20 没有人会怀疑,量子计算和机器学习是当前最炙手可热的两个研究领域. 在量子计算方面,理论和硬件的一个 ...

  7. java 接口的泛型方法_Java泛型/泛型方法/通配符/泛型接口/泛型泛型擦出

    从JDK1.5以后引入了三大常用新特性:泛型.枚举(enum).注解(Annotation).其中JDK1.5中泛型是一件非常重要的实现技术,它可以帮助我们解决程序的参数转换问题.本文为大家详细介绍一 ...

  8. 当稳定币遇上BCH,将会擦出什么样的火花?

    2019独角兽企业重金招聘Python工程师标准>>> 当稳定币遇上BCH,将会擦出什么样的火花? 币圈太黑了,什么都割,我空仓都被割,拿着USDT都被割-- 2018年10月15日 ...

  9. 5G与智慧杆将擦出什么样的火花?

    伴随着5G时代如火如荼地发展,整合了监控摄像头.5G微基站等硬件,通过信息感知和大数据交互技术,能实现智慧交通.信息发布等功能的智慧灯杆正在城市建设中广泛应用. 那么,问题来了! 在这样的大环境下, ...

最新文章

  1. java 实现 excel sheet 拷贝到另一个Excel文件中 poi
  2. 正则 不区分大小写_4.nginx的server_name正则匹配
  3. 由点到面(面试经验)
  4. EMOS SPF开启收不到信 及WEB收件箱不显示邮件列表等问题解决处理记录
  5. 数据结构哪本书比较好_东莞工厂电动伸缩门固定在哪一边比较好?
  6. shiro登陆成功后被拦截_Springboot+Shiro+redis整合
  7. 2021年黑龙江高考成绩查询,黑龙江省招生考试信息港:2021年黑龙江高考成绩查询入口、查分系统...
  8. 搭建sendmail邮件服务器
  9. 网络蜘蛛Spider的逻辑Logic(一)
  10. 杭电 1142 十字链表存储
  11. 用户注册页面的再次确认密码的验证方式
  12. html开网站弹窗代码大全,JS弹出窗口代码大全(详细整理)
  13. python-分分钟入门—idea配置开发环境
  14. 计算机应用研究是北大核心吗,计算机应用研究 CSCD核心期刊北大核心期刊统计源期刊...
  15. ETCD 源码学习--Watch(client)
  16. 阿拉伯字母及阿拉伯文排版规则
  17. Linux 系统licence,Linux系统中软件简单License的实现
  18. Bootstrap导航条鼠标悬停下拉菜单
  19. ANSYS_Maxwell平面电场仿真
  20. JavaScript菜鸟教程 grammar

热门文章

  1. 服务器1521端口被关闭,如何开启
  2. 人生效率手册:如何卓有成效地过好每一天--By张萌姐姐--读书笔记
  3. 图像恢复(加噪与去噪)
  4. 【整理】EFI/UEFI BIOS 入门 : All For Beginners
  5. 微信app支付和微信网页支付 java
  6. c#USB接收信息项目的总结
  7. Chrome Network面板工具之万文多图详解
  8. Django创建数据库(Django数据库字段类型)
  9. Java重写的7个规则
  10. 简单的git基本命令