在 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), "")

}

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]

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 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()

}

// :1: not allow to modify table

写在最后

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

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

参考资料

Go语言和php个和lua,当 Go 遇上了 Lua相关推荐

  1. Go语言和php个和lua,当Go遇上了Lua,会发生什么

    在 GitHub 玩耍时,偶然发现了 gopher-lua ,这是一个纯 Golang 实现的 Lua 虚拟机.我们知道 Golang 是静态语言,而 Lua 是动态语言,Golang 的性能和效率各 ...

  2. linux lua ide,mac os上开发lua用什么ide

    2016-09-22 回答 一.mac os x中make和gcc命令的安装 在编译lua前需要做的准备工作. 我将lua-5.2.2.tar.gz下载到苹果电脑中,解压后进行编译时,发现我的mac ...

  3. c和java的区别_论C语言和Java的区别

    原标题: 论C语言和Java的区别 在计算机的发展史上,出现过几百种编程语言,经过多年的发展,留下来的编程语言里面,历史最悠久的是最基础的C 语言,最流行的是Java,发展势头最好的是Python·· ...

  4. 为什么说C语言和linux是分不开的?

    为什么说C语言和linux是分不开的? 在很多人的眼里,C语言和linux常常是分不开的.这其中的原因很多,其中最重要的一部分是linux本身就是C语言的杰出作品.当然,linux操作系统本身对C语言 ...

  5. 语言翻译成汇编语言_学习编程有没有必要从C语言和C++学起?应该怎么学?

    硬件层级:这里也就是实体硬件,包括:CPU.内存.显卡等等...这些都不属于软件的范畴内. 汇编层级:在之前没有C/C++以及java之前,是汇编的年代.那么汇编语言对于我们来说,可读性是很查的,不适 ...

  6. 多语言互通:谷歌发布实体检索模型,涵盖超过100种语言和2000万个实体

    来源:新智元 本文约1500字,建议阅读5分钟 实体链接(Entity linking)通常在自然语言理解和知识图谱中起着关键作用.谷歌AI研究人员近期提出了一种新的技术,在这种技术中,可以将特定语言 ...

  7. 如何混合编译C语言和C++

    如何混合编译C语言和C++ 实际开发过程中,C++中会调用C与语言编写的代码,我在网络上面找到一篇写得很好的文章 http://blog.csdn.net/keensword/article/deta ...

  8. c bool 类型检查_C语言和C+的区别是什么?8个点通俗易懂的告诉你!

    有些初学的同学傻傻分不清其中的区别,下面我将详细的讲解C语言和C++的区别点帮助大家尽快的理解.(但是呢,C语言和C++可是有着密不可分的关系哦,一般学习其中一种都会需要学习另一种!) 1.关键字 蓝 ...

  9. r语言和python-r语言和python

    广告关闭 腾讯云11.11云上盛惠 ,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高返5000元! r的取值范围是,r=1表示完全正相关! r=-1表示完全负相关! r=0表示完全 ...

最新文章

  1. JSONAssert Spring Boot Test
  2. cmake 编译mysql_CentOS5.6下使用cmake编译MySQL5.5.13源码和安装
  3. Metasploit Framework 简介与使用
  4. 工艺路线和工序有差别吗_智能制造、数字化车间、数字化企业需要结构化工艺吗?...
  5. 海南大学计算机科学与技术专业考研,2021年海南大学计算机科学与技术(081200)硕士研究生招生信息_考研招生计划和招生人数 - 学途吧...
  6. 用框架的你,可能早已忽略了这些事件API
  7. react 消息订阅-发布机制(解决兄弟组件通信问题)
  8. 陶陶摘苹果(0)P2005_1
  9. 计算机二级-JAVA基础知识1
  10. EditPlus添加asm配置
  11. 4.26,最好用的新浪图床GG了,博客满屏的403
  12. GoAhead的asp过程与goform过程浅析
  13. 如何压缩PPT文档的大小
  14. 谷歌统计 Google Analytics使用gtag.js 进行数据统计(VUE)
  15. 基于STM32MP1的IOT参考设计分享
  16. 一文读懂什么是智能制造,企业又该如何实施智能制造?
  17. ROCKET PROPULSION ELEMENTS——DEFINITIONS AND FUNDAMENTALS笔记
  18. Altium designer2020基础使用教程
  19. c语言程序设计中植树问题,植树问题 (3).doc
  20. URL的作用是什么?它由几部分组成?

热门文章

  1. MySQL备份恢复方案
  2. 物联网开发笔记(50)- 使用Micropython开发ESP32开发板之控制HC-SR501人体红外感应传感器
  3. AI基础课正式进入高中教材,新课标改革2018秋季执行!
  4. 一套政务OA系统,助力高效线上办公
  5. PhotoShop 快速选择工具及选择并遮住使用
  6. 模糊控制在matlab中常见的疑难问题及模糊逻辑工具箱的使用
  7. 华硕ASUS VM591U内存条固态拆机安装教程
  8. 自然》子刊同期刊发两篇重磅论文:月球上的水很多-1
  9. 使用mysql Installer安装失败处理办法
  10. MySQL 安装流程 常见安装失败问题汇总!