0%

在Go中运行Lua脚本(一)

gopher-lua

一个用Go实现的Lua虚拟机,直接用原生Go代码运行Lua脚本

简介

Gopher-Lua 是一个用 Go 语言实现的 Lua 解释器,它允许开发者在 Go 程序中嵌入 Lua 脚本语言

优缺点(来自通义的评价)

优点

  1. 轻量级且高效

    • Lua 本身是一个非常轻量级的脚本语言,适合嵌入到其他应用程序中。
    • Gopher-Lua 继承了 Lua 的高效特性,同时利用 Go 的并发模型,能够很好地处理高并发任务。
  2. 与 Go 的无缝集成

    • Gopher-Lua 提供了丰富的 API,使 Lua 脚本可以方便地调用 Go 函数,反之亦然。
    • 开发者可以通过 Go 的类型系统将复杂的数据结构传递给 Lua 脚本,灵活性非常高。
  3. 易于扩展

    • 开发者可以通过 Go 编写自定义的 Lua 模块或函数,并将其注册到 Lua 环境中。
    • 这种扩展性使得 Gopher-Lua 非常适合需要动态配置或脚本化的应用场景。
  4. 跨平台支持

    • Go 和 Lua 都是跨平台的语言,因此 Gopher-Lua 可以轻松运行在多种操作系统上(如 Linux、Windows、macOS)。
  5. 社区活跃

    • 尽管 Gopher-Lua 不如标准 Lua 或其他 Lua 实现(如 LuaJIT)流行,但它的社区仍然相对活跃,文档和示例代码较多,便于学习和使用。
  6. 适合嵌入式场景

    • Lua 的设计初衷就是作为嵌入式脚本语言,Gopher-Lua 在这方面表现尤为突出,非常适合游戏开发、网络服务、自动化工具等需要脚本支持的场景。

缺点

  1. 性能不如原生 Lua

    • 虽然 Gopher-Lua 性能不错,但它毕竟是用 Go 实现的 Lua 解释器,性能上可能无法达到原生 Lua 或 LuaJIT 的水平。
    • 对于计算密集型任务,Gopher-Lua 可能会成为瓶颈。
  2. 缺乏 JIT 支持

    • Gopher-Lua 是一个纯解释器,没有像 LuaJIT 那样的即时编译(JIT)功能,这意味着对于需要高性能的场景,它可能不是最佳选择。
  3. API 学习曲线

    • 对于不熟悉 Lua 或 Go 的开发者来说,学习如何在 Go 中嵌入 Lua 并编写交互代码可能会有一定的门槛。
    • 需要理解 Lua 的栈机制以及 Go 的类型转换规则。
  4. 生态有限

    • 与 Lua 官方实现相比,Gopher-Lua 的生态系统较小,第三方库的数量和质量都有限。
    • 如果需要某些高级功能(例如特定的 Lua 模块),可能需要自行实现或寻找替代方案。
  5. 调试困难

    • Lua 脚本的调试本身就是一个挑战,而 Gopher-Lua 的调试工具和插件支持较少,可能会增加开发和维护的难度。
  6. 内存管理

    • Lua 的垃圾回收机制与 Go 的垃圾回收机制不同,可能会导致一些内存管理上的复杂性。
    • 在大规模应用中,这种差异可能会引发性能问题或内存泄漏。
  7. 不适合大型项目

    • Lua 本身是一种轻量级语言,虽然灵活,但在复杂性和模块化方面不如其他现代语言(如 Python 或 JavaScript)。
    • 如果项目规模较大,可能需要考虑更强大的脚本语言或框架。

适用场景

  • 游戏开发:Lua 是游戏开发中常用的脚本语言,Gopher-Lua 可以用于实现游戏逻辑或配置文件。
  • 网络服务:Go 的高性能网络能力与 Lua 的脚本灵活性相结合,适合构建动态配置的服务。
  • 自动化工具:Lua 的简单语法和 Gopher-Lua 的易用性使其成为编写自动化脚本的理想选择。

