分享

lontoken

 quasiceo 2014-01-10

(转载自个人新博客:http://www.

A template class for binding C++ to Lua

英文原文:A template class for binding C++ to Lua

最近在研究C/C++和Lua的交互问题,顺便看了下luna,自己尝试着翻译了此文,以供分享,初始翻译,权当练习。

摘要

此文介绍了一种将C++类绑定到Lua的方法。Lua没有直接提供此方法,而是通过底层的C接口和扩展机制来实现。我所描述的方法使用了Lua的C 接口、C++模板和Lua的扩展机制,构建了一个小巧、简单且高效的提供类注册服务的静态模板类。这个方法对你的类只有一个小小的要求,即只有签名为 int(T::*)(lua_State*) 的成员函数能被注册。但是,正如我将要展示的,这个限制也能被克服。The end result is a clean interface to register classes, and familiar Lua table semantics of classes in Lua。此处描述的解决方案依赖于一个我命名为Luna的模板类。

问题

Lua的接口的设计,不能注册C++类到Lua中,只提供了注册签名为 int()(lua_State*) 的C函数。实际上,这是Lua支持注册的唯一C数据类型。为了注册其它类型,你需要使用Lua提供的扩展机制,如tag methods、closures等。为了创建注册C++类到Lua的方案,必须使用这些扩展机制。

方案

此方案主要有4个元素:类注册、对象实例化、成员函数调用和垃圾回收。

类注册是通过以类的名字注册一个表构造函数(a table constructor function with the name of the class)。表构造函数是一个在lua_State中注册一个表的静态模板函数。

注释:静态类成员函数是和C函数相兼容的,如果它们的签名相同,则我们可以在Lua中注册它们。下面的代码片段是一个模板类的成员函数,T 是待注册的类。

 1 static void Register(lua_State* L) {
 2     lua_pushcfunction(L, &Luna<T>::constructor);
 3     lua_setglobal(L, T::className);
 4 
 5     if (otag == 0) {
 6         otag = lua_newtag(L);
 7         lua_pushcfunction(L, &Luna<T>::gc_obj);
 8         lua_settagmethod(L, otag, "gc"); /* tm to release objects */
 9     }
10 }

对象实例化是通过passing any arguments the user passed to the table constructor function to the constructor of the C++ object,创建一个代表对象的表,注册一些类的成员函数到表上,最后将表返回给Lua。对象的指针做为userdata保存在表中,其对应的索引为 0。成员函数的序号做为闭包的值保存在所有函数中。More on the member function map later。

 1 static int constructor(lua_State* L) {
 2     T* obj= new T(L); /* new T */
 3     /* user is expected to remove any values from stack */
 4 
 5     lua_newtable(L); /* new table object */
 6     lua_pushnumber(L, 0); /* userdata obj at index 0 */
 7     lua_pushusertag(L, obj, otag); /* have gc call tm */
 8     lua_settable(L, -3);
 9 
10     /* register the member functions */
11     for (int i=0; T::Register[i].name; i++) {
12         lua_pushstring(L, T::Register[i].name);
13         lua_pushnumber(L, i);
14         lua_pushcclosure(L, &Luna<T>::thunk, 1);
15         lua_settable(L, -3);
16     }
17     return 1; /* return the table object */
18 }

不像C函数,C++成员函数需要类的对象来调用。成员函数的调用是通过thunks函数,它由一个对象指针和成员函数指针来做实际的调用。成员函数 指针是保存在函数的闭包值里,对应成员函数的哈希表中,对象的指针是表的序号0对应的值。注意,在Lua中所有的类函数都是通过下面的函数来注册。

1 static int thunk(lua_State* L) {
2     /* stack = closure(-1), [args...], 'self' table(1) */
3     int i = static_cast<int>(lua_tonumber(L,-1));
4     lua_pushnumber(L, 0); /* userdata object at index 0 */
5     lua_gettable(L, 1);
6     T* obj = static_cast<T*>(lua_touserdata(L,-1));
7     lua_pop(L, 2); /* pop closure value and obj */
8     return (obj->*(T::Register[i].mfunc))(L);
9 }

垃圾回收是通过在设置表中userdata的垃圾回收标志方法。当垃圾回收被触发时,'gc'标志方法就会被调用以删除对象。'gc'标志方法是在类注册时通过一个新的标志注册的。在上面的实例化时,userdata就通过一个标签来标志。

1 static int gc_obj(lua_State* L) {
2     T* obj = static_cast<T*>(lua_touserdata(L, -1));
3     delete obj;
4     return 0;
5 }

注意,有一些规则类需要遵守:
1. 必须一个公开的构造函数,它有一个lua_State参数;
2. 被注册的成员函数签名必须是 int(T::
)(lua_State*);
3. 必须有一个 public static const char[] className成员;
4. 必须有一个 public static const Luna::RegType[] Register成员;

注释:这些要求是我所选择的设计的要求,你可以使用不同的接口,只需要代码做很小的改动。

Luna::RegType 是一个函数哈希表。name 是被注册的 mfunc 函数的名字。

1 struct RegType {
2     const char* name;
3     const int(T::*mfunc)(lua_State*);
4 };

这里有一个注册C++类到Lua中的例子。Luna::Register() 调用会注册这个类,它也是这个模板类唯一要求的公开接口。要在Lua中使用此类,你可通过调用表的构造函数来创建此类的一个实例。

 1 class Account {
 2     double m_balance;
 3     public:
 4     Account(lua_State* L) {
 5         /* constructor table at top of stack */
 6         lua_pushstring(L, "balance");
 7         lua_gettable(L, -2);
 8         m_balance = lua_tonumber(L, -1);
 9         lua_pop(L, 2); /* pop constructor table and balance */
10     }
11 
12     int deposit(lua_State* L) {
13         m_balance += lua_tonumber(L, -1);
14         lua_pop(L, 1);
15         return 0;
16     }
17     int withdraw(lua_State* L) {
18         m_balance -= lua_tonumber(L, -1);
19         lua_pop(L, 1);
20         return 0;
21     }
22     int balance(lua_State* L) {
23         lua_pushnumber(L, m_balance);
24         return 1;
25     }
26     static const char[] className;
27         static const Luna<Account>::RegType Register
28     };
29 
30     const char[] Account::className = "Account";
31     const Luna<Account>::RegType Account::Register[] = {
32         { "deposit",  &Account::deposit },
33         { "withdraw", &Account::withdraw },
34         { "balance",  &Account::balance },
35         { 0 }
36     };
37 
38     //[...]
39 
40 /* Register the class Account with state L */
41 Luna<Account>::Register(L);
1 -- In Lua
2 -- create an Account object
3 local account = Account{ balance = 100 }
4 account:deposit(50)
5 account:withdraw(25)
6 local b = account:balance()

Account实例的表如下:

0 = userdata(6): 0x804df80
balance = function: 0x804ec10
withdraw = function: 0x804ebf0
deposit = function: 0x804f9c8

说明

也许有些人不喜欢使用C++模板,但在此处却是合适的。它们提供了一个起初看起来复杂但快速严密的解决方案。作为使用模板的回报,类是类型安全的, 例如,它不可能在成员函数哈希表中混合不同类型的成员函数,因为编译器会报怨的。此外,静态模板类的设计,使它容易使用,在你做完事后没有模板实例需要清 除。

thunk机制是类的核心,因为它转换了函数调用。它通过将表调用的函数对应的对象指针索引到成员函数哈希表中。(Lua表的函数调用 table:function() 是 table.function(table) 调用的语法糖)。这个调用会将表最先压入栈中,接着是参数。成员函数的索引做为一个闭包值压入栈的最后。最开始,我将对象指针也做为一个闭包值,这意味着 有2个闭包值,一个void *的对象指针,一个成员函数的索引,这样花费更多,但访问更快。当然,表中用于垃圾回收的userdata对象还是需要的。最后,我选择了将对象指针索引 到表中,以节省资源,但增加调用的时间消耗。

实际上,这个实现只利用了Lua扩展机制很少的特性,闭包来保持成员函数的索引,'gc'标志成员用于垃圾回收,表的构造函数做函数注册和成员函数的调用。

为什么只允许签名为 int(T::*)(lua_State*) 成员函数被注册?这允许你的成员函数和Lua直接交互,接收Lua传入的参数并返回值到Lua,调用任何的Lua Api函数等。此外,它提供了与注册到Lua中的C函数相同的签名,这使得想使用C++的人更方便。

不足

这个模板类方案只能绑定特定签名的成员函数。如果你有之前写的类,或想在Lua和C++的环境中都能使用,这个方案对你来说可能不是最好的。理论上 这不是一个问题。使用代理模式,我们封装实际的类,并且代理任何对目标对象的调用。代理类的成员函数强迫Lua的参数和返回值,并且代理对目标对象的调 用。你将在Lua中注册代理类,而不是实际类。Additionally, you may use inheritance as well where the proxy class inherits from the base class and delegates the function calls up to the base class, but with one caveat, the base class must have a default constructor; you cannot get the constructor arguments from Lua to the base class in the proxy's constructor initializer list。代理模式解决了我们想在Lua和C++环境中使用的问题,但它要求我们写代理类并且维护它们。

对象都是简单的创建,但需要给用户创建对象时更多的控制。比如,用户也许希望注册一个单例类。一个单例需要用户实现一个返回一个对象的静态 create() 成员函数。这种方式,用户可以实现单例模式,简单的通过new分配对象或者其它方法。constructor函数需要做修改来调用create()而不是 使用new来获取一个对象的指针。这对类要求了更多的约束但更灵活。 A "hook" for garbage collection may be of use to some as well.

总结

这个文章说明了一个绑定C++类到Lua的简单方法。这个实现如此简单,你可以选择修改它来满足你的目的,也满足了一般的需求。也有一些其它的工具 来绑定C++类到Lua中,如 tolua、SWIGLua和一些其它像本文的简单实现。根据它们的优点、缺点来解决你特定的问题。希望本文能给你一些解决此类问题的提示。

模板类的所有源码大概在70行左右,可以Lua的add-ons面面获取。

参考

[1] R. Hickey, Callbacks in C++ using template functors, C++ Report February 95

Last update: Wed Mar 12 11:51:13 EST 2003 by lhf.


欢迎转载,原文地址:http://www./a-template-class-for-binding-cpp-to-lua-translate

 

posted @ 2014-01-06 23:52 lontoken 阅读(15) 评论(0) 编辑

lua的栈

lua的栈

lua中有两种栈:数据栈和调用栈.

栈的定义和结构

lua中的数据可以分为两类:值类型和引用类型,值类型可以被任意复制,而引用类型共享一份数据,复制时只是复制其引用,并由GC负责维护其生命期.lua使用一个unine Value来保存数据.

union Value {  
    GCObject *gc;    /* collectable objects */  
    void *p;         /* light userdata */  
    int b;           /* booleans */  
    lua_CFunction f; /* light C functions */  
    numfield         /* numbers */  
};  

引用类型用一个指针GCObject *gc来间接引用,而其它值类型都直接保存在联合中.

在lua_State中栈的保存如下:

struct lua_State {  
    StkId top;          /* first free slot in the stack */
    StkId stack_last;   /* last free slot in the stack */  
    StkId stack;        /* stack base */  
    int stacksize;
    CallInfo base_ci;  /* CallInfo for first level (C calling Lua) */
    ...
}  

StkId的定义:

typedef TValue *StkId;  
typedef struct lua_TValue TValue;  
struct lua_TValue {  
    TValuefields;  
};  

#define TValuefields    Value value_; int tt_  

在Windows VC下:

#define TValuefields  \  
union { struct { Value v__; int tt__; } i; double d__; } u  

这里使用了繁杂的宏定义,TValuefields和numfield是为了应用一个被称为NaN Trick的技巧.

lua初始化堆栈:

static void stack_init (lua_State *L1, lua_State *L) {
    int i; CallInfo *ci;
    /* initialize stack array */
    L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, TValue);        //BASIC_STACK_SIZE为40
    L1->stacksize = BASIC_STACK_SIZE;
    for (i = 0; i < BASIC_STACK_SIZE; i++)
        setnilvalue(L1->stack + i);  /* erase new stack */          //将tt_字段设置为0
    L1->top = L1->stack;
    L1->stack_last = L1->stack + L1->stacksize - EXTRA_STACK;       //EXTRA_STACK为5
    /* initialize first ci */
    ci = &L1->base_ci;
    ci->next = ci->previous = NULL;
    ci->callstatus = 0;
    ci->func = L1->top;
    setnilvalue(L1->top++);  /* 'function' entry for this 'ci' */
    ci->top = L1->top + LUA_MINSTACK;
    L1->ci = ci;
}

