一、编译

Lua 是一门解说型言语,意味着他能履行动态生成的代码,而这主要由于 dofileloadfile 函数的存在。

这两个函数能够让咱们的代码加载和履行代码,具体的咱们一个个进行共享

1-1、loadfile(filename, mode, env)

类似于 load 函数( 1-3 小节会共享),会从文件 filename 或规范输入中获取代码块。

loadfile 仅仅将文件内容进行编译,当需求运转时,调用一下即可。 比较于 dofile , 在屡次调用的情况下,能够节省不少的功用开支。

参数:

  • filename:需求编译的文件名。
  • mode:控制块是文本仍是二进制(即预编译块)。可选值:“b”(仅二进制块,即预编译代码)、“t”(仅文本块)、“bt”(二进制和文本)。默以为“bt”。
  • env:上值环境(具体什么是上值,咱们后续再共享)

回来值(有两个回来值):

  • 第一个回来值:经过 loadfile 加载的可运转的代码块函数,但假如加载文件有反常,则该值会回来 nil
  • 第二个回来值:假如加载文件有反常,则该值为过错信息,假如加载成功则为 nil

举些比如:

第一个比如:loadfile 运转正常的 Lua 代码文件

-- 此处不能用 local ,不然 loadfile 就无法运用
-- local age = 28
age = 28
function showAge()
    print("main age", age)
end
local load = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/编译/加载的文件.lua")
print("---- 第一次调用 ----")
load()
--> ---- 第一次调用 ----
--> Hello, jiang pengyong.
--> 28
--> main age	28
print("---- 第2次调用 ----")
load()
--> ---- 第2次调用 ----
--> Hello, jiang pengyong.
--> 28
--> main age	28
print(name)     --> 江澎涌
showName()      --> lua file	江澎涌

以下是 “加载的文件.lua” 的文件内容

print("Hello, jiang pengyong.")
print(age)
showAge()
--- 此处不能用 local ,不然外部就不能运用
--- local name = "江澎涌"
name = "江澎涌"
function showName()
    print("lua file", name)
end

第二个比如:loadfile 运转内部有反常的 Lua 代码文件

local load, error = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/编译/error 的文件.lua")
print(load, error)      --> function: 0x6000021e8a50	nil
load()                  --> 运转后有过错,如下图所示

以下是 error 的文件.lua 的文件内容

-- 由于 info 为 nil,调用会有反常
local name = info.name

Lua 编译执行和错误处理

第三个比如:loadfile 运转不存在的文件

local load, error = loadfile("")
print(load, error)              --> nil	cannot open : No such file or directory

1-2、dofile(filename)

翻开 filename 文件,并编译其内容作为 Lua 块,并履行。

当不带参数调用时,dofile 履行规范输入 stdin 的内容。

假如呈现过错,dofile 会将过错抛出来。

dofile 的内部仍是调用了 loadfile 函数,能够以为是以下伪代码:

function dofile(filename)
    -- 假如有过错,loadfile 则直接抛出
    local f = assert(loadfile(filename))
    -- 会履行 loadfile 编译完的代码函数,并回来
    return f()
end    

举些比如:

第一个比如:dofile 运转正常的 Lua 代码文件

-- 此处不能用 local ,不然 dofile 就无法运用
-- local age = 28
age = 28
function showAge()
    print("main age", age)
end
dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/编译/加载的文件.lua")
--> (由于 dofile 会直接运转加载的 lua 文件,以下为运转 lua 文件输出的内容)
--> Hello, jiang pengyong.
--> 28
--> main age	28
print(name)     --> 江澎涌
showName()      --> lua file	江澎涌

以下是 “加载的文件.lua” 的文件内容

print("Hello, jiang pengyong.")
print(age)
showAge()
--- 此处不能用 local ,不然外部就不能运用
--- local name = "江澎涌"
name = "江澎涌"
function showName()
    print("lua file", name)
end

第二个比如:dofile 运转内部有反常的 Lua 代码文件

dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/编译/error 的文件.lua")

以下是 error 的文件.lua 的文件内容

-- 由于 info 为 nil,调用会有反常
local name = info.name

Lua 编译执行和错误处理

第三个比如:dofile 运转不存在的文件

dofile("")

