Lua 那些坑爹的特性
協程只能在 Lua 代碼中使用
協程(coroutine)應該是 Lua 最大的賣點之一了。可是,它有一個在文檔中根本沒有提到過的弱點:只能在 Lua 代碼中使用,不能跨越 C 函數調用界限。也就是說,從 C 代碼中無法直接或者間接地掛起一個在進入這個 C 函數之前已經創建的協程。而 Lua 本身作為一種易於嵌入的語言,必然不時與 C 打交道。
比如以下程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
c =
require
(
'c'
)
co =
coroutine.create
(
function
()
print
(
'coroutine yielding'
)
c.callback(
function
()
coroutine.yield
()
end
)
print
(
'coroutine resumed'
)
end
)
coroutine.resume
(co)
coroutine.resume
(co)
print
(
'the end'
)
|
C 模塊代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include<stdio.h>
#include<stdlib.h>
#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>
static
int
c_callback(lua_State *L){
int
ret = lua_pcall(L, 0, 0, 0);
if
(ret){
fprintf
(stderr,
"Error: %s\n"
, lua_tostring(L, -1));
lua_pop(L, 1);
exit
(1);
}
return
0;
}
static
const
luaL_Reg c[] = {
{
"callback"
, c_callback},
{NULL, NULL}
};
LUALIB_API
int
luaopen_c (lua_State *L) {
luaL_register(L,
"c"
, c);
return
1;
}
|
在官方版 Lua 以及 LuaJIT 中會出現「attempt to yield across metamethod/C-call boundary」錯誤。只有打過 Coco 補丁的版本才能正常執行。
1
2
3
4
5
6
7
8
9
10
|
>>> lua5.1 co.lua
coroutine yielding
Error: attempt to yield across metamethod/C-call boundary
>>> luacoco co.lua
coroutine yielding
coroutine resumed
the end
>>> luajit co.lua
coroutine yielding
Error: co.lua:6: attempt to yield across C-call boundary
|
據說 LuaJIT 已經解決了這個問題,不過我想他們說的是內建函數支持 yield 而已。
在 Lua 5.2 中,提供了新的 API 來支持在 C 中 yield。不過,既然是 C API,當然得改代碼,而且看上去比異步回調更復雜。
幽靈一般的 nil
nil 相當於 Python 中的 None 或者 C 中的 NULL,表示「沒有這個值」的意思。但是,一個神奇的地方在於,所有未定義的變量的值均為 nil。所以,在 Lua 中有空值 nil,但是有時它又不存在:當你嘗試把 nil 值存到表里時,它會消失掉。
另外,當 nil 被傳入接受可變參數的函數時,官方版 Lua 只能通過select('#', ...)
獲取參數個數。至於 LuaJIT,很遺憾,沒有辦法。
LuaJIT 中還有這樣一個值,它等於 nil。但是根據 Lua 語言標准,只有 false 和 nil 的值為假。於是,在 LuaJIT 中,兩個相等的量,卻有着不同的真值。它就是 ffi 中的 NULL 指針。
在另外一些地方,也會有其它各種庫定義的 null 值,比如ngx.null
、cjson.null
。這些空值之間哪些相等哪些不等就難說了。
沒有 continue
Lua 一直不肯添加 continue 關鍵字。作者聲稱不添加不必要的特性。請問有誰認為「repeat ... until」結構比「continue」關鍵字更有必要?於是,凡是本來應當使用 continue 的地方,都不得不弄一個大大的 if 語句:
1
2
3
4
5
|
for
line
in
configfile:
if
line.startswith(
'#'
):
contine
parse_config(line)
|
在 Lua 中只能這么寫:
1
2
3
4
5
6
|
for
line
in
configfile
do
if
string.sub
(line,
1
,
1
) ==
'#'
then
else
parse_config(line)
end
end
|
所以,Lua 代碼的左邊空白的形狀都是些 45° 或者 135° 的斜線。
錯誤信息的表達
Lua 中,習慣的錯誤表達為,返回兩個值,第一個為 nil 表示發生了錯誤,第二個為字符串,是錯誤信息。字符串形式的錯誤信息顯示給用戶挺不錯的(想想微軟喜歡的長長的錯誤號)。可是,程序里只好用模式匹配去判斷是否發生了指定類型的錯誤。這多么像 VimScript 中的錯誤處理啊。journald 取代 syslog 的重要原因之一就是它存儲的是結構化文本。Lua 錯誤處理最偉大的一點則是我們又回到了字符串匹配。別以為你可以返回一個 table 或者 userdata 來表達錯誤。很多庫可不這么認為。當你的結構化錯誤被..
連接時你就會發現這廝沒救了。
下標
別的編程語言下標都從 0 開始。Lua 為了更「人性化」,其下標從 1 開始。其實寫多了也能習慣,除了當通過 ffi 獲得一個 C 數組的時候……
提前返回
return 語句之后必須跟着一個end
。於是,很多提前返回的時候只能寫do return end
。有意義么?
方法調用
訪問表或者 userdata 的域使用一個點.
,連接字符串使用兩個點..
。而方法定義和調用時,你需要垂直放置的兩個點——冒號:
。它與域訪問的一個點相比,也就多了四個像素,顯示器不干凈或者精神不佳的時候就得小心了!
面向對象
Lua 是不支持面向對象的。很多人用盡各種招術利用元表來模擬。可是,Lua 的發明者似乎不想看到這樣的情形,因為他們把取長度的__len
方法以及析構函數__gc
留給了 C API。純 Lua 只能望洋興嘆。
結論
Lua 只適合寫寫配置。做純計算用用 LuaJIT 也不錯。復雜的邏輯還是交給專業點的語言吧。
理解 Lua 的那些坑爹特性
1. 協程只能在Lua代碼中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
c =
require
(
'c'
)
co =
coroutine.create
(
function
()
print
(
'coroutine yielding'
)
c.callback(
function
()
coroutine.yield
()
end
)
print
(
'coroutine resumed'
)
end
)
coroutine.resume
(co)
coroutine.resume
(co)
print
(
'the end'
)
|
1
|
local
c =
require
'c'
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include<stdio.h>
#include<stdlib.h>
#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>
static
int
c_callback(lua_State *L){
int
ret = lua_pcall(L, 0, 0, 0);
if
(ret){
fprintf
(stderr,
"Error: %s\n"
, lua_tostring(L, -1));
lua_pop(L, 1);
exit
(1);
}
return
0;
}
static
const
luaL_Reg c[] = {
{
"callback"
, c_callback},
{NULL, NULL}
};
LUALIB_API
int
luaopen_c (lua_State *L) {
luaL_register(L,
"c"
, c);
return
1;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include<stdio.h>
#include<stdlib.h>
#define LUA_LIB /* 告訴Lua,這是一個LIB文件 */
#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>
static
int
c_cont(lua_State *L) {
/* 這里什么都不用做:因為你的原函數里面就沒做什么 */
return
0;
}
static
int
c_callback(lua_State *L){
/* 使用 lua_pcallk,而不是lua_pcall */
int
ret = lua_pcallk(L, 0, 0, 0, 0, c_cont);
if
(ret) {
fprintf
(stderr,
"Error: %s\n"
, lua_tostring(L, -1));
lua_pop(L, 1);
exit
(1);
}
/* 因為你這里什么都沒做,所以c_cont里面才什么都沒有。如果這里需要做
* 什么東西,將所有內容挪到c_cont里面去,然后在這里簡單地調用
* return c_cont(L);
* 即可。
*/
return
0;
}
static
const
luaL_Reg c[] = {
{
"callback"
, c_callback},
{NULL, NULL}
};
LUALIB_API
int
luaopen_c (lua_State *L) {
/* 使用新的 luaL_newlib 函數 */
luaL_newlib(L, c);
return
1;
}
|
1
2
3
4
|
lua -- co.lua
coroutine yielding
coroutine resumed
the end
|
1
2
3
4
5
6
7
8
9
|
function
do_login(server)
server:login(
function
(data)
-- 錯誤處理先不管,假設有一個全局處理錯誤的機制(后面會提到,實際
-- 上就是newtry/protect機制)
server:get_player_info(
function
(data)
player:move_to(data.x, data.y)
end
)
end
,
"username"
,
"password"
)
end
|
1
2
3
4
5
|
function
d_login(server)
server:login(
"username"
,
"password"
)
local
data = server:get_player_info()
player:move_to(data.x, data.y)
end
|
1
2
3
4
5
6
7
8
9
10
11
|
local
current
function
server:login(name, password)
assert
(
not
current,
"already send login message!"
)
server:callback_login(
function
(data)
local
cur = current
current =
nil
coroutine.resume
(cur, data)
end
, name, password)
current =
coroutine.running
()
coroutine.yield
()
end
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function
coroutinize(f, reenter_errmsg)
local
current
return
function
(...)
assert
(
not
current, reenter_errmsg)
f(
function
(...)
local
cur = current
current =
nil
coroutine.resume
(cur, ...)
end
, ...)
current =
coroutine.running
()
coroutine.yield
()
end
end
|
2. 幽靈一般的 nil
1
2
3
4
5
6
7
|
undefined = {}
-- 指定一個全局變量存在,但不指向任何地方:
a = undefined
-- 判斷這個全局變量是否不指向任何地方:
if
a == undefine
then
...
end
-- 徹底刪除這個變量:
a =
nil
|
3. 沒有continue
1
2
|
local
a =
true
repeat
a =
false
until
a
|
1
2
3
4
5
6
7
|
for
line
in
configfile
do
if
string.sub
(line,
1
,
1
) ==
'#'
then
goto
next
end
parse_config(line)
::
next
::
end
|
1
2
3
4
5
6
7
8
9
10
|
local
i
repeat
if
i ==
5
then
goto
next
end
local
j = i * i
print
(
"i = "
..i..
", j = "
..j)
i = i +
1
::
next
::
until
i ==
10
|
1
2
3
4
|
lua --
"noname\2013-01-03-1.lua"
lua: noname\2013-01-03-1.lua:10: <goto next> at line 4 jumps into the scope of
local
'j'
shell returned 1
Hit any key to close this window...
|
4. 錯誤信息的表達
5. 下標
6. 提前返回
7. 方法調用
8. 面向對象
9. 結論
5 年前
關於協程,我當然知道協程該怎么用。Lua C API 確實有些細節上不太清楚,文檔太簡略了。pcallk 只能解決一次 yield 吧?如果 yield 的次數不定該怎么辦?我有個庫的函數,在運行過程中可能需要調用一個回調函數來取某些數據。在 Lua 綁定中,這個回調函數就是調用一個 Lua 函數,然后由於涉及網絡操作,它是會 yield 不定次數的。
你說的所有這些,要么是 LuaJIT 2.0.0 還沒實現的特性(LuaJIT 比 Lua 快太多了),要么是要求作者對 Lua 該怎么編程很熟悉(如果我接手的那些代碼是你寫的就好了)。至於表達能力,還是不要太強的好,不然每個人的錯誤返回方式和面向對象的實現都不一樣,概念是統一了,實現千差萬別、各不相容。
沒錯,「do return end」就是調試時用的。
5 年前
@依雲: 恩,說句實話,只看reference的確很難搞明白k系列函數內部的核心思想。我是一開始就跟着郵件列表的討論才比較清楚的。不過你真的可以看看novelties-5.2.pdf,這里面有很詳細的說明。
另外不明白“一次yield”和“多次yield”有什么區別。只要用了k系列函數,你多少yield都沒問題的,因為Lua自己會幫你維護Lua內部yield時候的狀態。無論你如何yield,回到C層面(即從內部的coroutine返回)只會有一次,因此k系列函數一定能做到你想要的,而且並不需要特別的設計。
你仔細看看LuaJIT,很多特性已經實現了,包括goto。k系列函數沒實現是基於兩個原因:1.LuaJIT關注純Lua應用,甚至用ffi庫取代了C API的必要性;2.LuaJIT因為與Lua作者的巨大分歧(郵件里面吵了好幾架),所以不打算實現5.2兼容了。至少短期內是不想的。sigh……快的話,其實快不了多少,只是科學計算方面的確快了很多,如果你的代碼是C模塊密集的,那么LuaJIT很難提高效率,其次是如果你用了NYI的特性,那么也是不會快的(比如字符串模式匹配和coroutine),從我的經驗看,網游邏輯書寫用luaJIT對效率的提升不大,甚至可能比原Lua更慢。
表達能力問題的確是個雙刃劍,但有個朋友說得好“做得到總比做不到好”,這個就看怎么解讀了。
錯誤返回是有標准模式的,文章里面提到了newtry/protect模式,不過寫到后來寫忘了= =有時間補上吧,OO的話也是有標准模式的,而且是兩套。關鍵是,因為底層概念統一,所以即使是千差萬別的實現,最終也一定是兼容的。你如果處理過實現之間的糾葛就會體會到底層概念統一帶來的巨大好處。
do return end的話也就是一個詞和三個詞的區別吧……sigh……就當多打字了,實在不行做個imap或者iab唄……
5 年前
哇,偶像你也在這里!!
5 年前
newtry/protect??
luasocket中的那套,我當時看了也覺得蠻有意思的
可否補充介紹下實際過程的使用方式
5 年前
你好,能給我lua郵件列表的郵箱嗎?謝謝~~
4 年前
您好,想問下,如果想在pcall里使用coroutine,有什么辦法嘛? 文中的protect,指的就是luaSocket里那種封裝pcall的方式吧
4 年前
我覺得 lua 還有一個坑爹特性,table 當哈希表時,無法以 O(1) 的時間復雜度取得其元素個數。
4 年前
從lily那里過來的。看了你這文章,受益頗多。
4 年前
求解釋coroutinize中這部分的必要性
local cur = current
current = nil
3 年前
local a = true
repeat a = false until a == false
這樣吧
3 年前
關於continue的例子,我猜樓主的意思是這樣吧?
少寫了一個local?
local a = true
repeat
local a = false
until a