初始化之后,lua_State栈的情况:

lua供C使用的栈相关API是不检查数据栈越界的,因为通常编写C扩展都能把数据栈空间的使用控制在BASIC_STACK_SIZE以内,或是显式扩展.对每次数据栈访问都强制做越界检查是非常低效的.
数据栈不够用时,可以使用luaD_reallocstack\luaD_growstack函数扩展,每次至少分配比原来大一倍的空间.

/* some space for error handling */  
#define ERRORSTACKSIZE  (LUAI_MAXSTACK + 200)  

void luaD_reallocstack (lua_State *L, int newsize) {        
    TValue *oldstack = L->stack;        
    int lim = L->stacksize;     
    lua_assert(newsize <= LUAI_MAXSTACK || newsize == ERRORSTACKSIZE);  
    lua_assert(L->stack_last - L->stack == L->stacksize - EXTRA_STACK);  
    luaM_reallocvector(L, L->stack, L->stacksize, newsize, TValue);
    for (; lim < newsize; lim++)   
        setnilvalue(L->stack + lim); /* erase new segment */  
    L->stacksize = newsize;  
    L->stack_last = L->stack + newsize - EXTRA_STACK;  
    correctstack(L, oldstack);  
}  

void luaD_growstack (lua_State *L, int n) {  
    int size = L->stacksize;  
    if (size > LUAI_MAXSTACK)  /* error after extra size? */  
        luaD_throw(L, LUA_ERRERR);  
    else {  
        int needed = cast_int(L->top - L->stack) + n + EXTRA_STACK;  
        int newsize = 2 * size;  
        if (newsize > LUAI_MAXSTACK) newsize = LUAI_MAXSTACK;  
        if (newsize < needed) newsize = needed;  
        if (newsize > LUAI_MAXSTACK) {  /* stack overflow? */  
            luaD_reallocstack(L, ERRORSTACKSIZE);  
            luaG_runerror(L, "stack overflow");  
        }  
        else   
            luaD_reallocstack(L, newsize);  
    }  
}  

