2
13
2014
1

Lua的函数调用和协程中,栈的变化情况

 
 
 
1. lua_call / lua_pcall
 
对于这两个函数,对栈底是没有影响的——调用的时候,参数会被从栈中移除,当函数返
回的时候,其返回值会从函数处开始压入,可以通过新的栈顶减去旧的栈顶得到返回值数
量。
 
2. lua_yield
 
对 Lua 函数来说, yield相当于只是在调用一个函数,对C函数也是这样。yield的参数
是返回值的数量,这些返回值会被返回给resume,见下。
 
2. lua_resume
 
resume是最复杂的一个函数:首先,第一次调用的时候,它相当于是个lua_call,也就是
说,被resume的那个函数只能看到自己的参数在栈顶,更低的就看不见了。其次,当函数
被yield之后,resume返回,这时resume的栈顶只有yield返回的参数,其他的栈元素是看
不到的。
在这个基础上,coroutine的状态是LUA_YIELD,如果需要继续执行,就需要再次 resume
,这里就有一些微妙的区别了。在这种情况下,resume的nargs参数是无用的——也就是
说,无论你传递的是多少,所有栈顶的元素都会被返回给yield,也就是说,如果需要在
返回之前清除掉栈,那么就需要你自己手动去清除,然后再resume。
 
下面是测试的示例代码:
 
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>

int luv_dumpstack(lua_State *L) {
    int i, top;
    printf("-------------------\n");
    printf("dumpstack: L=%p\n", L);
    top = lua_gettop(L);
    printf("top: %d\n", top);
    for (i = 1; i <= top; ++i) {
        printf("[%d][%s]: %s\n",
                i,
                luaL_typename(L, i),
                luaL_tolstring(L, i, NULL));
        lua_pop(L, 1);
    }
    printf("-------------------\n");
    return top;
}

static int cont(lua_State *L) {
    printf("stack after yield\n");
    luv_dumpstack(L);
    lua_pushinteger(L, 21);
    lua_pushinteger(L, 22);
    lua_pushinteger(L, 23);
    return lua_gettop(L);
}

static int test_yield(lua_State *L) {
    printf("stack before yield\n");
    luv_dumpstack(L);
    lua_pushinteger(L, 1);
    lua_pushinteger(L, 2);
    lua_pushinteger(L, 3);
    lua_pushinteger(L, 4);
    lua_pushinteger(L, 5);
    return lua_yieldk(L, 2, 0, cont);
}

static int test_resume(lua_State *L) {
    lua_State *L1 = lua_newthread(L);
    lua_pushinteger(L1, 11);
    lua_pushinteger(L1, 12);
    lua_pushcfunction(L1, test_yield);
    lua_pushinteger(L1, 13);
    lua_pushinteger(L1, 14);
    lua_pushinteger(L1, 15);
    printf("stack before resume\n");
    luv_dumpstack(L1);
    printf("resume: %d\n", lua_resume(L1, L, 3));
    printf("stack after resume\n");
    luv_dumpstack(L1);
    lua_pushinteger(L1, 24);
    lua_pushinteger(L1, 25);
    printf("stack before second resume\n");
    luv_dumpstack(L1);
    /* XXX notice even we pass 2, all values in stack (4,5,24,25)
     * will passed to coroutine */
    printf("resume: %d\n", lua_resume(L1, L, 2));
    printf("stack after second resume\n");
    luv_dumpstack(L1);
    return 0;
}


int main(void) {
    lua_State *L = luaL_newstate();
    lua_pushcfunction(L, test_resume);
    lua_call(L, 0, 0);
    lua_close(L);
    return 0;
}
/* cc: libs+='-llua52' */

 

Category: 未分类 | Tags:
11
20
2013
1

在Lua里面声明小数组的最好方法是什么?

在很早以前,就看到“闭包比表要快”的这么一个言论。一直没有验证过,只是心里就这么觉得了。所以自己第一次写的Lua网游,大量利用了闭包,最后估计还是有很严重的内存问题……在我的对象模型里面,对象构造函数通常是这样的:

function Object()
   local t = {}
   local object_state1
   local object_state2
   -- init ...

   -- methods
   function t:foo(...) return ... end
   function t:bar(...) return ... end
   -- return object
   return t
end

