Lua 是一款免费开源的嵌入式脚本语言,使用C语言开发,轻量、敏捷、可扩展性强,因此有许多程序使用Lua作为自己的脚本开发环境,用于程序的热更新、插件系统、或是逻辑代码等等。

Lua采用C语言开发,因此C、C++可以直接将其集成到程序内部,其他语言也可以使用 Lua 的动态运行库,将其动态加载到自己的程序中。

如果想在.NET中使用Lua,目前的方案也有许多,但我个人比较推荐 MoonSharpNeoLuaNLua,其中 MoonSharp 和 NeoLua 是完全用C#写的基于Lua5.2、5.3的.NET库,而 NLua 则是基于Lua官方的DLL运行库所包装的.NET库(支持最新的LUA5.4)。
速度上应该是 NLua 更快,兼容性最高,所以本文将采用 NLua 作为教程。

目录 - Table of Contents


为什么你应该使用 LUA

LUA 是免费开源的脚本语言,采用MIT协议,并且其语法简单易学,与BASIC非常相似。
我喜欢LUA的原因就是因为它不需要分号来结尾,也没有讨厌的花括号!(跟VB一样

LUA作为脚本语言,属于弱类型语言,在面向对象编程方面肯定不如强类型语言。
不过也正因如此,在使用时没那么多的条条框框,编写一些需要热更新、或是经常调试的代码会非常方便。

LUA的学习门槛很低,稍有其他编程语言的基础,几天就能学会LUA。

安装 NLua

官方网站:
https://github.com/nlua/NLua
https://www.nuget.org/packages/NLua

建议使用 Nuget 进行安装,会自动安装 NLua 的依赖项以及 lua 的动态运行库文件。
NuGet\Install-Package NLua
安装完成并编译一次程序后,在程序的主目录下将会自动生成 NLua.dll KeraLua.dll lua54.dll(版本会有不同),并且如果编译模式为 AnyCPU 的话,还会生成 x86x64 两个文件夹,里面是不同版本的LUA运行库。

使用VB.NET与LUA交互

执行 LUA 脚本并获取返回值

Using mylua As New Lua
    ' 执行脚本
    Dim ret = mylua.DoString("return 10 + 3*(5 + 2)")(0)
    MsgBox(ret.GetType.ToString)
    MsgBox(ret)
End Using
记得先导入 Imports NLua 命名空间。
Using 关键字用于自动垃圾回收,如果你的脚本需要长时间运行、并且需要频繁初始化,无法使用Using的自动回收,记得在清理对象之前调用 mylua.Dispose()

DoString 方法用于直接执行一段LUA脚本命令,并且可以获取返回值。
需要注意的是:LUA的返回值是可以返回多个对象的,所以 DoString 返回的也是一个数组!
在此例中我们只返回了一个数据,因此只需要获取数组的第一个对象即可。

ret.GetType 获取返回值的数据类型,在 LUA 5.2 中,这个类型是 Float 八字节浮点值,而在最新版本的 Lua 5.4 中,这个类型是 Int64 八字节整数!!!
如果你需要获取浮点值,在进行数据计算的时候,可以将数值设置为浮点值,如 return 10.0 + 3*(5 + 2)
执行LUA脚本文件同样非常简单,只需要使用 mylua.DoFile 即可,同样可以直接获取LUA脚本的返回值:
Using mylua As New Lua
    Dim ret = mylua.DoFile("test.lua")(0)
    MsgBox(ret.GetType.ToString)
    MsgBox(ret)
End Using
-- lua 脚本可以直接在文件中返回一个值
return 10.0 + 3*(5 + 2)
PS:如果你只想载入一个LUA脚本,可以使用 mylua.LoadFile,它应该等同于LUA自身的 loadfile 函数,用于直接将LUA脚本载入到当前的脚本环境中。与 require 函数不同,loadfile 可以多次执行同一个LUA脚本,每次都会更新已载入的函数或者变量。

设置 LUA 脚本变量

Using mylua As New Lua
    ' 设置变量
    mylua("test") = "hello world"
    'mylua.Item("test") = "hello world" '等效语句,Item 是 NLua 的全局表

    ' 变量可被执行
    Dim ret = mylua.DoString("return test")(0)
    MsgBox(ret.GetType.ToString)
    MsgBox(ret)

    ' 直接获取变量
    Dim ret2 = TryCast(mylua("test"), String)
    'Dim ret2 = mylua.GetString("test") '另一种方法
    MsgBox(ret2)

    ' 将变量设置为 Nothing 或 nil 即为删除该变量
    mylua("test") = Nothing
    'mylua.DoString("test = nil") '等效命令
    ' 不存在的变量会返回 Nothing
    Dim ret3 = mylua.DoString("return test")(0)
    MsgBox(ret3 Is Nothing)
End Using
我们可以通过默认属性使用 mylua("变量名") = 变量值 来设置LUA的变量,这个变量可以直接在接下来的脚本中调用。
我们也可以直接通过默认属性读取当前 LUA 中的所有全局变量。

LUA 的删除命令就是将“变量、函数、表”设置为 nil,LUA会自动删除不再被引用的变量。

调用 LUA 中的函数

Using mylua As New Lua
    ' 先创建一个lua函数
    mylua.DoString(<string>
        function sub(val1,val2)
            return val1 - val2
        end
</string>)

    ' 调用该lua函数
    Dim func = TryCast(mylua("sub"), LuaFunction)
    'Dim func = mylua.GetFunction("sub")
    Dim ret = func.Call(5, 3).First()
    MsgBox(ret.GetType.ToString)
    MsgBox(ret)

End Using
我们先用LUA写入一个名为 sub 的函数,用于计算减法。(<string>这个语法为VB的多行文本,跟C#的@一样)
而在 dotNET 中调用LUA函数,可以直接通过默认属性获取到这个名为 subLuaFunction 对象。
调用它也非常方便,使用 LuaFunction.Call 即可,将需要的参数直接以数组传递进去。
注意:LuaFunction的返回值是数组,因为Lua的函数可以返回多个返回值,而我们只需要读取其第一个返回值即可。
如果运行没有出错,这个Lua函数会告诉我们 5-3=2。

将 dotNET 方法注册到 LUA 中

' 准备加入LUA中的方法
Public Shared Function myadd(ByVal i1 As Integer, ByVal i2 As Integer) As Integer
    Return i1 + i2
End Function

' 注册函数
Using mylua As New Lua
    ' 在Lua中的名字,实例对象(静态函数为NULL),方法
    mylua.RegisterFunction("vbadd", Nothing, GetType(Form1).GetMethod("myadd"))

    Dim ret = mylua.DoString("return vbadd(5,6)")(0)
    MsgBox(ret.GetType.ToString)
    MsgBox(ret)
End Using
将 DotNET 的方法(函数)注册到LUA中,我们需要使用RegisterFunction这个方法,参数需要:在LUA中的名称、方法的实例对象、方法原型。
如果你注册的是一个静态方法,则可以省略方法实例,如果不是的话就需要传递一个实例对象进去。
获取方法原型的方式也很简单,可以通过 GetType(类).GetMethod("方法名称") 或者 实例对象.GetType().GetMethod("方法名称") 来获取。

而在LUA中调用我们注册的方法就更简单了,使用你注册的方法名即可,用法与其他函数别无二致。
需要注意的是,请尽量使用LUA支持的数据类型作为返回值,否则可能会引发脚本错误。
(比如将示例中的32位整数Integer换成LUA所支持的64位整数Long会更适合,不过这都是小问题)

将 dotNET 类型注册到 LUA 中

' 一个测试类
Private Class testcls
    ' 类的方法
    Public Function myadd(ByVal i1 As Integer, ByVal i2 As Integer) As Integer
        Return i1 + i2
    End Function
    ' 类的属性
    Public Property myvalue As String = "一个中文字符串"
End Class
Dim tc As New testcls '测试类的实例

Using mylua As New Lua
    ' 先设置好编码,否则中文字符会乱码
    mylua.State.Encoding = Encoding.UTF8

    ' 将实例注册到Lua中
    mylua("testcls") = tc '直接将对象装入Lua的全局变量中

    ' 方法需要使用“冒号”来执行
    Dim ret1 = mylua.DoString("return testcls:myadd(3,4)")(0)
    ' 或者需要传递完整实例对象到映射的方法中
    'Dim ret1 = mylua.DoString("return testcls.myadd(testcls,3,4)")(0)
    MsgBox(ret1.GetType.ToString)
    MsgBox(ret1)

    ' 属性或字段直接使用“点”来获取
    Dim ret2 = mylua.DoString("return testcls.myvalue")(0)
    MsgBox(ret2.GetType.ToString)
    MsgBox(ret2)

    mylua.DoString("testcls.myvalue = '新的属性值'")
    ret2 = mylua.GetString("testcls.myvalue") '另一种获取变量的方法
    MsgBox(ret2.GetType.ToString)
    MsgBox(ret2)
    MsgBox(tc.myvalue) '实例的属性值也变化了
End Using
NLua 注册一个 dotNET 类就是如此简单,直接将实例赋值到默认属性中即可。

不过NLua会将实例方法注册为需要传递实例参数才可使用,因此调用结构与方法原型会不一样。
我们使用冒号:来调用函数,采用的是LUA的语法糖,表示调用的函数的第一个参数就是调用者自己,其语法结构为:
testcls:myadd(3,4) = testcls.myadd(testcls,3,4)
看出来NLua已经将我们的方法原型myadd(i1,i2)改变成了 myadd(实例,i1,i2)
而属性和字段则直接用点.来调用即可。

类中的静态方法是无法通过此方法注册和使用的,请使用RegisterFunction注册静态方法。

NLua 默认使用ANSI文本编码(并且是美区),所以无法直接显示其他语系的文字,最新版的NLua可以使用 NLua.State.Encoding 来调整编码。

比较老旧版本的只能手动将文本转化为UTF8数据集,再通过bytes传递给DoString函数。
我写了一个扩展方法DoUString,用于代替旧版本的NLua的DoString,新版不需要:
<Extension()>
Function DoUString(ByVal obj As NLua.Lua, ByVal chunk As String, Optional ByVal chunkName As String = "chunk") As Object()
    Return obj.DoString(Encoding.UTF8.GetBytes(chunk), chunkName)
End Function

<Extension()>
Function DoUString(ByVal obj As NLua.Lua, ByVal chunk As Byte(), Optional ByVal chunkName As String = "chunk", Optional ByVal enc As Encoding = Nothing) As Object()
    If enc Is Nothing Then enc = Encoding.Unicode
    Dim txt As String = enc.GetString(chunk)
    Dim bs As Byte() = Encoding.UTF8.GetBytes(txt)
    Return obj.DoString(bs, chunkName)
End Function

导入 dotNET 类库到 LUA 中

Using mylua As New Lua
    ' 先设置好编码,否则中文字符会乱码
    mylua.State.Encoding = Encoding.UTF8

    ' 要求加载CLR包
    mylua.LoadCLRPackage()

    ' 实例化 dotNET 对象
    mylua.DoString(<string>
        import ('System', 'System.Net')
        client = WebClient()
        client.Proxy = nil

        import ('mscorlib', 'System.Text')
        client.Encoding = Encoding.UTF8

        res = client:DownloadString('https://clso.fun/')
        --res = client.DownloadString(client,'https://clso.fun/')

        import('System.Windows.Forms')
        MessageBox.Show(res)
</string>)
End Using
-- WebClient 位于 System.dll 的 System.Net 命名空间下
import ('System', 'System.Net')
client = WebClient() --这里等于VB.NET的 client = new WebClient()

-- 将代理设置为空,会加快解析速度
client.Proxy = nil

-- 有需求也可自定义代理,不过某些版本的.NET只支持HTTP代理
-- WebProxy 与 WebClient 位于相同的类库和命名空间,所以不需要再次导入
-- 这里的 WebProxy 会自动实例化并设定初始参数
-- 等效于 client.Proxy = new WebProxy("127.0.0.1:1080")
client.Proxy = WebProxy("127.0.0.1:1080")

-- 调整网页默认编码,需要导入 mscorlib.dll 并定位到 System.Text 命名空间
import ('mscorlib', 'System.Text')
client.Encoding = Encoding.UTF8

-- 下载网页文本,下面两个调用方法等效,使用冒号更方便
res = client:DownloadString('https://clso.fun/')
res = client.DownloadString(client,'https://clso.fun/')

-- 弹窗类位于 System.Windows.Forms.dll 的 System.Windows.Forms 空间下
-- 两者名称相同,所以可以省略命名空间的声明
import('System.Windows.Forms')
MessageBox.Show(res) --静态方法用点来调用
先设置好文本编码,之后使用 mylua.LoadCLRPackage() 加载CLR包。
NLua 使用 import 关键字导入dotNET类库(不带.DLL后缀),后面还可以设置需要导入的命名空间。(如果省略,则自动导入与类库相同的命名空间)
此示例使用 WebClient 加载 clso.fun 的首页,并使用 MessageBox.Show 显示网页源码。
在LUA脚本中,使用 变量 = 类型(初始化参数) 来生成 .NET 类型的实例,注意在LUA中不使用new来实例化。
而创建的实例对象,可以直接使用.来访问字段和属性,使用:来调用方法(省去传递实例自身到参数的过程)。

类型的静态方法不能使用冒号调用,因为静态方法不需要实例参数。

虽然 NLua 可以很方便的调用 .NET 类库,不过这个方法有很多弊端,首先你会初始化许多不必要的类型与数据,可能会对全局变量的命名造成混乱和冲突,其次调用 .NET 库需要其处于全局GAC或者程序目录下,与脚本语言轻量便捷的属性相冲突,所以我非常不推荐直接调用 dotNET 的库。

如果你需要使用 dotNET 库中的功能,请将它手动注册到 LUA 的全局变量中!

总结

使用 NLua,我们可以为自己的 dotNET 程序快速添加 LUA 脚本支持。
不过一定要注意,不要过度导入 dotNET 类库,尽量只注册自己需要用到的模块。
轻便、敏捷,才是 LUA 最大的优点!!!