数据栈扩展的过程,伴随着数据拷贝,这些数据都是可能直接值复制的,所有不需要在扩展之后修正其中的指针.但有此外部对数据栈的引用需要修正为正确的新地址.这些需要修正的位置包括upvalue以及执行对数据栈的引用.此过程由correctstack函数实现.

static void correctstack (lua_State *L, TValue *oldstack) {  
    CallInfo *ci;  
    GCObject *up;  
    L->top = (L->top - oldstack) + L->stack;  
    for (up = L->openupval; up != NULL; up = up->gch.next)  
        gco2uv(up)->v = (gco2uv(up)->v - oldstack) + L->stack;  
    for (ci = L->ci; ci != NULL; ci = ci->previous) {  
        ci->top = (ci->top - oldstack) + L->stack;  
    ci->func = (ci->func - oldstack) + L->stack;  
    if (isLua(ci))  
        ci->u.l.base = (ci->u.l.base - oldstack) + L->stack;  
    }  
}  

栈的使用

入栈的函数或宏:

lua_pushnil\lua_pushnumber\lua_pushinteger\lua_pushunsigned\lua_pushlstring\lua_pushstring\lua_pushvfstring\lua_pushfstring\lua_pushcclosure\lua_pushboolean\lua_pushlightuserdata\lua_pushthread\lua_newtable\lua_register\lua_pushcfunction;  
lua_getglobal\lua_gettable\lua_getfield\lua_rawget\lua_rawgeti\lua_rawgetp\lua_createtable\lua_newuserdata\lua_getmetatable\lua_getuservalue  