这个设计可以保证,在访问对象的函数的时候,速度可以达到最快——因为没有元表查询。但是,这样做恰好就违背了这样的原则:“在设计的初期,不要过早地考虑优化”。是的,因为任何优化都是有代价的,这里的代价理所当然就是内存了。

另外,在Lua-5.1中,我后来自己测试的结果是,表貌似还是比闭包要快一点,关键点是,这样貌似占用内存还小很多,自从这么摆了一道以后,反正至少对于“看见大括号就有点恐惧内存分配”的心理上是好过多了。不过至少对于闭包的大小和速度什么的心里头反而就没底了。

今天突然想到了一个叫做lua-vararg的lua库。这个库可以对vararg进行包装,提供对用户来说“比较自然”的vararg的体验——说白了,这就是用vararg去实现了元组(tuple)嘛。这个库很早以前就知道了,我还自己亲自改过,但是性能到底怎么样呢?心里没底,所以决定今天写个伪测试来看看效果。

首先,我们看一下Lua源代码里面表和闭包的描述:

typedef struct Table {
  CommonHeader;
  lu_byte flags;  /* 1<<p means tagmethod(p) is not present */
  lu_byte lsizenode;  /* log2 of size of `node' array */
  struct Table *metatable;
  TValue *array;  /* array part */
  Node *node;
  Node *lastfree;  /* any free position is before this position */
  GCObject *gclist;
  int sizearray;  /* size of `array' array */
} Table;
#define ClosureHeader \
	CommonHeader; lu_byte nupvalues; GCObject *gclist

typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;
  TValue upvalue[1];  /* list of upvalues */
} CClosure;


typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;
  UpVal *upvals[1];  /* list of upvalues */
} LClosure;


typedef union Closure {
  CClosure c;
  LClosure l;
} Closure;

OK,代码有了,那么第一个问题是:表和闭包分别占多大的大小呢?

额= =这个问题不好搞啊,看起来好复杂的样子,还得考虑对齐……额,不好搞的话,直接写个程序不就可以了吗?

#include "src/lobject.h"

#include <stdio.h>

int main(void) {
    printf("size of Table: %d\n", sizeof(Table));
    printf("size of Closure: %d\n", sizeof(Closure));
    return 0;
}

恩,输出结果是32和24……(别打偶……)

好吧,闭包居然比表要小!恩,这是很正常的。而且,这里的表可是“裸表”哦,一点数据都没有的,而这个被声明的闭包是默认带上了一个upvalue的!

那么,对于有十个元素的表和闭包,大小又是怎么样的呢?注意到表保存数组元素用的是TValue指针,而闭包里面的upvalue(主要指C闭包)也同样是TValue,我们只需要分别给表和闭包的大小增加10个TValue的大小即可——额,闭包只需要加9,因为前面已经有个元素了,最后得到的大小是:112字节和96字节……恩~

好了,那么在大小方面,的确闭包(特指C闭包)是保存小数组的最好途径了,那么,除开大小,在速度上面,它们之间有什么变化吗?我们继续测试一下!首先声明一下,这里的测试对于真实的环境是没有意义的,通常来说,我们并不需要考虑像分配小数组之间的效率问题,这里的测试,只是在于检验“闭包比表快”这个说法罢了,这样的检验本身,可以当作是使用vararg模块的依据,对做出某些设计决策是有好处的。

我们准备测试四种情况下的速度情况

  1. 用C API创建一个表,向其中压入10个数字
  2. 用C API创建一个闭包,其中有10个upvalue
  3. 用Lua代码创建一个表,其中有10个数字
  4. 用Lua代码创建一个闭包,其中有10个upvalue

下面是测试结果:

test
c table time: 2.35s
c table memory: 5724
c closure time: 0.20s
c closure memory: 6020
lua table time: 0.40s
lua table memory: 5836
lua closure time: 1.51s
lua closure memory: 4556
Hit any key to close this window...

我们来分析一下这个数据,最简单的分析方法是排序,对于时间的顺序是c closure < lua table < lua closure < c table,对于内存剩余的顺序是lua closure < c table == lua table < c closure。

