浅谈如何使用 github.com/yuin/gopher-lua

科技资讯 投稿 24500 0 评论

浅谈如何使用 github.com/yuin/gopher-lua

本次介绍计划分为两篇文章,这一次主要介绍 github.com/yuin/gopher-lua 这个包的介绍以及基础使用,下一边将介绍 github.com/yuin/gopher-lua 是如何在项目中使用的。如有不对的地方,请不吝赐教,谢谢。

1、 gopher-lua 基础介绍

我们先开看看官方是如何介绍自己的:

GopherLua is a Lua5.1(+ goto statement in Lua5.2 VM and compiler written in Go. GopherLua has a same goal with Lua: Be a scripting language with extensible semantics . It provides Go APIs that allow you to easily embed a scripting language to your Go host programs.

GopherLua是一个Lua5.1(Lua5.2中的+goto语句)虚拟机和用Go编写的编译器。GopherLua与Lua有着相同的目标:成为一种具有可扩展语义的脚本语言。它提供了Go API,允许您轻松地将脚本语言嵌入到Go主机程序中。

使用插件后,也能够在 lua 脚本中调用 go 写好的代码。挺秀的!

官方测试例子是生成斐波那契数列,测试执行结果如下:

prog time
anko 182.73s
otto 173.32s
go-lua 8.13s
Python3.4 5.84s
GopherLua 5.40s
lua5.1.4 1.71s

2、 gopher-lua 基础介绍

v1.1.0 版本进行的。

go get github.com/yuin/gopher-lua@v1.1.0
Go的版本需要 >= 1.9

2.1 gopher-lua 中的 hello world

package main

import (
	lua "github.com/yuin/gopher-lua"


func main( {
	// 1、创建 lua 的虚拟机
	L := lua.NewState(
	// 执行完毕后关闭虚拟机
	defer L.Close(
	// 2、加载fib.lua
	if err := L.DoString(`print("hello world"`; err != nil {
		panic(err
	}

}

执行结果:

hello world

看到这里,感觉没啥特别的地方,接下来,我们看一看 gopher-lua 如何调用事先写好的 lua脚本

fib.lua 脚本内容:

function fib(n
    if n < 2 then return n end
    return fib(n-1 + fib(n-2
end
main.go

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"


func main( {
	// 1、创建 lua 的虚拟机
	L := lua.NewState(
	defer L.Close(
	// 加载fib.lua
	if err := L.DoFile(`fib.lua`; err != nil {
		panic(err
	}
	// 调用fib(n
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("fib", // 获取fib函数引用
		NRet:    1,                  // 指定返回值数量
		Protect: true,               // 如果出现异常,是panic还是返回err
	}, lua.LNumber(10 // 传递输入参数n
	if err != nil {
		panic(err
	}
	// 获取返回结果
	ret := L.Get(-1
	// 从堆栈中扔掉返回结果
    // 这里一定要注意,不调用此方法,后续再调用 L.Get(-1 获取的还是上一次执行的结果
    // 这里大家可以自己测试下
	L.Pop(1
	// 打印结果
	res, ok := ret.(lua.LNumber
	if ok {
		fmt.Println(int(res
	} else {
		fmt.Println("unexpected result"
	}
}

执行结果:

从上面我们已经能够感受到部分 gopher-lua 的魅力了。接下来,我们就一起详细的学习学习 gopher-lua

2.2 gopher-lua 中的数据类型

LValue . LValue is an interface type that has following methods。

    String( string
  • Type( LValueType
// value.go:29

type LValue interface {
	String( string
	Type( LValueType
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM.
	assertFloat64( (float64, bool
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM.
	assertString( (string, bool
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM.
	assertFunction( (*LFunction, bool
}

上面来自官方的介绍,接下来我们看看 gopher-lua 支持那些数据类型。

Type name Go type Type( value Constants
LNilType (constants LTNil LNil
LBool (constants LTBool LTrue, LFalse
LNumber float64 LTNumber -
LString string LTString -
LFunction struct pointer LTFunction -
LUserData struct pointer LTUserData -
LState struct pointer LTThread -
LTable struct pointer LTTable -
LChannel chan LValue LTChannel -

那我们是如何知道 go 调用 lua 函数后,得到结果的类型呢?我们可以通过以下方式来知道:

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"


func main( {
	// 1、创建 lua 的虚拟机
	L := lua.NewState(
	defer L.Close(
	// 加载fib.lua
	if err := L.DoFile(`fib.lua`; err != nil {
		panic(err
	}

	TestString(L
}

func TestString(L *lua.LState {
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("TestLString", // 获取函数引用
		NRet:    1,                          // 指定返回值数量
		Protect: true,                       // 如果出现异常,是panic还是返回err
	}
	if err != nil {
		panic(err
	}

	lv := L.Get(-1 // get the value at the top of the stack
	// 从堆栈中扔掉返回结果
	L.Pop(1
	if str, ok := lv.(lua.LString; ok {
		// lv is LString
		fmt.Println(string(str
	}
	if lv.Type( != lua.LTString {
		panic("string required."
	}
}

fib.lua中的代码:

function TestLString(
    return "this is test"
end

接下来看看指针类型是如何判断的:

lv := L.Get(-1 // get the value at the top of the stack
if tbl, ok := lv.(*lua.LTable; ok {
    // lv is LTable
    fmt.Println(L.ObjLen(tbl
}

特别注意:

    LBool , LNumber , LString 这三类不是指针类型,其他的都属于指针类型。
  • LNilType and LBool 这里没看懂官方在说什么,知道的可以告知下,谢谢。
  • lua 中,nil和false都是认为是错误的情况。nil表示一个无效值(在条件表达式中相当于false)。

2.3 gopher-lua 中的调用堆栈和注册表大小

官方还介绍了性能优化这块的内容,我就不介绍了,大家感兴趣可以去看官方。

一般来说,使用默认的方式,性能不会太差。对性能没有特别高的要求,也没有必要去折腾这个。

3、gopher-lua 中常用的API

3.1 lua 调用 Go 中的代码

print(double(100

main.go 中的内容:

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"


func main( {
	L := lua.NewState(
	defer L.Close(
	L.SetGlobal("double", L.NewFunction(Double /* Original lua_setglobal uses stack... */
	L.DoFile("test.lua"
}

func Double(L *lua.LState int {
	fmt.Println("coming go code............."
	lv := L.ToInt(1            /* get argument */
	L.Push(lua.LNumber(lv * 2 /* push result */
	return 1                    /* number of results */
}

执行结果:

coming go code.............
200

上面我们已经实现了一个简单的 lua 脚本中调用 go 代码的功能。

3.2 使用Go创建模块给lua使用

接下来我们一起看看怎么做。

package main

import (
	"fmt"
	lua "github.com/yuin/gopher-lua"


func Loader(L *lua.LState int {
	// register functions to the table
	mod := L.SetFuncs(L.NewTable(, exports
	// register other stuff
	L.SetField(mod, "name", lua.LString("testName"

	// returns the module
	L.Push(mod
	return 1
}

var exports = map[string]lua.LGFunction{
	"MyAdd": MyAdd,
}

func MyAdd(L *lua.LState int {
	fmt.Println("coming custom MyAdd"
	x, y := L.ToInt(1, L.ToInt(2
	// 原谅我还不知道怎么把计算结果返回给 lua,太菜了啦
    // 不过用上另外一个包后,我知道,具体看实战篇。
	fmt.Println(x
	fmt.Println(y
	return 1
}

main.go 的内容:

package main

import lua "github.com/yuin/gopher-lua"

func main( {
	L := lua.NewState(
	defer L.Close(
	L.PreloadModule("myModule", Loader
	if err := L.DoFile("main.lua"; err != nil {
		panic(err
	}
}

main.lua 的内容:

local m = require("myModule"
m.MyAdd(10, 20
print(m.name

运行 main.go 得到执行结果:

coming custom MyAdd
10      
20      
testName

3.3 Go 调用 lua 中的代码

lua 中的代码

function TestGoCallLua(x, y
    return x+y, x*y
end

go 中的代码

func TestTestGoCallLua(L *lua.LState {
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("TestGoCallLua", // 获取函数引用
		NRet:    2,                            // 指定返回值数量,注意这里的值是 2
		Protect: true,                         // 如果出现异常,是panic还是返回err
	}, lua.LNumber(10, lua.LNumber(20
	if err != nil {
		panic(err
	}

	multiplicationRet := L.Get(-1
	addRet := L.Get(-2
	if str, ok := multiplicationRet.(lua.LNumber; ok {
		fmt.Println("multiplicationRet is: ", int(str
	}

	if str, ok := addRet.(lua.LNumber; ok {
		fmt.Println("addRet is: ", int(str
	}

}

具体的可以看 xxx 中的 TestTestGoCallLua 函数。

multiplicationRet is:  200
addRet is:  30

3.4 lua中使用go中定义好的类型

这里我们直接使用官方的例子:

type Person struct {
    Name string
}

const luaPersonTypeName = "person"

// Registers my person type to given L.
func registerPersonType(L *lua.LState {
    mt := L.NewTypeMetatable(luaPersonTypeName
    L.SetGlobal("person", mt
    // static attributes
    L.SetField(mt, "new", L.NewFunction(newPerson
    // methods
    L.SetField(mt, "__index", L.SetFuncs(L.NewTable(, personMethods
}

// Constructor
func newPerson(L *lua.LState int {
    person := &Person{L.CheckString(1}
    ud := L.NewUserData(
    ud.Value = person
    L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName
    L.Push(ud
    return 1
}

// Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
func checkPerson(L *lua.LState *Person {
    ud := L.CheckUserData(1
    if v, ok := ud.Value.(*Person; ok {
        return v
    }
    L.ArgError(1, "person expected"
    return nil
}

var personMethods = map[string]lua.LGFunction{
    "name": personGetSetName,
}

// Getter and setter for the Person#Name
func personGetSetName(L *lua.LState int {
    p := checkPerson(L
    if L.GetTop( == 2 {
        p.Name = L.CheckString(2
        return 0
    }
    L.Push(lua.LString(p.Name
    return 1
}

func main( {
    L := lua.NewState(
    defer L.Close(
    registerPersonType(L
    if err := L.DoString(`
        p = person.new("Steeve"
        print(p:name( -- "Steeve"
        p:name("Alice"
        print(p:name( -- "Alice"
    `; err != nil {
        panic(err
    }
}

官方还讲解了如何使用 go 中的context 来结束lua代码的执行,这里我就不演示了,大家自行研究。

3.5 gopher_lua 中goroutine的说明

The LState is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.

LState不是goroutine安全的。建议每个goroutine使用一个LState,并通过使用通道在goroutine之间进行通信。

Channels are represented by channel objects in GopherLua. And a channel table provides functions for performing channel operations.

在GopherLua中,通道由通道对象表示。通道表提供了执行通道操作的函数。这意味着,我们可以使用通道对象来创建、发送和接收消息,并使用通道表中的函数来控制通道的行为。通道是一种非常有用的并发编程工具,可以帮助我们在不同的goroutine之间进行通信和同步。通过使用GopherLua中的通道对象和通道表,我们可以轻松地在Lua代码中实现并发编程。

Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.

某些对象无法通过通道发送,因为其内部有非goroutine安全的对象。

    a thread(state
  • a function
  • an userdata
  • a table with a metatable

package main

import (
	lua "github.com/yuin/gopher-lua"
	"time"


func receiver(ch, quit chan lua.LValue {
	L := lua.NewState(
	defer L.Close(
	L.SetGlobal("ch", lua.LChannel(ch
	L.SetGlobal("quit", lua.LChannel(quit
	if err := L.DoString(`
    local exit = false
    while not exit do
      -- 这个 channel 的写法是固定的 ??
      channel.select(
        {"|<-", ch, function(ok, v
          if not ok then
            print("channel closed"
            exit = true
          else
            print("received:", v
          end
        end},
        {"|<-", quit, function(ok, v
            print("quit"
            exit = true
        end}
      
    end
  `; err != nil {
		panic(err
	}
}

func sender(ch, quit chan lua.LValue {
	L := lua.NewState(
	defer L.Close(
	L.SetGlobal("ch", lua.LChannel(ch
	L.SetGlobal("quit", lua.LChannel(quit
	if err := L.DoString(`
    ch:send("1"
    ch:send("2"
  `; err != nil {
		panic(err
	}
	ch <- lua.LString("3"
	quit <- lua.LTrue
}

func main( {
	ch := make(chan lua.LValue
	quit := make(chan lua.LValue
	go receiver(ch, quit
	go sender(ch, quit
	time.Sleep(3 * time.Second
}

执行结果:

received:       1
received:       2
received:       3
quit

4、gopher_lua 性能优化

下面这些内容,主要来自参考的文章,大家可以点击当 Go 遇上了 Lua 查看原文。

4.1 提前编译

在查看上述 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

4.2 虚拟机实例池

看到这里的需要注意,官方提醒我们,在每个 goroutine

因为新建一个 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 结果显示,虚拟机实例池的确能够减少很多内存分配操作。

并未创建足够多的虚拟机实例(初始时,实例数为 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,
}

参考链接:

当 Go 遇上了 Lua

编程笔记 » 浅谈如何使用 github.com/yuin/gopher-lua

赞同 (119) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