lua_pushnumber定义如下:

LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) {  
    lua_lock(L);  
    //VS下展开之后:TValue *io_=(L->top); ((io_)->u.d__)=(n); ((void)0);  
    setnvalue(L->top, cast_num(n));  
    //VS下展开之后:L->top++; ((void)0);  
    api_incr_top(L);  
    lua_unlock(L);  
}  

lua_newtable的定义如下:

#define lua_newtable(L)     lua_createtable(L, 0, 0)  
LUA_API void lua_createtable (lua_State *L, int narray, int nrec) {     
    Table *t;   
    lua_lock(L);  
    luaC_checkGC(L);  
    t = luaH_new(L);  
    //VC下展开之后:TValue *io=(L->top); ((io)->u.i.v__).gc=((GCObject *)((t)));  
    //(((io)->u.i.tt__) = (0x7FF7A500 | (((5) | (1 << 6))))); ((void)0);  
    sethvalue(L, L->top, t); 
    //VC下展开之后:L->top++; ((void)0);  
    api_incr_top(L);   
    if (narray > 0 || nrec > 0)  
    luaH_resize(L, t, narray, nrec);  
    lua_unlock(L);  
}  

示例代码如下:

lua_State* L = luaL_newstate();
lua_pushnumber(L, 1);
lua_newtable(L);

lua_pushinteger(L, 1)之后,栈的情况:

lua_newtable(L)之后,栈的情况:

出栈的函数或宏:
lua_setglobal\lua_settable\lua_setfield\lua_rawset\lua_rawseti\lua_rawsetp\lua_setmetatable\lua_setuservalue
从栈中获取数据的函数或宏:
luaL_checklstring\luaL_checknumber\luaL_checkinteger\luaL_checkunsigned
lua_settable的定义如下:

LUA_API void lua_settable (lua_State *L, int idx) {  
    StkId t;  
    lua_lock(L);  
    api_checknelems(L, 2);  
    t = index2addr(L, idx);  
    luaV_settable(L, t, L->top - 2, L->top - 1);  
    L->top -= 2;  /* pop index and value */  
    lua_unlock(L);  
}  

luaL_checknumber的定义如下:

LUALIB_API lua_Number luaL_checknumber (lua_State *L, int narg) {  
    int isnum;  
    lua_Number d = lua_tonumberx(L, narg, &isnum);  
    if (!isnum)  
        tag_error(L, narg, LUA_TNUMBER);  
    return d;  
}  

示例代码如下(lua库的luaopen_base函数,用于注册):

#define lua_pushglobaltable(L)      lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)

