前 的个人资料miss your eye照片日志列表 工具 帮助

日志


11月3日

pcre正则表达式库的使用

对 于约定格式的字符串处理,假如需要对应的格式较多,而且随着需求变化成线性增长,那么最好从一开始就使用正则表达式,否则会演变成自虐行为。在 c/c++里可使用的库并不多,posix正则库在*nix上基本已经算是内建了,但是在mac上看man page,提及仍然是alpha阶段,不但性能不高,而且有一些bug,似乎从2004年开始就没有什么改进,而linux上没有这样的警告,不过在我草 草看了一遍pcre(perl compatible regular expressions)的man page之后,还是决定麻烦一下自己,给工程增加一个第三方依赖。

pcre吸引我的首要的好处就是支持utf8,我在工程里一直偷懒回避字符集问题,对于所有的字符串都视为utf8,工程所用到的xml,数据库字符集也 都设置为utf8,偷懒的结果就是许多分析串的代码都成了心病,内容无关的场合按字节处理也许不是问题,内容相关的场合,按字符处理就大大的麻烦,用支持 utf8的正则库分解字串正好可以消除一些隐患。

pcre的安装很简单,发行版linux上在线安装就可以,在fedora 8上,
# yum install pcre pcre-devel
即可。

mac上我没用port,直接下载的源代码编译安装了。configure时注意utf8支持默认是关闭的。

最基本的正则匹配过程分为两步,模式分析和匹配应用。
compile用于模式分析,exec用于匹配串。

举例:现有类似$MOVE#username#x,y#msg$的消息,其中x和y是整数,msg包含任意字符,那么模式为(^\$MOVE)#(.*)#([0-9]*),([0-9]*)#(.*)\$$,差不多了,来段代码看看


    pcre *re;
    const char *error;
    char *pattern;
    char *subject;
    unsigned char *name_table;
    int erroffset;
    int find_all;
    int namecount;
    int name_entry_size;
    int ovector[30];
    int subject_length;
    int rc, i;

    pattern = (char*) "(^\\$MOVE)#(.*)#([0-9]*),([0-9]*)#(.*)\\$$";
    subject = (char*) "$MOVE#robot#100,100#haha\ntest$";
   
    subject_length = (int) strlen(subject);

    re = pcre_compile(pattern,
        PCRE_UTF8,
        &error,
        &erroffset,
        NULL);

    if (re == NULL)
    {
        ...
    }

    rc = pcre_exec(re, NULL, subject, subject_length, 0, 0, ovector,     30);
    if (rc < PCRE_ERROR_NOMATCH)
    {
        printf("No match\n");
    }

    /* Show substrings stored in the output vector by number. Obviously, in a real
     application you might want to do things other than print them. */

    for (i = 0; rc == 0 && i < 10; i++)
    {
        char *substring_start = subject + ovector[2 * i];
        int substring_length = ovector[2 * i + 1] - ovector[2 * i];
        printf("%2d: %.*s\n", i, substring_length, substring_start);
    }

运行结果:
No Match

哈哈,我不是逗你玩,生活需要调剂嘛。目标串含有一个换行符是个问题,默认来说匹配是执行到行尾,有两个选择,一个是给pcre_compile的第二个 参数合并上PCRE_DOTALL,另一个是在模式前面加上选项:'(?s)(^\$MOVE)#(.*)#(\d+),(\d+)#(.*)\$$'

(?s)就是强制匹配到字串结尾。一般默认是多行匹配(?m),行分隔符在不同的系统上有着倔强的不同,windows是CRLF,苹果是CR,linux是LF,根据不同的系统,只好分别设置选项。
(*CR)对应PCRE_NEWLINE_CR
(*CRLF) - PCRE_NEWLINE_CRLF
(*LF) - PCRE_NEWLINE_LF
(*ANYCRLF) - PCRE_NEWLINE_ANYCRLF
或者(*ANY) - PCRE_NEWLINE_ANY

我个人还是倾向于把选项写到模式中,我是把模式都放在xml中的,这样自然比写死在代码里来得灵活。要注意(*)这样的选项需要出现在模式的最前面,而(?ms)随便放了,这样都行:'(^\$MOVE)#(.*)#(\d+),(\d+)#(?m)(.*)\$$'


我把[0-9]*换成\d+了,意思基本相同,不同的在于*和+,前者允许零匹配,后者至少得有一个。

