百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

当Go遇上了Lua,会发生什么

toyiye 2024-06-21 12:00 9 浏览 0 评论

分享



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

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

Hello World

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

package main 
import ( 
 "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_test 
import ( 
 "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] 
 plpl.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 main 
import ( 
 "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 的工具函数。

// 获取对应指令的 OpCode 
func 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 main 
import ( 
 "fmt" 
 "github.com/yuin/gopher-lua" 
) 
// 设置表为只读 
func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData { 
 ud := l.NewUserData() 
 mt := l.NewTable() 
 // 设置表中域的指向为 table 
 l.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 = mt 
 return 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 上的。由于信息的缺乏以及本人(学生党)开发经验不足的原因,并不能很好地评价该方案在实际生产中的可行性。因此,本篇文章也只能当作“闲文”了,哈哈。


天下数据是国内屈指可数的拥有多处海外自建机房的新型IDC服务商,被业界公认为“中国IDC行业首选品牌”。

天下数据与全球近120多个国家顶级机房直接合作,包括香港、美国、韩国、日本、台湾、新加坡、荷兰、法国、英国、德国、埃及、南非、巴西、印度、越南等国家和地区的服务器、云服务器的服务.

除提供传统的IDC产品外,天下数据的主要职责是为大中型企业提供更精细、安全、满足个性需求的定制化服务器解决方案,特别是在直销、金融、视频、流媒体、游戏、电子商务、区块链、快消、物联网、大数据等诸多行业,为广大客户解决服务器租用中遇到的各种问题。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码