LUALIB_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
    luaL_checkversion(L);
    luaL_checkstack(L, nup, "too many upvalues");
    for (; l->name != NULL; l++) {  /* fill the table with given functions */
        int i;
        for (i = 0; i < nup; i++)  /* copy upvalues to the top */
            lua_pushvalue(L, -nup);
        lua_pushcclosure(L, l->func, nup);  /* closure with those upvalues */
        lua_setfield(L, -(nup + 2), l->name);
    }
    lua_pop(L, nup);  /* remove upvalues */
}

LUAMOD_API int luaopen_base (lua_State *L) {  
    /* set global _G */  
    lua_pushglobaltable(L);  
    lua_pushglobaltable(L);  
    lua_setfield(L, -2, "_G");       
    /* open lib into global table */  
    luaL_setfuncs(L, base_funcs, 0);  
    lua_pushliteral(L, LUA_VERSION);  
    lua_setfield(L, -2, "_VERSION");  /* set global _VERSION */  
    return 1;  
}  

luaopen_base函数设置全局注册表的"_G"字段,base_funcs指定的函数列表,"_VERSION"字段.
第一个lua_pushglobaltable(L)之后:

第二个lua_pushglobaltable(L)之后:

lua_setfield(L, -2, "_G")之后:

luaL_setfuncs(L, base_funcs, 0)之后:

lua_pushliteral(L, LUA_VERSION)之后:

lua_setfield(L, -2, "_VERSION")

此时栈顶下的第一个元素为luaopen_base函数生成的table,返回值为1,标记函数的返回参数个数为1.

参考资源

 PS:个人新干博客地址 http://www./

posted @ 2013-12-24 12:53 lontoken 阅读(12) 评论(0) 编辑

文本文件的编码识别

文本文件的编码问题,困扰我很久,在跨平台、源程序中的中文字符、从文本文件中读取中文字符的时候,若对文件编码问题没有弄清楚,难免会走弯路。对此情况,我准备针对几个主题,记录下自己学习的心得,以备日后查阅和分享。

认识文本文件

文件分为两种类型:文本文件和二进制文件; 
文件文件:是以字符编码的方式进行保存的;每一行都以换行符结束(由于历史原因,各操作系统的换行符不一样,Windows的是"<回车><换行>",即"\r\n",Unix/Linux的是"<换行>",即"\n",Mac的是"回车",即"\r");在文件最后一行的结尾有文件结束标志EOF,它的值也依赖于系统,一般为-1。 
二进制文件:是将内存中数据原封不动的读取和写入文件中。

字符编码

一般常见的编码格式有:ASCII、UTF-8、UTF-16、GB2312、Big5、GBK、GB18030。 详细的字符编码知识,此处不再多说。

需要知道的事:

  • 在Windows的语境中,所谓的[ANSI]指的是对应当前系统 locale 的遗留(legacy)编码。
  • 在Windows的语境中,所谓的[Unicode]指的是带有 BOM 的小端序 UTF-16。
  • 在Windows的语境中,所谓的[UTF-8]指的是带 BOM 的 UTF-8。

文件编码的模式识别

知道了字符编码的细节,还不足以正确处理我们所面对的种类繁多的文本文件。 
首先,我们看看各编码格式的文件存储格式。

BOM:要识别UTF-8和UTF-16就不得不说到字节顺序标记(byte-order mark,BOM),它用来标识该字节流的字节序,是高位在前还是低位在前。从Unicode3.2开始,BOM只能位于流的开头,只能用于标识字节序。

UTF-16中,字节顺序标记被放置为文件或字符串流的第一个字符,以标示在此文件或字符串流中,以所有十六比特为单位的字码的尾序(字节顺序)。 * 如果十六比特单位被表示成大尾序,这字节顺序标记字符在串行中将呈现0xFE,其后跟着0xFF(其中的0x用来标示十六进制)。 * 如果十六比特单位使用小尾序,这个字节串行为0xFF,其后接着0xFE。

UTF-8则没有字节顺序的议题。UTF-8编码过的字节顺序标记则被用来标示它是UTF-8的文件。它只用来标示一个UTF-8的文件,而不用来说明字节顺序。 许多Windows程序(如记事本)会添加字节顺序标记到UTF-8文件。然而,在类Unix系统(大量使用文本文件,用于文件格式,用于进程间通信)中,这种作法则不被建议采用。 故,UTF-8分为UTF-8有BOM格式和UTF-8无BOM格式。

UTF-8: 若为UTF-8有BOM格式,则文件开头为 EF BB BF; 若为UTF-8无BOM格式,则不能依据上述规则;此时需要依据UTF-8编码格式来判断,其编码如下:

U-00000000 - U-0000007F: 0xxxxxxx 
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx 
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx 
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-16: 若为UTF-16(大端序),则文件开头为 FE FF; 若为UTF-16(小端序),则文件开头为 FF FE;

