GopherLua:Go语言编写的Lua虚拟机和编译器。
===============================================================================
GopherLua是一个用Go语言编写的Lua 5.1(加上Lua 5.2中的goto
语句)虚拟机和编译器。GopherLua与Lua有着相同的目标:成为一种具有可扩展语义的脚本语言。它提供了Go语言API,让您可以轻松地将脚本语言嵌入到您的Go宿主程序中。
目录 :depth: 1
设计原则
- 成为一种具有可扩展语义的脚本语言。
- 用户友好的Go API
- 像原始Lua实现中使用的基于栈的API会导致GopherLua的性能提升 (它将减少内存分配和具体类型 <-> 接口转换)。 GopherLua API不是基于栈的API。 GopherLua更注重用户友好性而非性能。
性能如何?
我认为GopherLua虽然不是最快的,但也不算太慢。
在微基准测试中,GopherLua的性能几乎与Python3相当(或略好)。
在wiki页面 <https://github.com/yuin/gopher-lua/wiki/Benchmarks>
_上有一些基准测试结果。
安装
.. code-block:: bash
go get github.com/yuin/gopher-lua
GopherLua支持Go 1.9及以上版本。
使用方法
GopherLua的API与Lua的使用方式非常相似,但栈仅用于传递参数和接收返回值。
GopherLua支持通道操作。请参阅**"Goroutines"**部分。
导入包。
.. code-block:: go
import ( "github.com/yuin/gopher-lua" )
在虚拟机中运行脚本。
.. code-block:: go
L := lua.NewState()
defer L.Close()
if err := L.DoString(print("hello")
); err != nil {
panic(err)
}
.. code-block:: go
L := lua.NewState() defer L.Close() if err := L.DoFile("hello.lua"); err != nil { panic(err) }
更多信息请参考Lua参考手册 <http://www.lua.org/manual/5.1/>
和Go文档 <http://godoc.org/github.com/yuin/gopher-lua>
。
请注意,Go文档 <http://godoc.org/github.com/yuin/gopher-lua>
中未注释的元素等同于Lua参考手册 <http://www.lua.org/manual/5.1/>
,但GopherLua使用对象而不是Lua栈索引。
数据模型
GopherLua程序中的所有数据都是LValue
。LValue
是一个接口类型,具有以下方法:
String() string
Type() LValueType
实现LValue接口的对象有:
================ ========================= ================== =======================
类型名称 Go类型 Type()值 常量
================ ========================= ================== =======================
LNilType
(常量) LTNil
LNil
LBool
(常量) LTBool
LTrue
, LFalse
LNumber
float64 LTNumber
-
LString
string LTString
-
LFunction
结构体指针 LTFunction
-
LUserData
结构体指针 LTUserData
-
LState
结构体指针 LTThread
-
LTable
结构体指针 LTTable
-
LChannel
chan LValue LTChannel
-
================ ========================= ================== =======================
您可以通过Go的方式(类型断言)或使用Type()
值来测试对象类型。
.. code-block:: go
lv := L.Get(-1) // 获取栈顶的值 if str, ok := lv.(lua.LString); ok { // lv 是 LString fmt.Println(string(str)) } if lv.Type() != lua.LTString { panic("需要字符串。") }
.. code-block:: go
lv := L.Get(-1) // 获取栈顶的值 if tbl, ok := lv.(*lua.LTable); ok { // lv 是 LTable fmt.Println(L.ObjLen(tbl)) }
注意,LBool
、LNumber
、LString
不是指针。
要测试LNilType
和LBool
,您必须使用预定义的常量。
.. code-block:: go
lv := L.Get(-1) // 获取栈顶的值
if lv == lua.LTrue { // 正确 }
if bl, ok := lv.(lua.LBool); ok && bool(bl) { // 错误 }
在Lua中,nil
和false
都使条件为假。LVIsFalse
和LVAsBool
实现了这个规范。
.. code-block:: go
lv := L.Get(-1) // 获取栈顶的值 if lua.LVIsFalse(lv) { // lv 是 nil 或 false }
if lua.LVAsBool(lv) { // lv 既不是 nil 也不是 false }
基于Go结构体的对象(LFunction
、LUserData
、LTable
)有一些公共方法和字段。您可以出于性能和调试的目的使用这些方法和字段,但有一些限制:
- 元表不起作用。
- 没有错误处理。
调用栈和注册表大小
LState
的调用栈大小控制脚本中Lua函数的最大调用深度(Go函数调用不计入)。
LState
的注册表为调用函数(包括Lua和Go函数)以及表达式中的临时变量实现了栈存储。它的存储需求会随着调用栈的使用和代码复杂度的增加而增加。
注册表和调用栈都可以设置为固定大小或自动调整大小。
当您在一个进程中实例化了大量LState
时,值得花时间调整注册表和调用栈的选项。
+++++++++ 注册表 +++++++++
注册表可以在每个LState
的基础上配置初始大小、最大大小和步进大小。这将允许注册表根据需要增长。增长后不会再缩小。
.. code-block:: go
L := lua.NewState(lua.Options{
RegistrySize: 1024 * 20, // 这是注册表的初始大小
RegistryMaxSize: 1024 * 80, // 这是注册表可以增长到的最大大小。如果设置为`0`(默认值),则注册表不会自动增长
RegistryGrowStep: 32, // 这是每次注册表空间不足时增加的步长。默认值为`32`。
})
defer L.Close()
对于给定的脚本,如果注册表太小,最终会导致崩溃。如果注册表太大,会浪费内存(如果实例化了多个LState
,这可能会很显著)。
自动增长的注册表在调整大小时会稍微影响性能,但不会影响其他方面的性能。
+++++++++ 调用栈 +++++++++ 调用栈可以以两种不同的模式运行:固定大小或自动调整大小。
固定大小的调用栈具有最高的性能,并且内存开销固定。
自动调整大小的调用栈会根据需求分配和释放调用栈页,确保在任何时候都使用最少的内存。缺点是每次分配新的调用帧页时都会产生一些性能影响。
默认情况下,LState 会以 8 个为一页分配和释放调用栈帧,因此不会在每次函数调用时都产生分配开销。对于大多数用例来说,自动调整大小的调用栈的性能影响可能微不足道。
.. code-block:: go
L := lua.NewState(lua.Options{
CallStackSize: 120, // 这是该 LState 的最大调用栈大小
MinimizeStackMemory: true, // 如果未指定则默认为 `false`。如果设置,调用栈将根据需要自动增长和收缩,最大不超过 `CallStackSize`。如果未设置,调用栈将固定为 `CallStackSize`。
})
defer L.Close()
++++++++++++++++ 选项默认值 ++++++++++++++++
上面的例子展示了如何为每个 LState 自定义调用栈和注册表大小。你也可以通过修改 lua.RegistrySize、lua.RegistryGrowStep 和 lua.CallStackSize 的值来调整未指定选项时的一些默认值。
由 *LState#NewThread() 创建的 LState 对象会继承父 LState 对象的调用栈和注册表大小。
其他 lua.NewState 选项
- Options.SkipOpenLibs bool(默认 false)
- 默认情况下,GopherLua 在创建新的 LState 时会打开所有内置库。
- 你可以通过将此设置为 true 来跳过此行为。
- 使用各种 OpenXXX(L *LState) int 函数,你可以只打开所需的库,下面有一个示例。
- Options.IncludeGoStackTrace bool(默认 false)
- 默认情况下,GopherLua 在发生 panic 时不显示 Go 堆栈跟踪。
- 你可以通过将此设置为 true 来获取 Go 堆栈跟踪。
API
有关更多信息,请参阅 Lua 参考手册和 Go 文档(LState 方法)。
+++++++++++++++++++++++++++++++++++++++++ 从 Lua 调用 Go +++++++++++++++++++++++++++++++++++++++++
.. code-block:: go
func Double(L lua.LState) int { lv := L.ToInt(1) / 获取参数 / L.Push(lua.LNumber(lv * 2)) / 推送结果 / return 1 / 结果数量 */ }
func main() { L := lua.NewState() defer L.Close() L.SetGlobal("double", L.NewFunction(Double)) /* 原始的 lua_setglobal 使用栈... */ }
.. code-block:: lua
print(double(20)) -- > "40"
任何在 GopherLua 中注册的函数都是一个 lua.LGFunction,定义在 value.go 中
.. code-block:: go
type LGFunction func(*LState) int
使用协程。
.. code-block:: go
co, _ := L.NewThread() /* 创建一个新线程 */ fn := L.GetGlobal("coro").(lua.LFunction) / 从 lua 获取函数 */ for { st, err, values := L.Resume(co, fn) if st == lua.ResumeError { fmt.Println("yield break(error)") fmt.Println(err.Error()) break }
for i, lv := range values {
fmt.Printf("%v : %v\n", i, lv)
}
if st == lua.ResumeOK {
fmt.Println("yield break(ok)")
break
}
}
+++++++++++++++++++++++++++++++++++++++++ 打开内置模块的子集 +++++++++++++++++++++++++++++++++++++++++
以下演示了如何在 Lua 中打开内置模块的子集,例如,为了避免启用具有访问本地文件或系统调用权限的模块。
main.go
.. code-block:: go
func main() {
L := lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close()
for _, pair := range []struct {
n string
f lua.LGFunction
}{
{lua.LoadLibName, lua.OpenPackage}, // 必须是第一个
{lua.BaseLibName, lua.OpenBase},
{lua.TabLibName, lua.OpenTable},
} {
if err := L.CallByParam(lua.P{
Fn: L.NewFunction(pair.f),
NRet: 0,
Protect: true,
}, lua.LString(pair.n)); err != nil {
panic(err)
}
}
if err := L.DoFile("main.lua"); err != nil {
panic(err)
}
}
+++++++++++++++++++++++++++++++++++++++++ 用 Go 创建模块 +++++++++++++++++++++++++++++++++++++++++
mymodule.go
.. code-block:: go
package mymodule
import (
"github.com/yuin/gopher-lua"
)
func Loader(L *lua.LState) int {
// 向表中注册函数
mod := L.SetFuncs(L.NewTable(), exports)
// 注册其他内容
L.SetField(mod, "name", lua.LString("value"))
// 返回模块
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"myfunc": myfunc,
}
func myfunc(L *lua.LState) int {
return 0
}
mymain.go
.. code-block:: go
package main
import (
"./mymodule"
"github.com/yuin/gopher-lua"
)
func main() {
L := lua.NewState()
defer L.Close()
L.PreloadModule("mymodule", mymodule.Loader)
if err := L.DoFile("main.lua"); err != nil {
panic(err)
}
}
main.lua
.. code-block:: lua
local m = require("mymodule")
m.myfunc()
print(m.name)
+++++++++++++++++++++++++++++++++++++++++ 从 Go 调用 Lua +++++++++++++++++++++++++++++++++++++++++
.. code-block:: go
L := lua.NewState() defer L.Close() if err := L.DoFile("double.lua"); err != nil { panic(err) } if err := L.CallByParam(lua.P{ Fn: L.GetGlobal("double"), NRet: 1, Protect: true, }, lua.LNumber(10)); err != nil { panic(err) } ret := L.Get(-1) // 返回值 L.Pop(1) // 移除接收到的值
如果 Protect 为 false,GopherLua 将会 panic 而不是返回 error 值。
+++++++++++++++++++++++++++++++++++++++++ 用户定义类型 +++++++++++++++++++++++++++++++++++++++++ 你可以用 Go 编写新类型来扩展 GopherLua。 为此提供了 LUserData。
.. code-block:: go
type Person struct {
Name string
}
const luaPersonTypeName = "person"
// 向给定的 L 注册我的 person 类型。
func registerPersonType(L *lua.LState) {
mt := L.NewTypeMetatable(luaPersonTypeName)
L.SetGlobal("person", mt)
// 静态属性
L.SetField(mt, "new", L.NewFunction(newPerson))
// 方法
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
}
// 构造函数
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
}
// 检查第一个 lua 参数是否为带有 *Person 的 *LUserData,并返回这个 *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,
}
// Person#Name 的 getter 和 setter
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)
}
}
+++++++++++++++++++++++++++++++++++++++++ 终止运行中的LState +++++++++++++++++++++++++++++++++++++++++ GopherLua支持"Go并发模式:上下文"。
.. code-block:: go
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 将上下文设置到我们的LState
L.SetContext(ctx)
err := L.DoString(`
local clock = os.clock
function sleep(n) -- 秒
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
// err.Error()包含"context deadline exceeded"
使用协程
.. code-block:: go
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
L.SetContext(ctx)
defer cancel()
L.DoString(`
function coro()
local i = 0
while true do
coroutine.yield(i)
i = i+1
end
return i
end
`)
co, cocancel := L.NewThread()
defer cocancel()
fn := L.GetGlobal("coro").(*LFunction)
_, err, values := L.Resume(co, fn) // err为nil
cancel() // 取消父上下文
_, err, values = L.Resume(co, fn) // err不为nil:子上下文被取消
注意使用上下文会导致性能下降。
.. code-block::
time ./glua-with-context.exe fib.lua
9227465
0.01s user 0.11s system 1% cpu 7.505 total
time ./glua-without-context.exe fib.lua
9227465
0.01s user 0.01s system 0% cpu 5.306 total
+++++++++++++++++++++++++++++++++++++++++
在LState之间共享Lua字节码
+++++++++++++++++++++++++++++++++++++++++
调用DoFile
将加载Lua脚本,将其编译为字节码并在LState
中运行字节码。
如果你有多个需要运行相同脚本的LState
,你可以在它们之间共享字节码,这将节省内存。
共享字节码是安全的,因为它是只读的,不能被Lua脚本修改。
.. code-block:: go
// CompileLua从磁盘读取传入的lua文件并编译它。
func CompileLua(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
}
// DoCompiledFile接受CompileLua返回的FunctionProto,并在LState中运行它。
// 这相当于在LState上用原始源文件调用DoFile。
func DoCompiledFile(L *lua.LState, proto *lua.FunctionProto) error {
lfunc := L.NewFunctionFromProto(proto)
L.Push(lfunc)
return L.PCall(0, lua.MultRet, nil)
}
// 示例展示了如何在多个VM之间共享Lua脚本的编译字节码。
func Example() {
codeToShare := CompileLua("mylua.lua")
a := lua.NewState()
b := lua.NewState()
c := lua.NewState()
DoCompiledFile(a, codeToShare)
DoCompiledFile(b, codeToShare)
DoCompiledFile(c, codeToShare)
}
+++++++++++++++++++++++++++++++++++++++++
Goroutines
+++++++++++++++++++++++++++++++++++++++++
LState
不是goroutine安全的。建议每个goroutine使用一个LState,并通过使用通道在goroutine之间通信。
在GopherLua中,通道由channel
对象表示。channel
表提供了执行通道操作的函数。
由于内部包含非goroutine安全的对象,某些对象不能通过通道发送。
- 线程(状态)
- 函数
- 用户数据
- 带有元表的表
你不能从Go API将这些对象发送到通道。
.. code-block:: go
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.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)
}
''''''''''''''' Go API '''''''''''''''
可以使用ToChannel
、CheckChannel
、OptChannel
。
更多信息请参考 Go doc(LState方法) <http://godoc.org/github.com/yuin/gopher-lua>
_。
''''''''''''''' Lua API '''''''''''''''
-
channel.make([buf:int]) -> ch:channel
- 创建一个缓冲区大小为
buf
的新通道。默认情况下,buf
为0。
- 创建一个缓冲区大小为
-
channel.select(case:table [, case:table, case:table ...]) -> {index:int, recv:any, ok}
- 与Go中的
select
语句相同。它返回所选case的索引,如果该case是接收操作,则返回接收到的值和一个布尔值,指示通道是否已关闭。 case
是一个如下所述的表。- 接收:
{"|<-", ch:channel [, handler:func(ok, data:any)]}
- 发送:
{"<-|", ch:channel, data:any [, handler:func(data:any)]}
- 默认:
{"default" [, handler:func()]}
- 接收:
- 与Go中的
channel.select
示例:
.. code-block:: lua
local idx, recv, ok = channel.select(
{"|<-", ch1},
{"|<-", ch2}
)
if not ok then
print("closed")
elseif idx == 1 then -- 从ch1接收
print(recv)
elseif idx == 2 then -- 从ch2接收
print(recv)
end
.. code-block:: lua
channel.select(
{"|<-", ch1, function(ok, data)
print(ok, data)
end},
{"<-|", ch2, "value", function(data)
print(data)
end},
{"default", function()
print("default action")
end}
)
- channel:send(data:any)
- 通过通道发送
data
。
- 通过通道发送
- channel:receive() -> ok:bool, data:any
- 通过通道接收数据。
- channel:close()
- 关闭通道。
''''''''''''''''''''''''''''''
LState池模式
''''''''''''''''''''''''''''''
要创建每个线程的LState实例,你可以使用类似sync.Pool
的机制。
.. code-block:: go
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() // 在此处设置L // 加载脚本,设置全局变量,共享通道等... 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() } }
// 全局LState池 var luaPool = &lStatePool{ saved: make([]*lua.LState, 0, 4), }
现在,你可以从 luaPool
中获取每个线程的LState对象。
.. code-block:: go
func MyWorker() {
L := luaPool.Get()
defer luaPool.Put(L)
/* 你的代码在这里 */
}
func main() {
defer luaPool.Shutdown()
go MyWorker()
go MyWorker()
/* 等等... */
}
Lua和GopherLua之间的差异
协程
- GopherLua支持通道操作。
- GopherLua有一个名为
channel
的类型。 channel
表提供了执行通道操作的函数。
- GopherLua有一个名为
不支持的函数
string.dump
os.setlocale
lua_Debug.namewhat
package.loadlib
- 调试钩子
其他注意事项
collectgarbage
不接受任何参数,会为整个Go程序运行垃圾收集器。file:setvbuf
不支持行缓冲。- 不支持夏令时。
- GopherLua有一个设置环境变量的函数:
os.setenv(name, value)
- GopherLua支持Lua5.2中的
goto
和::label::
语句。goto
是关键字,不是有效的变量名。
独立解释器
Lua有一个名为 lua
的解释器。GopherLua有一个名为 glua
的解释器。
.. code-block:: bash
go get github.com/yuin/gopher-lua/cmd/glua
glua
具有与 lua
相同的选项。
如何贡献
请参阅 贡献者指南 <https://github.com/yuin/gopher-lua/tree/master/.github/CONTRIBUTING.md>
_ 。
GopherLua的库
gopher-luar <https://github.com/layeh/gopher-luar>
_ :简化了与gopher-lua之间的数据传递gluamapper <https://github.com/yuin/gluamapper>
_ :将Lua表映射到Go结构体gluare <https://github.com/yuin/gluare>
_ :gopher-lua的正则表达式gluahttp <https://github.com/cjoudrey/gluahttp>
_ :gopher-lua的HTTP请求模块gopher-json <https://github.com/layeh/gopher-json>
_ :gopher-lua的简单JSON编码器/解码器gluayaml <https://github.com/kohkimakimoto/gluayaml>
_ :gopher-lua的Yaml解析器glua-lfs <https://github.com/layeh/gopher-lfs>
_ :部分实现了gopher-lua的luafilesystem模块gluaurl <https://github.com/cjoudrey/gluaurl>
_ :gopher-lua的URL解析器/构建器模块gluahttpscrape <https://github.com/felipejfc/gluahttpscrape>
_ :gopher-lua的简单HTML抓取模块gluaxmlpath <https://github.com/ailncode/gluaxmlpath>
_ :gopher-lua的xmlpath模块gmoonscript <https://github.com/rucuriousyet/gmoonscript>
_ :Gopher Lua VM的Moonscript编译器loguago <https://github.com/rucuriousyet/loguago>
_ :Gopher-Lua的Zerolog封装器gluacrypto <https://github.com/tengattack/gluacrypto>
_ :GopherLua VM的原生Go加密库实现gluasql <https://github.com/tengattack/gluasql>
_ :GopherLua VM的原生Go SQL客户端实现purr <https://github.com/leyafo/purr>
_ :HTTP模拟测试工具vadv/gopher-lua-libs <https://github.com/vadv/gopher-lua-libs>
_ :GopherLua VM的一些有用库gluasocket <https://gitlab.com/megalithic-llc/gluasocket>
_ :GopherLua VM的原生Go LuaSocket实现glua-async <https://github.com/CuberL/glua-async>
_ :gopher-lua的async/await实现gopherlua-debugger <https://github.com/edolphin-ydf/gopherlua-debugger>
_ :gopher-lua的调试器gluamahonia <https://github.com/super1207/gluamahonia>
_ :gopher-lua的编码转换器
捐赠
BTC: 1NEDSyUmo4SMTDP83JJQSWi1MvQUGGNMZB
许可证
MIT
作者
Yusuke Inuzuka