好奇怪的结果!!我们试着来分析一下吧!看起来内存里面有一项是相同的:即用c和用lua创建表,占用内存的大小是近似相同的。而用C创建闭包,占用内存最大,但是也大不了多少。这里面是什么情况呢?我们关闭一下垃圾回收试试?对了!原来是这样……关掉垃圾回收以后,内存的占用非常非常夸张,原来,这是垃圾回收以后的结果(顺便说一下,关闭垃圾回收,所有的测试都变慢了,不过相对结果不变)。

那么,内存的意义上就不大了。只能说是垃圾回收的策略不同罢了。我们来分析一下时间问题好了。

首先,这个数据实在是太奇怪了了:如果说C的API比Lua代码快,那么为什么c table是最慢的呢?如果说闭包比表快,那么为什么lua闭包不如lua的表呢?这是怎么回事呢?

很容易发现问题的是lua闭包,这是倒数第二慢的……因为Lua5.2出了一个新功能:在创建lua闭包的时候,会和之前缓存的进行比较,看看是新的闭包还是以前的,因为我们创建的闭包都是新的函数调用,显然和以前闭包的upvalue不可能一样,因此这个比较始终是会失败的……看来也只能是这个地方会占用时间了。当然了,关联upvalue、关闭upvalue等等都需要占据时间。lua闭包比较慢几乎是可以预见到的了。这一点我们不奇怪。

奇怪的是,为什么用C API写的新建表比lua代码还要慢呢?注意到,Lua代码是不需要再创建新的“整数”的,它们是直接从常量表(K表)载入的,所以pushnumber会拖慢一点速度,其次,lua_rawseti会比luaH_setint多做一些检查,除了这两点以外,还有一个很容易忽视的地方是API调用开销:因为Lua是在DLL里面的,因此API调用相对较慢,如果静态链接的话,对其他测试来说,结果影响不大,但是c table测试瞬间就快了20%——也许是因为每次推入新的包,c table测试比别的方法多了10个C API的原因吧……Lua里面没有一次给表设置多个值的方法,必须一个一个地设置,每次设置都必须压入新值,所以即使是相对较快的lua_rawseti,也架不住每次设置的两次API啊……当然,从这一点上也可以看出来,我们的测试是很不合理的,因为即使是API效率,都可以极大地影响测试结果。当然这里需要说明的是,即使是将API效率的原因去除,c table仍然是最慢的。

然后,c cloure和lua table就比较符合我们的期望了。看来的确是闭包会快一点啊……

这里只比较了创建的效率,对于取值的效率,大家可以自己去比较。但是从这里可以看出来,如果希望交换大量小数据块,那么closure的确是一个合理的选择:它体积较小,没有多余的功能(比如表支持的哈希查找或者增加大小或者独立的元表等等)。作为需要快速保存少量数据,lua-vararg的确是一个很好的选择——另外,尽量选择C实现的版本,如果无法使用C库,那就还是用表来实现吧……这样比较快……

 

最后,附上我自己写的vararg的C实现:https://gist.github.com/starwing/5893607

Category: Lua | Tags:
1
3
2013
34

理解 Lua 的那些坑爹特性

 

按:最近看到了依云的文章,一方面,为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比事件回调机制
有着 革命性的优秀之处。
 
    你的例子是这样的:
 
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')                 
 
    先说一下,将模块放到全局变量里通常不是一个好主意。所以第一行如果写成
local c = require 'c'
    就更好了。
 
    其他的地方倒是没什么需要修改的了。
 
    再看看你的C模块代码:
 
#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模块应该怎么写:
 
#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;                                                           
}                                                                     
    
    现在,你的例子可以完美运行了:
 
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呢?可能是这样的:
 
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提供了解决这个问题的一个极端优雅的办法。我们想想最
优雅的设计会是什么样子的:
 
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的!
 
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,这意味着**即使不修
改一行代码,现有的模型也可以完美支持这个模式**!
 
    可以看出将回调模式的函数改造成协程模式的函数是很简单的,我们甚至可以写一个
高阶函数来做这件事:
    
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,或者可以用下面这个巧妙的方法:
 
undefined = {}                              
-- 指定一个全局变量存在,但不指向任何地方: 
a = undefined                               
-- 判断这个全局变量是否不指向任何地方:     
if a == undefine then ... end               
-- 彻底删除这个变量:                       
a = nil                                     
 
    看!Lua可以灵活的控制一个变量的实际作用域!让一个变量真正的凭空消失掉!这