加了(?s)了,再试……

 0: $MOVE#robot#100,100#haha
test$
 1: $MOVE
 2: robot
 3: 100
 4: 100
 5: haha
test


好了

ovector中是每个字串的起始位置和结束位置,最前面那一对是匹配到的全长,假如输入串完全满足模式,那么第一对就是输入串的开始与结尾了。

试试吧,程序员都会爱上正则表达式这玩意的。
11月2日

南瓜头

水果刀不好用……
11月1日

c/c++结合lua制造NPC

lua是大虾推荐的脚本引擎,高效小巧,很容易和c语言交互。

我的工程起初的设计比较粗陋,并没有考虑太多扩展性,在面对增加task子系统的需求时,对于到底是保证时间还是更改结构颇为犹豫,最后还是狠下心决定编写一个扩展task子系统,代价就是花了3个通宵。 当时只是粗略的看了一下lua,边翻手册边google连抄带试的做,很多lua特性,比如关于coroutine(lua里一个有趣的伪并行机制),以及结合os thread的方式,完全没能理解,于是采取了一个低效但安全的方式回避了问题。

由于几乎全部的用户数据、状态都在server里维护,我将通信模块处理生成的事件中的一部分再次转译为一组消息派发给另外一套处理系统,这套处理系统可接受消息处理接口的注册请求,将这些接口实例组织成一个响应链,传递并处理消息,而可选的,这些实例可以实现为一个队列,转为异步处理消息。

task子系统实现了同步消息处理接口,每个task编写一个对应的lua脚本,其中除了task的各种定义数据之外,就是一组消息响应函数,根据需要实现。c则导出一些函数供lua调用,用来访问用户的状态和数据。

c通过lua_State指针与lua交互,lua_State以下简称lua状态,不考虑其实现细节,可以看作是一个栈,lua里一切都是对栈上的元素进行操作,而lua本身为了尽量减少系统依赖,并没有增加同步机制,所以多线程环境里,只能外部对lua状态加锁,或者给每一个线程创建一个单独的lua状态,不过虽然消息处理系统的线程数量固定,但线程池本身与task系统没什么关系,考虑到目前task脚本都很小,数量短期内也不会增加太多,而玩家是否拥有一个task,是在进行响应之前就判断过的,加上task中的响应函数本身是无状态的,并不参与维护任何用户数据和状态,所以我最后对于每个task消息请求,都加载了一次脚本。每次调用的时间开销小于1ms,可以接受。

实现NPC系统就不能这么偷懒了,我希望npc能够自己维护自己的状态,不需要c来记录,这样npc脚本就必须保持运行。c加载运行lua脚本之后,如果脚本保持运行状态,c线程将被阻塞,为每一个npc创建一个线程代价就太大了点儿。没办法,只能啃coroutine了,不过这次就犯不着玩通宵了。

coroutine是一种伪并行机制,它以一个函数为参数建立一个具有一般线程特性的lua状态,拥有自己的数据和栈,同时又共享全局数据,通过yield暂停,resume运行,在一个脚本里可以创建多个coroutine,不过并不存在真正的同时运行,仍然需要自己编写切换线程的逻辑。那么假如在c中可以控制coroutine,使得脚本在yield之后返回c线程,再从c线程调用resume恢复coroutine,如此反复,就可以虚拟出帧循环时间片,逐帧响应的npc就能够以投骰子的方式决定自己的行为。

非常幸运,lua具有这样的功能。

/* C code - start */
lua_getglobal(lua, "coroutine");
lua_pushstring(lua, "create");

lua_gettable(lua, -2);
lua_getglobal(lua, "thread_body");

lua_call(lua, 1, 1);

lua_State* co = lua_tothread(lua, -1);
lua_setglobal(lua, "co");
......
for(;TRUE;)
{
    while(npc)
    {
        lua_resume(npc->co, 0);
        npc = npc->next;
    }
    // wait for next frame
}

/* C code - end */

--------------------------
-- lua code - start
--------------------------

function on_frame()
    print ("Moving or Talking or Attacking ... ")
end

function thread_body()
    while 1 do
        on_frame()
        coroutine.yield()
    end
end

--------------------------
-- lua code - end
--------------------------

这样一个c线程就可以轮询数个npc了。

从lua_tothread这个函数就可以知道co这个状态是被lua定义为thread类型的,而创建一个thread不是只有coroutine.create能做,C API lua_newthread也可以,睡觉了,下回继续说吧。