文章目录
-
- [2.7 -- Error Handling](#2.7 – Error Handling)
- [2.8 -- Metatables](#2.8 – Metatables)
- [2.9 -- Environments](#2.9 – Environments)
2.7 -- Error Handling
Because Lua is an embedded extension language, all Lua actions start from C code in the host program calling a function from the Lua library (see lua_pcall
). Whenever an error occurs during Lua compilation or execution, control returns to C, which can take appropriate measures (such as printing an error message).
Lua code can explicitly generate an error by calling the error
function. If you need to catch errors in Lua, you can use the pcall
function.
例子:使用 lua_call 调用 Lua 函数
c
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main() {
lua_State *L = luaL_newstate(); // 创建一个新的 Lua 状态机
luaL_openlibs(L); // 打开 Lua 标准库
// Lua 脚本代码,定义一个简单的函数,产生错误
const char *script =
"function foo() \n"
" error('test error')\n"
"end\n";
// 加载并执行 Lua 脚本
if (luaL_dostring(L, script) != LUA_OK) {
printf("Error loading script: %s\n", lua_tostring(L, -1));
lua_close(L);
return 1;
}
// 调用 Lua 函数,并处理错误
lua_getglobal(L, "foo");
// 使用 lua_call,如果出现错误,会直接终止进程
lua_call(L, 0, 0);
lua_close(L); // 关闭 Lua 状态机
return 0;
}
输出
PANIC: unprotected error in call to Lua API ([string "function foo() ..."]:2: test error)
出现错误会直接终止进程
例子:使用 lua_pcall 调用 Lua 函数
c
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main() {
lua_State *L = luaL_newstate(); // 创建一个新的 Lua 状态机
luaL_openlibs(L); // 打开 Lua 标准库
// Lua 脚本代码,定义一个简单的函数,产生错误
const char *script =
"function foo() \n"
" error('test error')\n"
"end\n";
// 加载并执行 Lua 脚本
if (luaL_dostring(L, script) != LUA_OK) {
printf("Error loading script: %s\n", lua_tostring(L, -1));
lua_close(L);
return 1;
}
// 调用 Lua 函数,并处理错误
lua_getglobal(L, "foo");
int ret = lua_pcall(L, 0, 0, 0);
if (ret != LUA_OK) {
// 如果发生错误,打印错误信息
printf("Error occurred: %s\n", lua_tostring(L, -1));
}
lua_close(L); // 关闭 Lua 状态机
return 0;
}
输出
Error occurred: [string "function foo() ..."]:2: test error
例子:使用 lua_pcall,自定义错误处理函数
c
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
// 错误处理函数
int my_error_handler(lua_State *L) {
const char *msg = lua_tostring(L, -1); // 获取错误信息
printf("Error in Lua code: %s\n", msg); // 输出错误信息
lua_pushliteral(L, "Error handled in my_error_handler");
return 1; // 返回 1,返回新的错误消息
}
int main() {
lua_State *L = luaL_newstate(); // 创建一个新的 Lua 状态机
luaL_openlibs(L); // 打开 Lua 标准库
// 将错误处理函数压入栈中
lua_pushcfunction(L, my_error_handler);
// 定义 Lua 脚本,故意写一个错误的脚本(调用了未定义的函数 'foo')
const char *script =
"foo() -- 这是一个错误,foo 未定义\n";
if (luaL_loadstring(L, script) != 0) {
printf("load error!\n");
return 1;
}
// 使用 lua_pcall 执行 Lua 代码,并指定错误处理函数
int ret = lua_pcall(L, 0, 0, -2); // 执行 Lua 代码
if (ret != LUA_OK) {
// 如果发生错误,错误处理函数会将错误信息入栈
printf("error msg: %s\n", lua_tostring(L, -1));
}
lua_close(L); // 关闭 Lua 状态机
return 0;
}
输出
Error in Lua code: [string "foo() -- 这是一个错误,foo 未定义..."]:1: attempt to call global 'foo' (a nil value)
error msg: Error handled in my_error_handler
注意:error() 函数可以返回任意的值,并不是只能返回字符串。
2.8 -- Metatables
Every value in Lua can have a metatable . This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable.
For instance, when a non-numeric value is the operand of an addition, Lua checks for a function in the field "__add"
in its metatable. If it finds one, Lua calls this function to perform the addition.
We call the keys in a metatable events and the values metamethods. In the previous example, the event is "add"
and the metamethod is the function that performs the addition.
You can query the metatable of any value through the getmetatable
function.
You can replace the metatable of tables through the setmetatable
function. You cannot change the metatable of other types from Lua (except by using the debug library); you must use the C API for that.
Tables and full userdata have individual metatables (although multiple tables and userdata can share their metatables). Values of all other types share one single metatable per type; that is, there is one single metatable for all numbers, one for all strings, etc.
A metatable controls how an object behaves in arithmetic operations, order comparisons, concatenation, length operation, and indexing. A metatable also can define a function to be called when a userdata is garbage collected. For each of these operations Lua associates a specific key called an event. When Lua performs one of these operations over a value, it checks whether this value has a metatable with the corresponding event. If so, the value associated with that key (the metamethod) controls how Lua will perform the operation.
Metatables control the operations listed next. Each operation is identified by its corresponding name. The key for each operation is a string with its name prefixed by two underscores, '__
'; for instance, the key for operation "add" is the string "__add"
. The semantics of these operations is better explained by a Lua function describing how the interpreter executes the operation.
The code shown here in Lua is only illustrative; the real behavior is hard coded in the interpreter and it is much more efficient than this simulation. All functions used in these descriptions (rawget
, tonumber
, etc.) are described in §5.1. In particular, to retrieve the metamethod of a given object, we use the expression
lua
metatable(obj)[event]
This should be read as
lua
rawget(getmetatable(obj) or {}, event)
That is, the access to a metamethod does not invoke other metamethods, and the access to objects with no metatables does not fail (it simply results in nil).
-
"add": the + operation.
The function
getbinhandler
below defines how Lua chooses a handler for a binary operation. First, Lua tries the first operand. If its type does not define a handler for the operation, then Lua tries the second operand.luafunction getbinhandler (op1, op2, event) return metatable(op1)[event] or metatable(op2)[event] end
By using this function, the behavior of the
op1 + op2
isluafunction add_event (op1, op2) local o1, o2 = tonumber(op1), tonumber(op2) if o1 and o2 then -- both operands are numeric? return o1 + o2 -- '+' here is the primitive 'add' else -- at least one of the operands is not numeric local h = getbinhandler(op1, op2, "__add") if h then -- call the handler with both operands return (h(op1, op2)) else -- no handler available: default behavior error(···) end end end
-
"sub": the
-
operation. Behavior similar to the "add" operation. -
"mul": the
*
operation. Behavior similar to the "add" operation. -
"div": the
/
operation. Behavior similar to the "add" operation. -
"mod": the
%
operation. Behavior similar to the "add" operation, with the operationo1 - floor(o1/o2)*o2
as the primitive operation. -
"pow": the
^
(exponentiation) operation. Behavior similar to the "add" operation, with the functionpow
(from the C math library) as the primitive operation. -
"unm": the unary - operation.
luafunction unm_event (op) local o = tonumber(op) if o then -- operand is numeric? return -o -- '-' here is the primitive 'unm' else -- the operand is not numeric. -- Try to get a handler from the operand local h = metatable(op).__unm if h then -- call the handler with the operand return (h(op)) else -- no handler available: default behavior error(···) end end end
-
"concat": the ... (concatenation) operation.
luafunction concat_event (op1, op2) if (type(op1) == "string" or type(op1) == "number") and (type(op2) == "string" or type(op2) == "number") then return op1 .. op2 -- primitive string concatenation else local h = getbinhandler(op1, op2, "__concat") if h then return (h(op1, op2)) else error(···) end end end
-
"len": the # operation.
luafunction len_event (op) if type(op) == "string" then return strlen(op) -- primitive string length elseif type(op) == "table" then return #op -- primitive table length else local h = metatable(op).__len if h then -- call the handler with the operand return (h(op)) else -- no handler available: default behavior error(···) end end end
See §2.5.5 for a description of the length of a table.
-
"eq": the == operation. The function
getcomphandler
defines how Lua chooses a metamethod for comparison operators. A metamethod only is selected when both objects being compared have the same type and the same metamethod for the selected operation.
luafunction getcomphandler (op1, op2, event) if type(op1) ~= type(op2) then return nil end local mm1 = metatable(op1)[event] local mm2 = metatable(op2)[event] if mm1 == mm2 then return mm1 else return nil end end
The "eq" event is defined as follows:
luafunction eq_event (op1, op2) if type(op1) ~= type(op2) then -- different types? return false -- different objects end if op1 == op2 then -- primitive equal? return true -- objects are equal end -- try metamethod local h = getcomphandler(op1, op2, "__eq") if h then return (h(op1, op2)) else return false end end
a ~= b
is equivalent tonot (a == b)
. -
"lt": the < operation.
luafunction lt_event (op1, op2) if type(op1) == "number" and type(op2) == "number" then return op1 < op2 -- numeric comparison elseif type(op1) == "string" and type(op2) == "string" then return op1 < op2 -- lexicographic comparison else local h = getcomphandler(op1, op2, "__lt") if h then return (h(op1, op2)) else error(···) end end end
a > b
is equivalent tob < a
. -
"le": the <= operation.
luafunction le_event (op1, op2) if type(op1) == "number" and type(op2) == "number" then return op1 <= op2 -- numeric comparison elseif type(op1) == "string" and type(op2) == "string" then return op1 <= op2 -- lexicographic comparison else local h = getcomphandler(op1, op2, "__le") if h then return (h(op1, op2)) else h = getcomphandler(op1, op2, "__lt") if h then return not h(op2, op1) else error(···) end end end end
a >= b
is equivalent tob <= a
. Note that, in the absence of a "le" metamethod, Lua tries the "lt", assuming thata <= b
is equivalent tonot (b < a)
.
补充:
As we will see later, in Chapter 20, the string library sets a metatable for strings. All other types by default have no metatable:
lua
print(getmetatable("hi")) --> table: 0x80772e0
print(getmetatable(10)) --> nil
例子:元表的使用
lua
Set = {}
local mt = {} -- metatable for sets
-- create a new set with the values of the given list
function Set.new (l)
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do
set[v] = true
end
return set
end
function Set.union (a, b)
if getmetatable(a) ~= mt or getmetatable(b) ~= mt then
error("attempt to 'add' a set with a non-set value", 2)
end
local res = Set.new{}
for k in pairs(a) do
res[k] = true
end
for k in pairs(b) do
res[k] = true
end
return res
end
function Set.intersection (a, b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
function Set.tostring (set)
local l = {} -- list to put all elements from the set
for e in pairs(set) do
l[#l + 1] = e
end
return "{" .. table.concat(l, ", ") .. "}"
end
mt.__tostring = Set.tostring
mt.__add = Set.union
mt.__mul = Set.intersection
--[[
Metatables also allow us to give meaning to the relational operators, through
the metamethods __eq (equal to), __lt (less than), and __le (less than or equal
to). There are no separate metamethods for the other three relational operators,
as Lua translates a~=b to not(a==b), a>b to b<a, and a>=b to b<=a.
Until Lua 4.0, all order operators were translated to a single one, by translating
a<=b to not(b<a). However, this translation is incorrect when we have a partial order,
that is, when not all elements in our type are properly ordered.
For instance, floating-point numbers are not totally ordered in most machines,
because of the value Not a Number (NaN). According to the IEEE 754 standard,
currently adopted by virtually all floating-point hardware, NaN represents undefined values, such as the result of 0=0. The standard specifies that any comparison that involves NaN should result in false. This means that NaN<=x is
always false, but x<NaN is also false. It also implies that the translation from
a<=b to not(b<a) is not valid in this case.
In our example with sets, we have a similar problem. An obvious (and
useful) meaning for <= in sets is set containment: a<=b means that a is a subset
of b. With this meaning, again it is possible that both a<=b and b<a are false;
therefore, we need separate implementations for __le (less or equal) and __lt
(less than):
--]]
mt.__le = function (a, b) -- set containment
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
mt.__lt = function (a, b)
return a <= b and not (b <= a)
end
mt.__eq = function (a, b)
return a <= b and b <= a
end
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) --> table: 00672B60
print(getmetatable(s2)) --> table: 00672B60
s3 = s1 + s2
--[[
(Function print always calls tostring to format its output.) However, when
formatting any value, tostring first checks whether the value has a __tostring
metamethod. In this case, tostring calls the metamethod to do its job, passing
the object as an argument. Whatever this metamethod returns is the result of
tostring.
--]]
print(s3) --> {1, 10, 20, 30, 50}
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2) --> true
print(s1 < s2) --> true
print(s1 >= s1) --> true
print(s1 > s1) --> false
print(s1 == s2 * s1) --> true
Functions setmetatable and getmetatable also use a metafield, in this case to protect metatables. Suppose you want to protect your sets, so that users can neither see nor change their metatables. If you set a __metatable field in the metatable, getmetatable will return the value of this field, whereas setmetatable will raise an error:
lua
mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable
-
"index": The indexing access table[key].
luafunction gettable_event (table, key) local h if type(table) == "table" then local v = rawget(table, key) if v ~= nil then return v end h = metatable(table).__index if h == nil then return nil end else h = metatable(table).__index if h == nil then error(···) end end if type(h) == "function" then return (h(table, key)) -- call the handler else return h[key] -- or repeat operation on it end end
补充:I said earlier that, when we access an absent field in a table, the result is nil. This is true, but it is not the whole truth. Actually, such accesses trigger the interpreter to look for an __index metamethod: if there is no such method, as usually happens, then the access results in nil; otherwise, the metamethod will provide the result.
The archetypal example here is inheritance. Suppose we want to create several tables describing windows. Each table must describe several window parameters, such as position, size, color scheme, and the like. All these parameters have default values and so we want to build window objects giving only the non-default parameters. A first alternative is to provide a constructor that fills in the absent fields. A second alternative is to arrange for the new windows to inherit any absent field from a prototype window. First, we declare the prototype
and a constructor function, which creates new windows sharing a metatable:
luaWindow = {} -- create a namespace -- create the prototype with default values Window.prototype = {x=0, y=0, width=100, height=100} Window.mt = {} -- create a metatable -- declare the constructor function function Window.new (o) setmetatable(o, Window.mt) return o end Window.mt.__index = Window.prototype w = Window.new{x=10, y=20} print(w.width) --> 100
-
"newindex": The indexing assignment table[key] = value.
luafunction settable_event (table, key, value) local h if type(table) == "table" then local v = rawget(table, key) if v ~= nil then rawset(table, key, value); return end h = metatable(table).__newindex if h == nil then rawset(table, key, value); return end else h = metatable(table).__newindex if h == nil then error(···) end end if type(h) == "function" then h(table, key,value) -- call the handler else h[key] = value -- or repeat operation on it end end
补充:The default value of any field in a regular table is nil. It is easy to change this default value with metatables:
lualocal key = {} -- unique key local mt = {__index = function (t) return t[key] end} function setDefault (t, d) t[key] = d setmetatable(t, mt) end tab = {x=10, y=20} print(tab.x, tab.z) --> 10 nil setDefault(tab, 0) print(tab.x, tab.z) --> 10 0
Both __index and __newindex are relevant only when the index does not exist in the table. The only way to catch all accesses to a table is to keep it empty. So, if we want to monitor all accesses to a table, we should create a proxy for the real table. This proxy is an empty table, with proper __index and __newindex metamethods that track all accesses and redirect them to the original table.
lualocal index = {} -- create private index local mt = { -- create metatable __index = function (t, k) print("*access to element " .. tostring(k)) return t[index][k] -- access the original table end, __newindex = function (t, k, v) print("*update of element " .. tostring(k) .." to " .. tostring(v)) t[index][k] = v -- update original table end } function track (t) local proxy = {} proxy[index] = t setmetatable(proxy, mt) return proxy end t = {} t=track(t) t[2] = "hello" -- *update of element 2 to hello print(t[2]) -- *access to element 2
It is easy to adapt the concept of proxies to implement read-only tables. All we have to do is to raise an error whenever we track any attempt to update the table. For the __index metamethod, we can use a table --- the original table itself --- instead of a function, as we do not need to track queries; it is simpler and rather more efficient to redirect all queries to the original table. This use, however, demands a new metatable for each read-only proxy, with __index pointing to the original table:
luafunction readOnly (t) local proxy = {} local mt = { -- create metatable __index = t, __newindex = function (t, k, v) error("attempt to update a read-only table", 2) end } setmetatable(proxy, mt) return proxy end days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} print(days[1]) --> Sunday days[2] = "Noday" --> attempt to update a read-only table
-
"call": called when Lua calls a value.
luafunction function_event (func, ...) if type(func) == "function" then return func(...) -- primitive call else local h = metatable(func).__call if h then return h(func, ...) else error(···) end end end
2.9 -- Environments
Besides metatables, objects of types thread, function, and userdata have another table associated with them, called their environment. Like metatables, environments are regular tables and multiple objects can share the same environment.
Threads are created sharing the environment of the creating thread.
Userdata and C functions are created sharing the environment of the creating C function.
Non-nested Lua functions (created by loadfile
, loadstring
or load
) are created sharing the environment of the creating thread. Nested Lua functions are created sharing the environment of the creating Lua function.
例子:
lua
-- Non-nested function, created using loadstring (this is an example with loadstring)
local f1 = loadstring("return x + 10") -- 假设 x 是全局变量
x = 10
print(f1()) -- 这里会使用全局变量 x,输出 20
-- Nested function
function outer()
local x = 5
local function inner()
return x + 10 -- 这里会访问到 outer 函数的局部变量 x
end
return inner()
end
print(outer()) -- 输出 15,内层函数访问的是外层函数的局部变量 x
-- 去掉 local x = 5,内层函数访问的是全局变量 x,则输出 20
Environments associated with userdata have no meaning for Lua. It is only a convenience feature for programmers to associate a table to a userdata.
Environments associated with threads are called global environments . They are used as the default environment for threads and non-nested Lua functions created by the thread and can be directly accessed by C code (see §3.3).
补充:在 C 代码中使用 lua_getglobal 可访问全局环境获取全局变量的值
void lua_getglobal (lua_State *L, const char *name);
Pushes onto the stack the value of the global name. It is defined as a macro:
#define lua_getglobal(L,s) lua_getfield(L, LUA_GLOBALSINDEX, s)
The environment associated with a C function can be directly accessed by C code (see §3.3). It is used as the default environment for other C functions and userdata created by the function.
Environments associated with Lua functions are used to resolve all accesses to global variables within the function (see §2.3). They are used as the default environment for nested Lua functions created by the function.
You can change the environment of a Lua function or the running thread by calling setfenv
. You can get the environment of a Lua function or the running thread by calling getfenv
. To manipulate the environment of other objects (userdata, C functions, other threads) you must use the C API.