Lua 编译执行和错误处理

1-3、load(chunk, chunkname, mode, env)

从 chunk 中获取内容,编译为代码块。

参数:

  • chunk:能够是字符串或函数。假如是函数,会重复调用函数,然后将成果进行衔接为字符串,当函数回来空字符串、 nil 、没有值(即 return)时表明完毕。
  • chunkname:用作过错音讯和调试信息的块的称号。假如 chunk 是字符串,当没有设置时,则默以为 chunk 内容(即 chunk 字符串),若有设置 chunkname 则为 chunkname 的值;假如 chunk 是函数,没有设置时,默以为 load,若有设置 chunkname 则为 chunkname 的值。
  • mode:控制块是文本仍是二进制(即预编译块)。可选值:“b”(仅二进制块,即预编译代码)、“t”(仅文本块)、“bt”(二进制和文本)。默以为“bt”。
  • env:上值环境(具体什么是上值,咱们后续文章共享)

回来值:

假如没有语法过错,则将编译后的块作为函数回来;不然,回来 nil 加上过错音讯。

Lua 不查看二进制块的一致性,歹意制作的二进制块或许会使解说器崩溃。

load 的功用很强壮,但需求慎重运用。该函数的开支较大且或许会引起怪异问题,所以当有其他的可选计划时,则不运用该函数。

举些比如:

第一个比如:load 加载字符串

name = "江澎涌"
local l, error = load("name = name..'!'")
print("回来值", l, error)       --> 回来值	function: 0x600002088e10	nil
print(name)                     --> 江澎涌
l()
print(name)                     --> 江澎涌!
l()
print(name)                     --> 江澎涌!!

第二个比如,load 加载函数

local i = 0
function loadContent()
    i = i + 1
    if i == 1 then
        return "name"
    elseif i == 2 then
        return " = "
    elseif i == 3 then
        return "name.."
    elseif i == 4 then
        return "'.'"
    else
        -- 空字符串、 nil 、没有值表明完毕
        --return ""
        --return nil
        return
    end
end
print("惯例加载函数:")
name = "江澎涌"
local l, error = load(loadContent)
print("回来值", l, error)                --> 回来值	function: 0x60000371cf60	nil
print(name)                              --> 江澎涌
l()
print(name)                              --> 江澎涌.
l()
print(name)                              --> 江澎涌..

第三个比如,load 加载有语法反常的函数

-- info 是 nil,load 加载块则会出反常
local f, error = load("info.name")
print(f, error)     --> nil	[string "info.name"]:1: syntax error near <eof>

1-3-1、load 函数的词法定界

load 函数总是在全局环境中编译代码段, 所以即便身处一致效果域的 local 变量也不会被运用。

经过以下的代码,就能很明晰的体会出,load 拿的是全局的变量 num ,而非部分。

num = 10
local num = 1
local l, error = load("num = num + 1; print(num)")
print(l, error)     --> function: 0x600000499110	nil
l()                 --> 11
-- 打印部分的 num  
print(num)          --> 1
-- 打印全局的 num
print(_G.num)       --> 11

如何让 load 能运用部分的 num_G 是什么,后续 “Lua 环境” 的文章会进行共享。

1-3-2、load 的更多玩法

能够运用 io.lines 的办法,给 load 供给函数,让 load 的内容来源于文件,这样其实就和 loadfile 的效果是相同的了

local lines = io.lines("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/加载的文件.lua","L")
local l, error = load(lines)
--- 和下面是等效的
local load = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/加载的文件.lua")

1-4、loadfile、dofile、load 的差异

loadfile dofile load
过错处理 不会抛出过错,会作为回来值回来 直接抛出过错 不会抛出过错,会作为回来值回来
履行 编译文件中的代码后,回来一个可履行函数 编译完文件后,立马履行 编译完字符串,回来一个可履行函数

1-5、loadfile、dofile、load 副效果

这些函数没有任何的副效果,它们既不改变或创建变量,也不向文件写入等。 这些函数仅仅将程序段编译为一种中间办法,然后将成果作为匿名函数回来。Lua 言语中函数界说是在运转时而不是在编译时产生的一种赋值操作。

即加载文件代码,只有在履行了回来的函数后,内部的变量才会赋值。