一点即使是C或者C++都是做不到的(变量只有在作用域外才会消失),这也是Lua强大的
灵活性的一个佐证。
 
    千万不要弄错了,nil代表“没有”,它不代表“空”,也不代表“没有被初始化的
值”,它只是没有而已。因此它就应该是幽灵般的。这是Lua的语言设计一致性带来的优
势,而不是坑爹的特性。如果说学不会的特性就是坑爹的特性,那么是不是C语言的指针
也是坑爹的特性呢?
 
    其次,各种库定义的null值,本质上是代表微妙但不同的东西。仔细地体会其中的不
同,能让你更得心应手的使用那些库。如果你在这些库的互操作上感到困扰,请给库的作
者写邮件抱怨:Lua有一个很热情友好的社区!
 

3. 没有continue

 
    是的,Lua一直不肯加入continue。为什么呢?因为repeat until。而为什么强调“
不添加不必要的特性”的Lua作者会舍弃掉“那么常见”的continue,却保留不那么常见
的repeat呢?是Lua的作者傻么?
 
    不是。这是经过仔细设计的。我先告诉你答案,再仔细地分析:事实上,在加入
repeat以后,continue是逻辑上不可能实现的,而repeat语句实现了一个用其他的特性完
全无法取代的特性。
 
    注意看repeat的结构:
 
        repeat <block> until <exp>
 
    问题就在<exp>上了。Lua规定,<exp>是在<block>的作用域下计算的!这也就意味着
local a = true           
repeat a = false until a 
    会是一个死循环!看到了么?这种“表达式在一个block内的上下文计算”的循环特
性,是无法被其他任何特性模拟的!这种特性导致Lua作者为了语言的完整性,不得不将
repeat语句添加入Lua语言。
 
    那么continue呢?花开两朵,各表一枝,我们先介绍一下Lua5.2的新特性:goto语句
。lua5.2支持跳转语句了!看看你之前的那个例子吧,在没有continue的情况下,我改如
何写那个循环呢?答案是这样:
 
for line in configfile do                 
    if string.sub(line, 1, 1) == '#' then 
        goto next                         
    end                                   
    parse_config(line)                    
    ::next::                              
end                                       
 
    看上去有点小题大做,连goto都用上了!呵呵,其实还有一个细节你不知道哦,在
Lua5.2里,甚至连break都没有了!break语句只是goto break的一个语法糖而已。而
break标签会被自动插入到你的源代码中。
 
    那么,你可能会问了,事已至此,为什么不也加个continue的语法糖呢?毕竟break
都是语法糖了!好,我们试着在repeat里面用一下我们手写的“continue”:
 
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                     
 
    这个例子造的有点刻意了,但是至少也有continue的意思了吧!好,现在执行一下—
—出错了……
 
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...                                                      
 
    这是怎么回事??
 
    我们知道,在C里面,goto可以跳入任何地方,包括跳过变量的初始化,这会导致一
个变量可能在用的时候,是未初始化的。Lua为了避免这样的错误,要求goto不允许跳入
一个变量的作用域内部!而看看我们的代码,goto跳入了变量j的内部!
 
    在这种情况下,是根本没有办法continue的!
 
    这就是Lua作者不提供continue的真实意思。continue是根本不可能实现的。因为从
完整性考虑,必须提供支持作用域扩展的repeat语句(记得C/C++对for的三个子语句中的
作用域规定么?),而continue可能会在repeat的条件表达式中用到的变量还没有初始化
的时候,就开始条件表达式的计算!这是不允许的!
 
    现在我们知道了:
        1. 不是Lua作者故意不提供continue,而是continue和当前的一个关键特性冲突
           了,导致continue语句完全无法实现。
        2. 为了弥补这个问题,Lua作者为Lua加入了goto语句。然而该有的限制仍然存
           在,但是编译器会为你检查这个限制!
        3. 所以,现在在Lua里的大多数情况下,你仍然能使用你自己手工打造的
           continue,而且功能更为强大(labeled break,labeled continue都是可以
           模拟出来的)。
 
    关于goto语言可能的坏处以及作者的考虑,请参考Lua作者的novelties-5.2.pdf文件。
 
    对了,还要说一句:有读者可能为问:既然Lua已经把break做成语法糖了,为什么不
