南锋

南奔万里空,脱死锋镝余

Lua连续教程之C语言API总览

Lua是一种嵌入式语言,这就意味着Lua并不是一个独立运行的应用,而是一个库,它可以链接到其他应用程序,将Lua的功能融入这些应用。

因为能够当作库来扩展某个应用程序,所以Lua是一种嵌入式语言。同时,使用了Lua语言的程序也可以在Lua环境中注册新的函数,比如用C语言实现函数,从而增加一些无法直接用Lua语言编写的功能。因此Lua也是一种可扩展的语言。
上述两种对Lua语言的定位分别对应C语言和Lua语言之间的两种交互形式。在第一种形式中,C语言拥有控制权,而Lua语言被用作库,这种交互形式中的C代码被称为应用代码。在第二种形式中,Lua语言拥有控制权,而C语言被用作库,此时的C代码被称为库代码。应用代码和库代码都适用相同的API与Lua语言通信,这些API被称为C API。
C API是一个函数、常量和类型组成的集合,有了它,C语言代码就能与Lua语言交互。C API包括读写Lua全局变量的函数、调用Lua函数的函数、运行Lua代码段的函数,以及注册C函数的函数等。通过调用CAPI,C代码几乎可以做Lua代码能够做的所有事情。
CAPI遵循C语言的操作模式,与Lua的操作模式由很大的区别。在使用C语言编程时,我们必须注意类型检查、错误恢复、内存分配错误和其他一些复杂的概念。CAPI中的大多数函数都不会检查其参数的正确性,我们必须在调用函数前确保参数的合法性,一旦出错,程序会直接崩溃而不会收到规范的错误信息。此外,CAPI强调的是灵活性和简洁性,某些情况下会以牺牲易用性为代价,即便是常见的需求,也可能需要调用好几个API。这么做虽然有些繁琐,但我们却可以完全控制所有细节。

第一个示例

首先来学习一个简单的应用程序的例子:一个独立的解释器。

一个简单地额独立解释器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "luaxlib.h"
#include "lualib.h"

int main(void){
char buff[256];
int error;
lua_State *L = luaL_newstate(); /*打开Lua*/
luaL_openlibs(L); /*打开标准库*/

while (fgets(buff,sizeof(buff),stdin)!= NULL){
error = luaL_loadstring(L,buff)||lua_pcall(L,0,0,0);
if(error){
fprintf(stderr,"%s\n",lua_tostring(L,-1));
lua_pop(L,1); /*从栈中弹出错误信息*/
}
}
lua_close(L);
return 0;
}

头文件lua.h声明了Lua提供的基础函数,其中包括创建新Lua环境的函数、调用Lua函数的函数、读写环境中的全局变量的函数,以及注册供Lua语言调用新函数的函数等等。lua.h中声明的所有内容都有一个前缀lua_。
头文件luaxlib.h声明了辅助库所提供的函数,其中所有的声明均以luaL_开头。辅助库使用lua.h提供的基础API来提供更高层次的抽象,特别是对标准库用到的相关机制进行抽象。基础API追求经济性和正交性,而辅助库则追求对常见任务的实用性。当然,要在程序中创建其他所需的抽象也是非常简单的。请记住,辅助库不能访问Lua的内部元素,而只能通过lua.h中声明的官方基础API完成所有工作。辅助库能实现什么,你的程序就能实现什么。
Lua标准库没有定义任何C语言全局变量,它将其所有的状态都保存在动态的结构体lua_State中,Lua中的所有函数都接收一个指向该结构的指针作为参数。这种设计使得Lua是可重入的,并且可以直接用于编程多线程代码。
顾名思义,函数luaL_newstate用于创建一个新的Lua状态。当它创建一个新状态时,新环境中没有包含预定义的函数,甚至连print也没有。为了保持Lua语言的精炼,所有的标准库都被组织成不同的包,这样我们在不需要使用某些包时可以忽略它们。头文件lualib.h中声明了用于打开这些库的函数。函数luaL_openlibs用于打开所有的标准库。
当创建好一个状态并且在其中加载标准库以后,就可以处理用户的输入了。程序会首先调用函数luaL_loadstring来编译用户输入的每一行内容。如果没有错误,则返回零,并向栈中压入编译后得到的函数。然后,程序调用函数lua_pcall从栈中弹出编译后的函数,并以保护模式运行。与函数lua_loadstring类似,如果没有错误发生,函数lua_pcall则返回零;当发生错误时,这两个函数都会向栈中压入一条错误信息。随后我们可以通过函数lua_tostring获取错误信息,并在打印错误信息后使用函数lua_pop将从栈中删除。
在C语言中,真是的错误处理可能会相当复杂,并且如何处理错误取决于应用的性质。Lua核不会直接向任何输出流写入数据,它只会通过返回错误信息来提示错误。每个应用可以用其所需的最恰当的方式来处理这些错误信息。为了简化讨论,假设一下示例使用如下简单的错误处理函数,即打印一条错误信息,关闭Lua状态并结束整个应用:

1
2
3
4
5
6
7
8
9
10
11
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void error (lua_State *L,const char *fmt, ...){
va_list argp;
va_start(argp,fmt);
vfprintf(stderr,fmt,argp);
va_end(argp);
lua_close(L);
exit(EXIT_FAILURE);
}

由于Lua既可以作为C代码来编译,也可以作为C++代码来编译,因此lua.h中并没有包含以下这种在C标准库中的常见的写法:

1
2
3
4
5
6
7
#ifdef __cplusplus
extern "C"{
#endif
...
#ifdef __cplusplus
}
#endif

如果将Lua作为C代码编译出来后又要在C++中使用,那么可以引入lua.hpp来替代lua.h,定义如下:

1
2
3
extern "C"{
#include "lua.h"
}

Lua和C之间通信的主要组件是无处不在的虚拟栈,几乎所有的API调用都是在操作这个栈中的值,Lua与C之间所有的数据交换都是通过这个栈完成的。此外,还可以利用栈保存中间结果。
当我们想在Lua和C之间交换数据时,会面对两个问题:第一个问题是动态类型和静态类型体系之间不匹配;第二个问题是自动内存管理和手动内存管理之间不匹配。
在Lua中,如果我们写t[k]=v,k和v都可以是几种不同类型;由于元表的存在,甚至t也可以有不同的类型。然而,如果要在C语言中提供这种操作,任意给定的settable函数都必须有一个固定的类型。为了实现这样的操作,我们就需要好几十个不同的函数。
可以通过在C语言中声明某种联合体类型来解决这个问题,假设这种类型叫lua_Value,它能够表示Lua语言中所有的值,然后,可以把settable声明为:

1
void lua_settable (lua_Value a,lua_Value k,lua_Value v);

这种方法有两个缺点。首先,我们很难将如此复杂的类型映射到其他语言中;而在设计Lua时,我们又要求Lua语言不仅能方便地与C/C++交互,而且还能与JavaFortranC#等其他语言方便地交互。其次,Lua语言会做垃圾收集:由于Lua预压引擎并不知道Lua中的一个表可能会被保存在一个C语言变量中,因此它可能会认为这个表是垃圾并将其收回。
因此,LuaAPI中灭有定义任何类似于lua_Value的类型,而是使用栈在Lua和C之间交换数据。栈中的每个元素都能保存Lua中任意类型的值。当我们想要从Lua中获取一个值时,只需要调用Lua,Lua就会将指定的值压入栈中。当想要将一个值传给Lua时,首先要将这个值压入栈,然后调用Lua将其中栈中弹出即可。尽管我们仍然需要一个不同的函数将每种C语言类型的值压入栈,还需要另一个不同函数从栈中弹出每种C语言类型的值,但是避免了过多的组合。另外,由于这个栈是Lua状态的一部分,因此垃圾收集器知道C语言正在使用哪些值。
几乎CAPI中的所有函数都会用到栈。正如第一个示例,函数luaL_loadstring将其结果留在栈中;函数lua_pcall从栈中取出要调用的函数,并且也会将错误消息留在栈中。
Lua严格地按照LIFO的规则来操作栈。在调用Lua时只有栈顶部的部分会发生改变,而C语言代码则有更大的自由度。更具体地说,C语言可以检视栈中的任何一个元素,甚至可以在栈的任意位置插入或删除元素。

