lua 字符串是 lua 中的一种基本类型,本文将以 lua-5.4.1 版本进行源码解析。
简介
- 在 lua 中,使用字符串的方式和其他编程语言相似,如:
local str = "Hello World"
print(str)
- 然而,lua 中的 string ,实现上并非直接为 c 中的 char* 或者 char[] ,而是使用了封装的类型。
- 从 xlua 或 tolua 中可以知道,当 lua 侧访问 C# 侧的字符串变量时,C# 侧会通过调用 lua_pushstring/lua_pushlstring 方法将字符串传输到 lua 虚拟机的栈上,而 C# 侧访问 lua 侧的字符串变量时,则是通过调用 lua_tolstring 方法,从栈上读取字符串。
字符串压栈
- lua_pushstring 的实现如下:
// lapi.c
LUA_API const char *lua_pushstring (lua_State *L, const char *s) {
lua_lock(L);
if (s == NULL)
setnilvalue(s2v(L->top));
else {
TString *ts;
ts = luaS_new(L, s);
setsvalue2s(L, L->top, ts);
s = getstr(ts); /* internal copy's address */
}
api_incr_top(L);
luaC_checkGC(L);
lua_unlock(L);
return s;
}
- 可以看到,lua_pushstring 通过调用 luaS_new 方法,创建了一个 TString * 类型的变量,然后将其设置到栈上。
- TString 结构为:
// lobject.h
typedef struct TString {
CommonHeader;
lu_byte extra; /* reserved words for short strings; "has hash" for longs */
lu_byte shrlen; /* length for short strings */
unsigned int hash;
union {
size_t lnglen; /* length for long strings */
struct TString *hnext; /* linked list for hash table */
} u;
char contents[1];
} TString;
- TString 结构变量意义分别为:
- CommonHeader :需要 gc 的对象的公共头部。
- extra :长短字符串有不同的含义。
- 短字符串(len <= 40):大于 0 表示被持有,即不会被 gc 回收(lstring.h 的 isreserved 方法)。
- 长字符串(len > 40):为 0 表示字符串没有经过 hash 计算,经过 hash 计算后则为 1 (lstring.c 的 luaS_hashlongstr 方法)。
- shrlen :短字符串的长度(字节数,UTF-8 编码的中文字符通常占用 3 个字节)。
- hash :字符串经过 hash 计算后的结果。
- u :union 结构,有两个字段。
- lnglen :长字符串的长度(字节数)。
- hnext :TString 链表,存放下一个 hash 值对 G(L)->strt.size 取模结果相同的 TString。
- contents[1] :用来标记字符串的起始位置,创建 TString 对象时会根据字符串的长度和起始位置来确定需要申请的内存大小。
创建 lua 字符串
- 再看 luaS_new 方法的代码:
// lstring.c
TString *luaS_new (lua_State *L, const char *str) {
unsigned int i = point2uint(str) % STRCACHE_N; /* hash */
int j;
TString **p = G(L)->strcache[i];
for (j = 0; j < STRCACHE_M; j++) {
if (strcmp(str, getstr(p[j])) == 0) /* hit? */
return p[j]; /* that is it */
}
/* normal route */
for (j = STRCACHE_M - 1; j > 0; j--)
p[j] = p[j - 1]; /* move out last element */
/* new element is first in the list */
p[0] = luaS_newlstr(L, str, strlen(str));
return p[0];
}
- 为一个字符串创建 TString 对象时,首先通过查找 G(L)->strcache 的 TString 缓存中是否存在相同的字符串。其中,strcache 为二维数组,先通过字符串的长度对 STRCACHE_N (STRCACHE_N = 53)取模得到第一维索引 i,再对 strcache[i] 进行遍历。
- 存在 :直接返回对应 TString 对象。
- 不存在 :通过调用 luaS_newlstr 方法创建一个新的 TString 对象,插入到 strcache[i][0] 中,strcache[i] 最多保存 STRCACHE_M + 1 (STRCACHE_M = 2)个对象。
- strcache 为 lua 的 TString 缓存机制,gc 过程会对其进行检查,如果 TString 对象可以回收,则会 strcache[i][j] 替换为一个不会被回收的 lua 字符串 memerrmsg ,原有的对象则进行回收(lstring.c 的 luaS_clearcache 方法)。
- luaS_newlstr 用来创建新的 TString 对象,其代码如下:
// lstring.c
TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
if (l <= LUAI_MAXSHORTLEN) /* short string? */
return internshrstr(L, str, l);
else {
TString *ts;
if (unlikely(l >= (MAX_SIZE - sizeof(TString))/sizeof(char)))
luaM_toobig(L);
ts = luaS_createlngstrobj(L, l);
memcpy(getstr(ts), str, l * sizeof(char));
return ts;
}
}
- 可以看到,在这里字符串分成了两类处理,长度在 LUAI_MAXSHORTLEN(LUAI_MAXSHORTLEN = 40)以内的为短字符串,超过 LUAI_MAXSHORTLEN 的为长字符串。
- 需要注意的是,C# 中的 string.Length 方法则是计算 Unicode 字符的数量,而 lua 的字符串长度是使用 C 语言的 strlen 方法计算的,即得到的是字符串中所有字符编码所占的子节总数,而不是单纯的字符数量。如,在 GB2312 编码中,一个中文字符占 2 个字节,在 UTF-8 编码中,一个中文字符通常占 3 个字节。因此涉及到字符串的字符相关的操作,如字符查找、字符串截取等,对应的索引值都需要考虑多字节字符的情况。
lua 短字符串
- 短字符串,调用了 internshrstr 方法进行处理,其代码如下:
// lstring.c
static TString *internshrstr (lua_State *L, const char *str, size_t l) {
TString *ts;
global_State *g = G(L);
stringtable *tb = &g->strt;
unsigned int h = luaS_hash(str, l, g->seed, 1);
TString **list = &tb->hash[lmod(h, tb->size)];
lua_assert(str != NULL); /* otherwise 'memcmp'/'memcpy' are undefined */
for (ts = *list; ts != NULL; ts = ts->u.hnext) {
if (l == ts->shrlen && (memcmp(str, getstr(ts), l * sizeof(char)) == 0)) {
/* found! */
if (isdead(g, ts)) /* dead (but not collected yet)? */
changewhite(ts); /* resurrect it */
return ts;
}
}
/* else must create a new string */
if (tb->nuse >= tb->size) { /* need to grow string table? */
growstrtab(L, tb);
list = &tb->hash[lmod(h, tb->size)]; /* rehash with new size */
}
ts = createstrobj(L, l, LUA_VSHRSTR, h);
memcpy(getstr(ts), str, l * sizeof(char));
ts->shrlen = cast_byte(l);
ts->u.hnext = *list;
*list = ts;
tb->nuse++;
return ts;
}
- 首先,获取了 G(L) 中的 strt , 是一个 stringtable 类型,其代码为:
// lstate.h
typedef struct stringtable {
TString **hash;
int nuse; /* number of elements */
int size;
} stringtable;
- strt 是一个全局的字符串表,其中的字段含义为:
- hash :所有短字符串 TString 的链表数组,根据 hash 计算结果对 size 取模得到数组索引,相同索引的 TString 则存入数组下的链表中。
- nuse :当前 TString 的数量。
- size :字符串表的大小,即数量上限。
- 取模计算方法为:
// lobject.h
#define lmod(s,size) \
(check_exp((size&(size-1))==0, (cast(int, (s) & ((size)-1)))))
- 由于 size 为 2 的指数次幂,假设 size 为 2 ^ n ,size - 1 即为 0 ~ n - 1 位都为 1 的值,如 2 ^ 3 = 00001000,那 2 ^ 3 - 1 = 00000111,则位与运算,就能得到 0 ~ n - 1 位的值,也就是取模运算。
- 获取全局字符串表后,就对字符串进行 hash 计算,计算方法为:
// lstring.c
unsigned int luaS_hash (const char *str, size_t l, unsigned int seed,
size_t step) {
unsigned int h = seed ^ cast_uint(l);
for (; l >= step; l -= step)
h ^= ((h<<5) + (h>>2) + cast_byte(str[l - 1]));
return h;
}
- hash 计算的流程为:
- 通过一个种子 seed ,和字符串长度做异或计算,得到基础 hash 值。
- 根据步进长度 step ,对字符串的对应字符,和当前 hash 值移位后相加。
- 当前 hash 值和字符相加后的结果再进行异或计算。
- 这里的 seed ,是在创建 lua 虚拟机的时候进行设置的,其代码为:
// lstate.c
static unsigned int luai_makeseed (lua_State *L) {
char buff[3 * sizeof(size_t)];
unsigned int h = cast_uint(time(NULL));
int p = 0;
addbuff(buff, p, L); /* heap variable */
addbuff(buff, p, &h); /* local variable */
addbuff(buff, p, &lua_newstate); /* public function */
lua_assert(p == sizeof(buff));
return luaS_hash(buff, p, h, 1);
}
- 可以看到,seed 的值和当前时间、虚拟机地址等多个随机值进行 hash 计算得到的,因此具有比较强的随机性,不容易被直接获取。
- 计算出 hash 值后,就可以通过取模计算找到当前字符串应该进入哪个链表。通过查找链表,检查是否已经为相同字符串创建过 TString 对象,如果是则直接返回。如果没有找到,则申请新的内存创建新的对象。
// lstring.c
static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) {
TString *ts;
GCObject *o;
size_t totalsize; /* total size of TString object */
totalsize = sizelstring(l);
o = luaC_newobj(L, tag, totalsize);
ts = gco2ts(o);
ts->hash = h;
ts->extra = 0;
getstr(ts)[l] = '\0'; /* ending 0 */
return ts;
}
// lstring.h
#define sizelstring(l) (offsetof(TString, contents) + ((l) + 1) * sizeof(char))
- 可以看到,计算新对象所需大小时,是通过获取 contents 字段在 TString 结构中的偏移量,加上字符串长度,以及字符串结尾的 ‘\0’ 一个字符长度。这也就解释了前面提到的,TString 中的 contents[1] ,只是用来标记起始值,不是真实的数组大小,因为最终需要的内存大小会按需申请。
- 在创建新对象前,会检查当前的 strt 是否已经达到上限,如果达到上限,就调用 growstrtab 方法进行扩容,上限大小翻倍,并且全部对象都要以新的 size ,重新进行取模计算,设置到新的数组链表中。
// lstring.c
static void growstrtab (lua_State *L, stringtable *tb) {
if (l_unlikely(tb->nuse == MAX_INT)) { /* too many strings? */
luaC_fullgc(L, 1); /* try to free some... */
if (tb->nuse == MAX_INT) /* still too many? */
luaM_error(L); /* cannot even create a message... */
}
if (tb->size <= MAXSTRTB / 2) /* can grow string table? */
luaS_resize(L, tb->size * 2);
}
- 可以看到,扩容的上限为 2^31 ,当达到这个上限时,会尝试进行一次 gc 回收。如果 gc 后还是达到上限,则不能再继续创建。
- luaS_resize 不仅可以进行扩容,也可以进行缩小,gc 回收 strt 的过程,就会尝试进行缩小。
// lgc.c
static void checkSizes (lua_State *L, global_State *g) {
if (!g->gcemergency) {
if (g->strt.nuse < g->strt.size / 4) { /* string table too big? */
l_mem olddebt = g->GCdebt;
luaS_resize(L, g->strt.size / 2);
g->GCestimate += g->GCdebt - olddebt; /* correct estimate */
}
}
}
- 可以看到,当 strt 中实际的对象数量小于上限的 1/4 时,则会将 strt 的上限缩小为原来的 1/2 。
lua 长字符串
- 长字符串,使用 luaS_createlngstrobj 方法进行创建。
// lstring.c
TString *luaS_createlngstrobj (lua_State *L, size_t l) {
TString *ts = createstrobj(L, l, LUA_VLNGSTR, G(L)->seed);
ts->u.lnglen = l;
return ts;
}
- 长字符串的创建则很简单,直接创建新的对象,也不进行 hash 计算,hash 值设置为 G(L)->seed 。在这种机制下,长字符串对内存的需求会很大,如果频繁创建不同的长字符串,就会有较大的内存占用,也就容易频繁触发 gc 回收。
- 长字符串的 hash 计算方法为
// lstring.c
unsigned int luaS_hashlongstr (TString *ts) {
lua_assert(ts->tt == LUA_VLNGSTR);
if (ts->extra == 0) { /* no hash? */
size_t len = ts->u.lnglen;
size_t step = (len >> LUAI_HASHLIMIT) + 1;
ts->hash = luaS_hash(getstr(ts), len, ts->hash, step);
ts->extra = 1; /* now it has its hash */
}
return ts->hash;
}
- 可以看到,长字符的 hash 计算并不是每个字符都进行计算,步进 step 是 len » LUAI_HASHLIMIT + 1 ,即字符串长度除以 32(2 ^ LUAI_HASHLIMIT),也就是说,如果字符串长度小于 32,则每一个字符都进行 hash 计算,否则会跳过某些字符进行 hash 计算。
- 由于长字符串的长度最大可以达到 INT_MAX(2 ^ 31),如果大量长字符串都进行全字符 hash 计算,显然效率会比较低。因此长字符串只有在需要的时候才进行 hash 计算,并且字符串越长,计算的步进间隔也会越大,减少 hash 计算压力。
lua 字符串缓存
- 前面分析了 lua 短字符串和长字符串的创建流程,可以看到,lua 字符串总共有两个缓存机制:
- G(L)->strcache
- G(L)->strt
strcache
- strcache 适用于短字符串和长字符串,根据字符串长度取模计算结果进行缓存,但不会扩容。
- 当 strcache[i] 的数量达到上限后,新插入的就会替换掉最早缓存的值,因此如果多个长度取模结果相同的长字符串,反复调用创建时,由于缓存上限只有 3 个(STRCACHE_N),此时缓存机制就会失效,即基本上每一个新字符串的调用都会申请创建新的字符串对象。
- 因此,对于需要拼接参数的字符串,如使用 string.format ,尽量少使用长字符串,优先选择使用短字符串进行拼接。如果需要使用长字符串拼接时,则应该尽量避免频繁更改拼接的参数。
- 然而,也正是由于缓存数量少,查找的效率会很高,通过取模计算能直接定位到 strcache[i] ,遍历 strcache[i] 也最多需要进行 3 次即可完成,对已经创建好 TString 的字符串,查找效率相对较高。
- global_State 中,定义了 strcache 的大小。
// lstate.h
typedef struct global_State {
...
TString *strcache[STRCACHE_N][STRCACHE_M]; /* cache for strings in API */
...
} global_State;
- strcache 在 lua 虚拟机创建的时候进行初始化,代码如下:
// lstring.c
void luaS_init (lua_State *L) {
global_State *g = G(L);
int i, j;
...
/* pre-create memory-error message */
g->memerrmsg = luaS_newliteral(L, MEMERRMSG);
luaC_fix(L, obj2gco(g->memerrmsg)); /* it should never be collected */
for (i = 0; i < STRCACHE_N; i++) /* fill cache with valid strings */
for (j = 0; j < STRCACHE_M; j++)
g->strcache[i][j] = g->memerrmsg;
}
- 通过调用 luaS_newliteral 方法创建了一个 TString ,赋值到 G(L)->memerrmsg ,并设置这个对象不会被 gc 回收,然后对 strcache 进行初始化赋值,设置为 memerrmsg 。
- gc 触发时,调用 luaS_clearcache 方法,将需要回收字符串的索引值设置为 memerrmsg ,进而后续流程回收字符串。
// lstring.c
void luaS_clearcache (global_State *g) {
int i, j;
for (i = 0; i < STRCACHE_N; i++)
for (j = 0; j < STRCACHE_M; j++) {
if (iswhite(g->strcache[i][j])) /* will entry be collected? */
g->strcache[i][j] = g->memerrmsg; /* replace it with something fixed */
}
}
strt
- strt 只适用于短字符串,因此短字符串其实有双重缓存。短字符串通过对 hash 结果取模后,结果相同的保存到同一个链表中。和 strcache 不同,随着保存的短字符串增加,strt 会进行扩容,因此申请新内存的频率比 strcache 小得多。
- 因此,lua 中尽量使用短字符串,尤其在移动设备的应用中,对内存大小要求比较高,相比于频繁申请内存的长字符串,短字符串对内存大小的控制会更加有效。
- 然而,由于相同取模结果的字符串保存在一个链表中,所以查找效率相比 strcache 较低,尤其当取模结果集中到某个值时,查找效率会退化到 O(n) ,即需要对每一个 TString 进行检查。
- 为了尽量避免这种情况,在数量达到上限后,strt 扩容时会重新对所有对象重新进行取模计算,原来集中到同一个值的会被重新分到多个链表中去,提升查找效率。
- 同样,strt 在 lua 虚拟机创建时进行初始化,代码如下:
// lstring.c
void luaS_init (lua_State *L) {
global_State *g = G(L);
int i, j;
stringtable *tb = &G(L)->strt;
tb->hash = luaM_newvector(L, MINSTRTABSIZE, TString*);
tablerehash(tb->hash, 0, MINSTRTABSIZE); /* clear array */
tb->size = MINSTRTABSIZE;
...
}
- strt 初始大小设置为 128(MINSTRTABSIZE),达到上限后会进行扩容。当触发 gc 时,也会对 strt 进行检查,根据字符串对象数量来确定是否进行缩小。
Hash Dos
- 在 lua 5.2.0 以前,luaS_new 方法不区分长短字符串,也没有 strcache,所有字符串都会进入 strt 中缓存,其中 hash 计算方法为
// lstring.c
unsigned int h = cast(unsigned int, l); /* seed */
size_t step = (l>>5)+1; /* if string is too long, don't hash all its chars */
size_t l1;
for (l1=l; l1>=step; l1-=step) /* compute hash */
h = h ^ ((h<<5)+(h>>2)+cast(unsigned char, str[l1-1]));
- 对字符串长度大于 32 的字符串,步进间隔增大来降低计算耗时,同时所有字符串都统一进入 strt 缓存管理,对内存压力较小。然而,在 lua 5.2.0 发布后不久,有人提出这个设计会给予 Hash Dos 攻击的机会,因为攻击者可以轻易构造出上千万拥有相同 hash 计算结果的不同字符串,如:
- “a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1”
- “a2a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1”
- “a3a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1”
- 可以看到,上面几个字符串,长度都为 32,hash 计算步进长度都为 2,也就是说,最终进行 hash 计算的都是加粗的字符 a ,因此最终的 hash 结果也是相同的。当大量输入这类字符串到 lua 中, strt 的缓存链表就会非常长,频繁进行字符串处理时,效率就会变得非常低。在用于如 http 服务等大量依赖字符串处理的场景下,lua 的输入字符串不可控制,也就很容易被人恶意利用进行攻击。
- 因此,从 lua 5.2.1 开始,则开始划分长短字符串,长字符串不再进入全局字符串表中缓存,同时使用了随机种子 seed ,使得攻击者无法轻易构造出拥有相同 hash 计算结果的不同字符串,从而避免 Hash Dos 攻击风险。
- 然而,由于长字符串的特殊处理,所以长字符串的内存需求会变大。尤其在内存较小的移动设备上,对长字符串的使用需要进行把控,避免产生大量临时长字符串引发频繁 gc 。
字符串连接
- lua 中,使用 “..” 符号来实现字符串拼接。符号识别的代码如下:
// llex.c
static int llex (LexState *ls, SemInfo *seminfo) {
luaZ_resetbuffer(ls->buff);
for (;;) {
switch (ls->current) {
...
case '.': { /* '.', '..', '...', or number */
save_and_next(ls);
if (check_next1(ls, '.')) {
if (check_next1(ls, '.'))
return TK_DOTS; /* '...' */
else return TK_CONCAT; /* '..' */
}
else if (!lisdigit(ls->current)) return '.';
else return read_numeral(ls, seminfo);
}
...
}
}
}
// lparser.c
static BinOpr getbinopr (int op) {
switch (op) {
...
case TK_CONCAT: return OPR_CONCAT;
...
}
}
- 可以看到,在解析代码文本时,识别到 “..” 时,会返回一个 TK_CONCAT 枚举,通过 TK_CONCAT 可以得到对应的操作类型 OPR_CONCAT 。
- 解析表达式的主要代码如下:
// lparse.c
static BinOpr subexpr (LexState *ls, expdesc *v, int limit) {
BinOpr op;
UnOpr uop;
enterlevel(ls);
uop = getunopr(ls->t.token);
if (uop != OPR_NOUNOPR) { /* prefix (unary) operator? */
int line = ls->linenumber;
luaX_next(ls); /* skip operator */
subexpr(ls, v, UNARY_PRIORITY);
luaK_prefix(ls->fs, uop, v, line);
}
else simpleexp(ls, v);
/* expand while operators have priorities higher than 'limit' */
op = getbinopr(ls->t.token);
while (op != OPR_NOBINOPR && priority[op].left > limit) {
expdesc v2;
BinOpr nextop;
int line = ls->linenumber;
luaX_next(ls); /* skip operator */
luaK_infix(ls->fs, op, v);
/* read sub-expression with higher priority */
nextop = subexpr(ls, &v2, priority[op].right);
luaK_posfix(ls->fs, op, v, &v2, line);
op = nextop;
}
leavelevel(ls);
return op; /* return first untreated operator */
}
- 解析表达式的主要流程为:
- 读取每个表达式信息,获取操作类型。
- 在优先级表中,查找操作类型的左参数的值是否大于传入的值 limit 。
- 如果左侧参数比较大,则将右侧参数传入,递归读取下一个表达式(即先执行优先级高的操作类型)。
- 如果传入的 limit 比较大,则返回操作类型。
- 根据操作类型,进行相应的计算处理。
- 操作符的优先级表和操作符枚举 BinOpr 对应,为:
// lparser.c
static const struct {
lu_byte left; /* left priority for each binary operator */
lu_byte right; /* right priority */
} priority[] = { /* ORDER OPR */
{10, 10}, {10, 10}, /* '+' '-' */
{11, 11}, {11, 11}, /* '*' '%' */
{14, 13}, /* '^' (right associative) */
{11, 11}, {11, 11}, /* '/' '//' */
{6, 6}, {4, 4}, {5, 5}, /* '&' '|' '~' */
{7, 7}, {7, 7}, /* '<<' '>>' */
{9, 8}, /* '..' (right associative) */
{3, 3}, {3, 3}, {3, 3}, /* ==, <, <= */
{3, 3}, {3, 3}, {3, 3}, /* ~=, >, >= */
{2, 2}, {1, 1} /* and, or */
};
// lcode.h
typedef enum BinOpr {
/* arithmetic operators */
OPR_ADD, OPR_SUB, OPR_MUL, OPR_MOD, OPR_POW,
OPR_DIV, OPR_IDIV,
/* bitwise operators */
OPR_BAND, OPR_BOR, OPR_BXOR,
OPR_SHL, OPR_SHR,
/* string operator */
OPR_CONCAT,
/* comparison operators */
OPR_EQ, OPR_LT, OPR_LE,
OPR_NE, OPR_GT, OPR_GE,
/* logical operators */
OPR_AND, OPR_OR,
OPR_NOBINOPR
} BinOpr;
- 可以看到,字符串拼接的优先级为 {9, 8} ,也就是说,当连续多个 “..” 进行字符串拼接时,会从最右侧的字符串开始依次向左进行拼接。
string 方法
- lua 虚拟机创建的时候,会调用 luaL_openlibs 方法,将各个模块方法进行注册到全局表中。
// linit.c
static const luaL_Reg loadedlibs[] = {
{LUA_GNAME, luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_COLIBNAME, luaopen_coroutine},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_DBLIBNAME, luaopen_debug},
{NULL, NULL}
};
LUALIB_API void luaL_openlibs (lua_State *L) {
const luaL_Reg *lib;
/* "require" functions from 'loadedlibs' and set results to global table */
for (lib = loadedlibs; lib->func; lib++) {
luaL_requiref(L, lib->name, lib->func, 1);
lua_pop(L, 1); /* remove lib */
}
}
// lauxlib.c
LUALIB_API void luaL_requiref (lua_State *L, const char *modname,
lua_CFunction openf, int glb) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_getfield(L, -1, modname); /* LOADED[modname] */
if (!lua_toboolean(L, -1)) { /* package not already loaded? */
lua_pop(L, 1); /* remove field */
lua_pushcfunction(L, openf);
lua_pushstring(L, modname); /* argument to open function */
lua_call(L, 1, 1); /* call 'openf' to open module */
lua_pushvalue(L, -1); /* make copy of module (call result) */
lua_setfield(L, -3, modname); /* LOADED[modname] = module */
}
lua_remove(L, -2); /* remove LOADED table */
if (glb) {
lua_pushvalue(L, -1); /* copy of module */
lua_setglobal(L, modname); /* _G[modname] = module */
}
}
- 而字符串对应的模块,则通过 luaopen_string 方法,注册到 _G[“string”] 中,其中的方法有:
// lstrlib.c
static const luaL_Reg strlib[] = {
{"byte", str_byte},
{"char", str_char},
{"dump", str_dump},
{"find", str_find},
{"format", str_format},
{"gmatch", gmatch},
{"gsub", str_gsub},
{"len", str_len},
{"lower", str_lower},
{"match", str_match},
{"rep", str_rep},
{"reverse", str_reverse},
{"sub", str_sub},
{"upper", str_upper},
{"pack", str_pack},
{"packsize", str_packsize},
{"unpack", str_unpack},
{NULL, NULL}
};
string.format
- string.format (formatstring, ···)
- 返回不定数量参数的格式化版本,格式化串为第一个参数(必须是一个字符串)。
// lstrlib.c
static int str_format (lua_State *L) {
int top = lua_gettop(L);
int arg = 1;
size_t sfl;
const char *strfrmt = luaL_checklstring(L, arg, &sfl);
const char *strfrmt_end = strfrmt+sfl;
luaL_Buffer b;
luaL_buffinit(L, &b);
while (strfrmt < strfrmt_end) {
if (*strfrmt != L_ESC)
luaL_addchar(&b, *strfrmt++);
else if (*++strfrmt == L_ESC)
luaL_addchar(&b, *strfrmt++); /* %% */
else { /* format item */
char form[MAX_FORMAT]; /* to store the format ('%...') */
int maxitem = MAX_ITEM;
char *buff = luaL_prepbuffsize(&b, maxitem); /* to put formatted item */
int nb = 0; /* number of bytes in added item */
if (++arg > top)
return luaL_argerror(L, arg, "no value");
strfrmt = scanformat(L, strfrmt, form);
switch (*strfrmt++) {
case 'c': {
nb = l_sprintf(buff, maxitem, form, (int)luaL_checkinteger(L, arg));
break;
}
case 'd': case 'i':
case 'o': case 'u': case 'x': case 'X': {
lua_Integer n = luaL_checkinteger(L, arg);
addlenmod(form, LUA_INTEGER_FRMLEN);
nb = l_sprintf(buff, maxitem, form, (LUAI_UACINT)n);
break;
}
case 'a': case 'A':
addlenmod(form, LUA_NUMBER_FRMLEN);
nb = lua_number2strx(L, buff, maxitem, form,
luaL_checknumber(L, arg));
break;
case 'f':
maxitem = MAX_ITEMF; /* extra space for '%f' */
buff = luaL_prepbuffsize(&b, maxitem);
/* FALLTHROUGH */
case 'e': case 'E': case 'g': case 'G': {
lua_Number n = luaL_checknumber(L, arg);
addlenmod(form, LUA_NUMBER_FRMLEN);
nb = l_sprintf(buff, maxitem, form, (LUAI_UACNUMBER)n);
break;
}
case 'p': {
const void *p = lua_topointer(L, arg);
if (p == NULL) { /* avoid calling 'printf' with argument NULL */
p = "(null)"; /* result */
form[strlen(form) - 1] = 's'; /* format it as a string */
}
nb = l_sprintf(buff, maxitem, form, p);
break;
}
case 'q': {
if (form[2] != '\0') /* modifiers? */
return luaL_error(L, "specifier '%%q' cannot have modifiers");
addliteral(L, &b, arg);
break;
}
case 's': {
size_t l;
const char *s = luaL_tolstring(L, arg, &l);
if (form[2] == '\0') /* no modifiers? */
luaL_addvalue(&b); /* keep entire string */
else {
luaL_argcheck(L, l == strlen(s), arg, "string contains zeros");
if (!strchr(form, '.') && l >= 100) {
/* no precision and string is too long to be formatted */
luaL_addvalue(&b); /* keep entire string */
}
else { /* format the string into 'buff' */
nb = l_sprintf(buff, maxitem, form, s);
lua_pop(L, 1); /* remove result from 'luaL_tolstring' */
}
}
break;
}
default: { /* also treat cases 'pnLlh' */
return luaL_error(L, "invalid conversion '%s' to 'format'", form);
}
}
lua_assert(nb < maxitem);
luaL_addsize(&b, nb);
}
}
luaL_pushresult(&b);
return 1;
}
- string.format 的主要流程为:
- 从栈上读取字符串,得到字符串的起止地址。
- 创建一个 buffer ,用于存放新的字符串。
- 从字符串的起始地址开始,处理每个字符。
- 如果字符不为 % ,则写入 buffer 中。
- 如果字符为 % :
- 下一个字符也为 %,即 %%(转义字符),则写入一个 % ,并跳过下一个字符。
- 根据下一个字符的字母类型,将参数进行格式检查后,写入 buffer 中。
- 将 buffer 字符串创建一个 TString 对象,并压入栈上。
- 可以看到,lua 的 string.format 是按顺序拼入参数的,无法像 C# 一样指定参数拼入位置。
string.gsub
- string.gsub (s, pattern, repl [, n])
- 将字符串 s 中,所有的(或是在 n 给出时的前 n 个)pattern 都替换成 repl ,并返回其副本。 repl 可以是字符串、表、或函数。 gsub 还会在第二个返回值返回一共发生了多少次匹配。
- repl 为 string 字符串 :这个字符串作为替换字符串。其中,字符 % 是一个转义符, repl 中的所有 %d 表示 第 d 个捕获到的子字符串,d 可以是 1 到 9 , %0 表示整个匹配, %% 表示单个 %。
- repl 为 table 表 :每次匹配时都会用第一个捕获的字符串作为 key 去查这张表。
- repl 为 function 函数 :每次匹配发生时都会调用这个函数,所有捕获到的子串依次作为参数传入。
- 将字符串 s 中,所有的(或是在 n 给出时的前 n 个)pattern 都替换成 repl ,并返回其副本。 repl 可以是字符串、表、或函数。 gsub 还会在第二个返回值返回一共发生了多少次匹配。
// lstrlib.c
static int str_gsub (lua_State *L) {
size_t srcl, lp;
const char *src = luaL_checklstring(L, 1, &srcl); /* subject */
const char *p = luaL_checklstring(L, 2, &lp); /* pattern */
const char *lastmatch = NULL; /* end of last match */
int tr = lua_type(L, 3); /* replacement type */
lua_Integer max_s = luaL_optinteger(L, 4, srcl + 1); /* max replacements */
int anchor = (*p == '^');
lua_Integer n = 0; /* replacement count */
int changed = 0; /* change flag */
MatchState ms;
luaL_Buffer b;
luaL_argexpected(L, tr == LUA_TNUMBER || tr == LUA_TSTRING ||
tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3,
"string/function/table");
luaL_buffinit(L, &b);
if (anchor) {
p++; lp--; /* skip anchor character */
}
prepstate(&ms, L, src, srcl, p, lp);
while (n < max_s) {
const char *e;
reprepstate(&ms); /* (re)prepare state for new match */
if ((e = match(&ms, src, p)) != NULL && e != lastmatch) { /* match? */
n++;
changed = add_value(&ms, &b, src, e, tr) | changed;
src = lastmatch = e;
}
else if (src < ms.src_end) /* otherwise, skip one character */
luaL_addchar(&b, *src++);
else break; /* end of subject */
if (anchor) break;
}
if (!changed) /* no changes? */
lua_pushvalue(L, 1); /* return original string */
else { /* something changed */
luaL_addlstring(&b, src, ms.src_end-src);
luaL_pushresult(&b); /* create and return new string */
}
lua_pushinteger(L, n); /* number of substitutions */
return 2;
}
- str_gsub 的主要流程为:
- 检查参数 1 和参数 2 是否为字符串。
- 获取参数 3 的类型。
- 获取参数 4 ,作为替换的次数,如果没有传入则使用主字符串长度 + 1,即全部替换。
- 创建一个 buffer ,用于存放新的字符串。
- 调用 match 方法,根据参数 2 进行正则匹配。
- 匹配成功,得到一个匹配结果字符串的起止位置信息,调用 add_value 方法,将匹配结果根据参数 3 的类型将替换字符串写入 buffer 中。
- string :根据匹配结果的起止位置信息,从源字符串拷贝到 buffer 中。
- function :根据匹配结果的起止位置信息,将参数 3 的函数压入栈上,为匹配结果创建一个字符串压入栈上,执行方法,并将结果写入 buffer 中,如果失败则使将源字符串对应位置写入。
- table :根据匹配结果的起止位置信息,为匹配结果创建一个字符串压入栈上,根据栈上的 key 对 table 进行查询,将查询结果写入 buffer 中,如果失败则使将源字符串对应位置写入。
- 匹配失败,则将当前位置字符写入 buffer 中,移动一个字符位置,重新进行匹配,直到达到字符串的结尾。
- 匹配成功,得到一个匹配结果字符串的起止位置信息,调用 add_value 方法,将匹配结果根据参数 3 的类型将替换字符串写入 buffer 中。
- 如果源字符串没有一次匹配成功,则直接返回源字符串和匹配次数结果。如果有一次以上匹配成功,则将 buffer 创建一个新的字符串返回,并返回匹配次数结果。
- str_gsub 传入字符串替换的过程中,基本不会产生新的字符串。然而传入函数或表进行替换,由于需要将匹配的字符串结果作为函数参数或表查询键值,所以需要为其创建新的字符串,即在此过程中会产生临时字符串。因此,使用的时候需要注意,尽量避免出现大量临时长字符串出现,引起内存压力。
总结
- lua 的字符串并非直接为 c 中的字符串,而是一个自定义的 TString 类型,有长短字符串的区分。对于不同类型的字符串,有不同的全局缓存方式。由于字符串的使用频率非常高,而且相比其他类型,字符串对内存的占用比较多。因此,了解 lua 字符串的实现细节,能更好地对内存大小进行控制。