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

Lua 解释器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (32投票s)

2011 年 7 月 19 日

MIT

4分钟阅读

viewsIcon

104140

downloadIcon

3002

一个 Lua 解释器用 C# 实现。它允许用 C# 编写 Lua 扩展,并在 Lua 代码中调用这些扩展。

Lua 简介

虽然我几年前就听说过 Lua,但我最近才决定学习这门语言,因为 Lua 进入了 TIOBE 编程社区指数的前 10 名。令我惊讶的是,Lua 几乎是一种理想的动态类型函数式语言,因为它简单、高效且强大。作为学习实践,我用 Lua 编写了一个程序来绘制任何带有两个变量的方程或不等式的图形。如果您是 Lua 新手,我强烈建议您阅读Programming in Lua这本书,并访问lua.org

Lua 是一种可扩展的扩展语言。可扩展意味着库可以用 C 编写,并以自然的方式在 Lua 中访问。扩展意味着它可以嵌入到宿主应用程序中,以便用户可以对其进行编程。然而,由于以下两个原因,我想用 C# 编写一个库,并在 Lua 中调用该库

  1. 我不喜欢用 C/C++ 编程
  2. 没有哪个 Lua 库的功能可以与 .NET Framework 提供的功能相媲美

我没有找到现有的解决方案,所以我自己实现了一个 Lua 解释器。由于解释器是用 C# 编写的,因此 .NET 库可以在 Lua 代码中调用,前提是它已适当地封装在模块中。

解释器的实现

Lua 的语法使用解析表达式文法Lua.Grammar.txt 文件中定义。然后,给定语法文件作为输入,使用自制的解析器生成器来生成解析器代码。如果 lua 代码解析成功,则返回一个以 Chunk 作为根节点的语法树。然后,解释器根据 Lua 语义执行 Chunk。

大部分实现都很简单,与标准 Lua 的一点区别在于字符串是 unicode,并且库函数 string.format 使用与 C# 的 string.Format 相同的格式化语法。

项目代码编译成两个文件:lua.exewlua.exe,一个是命令行版本,另一个是 winform 版本。这是运行 test.lua 文件后的结果

LuaInterpreter/luatest.png

Windows Forms 库

作为概念证明,我编写了一个模块来使用 Windows Forms 创建 UI。阅读 WinFormLib.cs 中的代码后,您就会明白为什么 Lua 是一种神奇的语言,元表机制比最初想象的还要强大。该模块命名为“Gui”,可以使用相同的 .NET 类型、方法、属性名称来操作控件。以下是 WinFormExample.wlua 中的一个示例程序

form = Gui.Form{
    Text="Main Form", Height=200, StartPosition="CenterScreen",
    Gui.Label{ Text="Hello!", Name="lable", Width=80, Height=17, Top=9, Left=12 },
    Gui.Button{ Text="Click", Width=80, Height=23, Top=30, Left=12,
        Click=function(sender,e) Gui.ShowMessage(lable.Text,"Clicked") end },
}

Gui.Run(form)

Gui.ControlTypeName 返回一个 lua 函数来创建控件的实例,该函数接受一个 lua 表作为其参数,表中的键值对用于将值设置为控件属性,表中的数组项被添加为控件的子控件或子项。一个特别的事情是,当设置 Name 属性时,会为控件创建一个全局变量。如您所见,作为数据描述语言,Lua 比等效的 XAML 文件更紧凑。

此示例的屏幕截图是

LuaInterpreter/Example.png

Ledger.wlua 文件包含一个更完整和实用的示例,它可以添加和删除账本条目并保存到文件,保存的文件可以稍后打开。这是代码