local f = load("i=1")
print(i)        --> nil
f()
print(i)        --> 1

二、预编译

2-1、预编译办法

有两种办法能够进行预编译:

第一种: 指令行

luac -o 输出预编译的文件称号 需求被预编译的文件

能够运用 -l 选项,列出编译器为代码段生成的操作码。例如运转以下指令

luac -l -o 预编译的文件.lc 预编译的文件.lua

Lua 编译执行和错误处理

第二种: 凭借 string.dump(func, strip) 进行实现

string.dump 会回来将 func 函数回来的字符串编译的二进制字符串。

参数:

  • func:需求进行编译的内容函数
  • strip:假如设置为 true ,则编译的内容不包含调试信息,以节省空间

看个比如:

p = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/预编译/预编译的文件.lua")
f = io.open("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/预编译/预编译的文件1.lc","wb")
f:write(string.dump(p))
f:close()

2-2、运转预编译文件

有两种办法能够进行预编译:

第一种: 指令行进行运转,和未进行预编译的运转是相同的

lua 预编译的文件.lc

第二种:用前面共享的 loadfile、dofile、load 办法

这三个办法都能履行预编译的内容,所以能够和往常相同运用他们即可,仅仅 loadfiledofile 要留意他们的 mode 参数,需求运用有二进制的办法 bbt

下面这三段的运转成果是相同的

print("----------------------------")
print("loadfile 加载:")
local fun, error = loadfile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/预编译/预编译的文件.lc", "b")
print(fun, error)
fun()
--> ----------------------------
--> loadfile 加载:
--> function: 0x600002378cf0	nil
--> Hello, jiang pengyong.
--> 江澎涌	29
print("----------------------------")
print("dofile 加载:")
dofile("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/预编译/预编译的文件.lc", "b")
--> ----------------------------
--> dofile 加载:
--> Hello, jiang pengyong.
--> 江澎涌	29
print("----------------------------")
print("load 加载:")
local lines = io.lines("/Users/jiangpengyong/Desktop/code/Lua/lua_study_2022/10 编译、履行和过错/预编译/预编译的文件.lc", 1024)
local l = load(lines)
l()
--> ----------------------------
--> load 加载:
--> Hello, jiang pengyong.
--> 江澎涌	29

2-3、预编译的效果

  1. 预编译办法的代码纷歧定能比源代码更小,可是加载会更快。
  2. 能够避免被歹意篡改(代码块内部被篡改),但需求留意的是,不要被整个文件的替换,导致运转环境出问题。

三、过错

Lua 是一门嵌入于其他言语的扩展言语,所以当产生过错时,不能是简略的奔溃或闪退,终究导致宿主程序奔溃,而是要有一套牢靠的过错处理机制。

3-1、抛出反常

关于一个函数或一段代码产生反常时,基本是两个采纳两种办法:

  1. 回来过错码,一般采纳的结构是两个回来参数,第一个值为 nil 、 false ,第二个过错原因
  2. 经过调用函数 error 或 assert 往外抛出过错

3-1-1、error(message, level)

会以 message 为原因抛出反常,并终止程序。假如音讯是字符串,error 办法 会在音讯的最初增加一些关于过错方位的信息。(假如回来的不是字符串,则会导致过错方位丢掉)

参数:

  • message:抛出反常的内容,纷歧定是字符串,能够是任意的类型数据,例如结合 pcall(下一章节共享)则能够运用这一特性,获取更多的反常信息。
  • level:指定过错方位。假如为 1 则表明 error 抛出的方位,假如为 2 则表明调用 error 的函数的方位,依次往上走。假如为 0 则表明不将方位增加到音讯中。默认值为 1

举个比如:

function throwError()
    --- 不记载抛出反常的方位
    error("error test", 0)
    --- 抛出反常的方位,即当前
    --error("error test", 1)
    --- 抛出反常的函数方位,即调用 throwError 的方位
    --error("error test", 2)
end
throwError()

Lua 编译执行和错误处理

function throwError()
    --- 不记载抛出反常的方位
    --error("error test", 0)
    --- 抛出反常的方位,即当前
    error("error test", 1)
    --- 抛出反常的函数方位,即调用 throwError 的方位
    --error("error test", 2)
