lua学习笔记(持续更新中)

🌙 前言

开始之前先记录一下与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]]表示,会保存换行。
    • 连接字符串使用..运算符
    • tostring tonumber函数,没有报错只有返回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对接时还可能有类型错误……