• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

理解Lua的那些坑爹特性

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
来源 https://blog.lilydjwg.me/2012/12/29/lua-caveats.36879.html
 

协程只能在 Lua 代码中使用

协程(coroutine)应该是 Lua 最大的卖点之一了。可是,它有一个在文档中根本没有提到过的弱点:只能在 Lua 代码中使用,不能跨越 C 函数调用界限。也就是说,从 C 代码中无法直接或者间接地挂起一个在进入这个 C 函数之前已经创建的协程。而 Lua 本身作为一种易于嵌入的语言,必然不时与 C 打交道。

比如以下程序:

 
co.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')

C 模块代码:

 
c.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.nullcjson.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 的那些坑爹特性

 
来源  http://sw.is-programmer.com/2013/1/3/understand-lua-caveats.36905.html

 

按:最近看到了依云的文章,一方面,为Lua被人误解而感到十分难过,另一方面,也为我的好友,依云没有能够体会到Lua的绝妙和优雅之处而感到很遗憾,因此我写了这篇文章,逐条款地说明了依云理解中出现的一些问题。希望能够帮助到大家!
 

1. 协程只能在Lua代码中使用

 
    是的,协程在当你需要挂起一个C函数的时候无法使用。但是,在提出这个缺陷的时
候,是不是应该想一想:为什么Lua会有这个缺陷
 
    原因很简单:这一点完全避不开,这是C的问题,C无法保存函数执行的某个现场用于
返回这个现场继续执行,因此完全没有办法在Lua的协程被唤醒的时候,回到这个现场。
 
    那么怎么办呢?Lua5.2做出了很优秀的设计。既然无法回到C的现场,那么我们不回
去了,而是采取“事件通知”的方式告诉你,“hey哥们,你前面的逻辑被切了,想办法
补救吧”,这就是所谓的CPS——继续风格的编程。继续在这里是一个Scheme/Lisp术
语,意思是“当前的操作执行完了以后,下面该做什么?”这种风格是Lua能支持任意
Yield 的必要条件。在C的限制下,只有这一种方法能突破这个限制。
 
    至于你说的“比异步回调更复杂”,我想你弄混了两点:1.这只是C API层面的修改
完全不影响到Lua代码层面,你的Lua代码完全不必做出任何修改,而且,你对
coroutine的用法完全错了!等会儿我会教你coroutine到底怎么用。2.上面提到了,
这是唯一一种能支持coroutine的方式,既然是唯一一种,就无所谓复杂与否了。3.
我下面会演示给你,为什么说coroutine完全解放了程序员,使用coroutine的代码会带来
革命性的简化。
 
    我们分两步来说明这个问题:第一步,我们先来看你的例子:你想做的事情是,在执
行 c.callback的时候,能够yield出来,继续其他的流程。这里必须要说明,你的API设
计本身就是callback式的,因此这种API本身就犯不着coroutine,Lua本身能完全地处理
。这里我会给出一个支持coroutine的C模块设计,让这个模块能支持coroutine,第二步
,我会告诉你coroutine实际上是用在什么方面的,是如何取代事件回调机制的。在完成
这个说明后,我们来说明coroutine到底有什么好处,为什么说coroutine比事件回调机制
有着 革命性的优秀之处。
 
    你的例子是这样的:
 
co.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'
    就更好了。
 
    其他的地方倒是没什么需要修改的了。
 
    再看看你的C模块代码:
 
c.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的C模块,所以你得声明这的确是一个C模块,应该在
        #include <lua.h>
    之前加入这一行:
        #define LUA_LIB
 
    编译的时候就可以用下面的命令行了:
        gcc -mdll -DLUA_BUILD_AS_DLL c.c -oc.dll
 
    然后,Lua5.2已经没有luaL_register函数了,因为Lua不鼓励将模块设置到全局域,
而luaL_register会做这件事。所以将这行改为:
        luaL_newlib(L, c);
    最后一点不是问题,只是一个小建议:Lua只是会用luaL_Reg里的内容,但是却不会
保留里面的任何内容,所以你可以直接将其放在luaopen_c里面,并去掉static,这样可
以节省一点内存。
 
    我们来看看一个支持coroutine的C模块应该怎么写:
 
c.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
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           
    
    我们看到,让C模块支持yield是非常简单的:首先,你需要将lua_call/lua_pcall改
成对应的k版本,将函数其后的所有内容剪切到对应的cont函数里去,然后将原先的内容
改为return func_cont(L);即可。
 
    为什么要这么设计API?上面说了,这是为了解决C自身的问题,如是而已。
 
    现在我们来讨论第二个问题:Lua的coroutine用在什么地方呢?
 
    假设我们要书写游戏的登陆逻辑,我们需要干这样的事情:
        1. 登陆游戏
        2. 获取玩家角色数据
        3. 让玩家移动到上次退出前的坐标
 
    如果是事件回调引擎,你会怎么设计API呢?可能是这样的:
 
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
 
    看到了么?因为登陆需要等待网络请求,而等待的时候你不能把事件循环给阻塞了,