form = Gui.Form{
    Text="Ledger Sheet", Width=700, Height=500, StartPosition="CenterScreen",
    Gui.SplitContainer {
        Dock="Fill", Width=700, SplitterDistance=200,
        Gui.TreeView{ Name="treeviewCategory", Dock="Fill", HideSelection=false },
        Gui.Panel{
            Dock="Fill",
            Gui.ListView{ 
                Name="listviewEntries", Dock="Fill", View="Details", 
			GridLines=true, FullRowSelect=true,
                ContextMenuStrip=Gui.ContextMenuStrip {
                    Gui.ToolStripMenuItem { Text="Delete", 
			Click=function(sender,e) DeleteEntry() end }
                }
            },
            Gui.StatusStrip { Name="statusStrip", Dock="Bottom" }
        }
    },
    Gui.ToolStrip{
        Dock="Top", Top=0, Left=0, Width=700, Height=25,
        Gui.ToolStripButton { Name="btnOpen", Text="&Open", 
				Width=88, Height=22, Image="icon\\open.png" },
        Gui.ToolStripButton { Name="btnSave", Text="&Save", 
				Width=88, Height=22, Image="icon\\save.png" },
        Gui.ToolStripButton { Name="btnAdd", Text="&Add", 
				Width=88, Height=22, Image="icon\\add.png" },
    }
}

incomeNode = treeviewCategory.Nodes.Add("Income")
outgoNode = treeviewCategory.Nodes.Add("Outgo")
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Date", Width=100 })
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Detail", Width=260 })
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Amount", Width=120 })

dialog = Gui.Form{
    Text="Add Entry", Width=320, Height=220, StartPosition="CenterParent", 
		FormBorderStyle="FixedDialog", ShowInTaskbar = false;
    Gui.Label{ Text="Subject:", Width=60, Height=17, Top=14, Left=12 },
    Gui.RadioButton { Name="dialog_Income", Text="Income", 
				Width=80, Height=20, Top=9, Left=80 },
    Gui.RadioButton { Name="dialog_Outgo", Text="Outgo", 
				Width=80, Height=20, Top=9, Left=160, Checked=true },
    Gui.Label{ Text="Category:", Width=60, Height=17, Top=40, Left=12 },
    Gui.ComboBox { Name="dialog_Category", Width=160, Height=20, Top=36, Left=80 },
    Gui.Label{ Text="Detail:", Width=60, Height=17, Top=68, Left=12 },
    Gui.TextBox { Name="dialog_Detail", Width=160, Height=20, Top=64, Left=80 },
    Gui.Label{ Text="Amount:", Width=60, Height=17, Top=96, Left=12 },
    Gui.TextBox { Name="dialog_Amount", Width=128, Height=20, Top=92, Left=80 },
    Gui.Label{ Text="Date:", Width=60, Height=17, Top=128, Left=12 },
    Gui.DateTimePicker { Name="dialog_Date", Width=128, 
				Height=21, Top=124, Left=80, Format="Short" },
    Gui.Button{ Text="OK", Name="dialog_btnOK", Width=80, 
				Height=23, Top=156, Left=130, DialogResult="OK" },
    Gui.Button{ Text="Cancel", Name="dialog_btnCancel", Width=80, 
				Height=23, Top=156, Left=224, DialogResult="Cancel" },
    AcceptButton=dialog_btnOK,
    CancelButton=dialog_btnCancel
}

Entries = {}

btnAdd.Click = function (sender,e)
    dialog_Detail.Text = ""
    dialog_Amount.Text = ""
    if treeviewCategory.SelectedNode ~= nil and 
			treeviewCategory.SelectedNode.Tag ~= nil then
        dialog_Category.Text = treeviewCategory.SelectedNode.Text
    end
    if dialog.ShowDialog(form) == "OK" then
        local subject = dialog_Income.Checked and "income" or "outgo"
        local category = dialog_Category.Text
        local detail = dialog_Detail.Text
        local amount = dialog_Amount.Text
        local date = dialog_Date.Value.ToShortDateString()
        local entry = {date, subject, category, detail, amount}
        table.insert(Entries, entry)
        local categoryNode = UpdateCategoryTree(entry)
        if treeviewCategory.SelectedNode == categoryNode then
            AddEntryToListView(entry)
        else
            treeviewCategory.SelectedNode = categoryNode
        end
    end