总结

Gopher-Lua 是一个强大且灵活的工具,特别适合需要在 Go 程序中嵌入脚本语言的场景。然而,它也有一些局限性,特别是在性能要求极高或需要复杂功能的情况下。如果你的应用场景符合其优势范围,那么 Gopher-Lua 将是一个非常好的选择;否则,可能需要考虑其他解决方案,例如直接使用原生 Lua 或 LuaJIT。

如果需要进一步了解,可以查看 Gopher-Lua 的官方文档和示例代码。

实际使用中的一些优缺点

优点

  1. 可以在lua端直接使用channel

  2. 与go线程有较好的相容性,且数据在线程间流转时要处理的问题更少,毕竟都是基于go的原生数据类型。由于go是直接运行的Lua字节码,可以方便的将go线程嫁接到lua上去。例如直接将lua文件中的某个函数放到新线程执行:lua调用 -> go获取lua函数/代码段的字节码对象 -> go在新线程中启动lua虚拟机,直接运行这个字节码对象。

  3. 单个虚拟机由go维护时,所需内存更少

  4. lua代码可以提前编译成字节码对象存储在内存中,方便复用,减少lua虚拟机的初始化时间

  5. go、lua数据交互方便,在go->lua,lua-go等场景。数据交互方便,减少了拷贝次数

  6. 还原了lua的堆栈操作,但又方便了许多,不再那么笨比了

缺点

  1. 只支持lua5.1

  2. 并未完全还原lua5.1的底层逻辑。例如table,原生是c的双指针,go版是map+array。部分情况下和原生运行效果有差异,例如内存占用等

  3. go版本用了go的yacc工具,整了ast抽象语法树来解析lua代码,然后生成的lua字节码来跑lua逻辑,并重新实现了lua底层。和clua相差较大,需要时间熟悉