GB2312: GB2312中对所收汉字进行了"分区"处理,每区含有94个汉字/符号。这种表示方式也称为区位码。

  • 01-09区为特殊符号。
  • 16-55区为一级汉字,按拼音排序。
  • 56-87区为二级汉字,按部首/笔画 排序。

在使用GB2312的程序通常采用EUC储存方法,以便兼容于ASCII. 
ASCII字符,范围为0x20-0x7E,直接用单字节表示。 每个汉字及符号以两个字节来表示。第一个字节称为"高位字节",第二个字节称为"低位字节"。 "高位字节"使用了0xA1-0xF7(把01-87区的区号加上0xA0),"低位字节"使用了0xA1-0xFE(把01-94加上0xA0)。

Big5: Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为"高位字节",第二个字节称为"低位字节"。 "高位字节"使用了0x80-0xFE,"低位字节"使用了0x40-0x7E,及0xA1-0xFE。 因Big5相对使用较少,此处不做识别。

GBK: 字符有一字节和双字节编码,00–7F范围内是一位,和ASCII保持一致,此范围内严格上说有96个文字和32个控制符号。 
之后的双字节中,前一字节是双字节的第一位。总体上说第一字节的范围是81–FE(也就是不含80和FF),第二字节的一部分领域在40–7E,其他领域在80–FE.

GB18030: 标准采用单字节、双字节和四字节三种方式对字符编码。 
使用0×00至0×7F码位(对应于ASCII码的相应码位)。 
双字节部分,首字节码位从0×81至0×FE,尾字节码位分别是0×40至0×7E和0×80至0×FE。 四字节部分采用GB/T 11383未采用的0×30到0×39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0×81308130到0×FE39FE39。其中第 一、三个字节编码码位均为0×81至0×FE,第二、四个字节编码码位均为0×30至0×39。

面向字节的模式识别

UTF-16 直接根据其BOM识别; 
UTF-8 首先根据BOM识别,若不符,再以上述编码规则识别; 
GB2321、GB18030 因GB18030完全兼容GB2321,则只识别GB18030,根据如下规则识别:

单字节: 0到0x7F 0000.0000-0111.1111 双字节: 第一个字节的值从0x81到0xFE,第二个字节的值从0x40到0xFE(不包括0x7F) 1000.0001-1111.1110 0100.0000-1111.1110 四字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节从0x81到0xFE,第四个字节从0x30到0x39 1000.0001-1111.1110 0011.0000-0011.10001 1000.0001-1111.1110 0011.0000-0011.10001

 PS:个人新干博客地址 http://www./

posted @ 2013-12-12 23:14 lontoken 阅读(9) 评论(0) 编辑

编译器产生的输出文件的MD5值与生成代码的关系

背景

因发布给用户的产品需要升级,每次升级使用的是版本号加”增量更新“的方法,自动更新服务器保存当前版本号与所有文件的MD5值,用户本地保存本地的版本号。登录时,若用户本地的版本号与服务器上的版本号不一致,则根据嗠器上文件的MD5与本地所有文件计算的MD5值比较,若不同,则更新。但现在在Delphi6中,程序的代码没有作更新,前后再次产生的文件的MD5却不同。Delphi6将生成文件的当前时间戳添加到了输出文件中。从二进制进的角度来看,代码相同的程序,只是编译的时间不一样,产生的文件却是不同的。Delphi6的这种画蛇添足的做法,实在叫人费解。

既然Delphi6这么做,难道这是“业内行规”,为了弄明白,那就只有一一探个明白了。

Delphi6输出文件MD5

已经知道了,代码不一样编译时间不同,其输出文件的MD5也不同,但若是在输出文件中,将编译时间戳的影响去除了,是其它因素对它的影响是怎样的呢。

程序如下: 
program BuildOutput_6;

{$APPTYPE CONSOLE}  

uses  
  SysUtils;  

var  
  aNum: Integer;  

begin
  Writeln('==Delphi 6的输出文件==');  

  aNum := 0;

  Writeln(IntToStr(aNum)); 

  Writeln('按回车键退出。');
  readln;
end.

在不修改代码的情况下,编译两次产生的文件:BuildOutput61.exe、BuildOutput62.exe,另外将时间戳强制改为2008-08-08 08:08:08之后分别产生的文件为:BuildOutput610.exe和BuildOutput620.exe; 
4个文件的MD5值: 
BuildOutput61.exe 7C4574F2C7614273076D78BC06C5824F 
BuildOutput62.exe 5BF6B4BF7DE37A71BC4060F95C544FC5 
BuildOutput620.exe 68B6114E66DC2912656810278C616A94 
BuildOutput
620.exe 68B6114E66DC2912656810278C616A94

将代码做略微调整: 
program BuildOutput_6;