压入元素

针对每一种能用C语言直接表示的Lua数据类型,CAPI中都有一个对应的亚栈函数:常量nil使用lua_pushnil;布尔值使用lua_pushboolean;双精度浮点数使用lua_pushnumber;整型使用lua_pushinteger;任意字符串使用lua_pushlstring;以\0终止的字符串使用lua_pushstring。

1
2
3
4
5
6
void lua_pushnil         (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, lua_Number n);
void lua_pushinteger (lua_State *L, lua_Integer n);
void lua_pushlstring (lua_State *L, const char *s, size_t len);
void lua_pushstring (lua_State *L, const char *s);

当然,也有向栈中压入C函数和用户数据的函数。
类型Lua_Number相当于Lua语言的浮点数类型,默认为double,但可以在编译时配置Lua,让Lua_Number为float甚至long double。类型lua_Integer相当于Lua语言中的整型,通常被定义为long long ,既有符号64位整型。同样,要把Lua语言中的lua_Integer配置为使用int或long也很容易。如果使用float-int组合,也就是32浮点数类型和整型,即我们所说的精简Lua,对于资源受限的机器和硬件而言,相当高效。
Lua语言中的额字符串不是以\0结尾的,它们可以包含任意二进制数据。因此,将字符串压栈的基本函数lua_pushlstring需要一个明确的长度作为参数。对于以\0结尾的字符串,也可以使用函数lua_pushstring,该函数通过strlen来计算字符串的长度。Lua语言不会保留指向外部字符串的指针。对于不得不保留的字符串,Lua要么生成一个内部副本,要么复用已有的字符串。因此,一旦上述函数返回,即使立即释放或修改缓冲区也不会出现问题。
无论何时向栈内压入一个元素,我们都应该确保栈中有足够的空间。请注意,现在你是一个C语言程序员,Lua语言不会宠着你。当Lua启动时,以及Lua调用C语言时,栈中至少有20个空闲的位置。对于大多数情况,这个空间是完全够用,所以我们一般无须考虑栈空间的问题。不过,有些任务可能会需要更多的栈空间,特备是循环向栈中压入元素时。在这些情况下,就需要调用哈数lua_checkstack来检查栈中是否有足够的空间:

1
int lua_checkstack (lua_State *L, int sz);

这里,sz是我们所需要的额外栈位置的数量。如果可能,函数lua_checkstack会增加栈的大小,以容纳所需的额外空间;否则,该函数返回零。
辅助库也提供了一个高层函数来检查栈空间:

1
void luaL_checkstack (lua_State *L, int sz, const char *msg);

该函数类似于函数lua_checkstack,但是如果栈空间不能满足请求,该函数会使用指定的错误信息抛出异常,而不是返回错误码。

查询元素

CAPI使用索引来引用栈中的元素。第一个被压入栈的元素索引为1,第二个被压入的元素索引为2,依次类推。我们还可以以栈顶为参照,使用负数索引来访问栈中的元素,此时,-1表示栈顶元素,-2表示在它之前被压入栈的元素,依次类推。例如,调用lua_tostring(L,-1)会将栈顶的值作为字符串返回。正如你接下来要看到的,有些情况下从栈底对栈进行索引更加自然,而有些情况下则使用负数索引更好。
要检查栈中的一个元素是否为特定的类型,CAPI提供了一系列名为lua_is的函数,其中可以是任意一种Lua数据类型。这些函数包括lua_isnil、lua_isnumber、lua_isstring和lua_istable等。所有这些函数都有同样的原型:

1
int lua_is* (lua_State *L, int index);

实际上,函数lua_isnumber不会检查某个值是否为特定类型,而是检查该值是否能被转换为特定类型。函数lua_isstring与之类似,特别之处在于,它接受数字。
还有一个函数lua_type,用于返回栈中元素的类型,每一种类型都由一个对应的常量表示,包括LUA_INIT、LUA_TBOOLEAN、LUA_TUMBER、LUA_TSTRING等。还函数一般与switch语句连用。当需要检查字符串和数值是否存在潜在的强制类型转换时,该函数也同样有用。
函数lua_to*用于从栈中获取一个值:

1
2
3
4
5
int 				lua_toboolean(lua_State *L, int index);
const char *lua_tolstring(lua_State *L, int index, size_t *len);
lua_State *lua_tothread(lua_State *L, int index);
lua_Number lua_tonumber(lua_State *L, int index);
lua_Integer lua_tointeger(lua_State *L, int index);

即使指定的元素的类型不正确,调用这些函数也不会有问题。函数lua_toboolean适用于所有类型,它可以按照如下的规则将任意Lua值转换为C的布尔值:nil和false转换为0,所有其他的Lua值转换为1.对于类型不正确的值,函数lua_tolstring和lua_tothread返回NULL。不过,数值相关的函数都无法提示数值的类型错误,因此只能简单地返回0。以前我们需要调用函数lua_isnumber来检查类型,但是Lua5.2引入了如下的新函数:

1
2
lua_Number      lua_tonumberx(lua_State *L, int idx, int *isnum)
lua_Integer lua_tointegerx(lua_State *L,int idx, int *isnum)

出口参数isnum返回了一个布尔值,来表示Lua值是否被强制转换为期望的类型。
函数lua_tolstring返回一个指向该字符串内部副本的指针,并将字符串的长度存入到参数len指定的位置。我们无法修改这个内部副本。Lua语言保证,只要对应的字符串还在栈中,那么这个指针就是有效的。当Lua调用的一个C函数返回时,Lua就会清空栈。因此,作为规则,永远不要指向Lua字符串的指针存放到获取该指针的函数之外。
函数lua_tolstring返回的所有字符串在其末尾都会有一个额外的\0,不过这些字符串中也可能会有\0,因此可以通过第三个参数len获取字符串的真实长度。特别的,假设栈顶的值是一个字符串,那么如下推断永远成立:

1
2
3
4
size_t len;
const char *s = lua_tolstring(L, -1 ,&len); /*任意Lua字符串*/
assert(s[len] == '\0');
assert(strlen(s) <= len);

如果不需要长度信息,可以在调用函数lua_tolstring时将第三个参数设为NULL。不过,使用宏lua_tostring会更好,因此这个宏就是用NULL作为第三个参数来调用函数lua_tolstring的。
为了掩饰这些函数的用法,示例提供了一个有用的辅助函数,它输出整个栈的内容。

对栈进行Dump

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
static void stackDump(lua_State *L){
int i;
int top = lua_gettop(L); /*栈的深度*/
for (i = 1; i<= top;i++){
int t = lua_type(L,i);
switch(t){
case LUA_TSTRING:{
printf("'%s'",lua_tostring(L,i));
break;
}
case LUA_TBOOLEAN:{
printf(lua_toboolean(L,i)?"true":"false");
break;
}
case LUA_TUNMBER:{
printf("%g",lua_tonumber(L,i));
break;
}
default:{
printf("%s",lua_typename(L,t));
break;
}
}
printf(" ");
}
printf("\n");
}

这个函数从栈底向栈顶遍历,并根据每个元素的类型打印其值。它打印字符串时会用单引号将其括起来,对数值类型的值则使用格式”%g”输出,对于其他C语言中不存在等价类型的值则只打印出它们的类型。
在Lua5.3中,由于整型总是可以被强制转换为浮点型,因此仍然可以用函数lua_tonumber和”%g”的格式打印所有的数值。但是,我们倾向于将整数打印为整型,以避免损失精度。此时,我们可以用新函数lua_isinteger来区分整型和浮点型:

1
2
3
4
5
6
7
case LUA_TNUMBER:{
if (lua_isinteger(L,i))
printf("%lld",lua_tointeger(L,i));
else
printf("%g",lua_tonumber(L,i));
break;
}

其他栈操作

除了上述在C语言和栈之间交换数据的函数外CAPI还提供了下列用于通过栈操作的函数:

1
2
3
4
5
6
7
8
int lua_gettop			(lua_State *L);
void lua_settop (lua_State *L, int index);
void lua_pushvalue (lua_State *L, int index);
void lua_rotate (lua_State *L, int index, int n);
void lua_remove (lua_State *L, int index);
void lua_insert (lua_State *L, int index);
void lua_replace (lua_State *L, int index);
void lua_copy (lua_State *L, int fromidx,int toidx);

函数lua_gettop返回栈中元素的个数,也即栈顶元素的索引。函数lua_settop将栈顶设置为一个指定的值,即修改栈中的元素数量。如果之前的栈顶比新设置的更高,那么高出来的这些元素就会被丢弃;反之,该函数会向栈中压入nil来不足大小。特别的,函数lua_settop(L,0)用于清空栈。在调用函数lua_settop时也可以使用负数索引;基于这个功能,CAPI提供了下面的宏,用于从栈中弹出n个元素:

1
#define lua_pop(L,n)  lua_settop(L,-(n) -1)

函数lua_pushvalue用于将指定索引上的元素的副本压入栈。
函数lua_rotate是Lua5.3中新引入的。顾名思义,该函数将指定索引的元素向栈顶转动n个位置。若n为整数,表示将元素向栈顶方向转动,而n为负数则表示向相反的方向转动。这是一个非常有用的函数,另外两个CAPI操作实际上是基于使用该函数的宏定义的。其中一个是lua_remove,用于删除指定索引的元素,并将该位置上的所有元素下移以填补空缺,其定义如下:

1
#define lua_remove(L,idx) \ (lua_rotate(L,(idx),-1),lua_pop(L,1))

也就是说,该函数会将栈顶转动一格,把想要的那个元素移动到栈顶,然后弹出该元素。另一个宏是lua_insert,用于将栈顶元素移动到指定位置,并上移指定位置之上的所有元素以开辟出一个元素的空间:

1
#define lua_insert(L,inx)    lua_rotate(L,(idx),1)

函数lua_replace弹出一个值,并将栈顶设置为指定索引上的值,而不移动任何元素。最后,函数lua_copy将一个索引上的值复制到另一个索引上,并且原值不受影响。请注意,以下的操作不会对空栈产生影响:

1
2
3
4
lua_settop(L,-1);/*将栈顶设为当前值*/
lua_insert(L,-1);/*将栈顶的元素移动到栈顶*/
lua_copy(L,x,x);/*把一个元素复制到它当前的位置*/
lua_rotate(L,x,0);/*旋转零个位置*/

示例 栈操作示例

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
#include <stdio.h>
#include "lua.h"
#include "luaxlib.h"

static void stackDump(lua_State *L){
参见上面那个示例
}
int main(void){
lua_State *L = luaL_newstate();
lua_pushboolean(L,1);
lua_pushnumber(L,10);
lua_pushnil(L);
lua_pushstring(L,"hello");
stackDump(L):
/*将输出:true 10 nil 'hello' */
lua_pushvalue(L.-4); stackDump(L);
/*将输出:true 10 nil 'hello' true */
lua_replace(L,3); stackDump(L);
/*将输出:true 10 nil 'hello' */
lua_settop(L,6);stackDump(L);
/*将输出:true 10 true 'hello' nil nil */
lua_rotate(L,3,1);stackDump(L);
/*将输出:true 10 nil true 'hello' nil */
lua_remove(L,-3);stackDump(L);
/*将输出:true 10 nil 'hello' nil*/
lua_settop(L,-5);stackDump(L);
/*将输出:true */
lua_close(L);
return 0;
}