把continue也做成语法糖呢?如果遇到不合法的情况,直接出错不行么?
 
    这个问题我也没想明白。也许会有自己的原因吧,不过如果把这个想法当作Lua的坑
爹设计也未尝不可以,不过其“坑爹指数”已经大为降低了。
 
 

4. 错误信息的表达

 
    我只想说一句话:其实大多数在预见到会对错误进行处理的场合里面,错误的返回方
式其实并不是nil, errmsg,而是nil, errmsg, errno。别的你懂了。
 

5. 下标

 
    参看 novelties-5.2.pdf,说的非常明白了。
 

6. 提前返回

 
    这是一个语法问题,事实上return语句不跟着end的话,那么编译器就根本无法编译
return语句了。这是Lua“行无关语法”的一个必然折衷,我开始也不爽,但事实是,在
我数万行的Lua开发中,除了测试必要要注释一部分代码以外,我根本没用过do return
end这种表达——至于为什么,你实际开发一下就知道了:因为这种代码一定会导致完全
无法被执行到的死代码。
 

7. 方法调用

8. 面向对象

 
    这两点恰好就是Lua的优势啊!!有时间我会写一篇文章来讨论。这实际上是Lua能以
比其他语言小巧灵活得多地去处理复杂逻辑的一个必然原因了。这里只说一点:Lua5.2中
,表所具有的元方法已经和C API能处理的完全一样多了。纯Lua已经不必对着__len和
__gc而望洋兴叹了。
 
    关于这一点,MikePall(LuaJIT实现者)还专门和Lua作者吵了一架,因为让表支持
__gc会导致luaJIT的jit编译非常难写= =||||
 

9. 结论

 
    我开始学习Lua的时候,也几乎得到了跟你一样的结论。然而,在长达两年的Lua开发
中,我逐渐认识到了Lua的美,认识到了Lua实现的优雅和严谨。现在如果有新手想学习C
语言开发的诀窍和技巧,我通常会建议他去拜读Lua的C实现源代码。Lua的实现太优雅了
。而Lua的设计也凝聚着作者的一点一滴的心血。Lua精准绝妙的设计是Lua强大的表达能
力的表现。继续学习下去吧,我向你保证,你一定会发现,Lua实际上脚本语言里面表达
能力最强,概念最统一,设计最优雅的语言了。Lua无愧脚本语言之王!
 
    
 
 
    
 
Category: 未分类 | Tags: Lua
10
26
2011
1

metalua - 带语法的 scheme

 

 
    最近项目稳定下来,在开发资料片的间隙,我看了看传说中的 metalua。
 
    其实很早以前我就关注了 metalua 了,可是一直都没有文档。所以感觉很混乱。不
过最近在 lua 的邮件列表有人问了关于 metalua 的问题,所以我找了找相关的信息。发
现这个东西实在是强啊。
 
    metalua 是一个用 lua 语言编写的,带元编程扩展的 lua 编译器。它分为两个部分
, mlp 和 bytecode。 其中 mlp 是在 gg (Grammer Generator)的基础上将 lua 的源
代码转换成语法树的库,而 bytecode 则是将语法树解析成 lua 的字节码。metalua 最
大的特点就是在 mlp 中加入了元编程的扩展。即可以在生成语法树的同时,执行一些代
码。这一点很像 Lisp 的宏机制。但是比起 Lisp,metalua 将编译和执行划分成了两个
完全不相关的部分。因此在运行时是不需要带上庞大的运行时库的。 metalua 生成的字
节码可以在标准的 lua 环境中执行。这就解决了 Lisp 发布的执行文件体积巨大的问题
。metalua 的原理是,在编译的时候,你可以直接写出语法树,嵌入待编译的程序。这样
你可以做到 lua 的语法不能做到的一些事情。比如 goto 等等。而且也可以实现宏机制
。宏的好处是它是在编译的时候展开的。因此可以避免运行时效率问题。我们来看一个例
子,这里用到了 metalua 的 dollar 库。这个库的作用是,所有以 $ 开头的标识符都会
被当做一个宏看待, metalua 会去编译期的一个表 dollar 中查找这个标识符。并将其
对应的语法树嵌入到调用的位置,下面是一个 max 宏,用于获得两个值中的较大者:
 
