🌙 前言
开始之前先记录一下与lua的结缘过程:
- Noita的法术编程系统是用lua写的,早就想从源码层面研究一下noita了
- 上学期cpp课老师建议我用lua给我的小游戏写个引擎
- lua是葡萄牙语中"月亮"的意思,对美丽的名字没有任何抵抗力!
- 听说了一些诸如某初中生在 nspire 上用lua 写东西的故事
总而言之,既c和cpp之后第三门认真学的语言(是的到现在py、js、java、html、asm还没认真学过)就这么愉快的决定了
环境
在线运行环境:可以在网页里运行
把.exe文件所在目录添加到环境变量里即可,lua对环境的要求很低
资料
语法
Hello world
print("hello world")
简洁明了
变量声明
赋值就是声明,默认为全局变量,加上local关键字变为局部变量
一切没声明的变量都是nil
支持多重赋值:
a,b = 1 ,2
数据类型
- number: lua的数字只有一个类型,为c里的double,支持16进制表示、科学计数法、左移右移运算
- string: 单/双引号之间的就是字符串,支持转义字符。多行文本可以用
[[string]]表示,会保存换行。- 连接字符串使用
..运算符 tostringtonumber函数,没有报错只有返回nil#表示获取长度
- 连接字符串使用
使用函数type可获取一个值对应的类型名称
函数
- 声明函数的关键字
function - 作用域用
end标识,很有特色 - 返回值默认是nil, 参数没传值也默认成nil
- 可以有多个返回值
function a(b,c)
local i = 1 + b
local j = "deadbeef" .. c
return i,j
end
c = 1
d = "a"
e,f = a(c,d)
print(e)
print(f)
可以用…表示不定长参数
function f1(...)
local a,b,c = ...
return ...
end
数组
数组这个名字实在是配不上lua的table,因为这是一种用起来极为强大的数据结构,比vector、array等都强太多了
table用大括号声明,不同元素用","分隔,可以存任何类型的数据,包括函数
索引从1开始
同样可以用#获取长度,值为nil与不存在无异
插入:table.insert(tableName, (position), target)
移除:table.remove(tableName, position) 返回值是对应位置的值
可以自由的调整table大小
如果存的是键值对,那table就是map。不符合命名规范的键可以用[]完成转义。同样,所有未定义的元素都是nil,同样一个值变成了nil就等同于未定义
在循环中提到了多种遍历方法
local table = {1, "hello", true, function() print("这是一个函数") end}
-- 访问元素
print(fruits[1]) -- 输出: apple
print(fruits[2]) -- 输出: banana
-- 获取长度
print(#fruits) -- 输出: 3
-- 键值对形式(作为map使用)
local table2 = {
name = "this is a name",
[01101001001] = 110011100110
}
print(table2.name) -- 输出: this is a name
print(table2[01101001001]) -- 输出: 110011100110.0
lua的表是引用类型,无论用哪个变量修改表内容,都会影响原表:
a = {1,2,3}
b = a -- b 和 a 指向同一张表
b[1] = 10
print(a[1]) -- 输出 10
全局表
所有的全局变量都以键值对的形式存在一个大表里,即全局表_G
判断语句
真假为 true 和 false
不等于是 ~=
支持and or not逻辑运算
重要:只有nil和false代表假,其余所有包括0都表示真
分支判断格式:
if 1 <= 10 then
print("1 <= 10")
elseif 1 > 10 then
print(" 1 > 10")
else
print("no")
end
循环
for循环,break退出循环
-- 数值循环:从1到10,步长为2
for i = 1, 10, 2 do
print(i)
end
对于遍历表,可以有更简单的写法:
t = {a = 10, b = 20, 1, 2, 3}
for k, v in pairs(t) do
print(k, v)
end
-- 输出可能是:
-- 1 1
-- 2 2
-- 3 3
-- a 10
-- b 20
严格按照索引顺序输出,用ipairs,遇到nil就会停止:
t = {10, 20, 30, a = 100}
for i, v in ipairs(t) do
print(i, v)
end
-- 输出:
-- 1 10
-- 2 20
-- 3 30
跨文件操作
dofile
相当于把一个lua文件搬过来,全局变量也会生效,并在新文件执行时跟着执行
loadfile
把文件里的内容看做是一个函数,可在新文件里被调用
require
获取文件里的内容,不会污染当前作用域,通过变量调用里面的变量或方法,在某些沙盒环境下不可用
元表(metatables)
用来"自定义行为"。每个table都有一个metatable,有点像默认函数,未定义则使用lua的默认实现
部分常用的元表方法包括:
__add(加法操作)
__call(表作为函数调用)
__div(除法操作)
__eq(等于比较)
__index(访问不存在的键)
__le(小于等于比较)
__lt(小于比较)
__mod(取余操作)
__mul(乘法操作)
__newindex(赋值给不存在的键)
__sub(减法操作)
__tostring(字符串表示)
__unm(取反操作)
使用举例:
-- 创建一个普通表
local myTable = {a = 1, b = 2}
-- 创建元表
local myMetatable = {
__index = function(table, key)
return "Key " .. key .. " not found"
end
}
-- 设置元表
setmetatable(myTable, myMetatable)
-- 测试
print(myTable.a) -- 输出 1
print(myTable.c) -- 输出 Key c not found
在lua中实现OOP
一个类需要有一些数据,一些方法,构造函数(析构函数),并将它们封装起来,以及需要能完成继承
lua的table天然可以存数据和方法,继承就是将现有数据拷贝一份,不得不再次感慨table的强大
构造函数
Person = {} -- 类是一个表
Person.__index = Person -- 使得对象能访问到类的方法
function Person:new(name)
local self = setmetatable({}, Person) -- 创建一个新表,并将其元表设置为 Person
self.name = name -- 为对象添加属性
return self -- 返回这个新对象
end
function Person:speak()
print("My name is " .. self.name)
end
person1 = Person:new("John") -- 创建一个对象
person1:speak() -- 调用方法
可以看到,对Person类定义一个方法,实现构造函数,就是new一个新表,然后把类作为新表的元表,并提前设置如果访问新表没有的元素就去Person类的表里找(__index)
在此基础上,我们在Person里增加方法speak,新表person1里调用时,里面没有speak函数,就触发找不到元素的行为,即其元表里的__index函数,这里__index = 后面是个表,就是把对应的函数名在表里接着找
继承
继承即新建一个类,然后把父类设置成自己的元表。
-- 继承示例
Student = {}
Student.__index = Student
setmetatable(Student, Person) -- Student 继承自 Person
function Student:new(name, grade)
local self = Person:new(name) -- 调用父类构造函数
setmetatable(self, Student) -- 设置为 Student 类型
self.grade = grade
return self
end
function Student:study()
print(self.name .. " is studying in grade " .. self.grade)
end
多态
子类可以覆写父类方法,原理也很直观,表里有对应的方法就不会触发__index去找元表了:
function Student:speak() -- 重写父类方法
print("Hi, I'm " .. self.name .. ", a student in grade " .. self.grade)
end
-- 测试多态
local people = {
Person:new("Alice"),
Student:new("Bob", 10)
}
for _, person in ipairs(people) do
person:speak() -- 会调用各自的speak方法
end
冒号语法糖
Person:speak等价于Person.speak(self),也就是帮你把函数的调用者作为参数传进去了
协程
Lua不处理多线程
Lua用协程处理多任务。
协程不是抢占式的,而是需要再代码中显式标注出什么时候可以安全地暂停当前任务,因而更容易理解,也能免去很多并行的问题,当然同时也失去了多核处理的能力
一个例子
coroutine.create(function)函数会创建一个协程,遇见一个yield()调用后就会回到调用者处执行
coroutine.resume()会回到上次yield()被调用之后的地方继续执行,返回一个状态码以及被调用时使用的参数
function func()
local a = 1
local b = 1
for v = 1, 100 do
local c = a + b
a = b
b = c
print(a .. " " .. b)
end
return a
end
print("start")
start = coroutine.create(func)
coroutine.resume(start)
print("end")
适用于耗时较长的计算或者网络操作
常用库
math
math提供了很多数学函数,包括三角函数,随机数,取整等
--三角函数
math.cos(3)
math.sin(3)
math.pi
--随机数
math.random() --默认生成一个小于1的随机实数,输入参数将范围调整到0-rdi或者rdi到rsi
math.randomseed(os.time()) --修改随机数种子
--取整
math.floor(3.3) --3
math.ceil(3.3) --4
math.modf(3.3) --3,向0取整
--极限值
math.maxinteger --0x7fff……fff = 2^63 - 1
math.mininteger
string
一些对字符串进行操作的标准函数
stringl.len(s) --返回长度,等价于#s
string.rep("adb",3) --adbadbadb
string.reverse("abc") --cba
string.upper("abc") --ABC
string.lower("ABC") --abc
string.sub(s,i,j) --截取s字符串的从i到j部分,j是负数时表示从后开始数,lua里从1开始,返回新字符串
io
local name = io.read()
io.write("你好,", name, "!\n")
-- 文件操作(io库)
local f = io.open("hello.txt", "w")
f:write("Hello, Lua!\n")
f:close()
错误处理
一般来说,lua行为是被主程序调用的,错误该由主程序来处理,但是lua中依然实现了错误处理机制
pcall(protected call)用于安全地调用函数,同时支持自定义的错误处理方法。会返回成功与否:
local ok, msg = pcall( function ()
--[[some code]]
if --[[panic]] then error() end
--[[some code]]
print(a[i])
--[[some code]]
end)
if ok then
……
else
--[[handle error here]]
end
嵌入cpprust语言项目
lua设计之初就是为了方便嵌入,自然是可以在其他语言的项目中方便的引入
首先在cargo.toml里添加:
mlua = { version = "0.9", features = ["lua54", "vendored"] }
mlua
README里给出了如下的示例代码:
use mlua::prelude::*;
fn main() -> LuaResult<()> {
let lua = Lua::new();
let map_table = lua.create_table()?;
map_table.set(1, "one")?;
map_table.set("two", 2)?;
lua.globals().set("map_table", map_table)?;
lua.load("for k,v in pairs(map_table) do print(k,v) end").exec()?;
Ok(())
}
首先Lua::new()的时候创建了一个lua虚拟机,与rust程序内存隔离,和c/cpp一样通过虚拟栈完成沟通
比如这里的let map_table = lua.create_table()?;,rust程序告诉虚拟机要调用create_table()函数,lua VM执行后将地址压入栈,然后rust取出来给变量赋值
接下来同理,准备表地址压入参数,触发执行并pop出参数,相比如c/cpp里手动管理这个栈,mlua已经完成了封装。这个流程是全串行的,虽然lua VM看起来像一个独立的进程,但是实际上始终没出现新的线程/进程,这里也完全没有并发问题
声明函数
所谓嵌入rust项目,其实rust用mlua理论上就可以不用单独的.lua文件了
pub fn create_function<'lua, A, R, F>(&'lua self, func: F) -> Result<Function<'lua>>
where
A: FromLuaMulti<'lua>,
R: IntoLuaMulti<'lua>,
F: Fn(&'lua Lua, A) -> Result<R> + MaybeSend + 'static,
其实是一个转化函数,可以把rust函数转化成lua函数,封装了一切底层细节,用的时候只需要:
let add = lua.create_function(|_, (a, b): (i32, i32)| {
Ok(a + b)
})?;
传入一个闭包
执行lua脚本
pub fn load<'lua, 'a>(&'lua self, chunk: impl AsChunk<'lua, 'a>) -> Chunk<'lua, 'a>
lua.load函数输入lua文件字符串,完成语法检查、编译、看做一个匿名函数压栈、包装返回一个Chunk对象,这里还没有运行
接下来是运行,exec()不需要返回值,用于定义函数,或者eval()用于获取某个表达式的值等等,call()还可以传参:
// 假设这段代码是 "script.lua" 的内容
let script = r#"
local name, age = ... -- 接收参数
return "User " .. name .. " is " .. age
"#;
// 加载脚本,然后调用它,传入 ("Alice", 18)
let res: String = lua.load(script).call(("Alice", 18))?;
println!("{}", res);
set_name("name.lua")可以把一段代码(函数)重命名以从大大的lua文件里抽理出来看做一个独立的文件,便于调试
其它
global是rust和lua交流的重要媒介,rust里定义的lua函数想在lua文件里调用需要,反之亦然
let globals = self.lua.globals();
let tool_call: Function = globals.get("tool_call")?;
globals.set("SDK", sdk)?;
rust嵌入lua项目
mlua已经解决了互相嵌入的问题,本段主要是做个尝试,做一个输入url输出html的用lua调用crate
crate
首先把这个crate写出来:
use mlua::ExternalResult;
use anyhow::{Context, Result};
use mlua::{Lua, ExternalError};
use reqwest::blocking::Client;
use reqwest::Url;
fn main() -> Result<()> {
let lua = Lua::new();
let globals = lua.globals();
let client = Client::new(); // blocking::Client
let request = lua.create_function(move |_, url: String| {
let u = Url::parse(&url).into_lua_err()?;
let res = client.get(u)
.send()
.into_lua_err()?;
let body = res.text().into_lua_err()?;
Ok(body)
})?;
globals.set("http_get", request)?;
Ok(())
}
调用与链接
刚才的一系列操作都是在rust进程的lua vm里活动,如果想要另外运行lua文件就不能编译成可执行文件,而是动态链接文件,所以重写crate:
mlua改成module
[package]
name = "request"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
mlua = { version = "0.9", features = ["lua54", "module"] }
reqwest = { version = "0.11", features = ["blocking"] }
use mlua::{Lua, ExternalError};
use mlua::prelude::*;
use reqwest::blocking::Client;
use reqwest::Url;
#[mlua::lua_module]
fn request(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?;
let client = Client::new(); // blocking::Client
let request = lua.create_function(move |_, url: String| {
let u = Url::parse(&url).into_lua_err()?;
let res = client.get(u)
.send()
.into_lua_err()?;
let body = res.text().into_lua_err()?;
Ok(body)
})?;
exports.set("http_get", request)?;
Ok(exports)
}
接下来cargo build再去target/release下找libxxxxxx.so文件,改名复制到根目录,就可以在lua里用require完成调用了
local t = require("request")
如果想rust-lua-rust这样,还需要一些额外的步骤
首先这样加载外部模块肯定是unsafe的let lua = unsafe { Lua::unsafe_new() };
然后.so文件很多依赖需要调用者提供,而主程序的静态链接库对动态库默认不可见,需要在/.cargo/config.toml里补上一句
[build]
rustflags = ["-C", "link-arg=-rdynamic"]
还有lua的{}即是空数组也是空对象,和rust对接时还可能有类型错误……