使用CAPI进行错误处理

Lua中所有的结构都是动态的:它们会按需扩展,并且在可能时最后重新收缩。这意味着在Lua中内存分配失败可能无处不在,几乎所有的操作最终都可能会面临内存分配失败。此外,许多操作可能会抛出异常。例如,访问一个全局变量可能会触发__index元方法,而该元方法又可能会抛出异常。最后,分配内存的操作会触发垃圾收集器,而垃圾收集器又可能会调用同样可能抛出异常的析构器。简而言之,Lua API中的绝大部分函数都可能抛出异常。
Lua语言使用异常来提示错误,而没有再API的每个操作中使用错误码。与C++或Java不同,C语言没有提供异常处理机制。为了解决这个问题,Lua使用了C语言中的setjmp机制,setjmp营造了一个类似异常处理的机制。因此,大多数API函数都可以跑出异常而不是直接返回。
在编写库代码时,由于Lua会捕获所有异常,因此,对我们来说使用longjmp并不是进行额外的操作。不过,在编写应用程序代码时,则必须提供一种捕获异常的方法。

处理应用代码中的错误

如果应用调用了Lua API中的函数,就可能发生错误。Lua语言通常通过长跳转来提示错误。但是,如果没有相应的setjmp,解释器就无法进行长跳转。此时,API中的任何错误都会导致Lua调用紧急函数,当这个函数返回后,应用就会退出。我们可以通过函数lua_atpanic来设置自己的紧急函数,但作用不大。
要正确地处理应用代码中的错误,就必须通过Lua语言调用我们自己的代码,这样Lua语言才能设置适合的上下文来捕获异常,即在setjmp的上下文中运行代码。类似于通过函数pcall在保护模式中运行Lua代码,我们也可以用函数lua_pcall运行C代码。更具体地说,可以把C代码封装到一个函数F中,然后使用lua_pcall调用这个函数F。通过这种方式,我们的C代码会在保护模式下运行。即便发生内存分配失败,函数lua_pcall也会返回一个对应的错误码,是解释器能够保持一致的状态,如下所示:

1
2
3
4
5
6
7
8
static int foo(lua_State *L){
code to run in protected mode(要以保护模式运行的代码)
return 0;
}
int secure_foo(lua_State *L){
lua_pushcfunction(L,foo)/*将foo作为Lua函数压栈*/
return (lua_pcall(L,0,0,0) == 0);
}

在上述示例中,无论发生什么,调用secure_foo时都会返回一个布尔值,来表示foo执行是否成功。特别的,请注意,栈中已经预先分配了空间,而且函数lua_pushcfunction不会分配内存,这样才不会引发错误。

处理库代码中的错误

Lua是一种安全的语言。这意味着不管用Lua写什么,也不管写出来的内容多么不正确,我们总能用它自身的机制来理解程序的行为。此外,程序中的错误也是通过Lua语言的机制来检测和解释的。与之相比,许多C语言代码中的错误只能从底层硬件的角度来解释。
只要往Lua中加入新的C函数,这种安全性就可能被打破。例如,一个等价于BASIC命令poke的函数就可能导致各种各样的内存崩溃。因此,我们必须确保新加入的内容对Lua语言来说是安全的,并提供妥善的错误处理。
正如之前讨论的,C语言程序必须通过lua_pcall设置错误处理。不过,在为lua编写库函数时,通常无须处理错误。库函数抛出的错误要么被Lua中的pcall捕获,要么被应用代码中的lua_pcall捕获。因此,当C语言库中的函数检测到错误时,只需要简单地调用lua_error即可。函数lua_error会收拾Lua系统中的残局,然后跳转回保护模式调用处,并传递错误信息。

内存分配

