65.9K
CodeProject 正在变化。 阅读更多。
Home

NeoLua(用于 .NET 动态语言运行时的 Lua)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (47投票s)

2013年10月25日

Apache

10分钟阅读

viewsIcon

140660

downloadIcon

1608

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 的缺点

与 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 执行它。脚本可以有参数,例如示例中的 ab。每个脚本都可以返回值。脚本的返回值总是 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());
}

方法(宿主应用程序)

下一个示例定义了三个成员。

ab 是成员,它们持有普通的整数。而 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 脚本中通常存在的许多动态部分(因此脚本应该运行得很快)。

GlobalsTables 不能拥有类型化的动态成员,它们在实现上始终是 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。方法/函数返回 LuaOverloadedMethodLuaMethod。事件返回 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:

  • 更改:重新设计类型系统(LuaTypeLuaMethodLuaEvent
  • 添加:在可能的情况下进行严格解析
  • 添加:运算符支持
  • 添加:元表支持
  • 添加:更多单元测试
  • Bug 修复

0.8.2:

  • 添加:协同程序包
  • 添加:io 包

后续步骤

在接下来的几个月里,我将在引入新功能之前稳定语言。我希望收到很多错误报告。之后,我将研究 NeoLua 的调试/跟踪,可能借助调试包的支持。

© . All rights reserved.