{$APPTYPE CONSOLE} 

uses
  SysUtils; 

var
  aNum: Integer;

begin
  Writeln('==Delphi 6的输出文件==');

  aNum := 1;

  Writeln(IntToStr(aNum));

  Writeln('按回车键退出。');
  readln;
end.

此次产生的BuildOutput630.exe的MD5值: 
BuildOutput
630.exe 4B44FC3E49A35081004D1D7BCE4BDAB4 
BuildOutput63_0.exe的MD5与前面2对应文件的MD5值不同,代码的改动,已经影响的最后的输出文件。

将代码的语句顺序调整: program BuildOutput_6;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  aNum: Integer;

begin
  aNum := 0;

  Writeln('==Delphi 6的输出文件==');

  Writeln(IntToStr(aNum));

  Writeln('按回车键退出。');
  readln;
end.

此次产生的BuildOutput640.exe的MD5值: 
BuildOutput
640.exe DB2DA5F6F3AE5B3F71330DB17A116A98 
BuildOutput64_0.exe的MD5与前面2对应文件的MD5值不同,代码的改动,已经影响的最后的输出文件。

将代码的空白行去除: 
program BuildOutput_6;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  aNum: Integer;

begin
  Writeln('==Delphi 6的输出文件==');
  aNum := 0;

  Writeln(IntToStr(aNum));

  Writeln('按回车键退出。');
  readln;
end.

此次产生的BuildOutput650.exe的MD5值: 
BuildOutput
650.exe 68B6114E66DC2912656810278C616A94 
BuildOutput65_0.exe的MD5与前面2对应文件的MD5值一样同。

JAVA输出文件MD5

JAVA代码如下: public class BuildOutput_6 {

     public static void main(String[] args){
         System.out.println("==Java 6的输出文件==");

         int i = 0;

         System.out.println("i = " + i);

     }
 }

两次编译产生的.class文件的MD5如下: BuildOutput61.class 5F50C342B17C8DBFE22E3E71311CB358 
BuildOutput62.class 5F50C342B17C8DBFE22E3E71311CB358 
两个MD5值相同。

VC9.0输出文件MD5

代码如下: public class BuildOutput_6 {

     public static void main(String[] args){
         System.out.println("==Java 6的输出文件==");

         int i = 0;

         System.out.println("i = " + i);

     }
 }

两次编译产生的.exe文件的MD5如下: BuildOutput1.exe EB77754D7216FD4AC3FF62EB7A0E3A2B 
BuildOutput
2.exe 17BFC8E72EEC200E5E792673C56BB236 
两个MD5值不同,应该与Delphi6类似。

似乎事情有些线索了,输出为可执行文件时,相同代码两次产生的MD5值会不同。但学需要进一步验证。下次有时间之后继续。

PS:个人新干博客地址 http://www./

posted @ 2013-04-02 18:31 lontoken 阅读(55) 评论(0) 编辑

案例情形:在通过控件的构造函数Create(AOwner: TComponent)创建对象a时传入Application,之后又自作多情的主动调用FreeAndNil释放此对象a,在程序退出时问题就会来了,由于Application会主动释放自己的Components内的元素,而我们自己再次调用FreeAndNil时就会出现对象的多次释放,导致程序无法正常退出!!!

反例代码:

//在Create时创建对象
FFoolPan := TPanel.Create(Application);
 
//在Destroy时释放资源
//旁白:不要以为做了Assigned判断就万事大吉了,遇到”悬空指定”你会死得很难看 
if Assigned(FFoolPan ) then FreeAndNil(FFoolPan );

 

好了,现在开始分析问题的原因,为了刨根问底,我们只有深入Delphi的VCL去探险了…
要知道TPanel.Create()到底做了什么,我们得去问TPanel的祖先类TComponent,因因它有了组件列表的概念,列表中的元素必须是TComponent的实例,且属于此TComponent,它会在其构造函数中主动的释放列表中的实例,注意,它只会调用列表中元素实例的Destroy方法,而不将其置为nil,“悬空指针”从此诞生。

//------在Create时将自己插入到组件列表Components当中  Start------//
constructor TComponent.Create(AOwner: TComponent);
begin
  FComponentStyle := [csInheritable];
  if AOwner <> nil then AOwner.InsertComponent(Self);       //将自己插入到AOwner的组件列表中
end;

//我们来看看InsertComponent到底做了什么    (PS:由贴出了与此问题相关的代码,下同)
procedure TComponent.InsertComponent(AComponent: TComponent);
begin
  AComponent.ValidateContainer(Self);
  ValidateRename(AComponent, '', AComponent.FName);
  Insert(AComponent);       //Insert就发生在此时
end;