Lua语言核心对内存分配不进行任何假设,它既不会调用malloc也不会调用realloc来分配内存。相反,Lua语言核心只会通过一个分配函数来分配和释放内存,当用户创建Lua状态时必须提供该函数。
luaL_newstate是一个用默认分配函数来创建Lua状态的辅助函数。该默认分配函数使用了来自C语言标准函数库的标准函数malloc-realloc-free,对于大多数应用程序来说,这几个函数够用了。但是,要完全控制Lua的内存分配也很容易,使用原始的lua_newstate来创建我们自己的Lua状态即可:

1
lua_State *lua_newstate(lua_Alloc f, void *ud);

该函数有两个参数:一个是分配函数,另一个是用户数据。用这种方式创建的Lua状态会通过调用f完成所有的内存分配和释放,甚至结构lua_State也是由f分配的。
分配函数必须满足lua_Alloc的类型声明:

1
2
3
4
typedef void * (*lua_Alloc) (void *ud,
void *ptr,
size_t osize,
size_t nsize);

第一个参数始终为lua_newstate所提供的用户数据;第二个参数是正要被分配或者释放的地址;第三个参数是原始块的大小;最后一个参数是请求的块大小。如果ptr不是NULL,Lua会保证其之前被分配的大小就是osize。
Lua语言使用NULL表示大小为零的块。当nsize为零时,分配函数必须释放ptr指向的块并返回NULL,对应于所要求的大小的块。当ptr是NULL时,该函数必须分配并返回一个指定大小的块;如果无法分配指定的块,则必须返回NULL。如果ptr是NULL并且nsize为零,则两条规则都适用:最终结果是分配函数什么都不做,返回NULL。
最后,当ptr不是NULL并且nsize不为零时,分配函数应该像realloc一样重新分配块并且返回新地址。同样,当出现错误分配函数必须返回NULL。Lua假定分配函数在块的新尺寸小于或等于旧尺寸时不会失败。
luaL_newstate使用的标准分配函数定义如下:

1
2
3
4
5
6
7
8
9
void *l_alloc (void *ud , void *ptr, size_t osize, size_t nsize){
(void)ud; (void)osie;/*未使用*/
if(nsize == 0 ){
free(ptr);
return NULL;
}
else
return realloc (ptr,nsize);
}

该函数假设free(NULL)什么也不做,并且realloc(NULL,size)等价于malloc(size)。IOSC标准会托管这两种行为。
我们可以通过调用lua_getallocf恢复Lua状态的内存分配器:

1
lua_Alloc lua_getallocf (lua_State *L, void **ud);

如果ud不是NULL,那么该函数会把*ud设置为该分配器的用户数据。我们可以通过调用lua_setallocf来更改Lua状态的内存分配器:

1
void lua_setallocf (lua_State *L, Lua_Alloc f, void *ud);

请记住,所有新的分配函数都有责任释放由前一个分配函数分配的块。通常情况下,新的分配函数是在旧分配函数的基础上做了包装,来追踪或同步访问堆的。
Lua在内部不会为了重用而缓存空闲内存。它假定分配函数会完成这种缓存工作;而优秀的分配函数确实也会这么做。Lua不会试图压缩内存碎片。研究表明,内存碎片更多是由糟糕的分配决策导致的,而非程序的行为造成的;而优秀的分配函数不会造成太多内存碎片。
对于已有的优秀分配函数,想要做到比它更好是很难的,但有时候也不妨试一试。例如,Lua会告诉你已经释放或者重新分配的块的大小。因此,一个特定的分配函数不需要保存有关块大小的信息,以此减少每个块的内存开销。
还有一种可以改善的内存分配的场景,是在多线程系统中。这种系统通常需要对内存分配函数进行线程同步,因为这些函数使用的是全局资源。不过,对Lua状态的访问也必须是同步的——或者更好的情况是,限制只有一个线程能够访问Lua状态。如果每个Lua状态都从私有的内存池中分配内存,那么分配函数就可以避免线程同步导致的额外开销了。

+