所以你不得不用回调机制,但是,一旦你一次要做几件事情,回调立即就会让你的代码狼
狈不堪。这还只是简单的顺序代码。如果是判断或者是循环呢?我告诉你,上面的代码是
一个真实的例子,是我以前设计的手机网游里面关于登陆部分的实际例子,而另一个例子
是在客户端批量购买N个道具!可以想象这会是一个很复杂的递归代码了,而实际上你仅
仅是想做for在做的事情而已!
 
    那么怎么办呢?coroutine提供了解决这个问题的一个极端优雅的办法。我们想想最
优雅的设计会是什么样子的:
 
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                                      
 
    是不是简单多了?慢着!看起来login等函数是阻塞的,这样的阻塞难道不会阻塞事
件循环,导致界面僵死么?好!现在coroutine上场了!看看我们是如何实现login的!
 
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                                                   
 
    看到了吗?login先用正常的方式调用了基于回调的callback_login,然后设置当前
在等待的coroutine为自身,最后yield掉自己。在回调执行的时候,回调会resume那个上
次被yield掉的coroutine,这样就完美的支持了阻塞的语法并且还能够满足事件循环的约
束!能够重新整理程序的执行流程,这就是coroutine的强大之处。最奇妙的是,在
这个设计之中,回调中唯一会做的事情只有resume,而不是yield,这意味着**即使不修
改一行代码,现有的模型也可以完美支持这个模式**!
 
    可以看出将回调模式的函数改造成协程模式的函数是很简单的,我们甚至可以写一个
高阶函数来做这件事:
    
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                                        
 
    这样,上面的login函数就很简单了:
 
        server.login = coroutinize(server.login)
 
    看到Lua在表达复杂逻辑时的巨大优势了吗?coroutine机制同样也是可以支持函数重
入的:如果一个函数被调用多次,那么对应被调用的回调调用时,对应的那个coroutine
会被resume。至于如何实现,就交给读者作为练习了。提示:Programming in Lua这本书
已经说明了该如何去做。
 
    我们总结一下:
        1. coroutine无法穿越C边界是C语言的固有缺陷,Lua无法在保持其代码是Clean
           C的前提下完成这个impossible的任务。
        2. 那么,要支持这个特性,就只有要求C模块的编写者能采用CPS的方式编程了
           。当然Lua的代码可以完全不做任何修改。
        3. 而,coroutine很少需要在C函数内部yield(可能有实际场景会需要,但事实
           是在我所书写的上万行的Lua富coroutine的代码中,完全没有用到过这种策
           略)。
        4. 如果你能深入了解coroutine,你会发现即使coroutine无法在C内部yield,
           coroutine依然可以展现其绝大多数的威力。
        5. Lua本身的设计可以让Lua在表现极端复杂的逻辑关系时游刃有余。
 

2. 幽灵一般的 nil

 
    我不否认,在我刚刚学习Lua的时候,我的确被nil坑过很多遍。我们先抛弃掉luaJIT
关于NULL设计的问题(这个设计本身也是一种无奈,而且LuaJIT毕竟并不能完全继承Lua
作者对Lua的理念),先来看看nil究竟是什么——从nil中,我学习到了,在遇到坑爹特
性之前,先不要急着抱怨,想想为什么作者会设计这么坑爹的特性。要么作者是比你低能
的傻逼,要么这么设计就的确是有充分的考虑和不得已的苦衷的。这点你想到过吗?
 
    nil是一个表示“没有”的值。是的,就是真的“没有”,因此nil本身就是一个幽灵
——它除了表示“这里没有东西”以外,没有其他的任何含义!它不是None(None是一个
表示“空”的对象),它也不是NULL(NULL表示没指向任何地方的指针——总所周知指针
本身必定是有值的,哪怕那个值是NULL)。Lua的作者十分聪明的将“没有”这个概念也
引入了语言,并且还保持了语言的一致性:请问,将“没有”存入一个表里面,它如果不
消失,还能发生什么事呢?
 
    那么如何表示“空”或者“没有指向任何地方的引用”呢?两个办法,你可以存入
false,或者可以用下面这个巧妙的方法:
 
1
2
3
4
5
6
7
undefined = {}                             
-- 指定一个全局变量存在,但不指向任何地方:
a = undefined                              

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
tengine安装ngx_http_lua_module发布时间:2022-07-22
下一篇:
FreeSWITCHIVR中lua调用并执行nodejs代码发布时间:2022-07-22
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap