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

技术Golang和Lua相遇会擦出什么火花这篇文章主要介绍“Golang和Lua相遇会擦出什么火花”,在日常操作中,相信很多人在Golang和Lua相遇会擦出什么火花问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操

本文主要介绍“Golang和Lua相遇会产生什么火花”。在日常操作中,相信很多人都怀疑Golang和Lua相遇会产生什么火花。边肖查阅了各种资料,整理出简单易用的操作方法,希望能帮你解答“Golang和Lua相遇会产生什么火花”的疑惑!接下来,请和边肖一起学习!

在GitHub玩的时候,偶然发现了gopher-lua,一个由Golang实现的lua虚拟机。我们知道Golang是静态语言,而Lua是动态语言。Golang的性能和效率在所有语言中都非常好,但在动态能力方面肯定无法与Lua相比。然后,如果能把两者结合起来,就可以整合它们各自的长处(手动搞笑)。

在项目Wiki中,我们可以知道gopher-lua的执行效率和性能只比c实现的绑定差,所以从性能上来看,这应该是一个非常不错的虚拟机解决方案。

Hello World

这里有一个简单的Hello World程序。首先,我们创建了一个新的虚拟机,然后解释了通过DoString(.),最后关闭虚拟机。执行程序,我们会在命令行看到“Hello World”的字符串。

packagemain

导入(

github.com/yuin/gopher-lua '

)

func main(){ 0

l:=lua。新闻状态()

德雷尔。关闭()

ifer :=l . DOSTRING(` print(' hello world ')`);呃!=零

恐慌

}

}

//hello world提前编译

看了上面提到的DoString的调用链后(.)方法,我们发现每次DoString(.)或DoFile(.)执行,解析和编译分别执行。

func(ls * LState)DOSTRING(source string)错误{

iffn,err:=ls。LoadString(源);呃!=零

returnerr

}else{

ls。推动(fn)

返回。PCall(0,多项测试,零)

}

}

func(ls * LState)LoAdStrIng(source StrIng)(* lfu function,错误){ 0

返回。加载(字符串。NewReader(源),“字符串”)

}

func(ls*LState)Load(readerio。Reader,name string)(* lfu function,error){ 0

chunk,err:=parse。解析(读者、姓名)

//.

proto,err:=编译(块,名称)

//.

}从这个角度来看,在同一个Lua代码会被多次执行的场景下(比如在http服务器中,每次都会执行同一个Lua代码),如果我们能够提前编译代码,应该可以减少解析和编译的开销(如果这是一个hotpath代码)。根据Benchmark的结果,提前编译确实可以减少不必要的开销。

packageglua_test

导入(

布菲奥

操作系统

'字符串'

lua'github.com/yuin/gopher-lua '

github.com/yuin/gopher-Lua/parse '

)

//编译lua代码字段

funcompilestring(source string)(* Lua。功能原型

rror) {
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()
}
// <string>:1: not allow to modify table

到此,关于“Golang和Lua相遇会擦出什么火花”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/77884.html

(0)

相关推荐

  • 视频服务器为什么推荐美国服务器

    技术视频服务器为什么推荐美国服务器美国视频服务器主要是指用于部署视频直播或者点播的流媒体SDK服务器。美国视频服务器一般采用独享的物理服务器。虚拟机不同,用户可以自行管理所有硬件资源,直接控制服务器的负载,不受于其他用户

    礼包 2021年11月1日
  • c语言中主要有几种循环语句(c语言循环语句基础知识)

    技术怎么深入了解c语言的循环语句怎么深入了解c语言的循环语句,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。C语言循环语句大多数人都希望自己是体格强健,天

    攻略 2021年12月14日
  • keil c51怎么修改字体大小(keilc51怎么设置字体大小)

    技术KeilC51基础中怎么改变代码的字体大小KeilC51基础中怎么改变代码的字体大小,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。    IDE

    攻略 2021年12月24日
  • 如何使用Python进行社交媒体情感分析

    技术如何使用Python进行社交媒体情感分析这篇文章给大家介绍如何使用Python进行社交媒体情感分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。学习自然语言处理的基础知识并探索两个有用的 Pyt

    攻略 2021年10月26日
  • VSCode如何进行安卓开发

    技术VSCode如何进行安卓开发这篇文章给大家介绍VSCode如何进行安卓开发,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。vs code 大部分是由 ts 编写,上层 UI 可以运行在各个系统的浏

    攻略 2021年11月24日
  • 如何基于RBAC设计模型设计权限管理系统

    技术如何基于RBAC设计模型设计权限管理系统 如何基于RBAC设计模型设计权限管理系统RBAC是取自(Role-Based Access Control)四个单词首字母的缩写成的名称或者术语,意思是基于

    礼包 2021年11月20日