您好,欢迎访问一九零五行业门户网

Golang和Lua相遇会擦出什么火花?

本文由go语言教程栏目给大家介绍golang和lua ,希望对需要的朋友有所帮助!
在 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(...) ,都会各执行一次 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
虚拟机实例池
在同份 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.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 poolvar 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)。
到这里,已经有了大致的思路:我们可通过判断字节码是否含有 getglobal 和 setglobal 进而限制代码的全局变量的使用。至于字节码的获取,可通过调用 compilestring(...) 和 compilefile(...) ,得到 lua 代码的 functionproto ,而其中的 code 属性即为字节码 slice,类型为 []uint32 。
在虚拟机实现代码中,我们可以找到一个根据字节码输出对应 opcode 的工具函数。
// 获取对应指令的 opcodefunc opgetopcode(inst uint32) int {return int(inst >> 26)}
有了这个工具函数,我们即可实现对全局变量的检查。
package main// ...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()}// <string>:1: not allow to modify table
写在最后
golang 和 lua 的融合,开阔了我的视野:原来静态语言和动态语言还能这么融合,静态语言的运行高效率,配合动态语言的开发高效率,想想都兴奋(逃。
在网上找了很久,发现并没有关于 go-lua 的技术分享,只找到了一篇稍微有点联系的文章(京东三级列表页持续架构优化 — golang + lua (openresty) 最佳实践),且在这篇文章中, lua 还是跑在 c 上的。由于信息的缺乏以及本人(学生党)开发经验不足的原因,并不能很好地评价该方案在实际生产中的可行性。因此,本篇文章也只能当作“闲文”了,哈哈。
以上就是golang和lua相遇会擦出什么火花?的详细内容。
其它类似信息

推荐信息