使用技巧

  1. 直接执行lua文件

    1
    2
    l := lua.NewState()
    l.DoFile(filePath)
  2. 将go对象封装为元表,并通过元表__index字段实现继承关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 1. 将go对象的成员方法注册进lua元表中
    func RegMeta(L *lua.LState, key string, api map[string]lua.LGFunction, super string) *lua.LTable {
    meta := L.NewTypeMetatable(key) // 新建元表
    tb := L.SetFuncs(L.NewTable(), api) // 设置元表的方法
    if len(super) > 0 {
    L.SetMetatable(tb, L.GetTypeMetatable(super))
    }
    L.SetField(meta, "__index", tb) // 设置元方法
    return tb
    }
    // 2. 将go对象实例注册进lua虚拟机
    func UseMeta(L *lua.LState, key string, obj interface{}) *lua.LUserData {
    ud := L.NewUserData()
    ud.Value = obj
    L.SetMetatable(ud, L.GetTypeMetatable(key))
    return ud
    }
    // 3. 实现一个供lua调用的go方法。例如gin库中的Context类的String方法
    func lua_Context_String(L *lua.LState) int {
    ud := L.CheckUserData(1)
    if v, ok := ud.Value.(Context); ok {
    code := L.ToInt(2)
    str := L.ToString(3)
    v.String(code, str)
    } else {
    // error
    L.ArgError(1, "lua_Context_String expected")
    }
    return 0
    }
  3. 在lua中调用go对象实例的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -- context对象可通过ch,从go发送至lua端
    channel.select(
    { "|<-", chIn, function(ok, context)
    if ok then
    -- ...处理请求...
    -- 调用go对象context的String方法
    context:String("success")
    end
    end },
    )
  4. 预编译lua脚本

    1
    2
    3
    4
    5
    6
    // 预编译lua文件
    proto, err := compileFile(filePath)
    // 使用预编译的lua脚本
    L.Push(slf.L.NewFunctionFromProto(proto))
    // 调用预编译的脚本
    L.PCall(0, lua.MultRet, nil)
  5. 利用其将lua代码编译为字节码特性,将lua扩展为多线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    // 注册一个名为thread的talbe对象,含有一个run方法
    func initThread(l *lua.LState) {
    tb := l.NewTable()
    tb.RawSetH(lua.LString("run"), l.NewFunction(lua_run))
    l.SetGlobal("thread", tb)
    }

    // 通过thread.run方法,将lua代码段,放入go线程中执行
    func lua_run(L *lua.LState) int {

    //...

    // 参数封装为table从lua传入go中
    param := L.ToTable(1)

    // 要执行的lua代码段,实际是一个字节码对象
    fn := param.RawGetString("func").(*lua.LFunction)
    // 传递给代码段执行的参数
    args := param.RawGetString("args").(*lua.LTable)
    // 启用的线程数量
    count := int(math.Min(float64(args.Len()), float64(param.RawGetString("count").(lua.LNumber))))

    // 用于收集每个线程执行的结果
    // 这里采用table+lock的方式,也可以使用channel收集
    result := rt.l.NewTable()

    // 通过channel传递执行的参数
    ch := make(chan task, count)

    var mutex sync.Mutex
    wg := &sync.WaitGroup{}
    wg.Add(count)

    // 启用count个线程
    for i := 0; i < count; i++ {
    // ...
    // 获取一个新的lua虚拟机
    rt := rt.GetPool().GetRuntime()
    // 启动线程并执行lua代码段
    go do_task(i, rt, fn, ch, result, wg, &mutex)
    }

    // 通过channel发送参数
    go func() {
    for i := 0; i < args.Len(); i++ {
    ch <- task{Idx: i + 1, Param: args.RawGetInt(i + 1)}
    }
    close(ch)
    }()

    // 等待执行完
    wg.Wait()

    // 执行完成
    L.Push(result)

    return 1
    }

    func do_task(
    idx int,
    rt *Runtime,
    fn *lua.LFunction,
    ch chan task,
    out *lua.LTable,
    wg *sync.WaitGroup,
    mutex *sync.Mutex,
    ) {
    // 通过字节码对象,生成新虚拟机的lua function
    proto := rt.l.NewFunctionFromProto(fn.Proto)
    for {
    // 获取参数
    task, ok := <-ch
    if !ok {
    break
    }
    var err error
    rt.l.Push(proto)
    rt.l.Push(lua.LNumber(idx))
    // 执行lua代码段
    if task.Param != nil {
    rt.l.Push(task.Param)
    err = rt.l.PCall(2, 1, nil)
    } else {
    err = rt.l.PCall(1, 1, nil)
    }
    var result lua.LValue
    if err != nil {
    result = lua.LString(err.Error())
    } else {
    if rt.l.GetTop() == 1 {
    result = rt.l.Get(1)
    rt.l.Pop(1) // 使用完毕的参数,出栈
    } else {
    result = lua.LNil
    }
    }
    // 写入执行结果
    mutex.Lock()
    out.Append(result)
    mutex.Unlock()
    }
    if wg != nil {
    wg.Done()
    }
    // ...
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
--由其他线程执行的函数
local function sub(idx, param)
local i = param
while i > 0 do
i = i - 1
end
return { idx, param }
end

local result = thread.run({
count = 4, --启用4个线程
func = sub,
args = { 9000000, 30000000, 70000000, 1000000, 5000, 320, 100000, 88880888 },
})
--[[
输出结果:
- "<var>" = {
- 1 = {
- 1 = 3
- 2 = 1000000
- }
- 2 = {
- 1 = 3
- 2 = 5000
- }
- 3 = {
- 1 = 3
- 2 = 320
- }
- 4 = {
- 1 = 3
- 2 = 100000
- }
- 5 = {
- 1 = 0
- 2 = 9000000
- }
- 6 = {
- 1 = 1
- 2 = 30000000
- }
- 7 = {
- 1 = 2
- 2 = 70000000
- }
- 8 = {
- 1 = 3
- 2 = 88880888
- }
- }
启用了4个线程,共处理了8个参数
]]