procedure TComponent.Insert(AComponent: TComponent);
begin
  if FComponents = nil then FComponents := TList.Create;
  FComponents.Add(AComponent);
  AComponent.FOwner := Self;
end;
//------在Create时将自己插入到组件列表Components当中  Edn------//

 

//------TComponent释放组件列表Components中的实例  Start------//
destructor TComponent.Destroy;
begin
  Destroying;
  DestroyComponents;            //释放Components
  if FOwner <> nil then FOwner.RemoveComponent(Self);
  inherited Destroy;
end;

procedure TComponent.DestroyComponents;
var
  Instance: TComponent;
begin
  while FComponents <> nil do
  begin
    Instance := FComponents.Last;
    if (csFreeNotification in Instance.FComponentState)
      or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then
      RemoveComponent(Instance)
    else
      Remove(Instance);
    Instance.Destroy;           //只调用了Destroy,却没置为nil,引入悬空指针,情何以堪...
  end;
end;
//------TComponent释放组件列表Components中的实例  End------//

现在我们明白了Create(Application)和Create(nil)的一个重要的区别了:使用Create(Application)所创建的对象的释放由Application来做,Create(nil)构造的对象需要自己来做资源的释放。

那程序退出时,Delphi都做了些什么呢?

我们从简单的情况入手,看看在系统的主窗体关闭时,我们的程序都执行了些什么操作。我们来看TCustomForm的WMClose,它的声明如下:

procedure WMClose(var Message: TWMClose); message WM_CLOSE;
既然接收了WM_CLOSE消息,那到底做了什么呢?

procedure TCustomForm.WMClose(var Message: TWMClose);
begin
  Close;      //很简单,只是调用了Close而已
end;

真像会在Close里面吗?

procedure TCustomForm.Close;
var
  CloseAction: TCloseAction;
begin
  if fsModal in FFormState then
    ModalResult := mrCancel
  else
    if CloseQuery then
    begin
      if FormStyle = fsMDIChild then
        if biMinimize in BorderIcons then
          CloseAction := caMinimize else
          CloseAction := caNone
      else
        CloseAction := caHide;
      DoClose(CloseAction);
      if CloseAction <> caNone then
        if Application.MainForm = Self then Application.Terminate   //我们找到Application.Terminate了,不错
        else if CloseAction = caHide then Hide
        else if CloseAction = caMinimize then WindowState := wsMinimized
        else Release;
    end;
end;

Application.Terminate的作用时让程序终止执行,即退出应用程序。更详细的说明是Terminate会通过调用PostQuitMessage(0)发送消息WM_QUIT面终止程序。

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
  Handled: Boolean;
begin
  Result := False;
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then   //在消息队列中获取消息
  begin
    Result := True;
    if Msg.Message <> WM_QUIT then
    begin
      Handled := False;
      if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
      if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
        not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
      begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      end;
    end
    else    //如果接收到WM_QUIT消息,将退出标志置为true 
      FTerminate := True;           
  end;
end;

在中我们可以在TApplication.Run看到,系统会通过HandleMessage调用ProcessMessage处理消息,直到退出标志为true时,才终止。

 

 

 

procedure TApplication.Run;
begin
  FRunning := True;
  try
    AddExitProc(DoneApplication);    //将DoneApplication添加到TApplication.Run退出之后执行列表中
    if FMainForm <> nil then
    begin
      case CmdShow of
        SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
        SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
      end;
      if FShowMainForm then
        if FMainForm.FWindowState = wsMinimized then
          Minimize else
          FMainForm.Visible := True;
      repeat
        try
          HandleMessage;
        except
          HandleException(Self);
        end;
      until Terminated;         //退出标志为true时退出
    end;
  finally
    FRunning := False;
  end;
end;

 

Application.Ran退出后,我们看看DoneApplication会做些什么。

procedure DoneApplication;
begin
  with Application do
  begin
    if Handle <> 0 then ShowOwnedPopups(Handle, False);
    ShowHint := False;      
    Destroying;             
    DestroyComponents;      //调用Application.DestroyComponents方法
  end;
end;

 

我们现在又回到了DestroyComponents方法,很熟悉的感觉,Application的DestroyComponents会有什么不同呢?

情况并没有不同,它没有重写DestroyComponents,还是使用的TComponent.DestroyComponents方法。好了,现在我们也该明白为什么在TPanel.Create(Application)之后,不会再手动调用FreeAndNil(FFoolPan )了。

切忌:内存的重复释放引发的危害,远远比内存泄漏来得大来得猛烈。

有一篇博文就是讲“为什么重复free()比内存泄漏危害更大”,有兴趣的同学可以过去瞧瞧。

说了这么多,我们也该休息下了 :)

 

------------仅以此文,献给我自己、HOMS开发的同学们,还有深受客户端退出无响应的受害者------------

 PS:个人新干博客地址 http://www./

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多