Lua 解释器
一个 Lua 解释器用 C# 实现。它允许用 C# 编写 Lua 扩展,并在 Lua 代码中调用这些扩展。
Lua 简介
虽然我几年前就听说过 Lua,但我最近才决定学习这门语言,因为 Lua 进入了 TIOBE 编程社区指数的前 10 名。令我惊讶的是,Lua 几乎是一种理想的动态类型函数式语言,因为它简单、高效且强大。作为学习实践,我用 Lua 编写了一个程序来绘制任何带有两个变量的方程或不等式的图形。如果您是 Lua 新手,我强烈建议您阅读Programming in Lua这本书,并访问lua.org。
Lua 是一种可扩展的扩展语言。可扩展意味着库可以用 C 编写,并以自然的方式在 Lua 中访问。扩展意味着它可以嵌入到宿主应用程序中,以便用户可以对其进行编程。然而,由于以下两个原因,我想用 C# 编写一个库,并在 Lua 中调用该库
- 我不喜欢用 C/C++ 编程
- 没有哪个 Lua 库的功能可以与 .NET Framework 提供的功能相媲美
我没有找到现有的解决方案,所以我自己实现了一个 Lua 解释器。由于解释器是用 C# 编写的,因此 .NET 库可以在 Lua 代码中调用,前提是它已适当地封装在模块中。
解释器的实现
Lua 的语法使用解析表达式文法在 Lua.Grammar.txt 文件中定义。然后,给定语法文件作为输入,使用自制的解析器生成器来生成解析器代码。如果 lua 代码解析成功,则返回一个以 Chunk 作为根节点的语法树。然后,解释器根据 Lua 语义执行 Chunk。
大部分实现都很简单,与标准 Lua 的一点区别在于字符串是 unicode,并且库函数 string.format
使用与 C# 的 string.Format
相同的格式化语法。
项目代码编译成两个文件:lua.exe 和 wlua.exe,一个是命令行版本,另一个是 winform 版本。这是运行 test.lua 文件后的结果

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 文件更紧凑。
此示例的屏幕截图是

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)
账本表格
添加条目对话框
运行 Lua 代码文件的提示
第一种方法是在 Visual Studio 项目属性中,在“调试”选项卡页面中将文件名设置为命令行参数。
这样,可以通过执行解释器间接调试 lua 代码。
第二种方法是在 Windows 文件资源管理器中,将 .lua 文件拖到 lua.exe,它将启动 lua.exe 并传递 .lua 文件作为参数,.wlua 文件和 wlua.exe 也是如此。
第三种方法是在您发布软件时,将解释器与 lua 代码文件路径硬编码编译。
注意事项
该项目的可能用途是将 Lua 用作 .NET 应用程序的数据描述语言或脚本语言。目前,该代码仅足以进行演示,许多功能都不完整且未经测试,如果您想将 Lua 解释器包含在一个大型项目中,则可能需要自己完成。
历史
- 2011-07-19 首次发布
- 2012-09-22 修复解析“i == -1”中的错误
- 2012-09-23 修复解析 "t={f=function() end}" 中的错误