end
throwError()

此时 error 在第 14 行,具体可移步 github

Lua 编译执行和错误处理

function throwError()
    --- 不记载抛出反常的方位
    --error("error test", 0)
    --- 抛出反常的方位,即当前
    --error("error test", 1)
    --- 抛出反常的函数方位,即调用 throwError 的方位
    error("error test", 2)
end
throwError()

此时 throwError 在第 18 行,具体可移步 github

Lua 编译执行和错误处理

3-1-2、assert(v, message)

假如参数 v 的值为 false(即 nilfalse),则会抛出过错;不然,回来 assert 一切参数。 假如呈现过错,则会以 message 为内容抛出过错。

值得留意,假如验证经过,assert 回来值是他的两个入参,而不是 v 的一切回来值。

function showInfo()
    return "江澎涌", 29, 170
end
print(showInfo())                           --> 江澎涌	29	170
print(assert(showInfo(), "error test"))     --> 江澎涌	error test
print(assert(nil, "error test"))

Lua 编译执行和错误处理

小技巧:

还记得多值回来和多值入参吗?其实这儿能够将函数的回来值直接作为 assert 的入参,直接作为 assert 的过错 message

function showInfoWithError()
    return false, "error test inner"
end
print(showInfoWithError())              --> false	error test inner
print(assert(showInfoWithError()))

Lua 编译执行和错误处理

3-2、反常捕获

和 java、kotlin 相同,有反常的抛出,就有反常的捕获,Lua 运用 pcall 进行对反常的捕获

3-2-1、pcall(f, arg1, …)

会在保护办法下运用给定参数(arg1 , ...)调用函数 f

f 中的任何过错都不会传达,pcall 会捕获过错并回来状况码。

参数:

  • f:需求被捕获反常的函数
  • arg1 , … :传入 f 函数的参数

回来值:

  • 第一个成果是状况码(一个 boolean ), 假如调用成功且没有过错,则为 true,后面会回来调用的 f 函数一切回来值
  • 假如呈现任何过错,pcall 会回来 false 以及过错音讯

举几个比如

正常捕获反常

local ok, msg = pcall(function()
    error("error inner")
end)
print(ok, msg)      --> false	...2022/10 编译、履行和过错/过错/过错处理.lua:43: error via pcall catch

无反常,携带参数而且多值回来,这儿需求多个值承载

local ok, name, age = pcall(function(name, age)
    print("receive args: ", name, age)      --> receive args: 	Jiang pengyong	29
    return name, age
end, "Jiang pengyong", 29)
print(ok, name, age)                        --> true	Jiang pengyong	29

error 回来一个 table,正如前面所说,error 不止只能回来 string ,能够回来 Lua 各种类型,这儿运用 table 能够携带更多的信息。但这儿会有一个问题,就是丢掉了过错方位。

local ok3, error = pcall(function()
    error({ code = 100, msg = "error in a table" })
end)
print(ok3, error.code, error.msg)       --> false	100	error in a table

3-2-2、xpcall(f, msgh, arg1, …)

pcall 的功用是相同的,都是 try-catch 捕获反常。但 pcall 有一个问题缺少调用栈,虽然有出错代码的方位,可是在排查问题时,这是不够的。所以就有了 xpcall 函数,多了一个 msgh 参数,其他和 pcall 是相同的。

为什么 pcall 没有调用栈?由于在 pcall 回来过错时,部分的调用栈就已经被破坏了(从 pcall 到出错之处的部分)

参数:

  • msgh:会在产生过错的时分,先调用该函数,咱们能够借用 debug 的函数进行调试,假如需求查看调用栈 debug.traceback 便可查看。
local ok, msg = xpcall(function()
    error("error via pcall catch")
end, function()
    print(debug.traceback())
    return "error via msg handle"
end)
print(ok, msg)          --> false	error via msg handle

Lua 编译执行和错误处理

debug.traceback() 的具体用法会在后续的文章中共享

四、写在最终

Lua 项目地址:Github传送门 (假如对你有所协助或喜欢的话,赏个star吧,码字不易,请多多支撑)

假如觉得本篇博文对你有所启示或是解决了困惑,点个赞或重视我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间共享与你。

Lua 编译执行和错误处理