end

function FindCategoryNode(entry)
    local subject = entry[2]
    local category = entry[3]
    local subjectNode = subject == "outgo" and outgoNode or incomeNode
    local subNodes = subjectNode.Nodes.Find(category, false)
    if #subNodes == 0 then
        return nil, subjectNode
    else
        return subNodes[1], subjectNode
    end
end

function UpdateCategoryTree(entry)
    local categoryNode, subjectNode = FindCategoryNode(entry)
    if categoryNode == nil then
        local category = entry[3]
        categoryNode = subjectNode.Nodes.Add(category, category)
        categoryNode.Tag = {}
        dialog_Category.Items.Add(category)
    end
    table.insert(categoryNode.Tag, entry)
    return categoryNode
end

treeviewCategory.AfterSelect = function (sender, e)
    local entries = treeviewCategory.SelectedNode.Tag
    if entries ~= nil then
        listviewEntries.Items.Clear()
        for _,entry in ipairs(entries) do
            AddEntryToListView(entry)
        end
    end
end

function AddEntryToListView(entry)
    local item = Gui.ListViewItem{ Text=entry[1] }
    item.SubItems.Add(entry[4])
    item.SubItems.Add(entry[5])
    item.Tag = entry
    listviewEntries.Items.Add(item)
end

function DeleteEntry()
    local item = listviewEntries.SelectedItems[1]
    local entry = item.Tag
    if entry ~= nil then
        local categoryNode = FindCategoryNode(entry)
        table.removeitem(categoryNode.Tag, entry)
        table.removeitem(Entries, entry)
        listviewEntries.Items.Remove(item)
    end
end

btnSave.Click = function (sender, e)
    local sfd = Gui.SaveFileDialog{ Title="Save data", Filter="data file(*.dat)|*.dat" }
    if sfd.ShowDialog() == "OK" then
        local file = io.open(sfd.FileName, "w")
        file:write("Entries = {\r\n")
        for _,entry in ipairs(Entries) do
            file:write('{"', table.concat(entry, '", "'), '"},\r\n')
        end
        file:write('}')
        file:close()
    end
end

btnOpen.Click = function (sender, e)
    local ofd = Gui.OpenFileDialog{ Title="Open data file", 
			Filter="data file(*.dat)|*.dat" }
    if ofd.ShowDialog() == "OK" then
        dofile(ofd.FileName)
        incomeNode.Nodes.Clear()
        outgoNode.Nodes.Clear()
        dialog_Category.Items.Clear()
        for _,entry in ipairs(Entries) do
            UpdateCategoryTree(entry)
        end
        treeviewCategory.ExpandAll()
        listviewEntries.Items.Clear()
    end
end

Gui.Run(form)

账本表格

LuaInterpreter/ledger.png

添加条目对话框

LuaInterpreter/addentry.png

运行 Lua 代码文件的提示

第一种方法是在 Visual Studio 项目属性中,在“调试”选项卡页面中将文件名设置为命令行参数。

这样,可以通过执行解释器间接调试 lua 代码。

LuaInterpreter/StartOptions.png

第二种方法是在 Windows 文件资源管理器中,将 .lua 文件拖到 lua.exe,它将启动 lua.exe 并传递 .lua 文件作为参数,.wlua 文件和 wlua.exe 也是如此。

LuaInterpreter/DragOpen.png

第三种方法是在您发布软件时,将解释器与 lua 代码文件路径硬编码编译。

注意事项

该项目的可能用途是将 Lua 用作 .NET 应用程序的数据描述语言或脚本语言。目前,该代码仅足以进行演示,许多功能都不完整且未经测试,如果您想将 Lua 解释器包含在一个大型项目中,则可能需要自己完成。

历史

  • 2011-07-19 首次发布
  • 2012-09-22 修复解析“i == -1”中的错误 
  • 2012-09-23 修复解析 "t={f=function() end}" 中的错误 
© . All rights reserved.