最近熟悉 go 專案時,發現專案中有用到 github.com/yuin/gopher-lua 這個包,之前並沒有接觸過,特意去看了官方文件和找了些網上的資料,特此記錄下。
本次介紹計劃分為兩篇文章,這一次主要介紹 github.com/yuin/gopher-lua 這個包的介紹以及基礎使用,下一邊將介紹 github.com/yuin/gopher-lua 是如何在專案中使用的。如有不對的地方,請不吝賜教,謝謝。
文章中的 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主機程式中。
看上面的翻譯還是有點抽象,說說自己的理解。 github.com/yuin/gopher-lua 是一個純 Golang 實現的 Lua 虛擬機器,它能夠很輕鬆的在 go 寫的程式中呼叫 lua 指令碼。另外提一嘴,使用外掛後,也能夠在 lua 指令碼中呼叫 go 寫好的程式碼。挺秀的!
接下來我們看一看, github.com/yuin/gopher-lua 的效能如何,這裡就直接引用官方自己做的測試來介紹。詳情見 wiki page 連結。點進連結過後,發現效能還不錯,執行效率和效能僅比 C 實現的 bindings 差點。
官方測試例子是生成斐波那契數列
,測試執行結果如下:
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
這裡寫一個簡單的程式,瞭解 gopher-lua 是如何使用的。
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")
}
}
執行結果:
55
從上面我們已經能夠感受到部分 gopher-lua
的魅力了。接下來,我們就一起詳細的學習學習 gopher-lua
。
2.2 gopher-lua 中的資料型別
All data in a GopherLua program is an LValue
. LValue
is an interface type that has following methods.
GopherLua程式中的所有資料都是一個LValue。LValue是一種具有以下方法的介面型別。
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 中的程式碼
test.lua 指令碼內容:
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使用
上面介紹了 lua 中呼叫 Go中的程式碼,Go提供的功能不多還好,直接使用即可,但是實際專案中,既然使用到了Go和lua結合的模式,必然會存在Go提供基礎功能,lua來編寫業務的方式,這個時候如果還是使用上面的方式,使用起來將非常不方便。這裡提供了一種方式,將Go中的功能封裝成一個模組,提供給 lua 使用,這樣就方便許多。
接下來我們一起看看怎麼做。
mymodule.go 的內容:
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 程式碼被執行的場景下,除了可使用提前編譯最佳化效能外,我們還可以引入虛擬機器例項池。
因為新建一個 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),
}
參考連結: