NeoLua(用于 .NET 动态语言运行时的 Lua)
NeoLua 将 Lua 实现为 .NET 动态语言。
引言
NeoLua 是 Lua 语言的一个完全从头开始实现的版本。目前,该实现处于 Lua 5.2 的水平(https://lua.ac.cn/manual/5.2/manual.html),并包含 Lua 5.3 的许多部分。目标是遵循 C-Lua 实现的参考,并将其与完整的 .NET Framework 支持相结合。这意味着应该可以轻松地从 Lua 调用 .NET 函数/类/接口/事件,并且应该可以轻松地从 .NET 语言(例如 C#、VB.NET 等)访问 Lua 的变量和函数。
NeoLua 用 C# 实现,并使用 动态语言运行时。目前,NeoLua 只有一个对 .NET Framework 4 的依赖,它也可以在当前的 Mono 框架下工作。
背景
NeoLua 的想法诞生是因为我需要在服务应用程序中使用 Lua 作为脚本语言。在第一个版本中,我使用了 LuaInterface。但事实证明,将 LuaInterface 作为 .NET 和 C-Lua 之间的桥梁正确使用非常困难。
我遇到了很多内存泄漏!在调试过程中,我发现我没有正确地取消引用 Lua 变量,并了解到要正确地做到这一点非常困难。
原则
NeoLua 的用途
- 将应用程序的逻辑外包给脚本
- 逻辑结构化
- 构建一个包含函数和变量的动态配置系统
- 作为公式解析器
- ...
因此,它可能是您的编译 .NET 应用程序或引擎(例如游戏引擎)的可靠伙伴。
我没有考虑到的
- 编译库
- 独立应用程序
NeoLua 的优点
- Lua 脚本与宿主应用程序/.NET 框架之间以及反向的动态访问。
- NeoLua 基于 DLR。因此,您将获得可收集且经过良好优化的编译代码。
- 可以编写严格的脚本。运行时速度类似于 C# 等。
- 它与 .NET 世界兼容(例如 C#、VB.NET、IronPython 等)。
- 完全轻松地访问 .NET 框架或您自己的库(无需存根代码)。
- 一个经过良好测试且速度非常快的 .NET Framework 垃圾回收器。
- 纯 IL(x86、x64 支持)
NeoLua 的缺点
- 它与 Lua 并非 100% 兼容。
- 无法部署预编译脚本。
与 C-Lua 桥接的缺点,这些缺点已通过 NeoLua 解决
- 使用 C-Lua,您必须处理两个内存管理器,因此必须在这两个世界之间封送所有数据。这需要时间,而且有很多陷阱会导致内存泄漏。
- C-Lua 解释它自己的字节码。代码没有编译成机器码。
Hello World
NeoLua 的入门非常简单。首先,您必须添加对 Neo.Lua-Assembly 的引用。有两种方法可以获取 NeoLua。第一,从 neolua.codeplex.com 下载。
第二种方法是使用 nuget 包。
接下来,创建 Neo.IronLua.Lua
(称为 Lua 脚本引擎)的实例,并获取一个新环境来执行 Lua 代码。
这是一个在调试输出中打印“Hello World!
”的示例
using Neo.IronLua;
namespace Test
{
public static class Program
{
public static void Main(string[] args)
{
// Create the Lua script engine
using (Lua l = new Lua())
{
// create a Lua script environment (global)
var g = l.CreateEnvironment();
// run a chunk, first the code, than the name of the code
g.DoChunk("print('Hello World!');", "test.lua");
}
}
}
}
玩转 NeoLua
LuaCmd 是一个交互式程序,用于测试 NeoLua。试试看。只需编写 Lua 代码,然后用空行执行它。
运行 Lua 片段
要运行一个小的片段,只需通过 DoChunk
执行它。脚本可以有参数,例如示例中的 a
和 b
。每个脚本都可以返回值。脚本的返回值总是 LuaResult
对象,因为在 Lua 中,可以返回多个结果。
using (Lua l = new Lua())
{
var g = l.CreateEnvironment();
var r = g.DoChunk("return a + b", "test.lua",
new KeyValuePair<string, object>("a", 2),
new KeyValuePair<string, object>("b", 4));
Console.WriteLine(r[0]);
}
因为 NeoLua 是一种动态语言,所以也有动态实现的方法。LuaResult
也有一个动态接口。
using (Lua l = new Lua())
{
dynamic g = l.CreateEnvironment();
dynamic dr = dg.dochunk("return a + b", "test.lua", "a", 2, "b", 4);
Console.WriteLine((int)dr);
}
值和类型
NeoLua 不是一种动态类型语言,它只是看起来像。变量总是有类型(至少是 System.Object
)。
NeoLua 支持所有 CLR 类型。如果需要类型转换,它会自动完成。还支持动态类型。
local a = "5"; -- a string is assigned to a local variable of the type object
local b = {}; -- object assigned with an empty table
b.c = a + 8; -- the variable "a" is converted into an integer and assigned to the dynamic member of a table
NeoLua 的一个很好的特性是严格类型。但在后面的部分会有更多介绍。
使用全局变量
环境是一个特殊的 Lua 表。在环境中设置或获取变量,它们可以在脚本代码中作为全局变量访问。
using (Lua l = new Lua())
{
var g = l.CreateEnvironment();
dynamic dg = g;
dg.a = 2; // dynamic way to set a variable
g["b"] = 4; // second way to access variable
g.DoChunk("c = a + b", "test.lua");
Console.WriteLine(dg.c);
Console.WriteLine(g["c"]);
}
使用 LuaTable
NeoLua 的实现支持动态类 LuaTable
。脚本代码按原样使用此类。接下来的示例希望能展示这个想法。
成员
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.t = new LuaTable();
dg.t.a = 2;
dg.t.b = 4;
dg.dochunk("t.c = t.a + t.b", "test.lua");
Console.WriteLine(dg.t.c);
}
索引访问
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.t = new LuaTable();
dg.t[0] = 2;
dg.t[1] = 4;
dg.dochunk("t[2] = t[0] + t[1]", "test.lua");
Console.WriteLine(dg.t[2]);
}
函数
定义函数感觉很自然。它基本上是一个委托,分配给一个成员。
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.myadd = new Func<int, int, int>((a, b) => a + b); // define a new function in c#
dg.dochunk("function Add(a, b) return myadd(a, b) end;", "test.lua"); // define a new function in lua that calls the C# function
Console.WriteLine((int)dg.Add(2, 4)); //call the Lua function
var f = (Func<object,>)dg.Add;// get the Lua function
Console.WriteLine(f(2, 4).ToInt32());
}
方法(宿主应用程序)
下一个示例定义了三个成员。
a
、b
是成员,它们持有普通的整数。而 add
持有一个函数,但这个定义不是一个方法。因为如果你尝试像 C# 中那样调用这个函数作为一个方法,它会抛出一个 NullReferenceException
。你必须总是将表作为第一个参数传递给它。要声明一个真正的方法,你必须调用显式方法 DefineMethod
。Lua 不关心这个区别,但 C# 或 VB.NET 会关心。
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.t = new LuaTable();
dg.t.a = 2;
dg.t.b = 4;
dg.t.add = new Func<dynamic, int>(self =>
{
return self.a + self.b;
});
((LuaTable)dg.t).DefineMethod("add2", (Delegate)dg.t.add);
Console.WriteLine(dg.dochunk("return t:add()", "test.lua")[0]);
Console.WriteLine(dg.dochunk("return t:add2()", "test.lua")[0]);
Console.WriteLine(dg.t.add(dg.t));
Console.WriteLine(dg.t.add2());
}
方法(Lua)
add
是一个普通函数,由委托创建。add1
被声明为一个函数。add2
是一个由委托创建的方法。请注意,委托定义对方法的概念一无所知,因此您必须声明 self 参数。add3
展示了方法声明。
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
LuaResult r = dg.dochunk("t = { a = 2, b = 4 };" +
"t.add = function(self)" +
" return self.a + self.b;" +
"end;" +
"function t.add1(self)" +
" return self.a + self.b;" +
"end;" +
"t:add2 = function (self)" +
" return self.a + self.b;" +
"end;" +
"function t:add3()" +
" return self.a + self.b;" +
"end;" +
"return t:add(), t:add2(), t:add3(), t.add(t), t.add2(t), t.add3(t);",
"test.lua");
Console.WriteLine(r[0]);
Console.WriteLine(r[1]);
Console.WriteLine(r[2]);
Console.WriteLine(r[3]);
Console.WriteLine(r[4]);
Console.WriteLine(r[5]);
Console.WriteLine(dg.t.add(dg.t)[0]);
Console.WriteLine(dg.t.add2()[0]);
Console.WriteLine(dg.t.add3()[0]);
}
元表
如果您在表上定义元表,您也可以在 C# 等宿主语言中使用它。
using (Lua l = new Lua())
{
dynamic g = l.CreateEnvironment();
dynamic r = g.dochunk(String.Join(Environment.NewLine,
"tm = {};",
"tm.__add = function (t, a) return t.n + a; end;",
"t = { __metatable = tm, n = 4 };",
"return t + 2"));
LuaTable t = g.t;
Console.WriteLine((int)r);
Console.WriteLine(t + 2);
}
反之亦然。
using (Lua l = new Lua())
{
dynamic g = l.CreateEnvironment();
LuaTable t = new LuaTable();
t["n"] = 4;
t.MetaTable = new LuaTable();
t.MetaTable["__add"] = new Func<LuaTable, int, int>((_t, a) => (int)(_t["n"]) + a);
g.t = t;
dynamic r = g.dochunk("return t + 2");
Console.WriteLine((int)r);
Console.WriteLine(t + 2);
}
NeoLua 不支持在 userdata 上设置元表,因为没有 userdata 的定义。但是 NeoLua 使用类型的运算符定义。结果是一样的。
public class ClrClass
{
private int n = 4;
public static int operator +(ClrClass c, int a)
{
return c.n + a;
}
}
using (Lua l = new Lua())
{
dynamic g = l.CreateEnvironment();
g.c = new ClrClass();
Console.WriteLine((int)g.dochunk("return c + 2;"));
}
类/对象
要创建一个类,您必须编写一个创建新对象的函数。该函数根据定义是类,函数的结果是对象。
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.dochunk("function classA()" +
" local c = { sum = 0 };" +
" function c:add(a)" +
" self.sum = self.sum + a;" +
" end;" +
" return c;" +
"end;", "classA.lua");
dynamic o = dg.classA()[0];
o.add(1);
o.add(2);
o.add(3);
Console.WriteLine(o.sum);
}
显式类型
NeoLua 的一个很好的特性是它使用了严格类型。这意味着您可以为局部变量或参数指定严格类型(就像 C# 中一样)。此功能的一个巨大优势是解析器可以在编译时执行大量操作。这将缩短 NeoLua 脚本中通常存在的许多动态部分(因此脚本应该运行得很快)。
Globals
和 Tables
不能拥有类型化的动态成员,它们在实现上始终是 object
类型。一个例外是在继承自 LuaTable
的 .net 类中的显式声明。
没有显式类型的示例
// create a new instance of StringBuilder
local sb = clr.System.Text.StringBuilder();
sb:Append('Hello '):Append('World!');
return sb:ToString();
脚本将被翻译成类似这样的东西
object sb = InvokeConstructor(clr["System"]
["Text"]["StringBuilder"], new object[0]);
InvokeMethod(InvokeMethod(sb, "Append", new string[]
{ "Hello " }), "Append", new string[]{ "World!"});
return InvokeMethod(sb, "ToString", new object[0]);
此代码并非实际执行的精确代码,实际上它要复杂一些,但效率更高(例如,Append
在运行时只会解析一次,而不是像此示例中那样解析两次)。如果您对如何实现此功能感兴趣,我建议阅读 DLR 的文档。
相同示例,使用严格类型
// define a shortcut to a type, this line is only seen by the parser
const StringBuilder typeof clr.System.Text.StringBuilder;
// create a new instance of StringBuilder
local sb : StringBuilder = StringBuilder();
sb:Append('Hello '):Append('World!');
return sb:ToString();
此脚本的编译方式与 C# 等语言相同
StringBuilder sb = new StringBuilder();
sb.Append(("Hello (").Append(("World!(");
return sb.ToString();
类型对于创建具有严格签名的函数非常有用。
using (Lua l = new Lua())
{
dynamic dg = l.CreateEnvironment();
dg.dochunk("function Add(a : int, b : int) : int return a + b; end;", "test.lua");
Console.WriteLine((int)dg.Add(2, 4)); // the cast is needed because of the dynamic call
var f = (Func<int,int,int>)dg.Add;
Console.WriteLine(f(2, 4));
}
编译代码
在 NeoLua 中执行脚本总是意味着它将被编译,然后结果将被执行。如果结果例如是一个函数,并且您调用该函数,则不会进行解释。
如果您想多次使用脚本,请使用 CompileChunk
预编译脚本代码,并在不同的环境中运行它。这可以节省编译时间和内存。
using (Lua l = new Lua())
{
LuaChunk c = l.CompileChunk("return a;", "test.lua", false);
var g1 = l.CreateEnvironment();
var g2 = l.CreateEnvironment();
g1["a"] = 2;
g2["a"] = 4;
Console.WriteLine((int)(g1.DoChunk(c)[0]) + (int)(g2.DoChunk(c)[0]));
}
CompileChunk
函数的第三个参数决定 NeoLua 是否应创建动态函数 (false
) 或运行时函数 (true
)。动态函数可用于垃圾回收,但不可调试。它们是小代码块最高效的选择。运行时函数在动态程序集中的动态类型中编译。当脚本引擎 (Lua-Class) 被处置时,程序集/类型/函数将被丢弃。运行时函数是脚本的好选择——它们很大、经常使用并且需要调试信息。
Lambda 支持
此函数生成的委托没有调试信息,也没有环境。不允许在代码中使用全局变量或 Lua 函数/库。只能使用 CLR 包。它总是作为动态函数编译。
using (Lua l = new Lua())
{
var f = l.CreateLambda<Func<double, double>>("f", "return clr.System.Math:Abs(x) * 2", "x");
Console.WriteLine("f({0}) = {1}", 2, f(2));
Console.WriteLine("f({0}) = {1}", 2, f(-2));
}
.NET
要访问 .NET Framework 类,有一个名为 clr
的包。此包引用命名空间和类。
此示例演示如何调用 static String.Concat
函数
function a()
return 'Hello ', 'World', '!';
end;
return clr.System.String:Concat(a());
clr
背后是 LuaType
类,它尝试以最有效的方式访问类、接口、方法、事件和成员。
-- Load Forms
clr.System.Reflection.Assembly:Load("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
local Forms = clr.System.Windows.Forms; -- create a local variable with a namespace
local iClicked : int = 0;
Forms.Application:EnableVisualStyles(); -- call a static method
do (frm, cmd = Forms.Form(), Forms.Button()) -- create a from and button in a using block
frm.Text = 'Hallo Welt!';
cmd.Text = 'Click';
cmd.Left = 16;
cmd.Top = 16;
cmd.Click:add( -- add a event the counter part is 'remove'
function (sender, e) : void
iClicked = iClicked + 1;
Forms.MessageBox:Show(frm, clr.System.String:Format('Clicked {0:N0} times!', iClicked), 'Lua', Forms.MessageBoxButtons.OK, Forms.MessageBoxIcon.Information);
end);
frm.Controls:Add(cmd);
Forms.Application:Run(frm);
end;
一个很好的例子,展示了什么可能
local writeLine = clr.System.Console.WriteLine;
writeLine('Calc:');
writeLine('{0} + {1} = {2}', 2, 4, 6);
writeLine();
但更高效的方法是使用成员调用 clr.System.Console:WriteLine()
。这是因为 NeoLua 会创建一个 LuaOverloadedMethod
类的实例来管理上述示例中的方法。
当您在非 .NET 成员上使用 getmember (.)
时,您应该始终记住这一点。类、接口返回 LuaType
。方法/函数返回 LuaOverloadedMethod
或 LuaMethod
。事件返回 LuaEvent
。
复杂示例
此示例读取 Lua 脚本文件并执行一次。该示例展示了如何编译脚本并捕获异常以及如何检索堆栈跟踪。只有在脚本以调试信息编译时才能检索堆栈跟踪。要打开此开关,请将 CompileChunk
的第二个参数设置为 true
。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
namespace Neo.IronLua
{
public static class Program
{
public static void Main(string[] args)
{
using (Lua l = new Lua())
{
// create an environment that is associated with the Lua scripts
dynamic g = l.CreateEnvironment();
// register new functions
g.print = new Action<object[]>(Print);
g.read = new Func<string,>(Read);
foreach (string c in args)
{
using (LuaChunk chunk = l.CompileChunk(c, true)) // compile the script with
// debug information which is needed for a complete stack trace
try
{
object[] r = g.dochunk(chunk); // execute the chunk
if (r != null && r.Length > 0)
{
Console.WriteLine(new string('=', 79));
for (int i = 0; i < r.Length; i++)
Console.WriteLine("[{0}] = {1}", i, r[i]);
}
}
catch (TargetInvocationException e)
{
Console.ForegroundColor = ConsoleColor.DarkRed;
Console.WriteLine("Exception: {0}", e.InnerException.Message);
LuaExceptionData d = LuaExceptionData.GetData(e.InnerException); // get stack trace
Console.WriteLine("StackTrace: {0}", d.GetStackTrace(0, false));
Console.ForegroundColor = ConsoleColor.Gray;
}
}
}
} // Main
private static void Print(object[] texts)
{
foreach (object o in texts)
Console.Write(o);
Console.WriteLine();
} // proc Print
private static string Read(string sLabel)
{
Console.Write(sLabel);
Console.Write(": ");
return Console.ReadLine();
} // func Read
}
}
Lua 脚本如下
local a, b = tonumber(read("a")), tonumber(read("b"));
function PrintResult(o, op)
print(o .. ' = ' .. a .. op .. b);
end;
PrintResult(a + b, " + ");
PrintResult(a - b, " - ");
PrintResult(a * b, " * ");
PrintResult(a / b, " / ");
性能
我将性能与 LuaInterface
项目进行了比较。
未预编译
Empty : LuaInterface 0,7 ms NeoLua 2,0 ms 0,358
Sum : LuaInterface 0,2 ms NeoLua 0,3 ms 0,581
Sum_strict : LuaInterface 0,3 ms NeoLua 0,0 ms 12,500
Sum_echo : LuaInterface 2,2 ms NeoLua 1,4 ms 1,586
String : LuaInterface 2,0 ms NeoLua 0,2 ms 11,941
String_echo : LuaInterface 5,0 ms NeoLua 2,0 ms 2,518
Delegate : LuaInterface 2,1 ms NeoLua 1,0 ms 2,099
StringBuilder : LuaInterface 3,5 ms NeoLua 0,4 ms 8,262
CallStd : LuaInterface 4,0 ms NeoLua 4,2 ms 0,952
TableIntSet : LuaInterface 1,8 ms NeoLua 2,4 ms 0,721
TableIntGet : LuaInterface 96,3 ms NeoLua 8,7 ms 11,021
TableVsList : LuaInterface 2,8 ms NeoLua 7,5 ms 0,373
TableString : LuaInterface 34,4 ms NeoLua 36,5 ms 0,941
该表显示,NeoLua 在与 .NET Framework 交互越复杂、越频繁时,相对于编译开销而言,其优势越明显。
下表显示了预编译脚本的结果。
预编译
Empty : LuaInterface 0,0 ms NeoLua 0,0 ms NaN
Sum : LuaInterface 0,0 ms NeoLua 0,1 ms 0,333
Sum_strict : LuaInterface 0,0 ms NeoLua 0,0 ms NaN
Sum_echo : LuaInterface 1,6 ms NeoLua 0,3 ms 4,727
String : LuaInterface 1,2 ms NeoLua 0,6 ms 2,000
String_echo : LuaInterface 3,8 ms NeoLua 0,7 ms 5,846
Delegate : LuaInterface 1,5 ms NeoLua 0,3 ms 5,654
StringBuilder : LuaInterface 2,8 ms NeoLua 0,0 ms 275,00
CallStd : LuaInterface 3,3 ms NeoLua 3,3 ms 1,006
TableIntSet : LuaInterface 1,2 ms NeoLua 1,7 ms 0,704
TableIntGet : LuaInterface 94,1 ms NeoLua 3,9 ms 23,876
TableVsList : LuaInterface 2,3 ms NeoLua 1,9 ms 1,214
TableString : LuaInterface 30,4 ms NeoLua 33,7 ms 0,903
预编译(调试)
Empty : LuaInterface 0,0 ms NeoLua 0,0 ms NaN
Sum : LuaInterface 0,0 ms NeoLua 0,1 ms 0,429
Sum_strict : LuaInterface 0,0 ms NeoLua 0,0 ms NaN
Sum_echo : LuaInterface 1,5 ms NeoLua 0,4 ms 4,108
String : LuaInterface 1,2 ms NeoLua 0,4 ms 3,105
String_echo : LuaInterface 4,0 ms NeoLua 0,7 ms 5,812
Delegate : LuaInterface 1,5 ms NeoLua 0,3 ms 5,179
StringBuilder : LuaInterface 2,7 ms NeoLua 0,0 ms 90,667
CallStd : LuaInterface 3,3 ms NeoLua 3,2 ms 1,022
TableIntSet : LuaInterface 1,2 ms NeoLua 1,8 ms 0,670
TableIntGet : LuaInterface 94,6 ms NeoLua 4,3 ms 22,056
TableVsList : LuaInterface 2,2 ms NeoLua 2,1 ms 1,077
TableString : LuaInterface 30,8 ms NeoLua 34,1 ms 0,903
这些值表明 NeoLua 在重复性任务和与框架交互方面表现出色。例如,在游戏引擎或服务应用程序中,这些特性应该很有趣。
未实现的部分
并非所有 Lua 功能都已实现,有些部分可能永远不会实现。但是,Lua 参考的大部分内容都可以正常工作。
未来可能实现的功能
双精度浮点数的十六进制表示- 缺少的运行时部分
调试(困难)
最不可能实现的功能
使用 Upvalue- Lua 字节码支持
资源
NeoLua 托管在 CodePlex 上:neolua.codeplex.com。您可以在那里找到发布版本和论坛。
源代码托管在 GitHub 上:https://github.com/neolithos/neolua
变更
0.9.9:
- 新增:操纵闭包/upvalue
- 新增:TraceLine 调试
- 新增:数组初始化器
- 新增:支持扩展方法
- 更改:块可以在 LuaTable 上运行
- 更改:Lua 表的完全重写(速度、大小、兼容性)
- 更改:重新设计 c#/vb.net/lua 运算符
- 更改:RtInvoke 现在可以调用所有可调用的内容
- 更改:算术修正(与 Lua 不完全兼容,但与 .net 兼容)
- 更改:Lua 表的类部分
0.8.18:
- 新增:重写表包
- 新增:词法分析器现已公开
- Bug 修复
0.8.17:
- 添加:require
- Bug 修复
0.8.12:
- Bug 修复
0.8.9:
- 更改:重新设计类型系统(
LuaType
、LuaMethod
、LuaEvent
) - 添加:在可能的情况下进行严格解析
- 添加:运算符支持
- 添加:元表支持
- 添加:更多单元测试
- Bug 修复
0.8.2:
- 添加:协同程序包
- 添加:io 包
后续步骤
在接下来的几个月里,我将在引入新功能之前稳定语言。我希望收到很多错误报告。之后,我将研究 NeoLua 的调试/跟踪,可能借助调试包的支持。