Lua 编译执行和错误处理

一、编译

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("---- 第二次调用 ----")
load()
--> ---- 第二次调用 ----
--> 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

第三个例子: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 编译执行和错误处理_第1张图片

第三个例子:dofile 运行不存在的文件

dofile("")

Lua 编译执行和错误处理_第2张图片

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 

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 编译执行和错误处理_第3张图片

第二种: 借助 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 编译执行和错误处理_第4张图片

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

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

Lua 编译执行和错误处理_第5张图片

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

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

Lua 编译执行和错误处理_第6张图片

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 编译执行和错误处理_第7张图片

小技巧:

还记得多值返回和多值入参吗?其实这里可以将函数的返回值直接作为 assert 的入参,直接作为 assert 的错误 message

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

Lua 编译执行和错误处理_第8张图片

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 编译执行和错误处理_第9张图片

debug.traceback() 的详细用法会在后续的文章中分享

四、写在最后

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

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

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

你可能感兴趣的:(Lua,lua,c语言,android,c++,开发语言)