-{block:
   require 'metalua.dollar'
   dollar.max = |a, b| `Stat { +{block:
       local a, b, res = -{a}, -{b}
       if a > b then res = a else res = b end
    }, +{res} }
}

print($max(100, 2))
 
    首先最引人注目的是 -{ ... } 语法结构。这就是元编程的符号。所有在 -{ ... }
中出现的语句是在编译期,而不是运行期被执行,而它在执行时的环境是编译时环境,包
含了所有的编译时工具,而不是运行时的环境。换句话说,-{ ... } 中的代码是根本“
看不见”你在正常的原代码中声明的函数、变量的。
 
    首先,我们在编译期载入了 metalua 的 dollar 库。这个库修改了编译器,在其中
加入了对 $ 的支持。其次,我们在 dollor 表(由 metalua.dollar 创建)中加入了一
个条目:如果发现名为 max 的宏,则调用我们的函数。参数则是 max 宏的参数,注意,
宏的参数一定是语法树。也就是说,这里的参数 a 和 b 实际上是两颗语法树,而不是参
数执行以后的结构。
 
    |args...| expr 是 metalua 的扩展。相当于 function(args...) return expr
end,上面的函数也可以写成:
 
function(a, b)
   return `Stat { +{block:
        local a, b, res = -{a}, -{b}
        if a > b then res = a else res = b end
   }, +{res} }
end
   
    当 max 宏被找到时,该函数被调用,参数是 max 宏的参数(即两颗语法树),我们
返回了一个新的语法树。这里的 `Stat { ... } 也是 metalua 的扩展,是
 
{ tag='Stat', ... }
 
    的语法糖,由此可以看出,所谓的语法树实际上是 Lua 的一个表。表里有个特殊的
项 tag 表示语法树节点的种类,而表中的各个项则是语法树中的各个子项。
 
    在 `Stat 语句语法树中,我们采用了 +{ ... } 结构,不同于可以在程序中任意使
用的,类似于语句的 -{ ... } 结构, +{ ... } 结构更像是个函数:它接受一系列的“
正常”的 lua 语句,并将其转换成语法树,比如, +{ f(a + b) } 会返回这样一个表:
 
{
   tag = 'Call',
   {
        tag = 'Id',
        "f"
   },
   {
        tag = 'Op',
        "add",
        {
           tag = 'Id',
           "a"
        },
        {
           tag = 'Id',
           "b"
        }
   }
}
 
    这个表表示出了 f(a + b) 这个简单语句的结构,而编译的过程则是遍历这个表,为
表的每一项生成合适的字节码。
 
    metalua 中,每一种不同的语法树的结构都不一样,比如,Stat 树的第一项是一个
列表,表中的每一项是一个语句的语法树。Stat 还有一个扩展。如果 Stat 有第二项的
话,则这颗语法树可以作为表达式使用,而这个表达式的值则是第二个项所代表的语法树
,而且这第二项的作用域是在 Stat 内的。这意味着你可以返回 Stat 中的局部变量的值
 
    最后要说的是在 +{ ... } 中的 -{ ... },+块用于将 lua 代码转换成语法树,那
么+块中的-块则用于将某个已有的语法树“嵌入”到生成的语法树里面去,这有点像
Lisp的 ` 和 ,@ 的功能。
 
    所以,这个函数返回了一颗人工构造的语法树。首先分配了三个局部变量,然后用
if 找到较大的那个,赋值。最后指定整个块返回 res 变量的值。这颗语法树是直接被嵌
到程序里面去的,这意味比如你写 print($max(100, 2)),实际生成的代码是这样的:
 
local a, b, res = 100, 2
if a > b then res = a else res = b end
print(res)
 
    有人说, Scheme 实际上是“没有语法的 Lua”,虽然我很不赞同这句话,但是从刚
才的简短介绍中可以看出, metalua 的确是可以当之无愧地称之为“带着语法的 Scheme
”了。metalua 可以在编译期执行计算、允许编译时修改编译器等等特性,可以完成许多
十分强大的功能。这种能力用在 DSL(领域描述语言)上是威力强大的。可以说,Lua 带
上元编程,就为本来就很简洁强大的 Lua,插上了一双腾飞的翅膀。
    
 
Category: 未分类 | Tags:
1
4
2011
0

2010工作计划

现在太晚了,我稍微写写,明天还要上班= =|||

现在做游戏引擎,但是我想深入一下,既然公司不打算做三维(其实我目前也没有做三维的打算),所以近一段时间的目标是这样的:

1. 了解各个手机平台的native开发。

2. 了解目前成熟的各种2D图形渲染引擎。

3. 了解物理引擎。

4. 了解脚本语言(主要是lua)。

第四个是很简单的,重点在前三个。手机平台的开发,目前盯在了Symbian,虽然这个系统不咋地,但是毕竟是目前我们的工作重心。目前的要点是实现一套自己的自绘控件系统。

而做游戏,图形渲染引擎不行是不可以的。因此这是肯定需要了解的。目前的目标是Skia,Anti-Grain Geometry 十分有研究价值,可是不大可能用在手机上面。所以Skia是重点。

物理引擎肯定是box2d了。

目前的目标是,在今年开发《宗师》的同时,在年中完成GameFramework2.0,并且在这个基础上完成《宗师》的移植。

Category: 未分类 | Tags:
1
1
2011
1

2011新年计划

额……我果然还是比较懒啊…………

今年还是有很多事情要做的。首先,必须得配置Vim能够自动推送到这个页面发送文章,否则我一定会懒得写文章的= =

其次,关于scheme,关于haskell,还有ruby和python还有lua……饶了我吧……我一定认真学习——在我有时间的时候。

KBTiller寄给我了《狂人C》我得找机会看完,不然太对不起他了。

kitten语言在设计中,我希望尽量能将其设计为简单的LALR(1)语法,不超过100个规则,我想这个难度会有点高= =

VimE要等到kitten完成以后才能继续开发。

至于公司的工作……随便吧。我得找一个普适的游戏引擎,现在看来love不错。

总的来说就这么多吧。明年的目标:函数式,网络,语言设计!

Category: 未分类 | Tags:
12
15
2010
92

continuations初练:序列生成器

刚刚注册这个空间的时候,看到了lispor的博客,感叹终于找到一个lisp的同好了,于是拿出当初翻看 Elisp 的 Info 的劲头,看完了他的 Scheme 笔记。实在是很不错呀!

不过,看到 continuation 就卡住了,当初就是因为这个原因没有继续学习 Scheme 的,觉得什么 tail position 什么的太晦涩了……不过顺次看到 lispor 指出的教程《Teach Yourself Scheme in Fixnum Days》,发现这个教程实在是很不错——这不,才一会儿时间基本上懂得了 call/cc 的原理了。

作为练习,我自己写了一个 range->generator 函数,调用函数得到一个生成器。首先给出源代码:

 

; range->generator 函数,生成一个生成器,每次调用生成器,给出从 i 开始
; 直到 j 的一个数字。
;
; 测试样例:
; > (define gen (step->generator 1 3))
; > (gen)
; 1
; > (gen)
; 2
; > (gen)
; 3
; > (gen)
; ()
; > (gen)
; ()
; 

(define (step->generator i j)
  (letrec ((caller '())
           (steper
             (lambda ()
               (let loop ()
                 (cond ((= i j)
                        (set! steper
                          (lambda () '()))
                        '())
                       (else
                         (call/cc
                           (lambda (k)
                             (set! steper k)
                             (caller i)))
                         (set! i (add1 i))
                         (loop)))))))
    (lambda ()
      (call/cc
        (lambda (k)
          (set! caller k)
          (steper))))))

详细的解释嘛……我还是有点懒,下次再说吧~~

这个代码有没有什么需要改进的地方呢?希望 lispor 大大能给一点意见~~~

Category: Scheme | Tags:
12
14
2010
3

额……新的博客……

恩,今天主要是看见依云的博客了,心血来潮就注册了这里,其实感觉真挺不错的。这里以后就写关于 Vim 和 Lua 的东西吧。

 

其实在很多地方都有博客了,但是自己就是懒,这次估计也是会懒,不过,只要有Vim或者Lua的心得都会贴这里的哦~~

 

 

require "std"
print{"hello", "world"}

 

Category: 未分类 | Tags:

Host by is-Programmer.com | Power by Chito 1.3.3 beta | Theme: Aeros 2.0 by TheBuckmaker.com