Q:什么是Lua的虚拟栈?
A:C与Lua之间通信关键内容在于一个虚拟的栈。几乎所有的调用都是对栈上的值进行操作,所有C与Lua之间的数据交换也都通过这个栈来完成。另外,你也可以使用栈来保存临时变量。
每一个与Lua通信的C函数都有其独有的虚拟栈,虚拟栈由Lua管理。
栈的使用解决了C和Lua之间两个不协调的问题:第一,Lua会自动进行垃圾收集,而C要求显式的分配存储单元,两者引起的矛盾。第二,Lua中的动态类型和C中的静态类型不一致引起的混乱。
Q:虚拟栈索引?
A:正索引的顺序为元素入栈的先后顺序,栈中的第一个元素(也就是第一个被入栈的元素)索引为1,第二个元素索引为2,以此类推。我们也可以使用负索引,以栈顶作为索引的参照来存取元素,栈顶元素的索引为-1,其前面的一个元素索引为-2,以此类推。正负索引各有其适用的场合,灵活应用,简化编码。
Q:第一个例子?
A:一个简单的Lua解释器。
#include <stdio.h>
#include <string.h>
/* 定义对Lua的基本操作的函数,其中定义的所有函数都有"lua_"前缀。
* 例如调用Lua函数(如"lua_pcall()"),获取以及赋值Lua的全局变量,注册函数供Lua调用,等等。
*/
#include <lua.h>
/* 定义"auxiliary library",其中定义的所有函数都有"luaL_"前缀。
* "auxiliary library"使用lua.h中提供的基本API实现了更高一层抽象方法的实现。
* 所有的Lua标准库都使用了auxiliary library。
*/
#include <lauxlib.h>
/* 定义打开各种库的函数。
*/
#include <lualib.h>
int main(void)
{
char buff[256] = {0};
int error = 0;
lua_State *L = luaL_newstate(); // 创建Lua状态机。
luaL_openlibs(L); // 打开Lua状态机"L"中的所有Lua标准库。
while (fgets(buff, sizeof(buff), stdin) != NULL) { // 获取用户输入。
/* "luaL_loadbuffer()",编译用户输入为Lua的"chunk"并将其入栈。
* "line"是"chunk"的名字,用于调试信息和错误消息。
* "lua_pcall()",以保护模式运行"chunk"并将其从栈顶弹出。
* 两个函数均是在成功时返回"LUA_OK"(实际的值是0),
* 失败时返回错误码,并将错误信息入栈。
*/
error = luaL_loadbuffer(L, buff, strlen(buff), "line") || lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s", lua_tostring(L, -1)); // 从栈顶取出错误信息打印。
lua_pop(L, 1); // 弹出栈顶的错误信息。
}
}
lua_close(L); // 关闭Lua状态机。
return 0;
}
prompt> gcc main.c -llua -ldl -lm
prompt> ./a.out
print("Hello World!")
Hello World!
Q:如何将元素入栈与出栈?
A:Lua中的数据类型与C中的并不一一对应,对于不同的数据类型,需要不同的入栈函数。
// 将"b"作为一个布尔值入栈。
void lua_pushboolean(lua_State *L, int b)
/* 将C函数"fn"以及其在虚拟栈上关联的"n"个值作为"Closure"入栈。
* "n"最大为255,第一个被关联的值首先入栈,栈顶是最后一个被关联的值,
* 这些值会在函数调用成功后被出栈。
*/
void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n)
// 将C函数"f"作为函数入栈。内部实际调用"lua_pushcclosure(L, f, 0)"。
void lua_pushcfunction(lua_State *L, lua_CFunction f)
/* 将一个被格式化后的字符串入栈。函数返回这个字符串的指针。
* 与C语言中的"sprintf()"类似,其区别在于:
* 1、不需要为结果分配空间。
* 其结果是一个Lua字符串,由Lua来关心其内存分配(同时通过垃圾收集来释放内存)。
* 2、"fmt"不支持符号、宽度、精度。只支持:
* "%%": 字符'%'。
* "%s": 带零终止符的字符串,没有长度限制。
* "%f": "lua_Number"(Lua中的浮点数类型)。
* "%L": "lua_Integer"(Lua中的整数类型)。
* "%p": 指针或是一个十六进制数。
* "%d": "int"。
* "%c": "char"。
* "%U": 用"long int"表示的UTF-8字。
*/
const char *lua_pushfstring(lua_State *L, const char *fmt, ...)
/* 将长度为"len",非字面形式的字符串"s"入栈。
* Lua对这个字符串做一个内部副本(或是复用一个副本),
* 因此"s"处的内存在函数返回后,可以释放掉或是立刻重用于其它用途。
* 字符串内可以是任意二进制数据,包括'\0'。函数返回内部副本的指针。
*/
const char *lua_pushlstring(lua_State *L, const char *s, size_t len)
// 将字面形式的字符串"s"入栈,函数自动给出字符串的长度。返回内部副本的指针。
const char *lua_pushliteral(lua_State *L, const char *s)
/* 将以'\0'结尾的字符串"s"入栈。
* Lua对这个字符串做一个内部副本(或是复用一个副本),
* 因此"s"处的内存在函数返回后,可以释放掉或是立刻重用于其它用途。
* 函数返回内部副本的指针。如果"s"为"NULL",将"nil"入栈并返回"NULL"。
*/
const char *lua_pushstring(lua_State *L, const char *s)
// 等价于"lua_pushfstring()"。不过是用"va_list"接收参数,而不是用可变数量的实际参数。
const char *lua_pushvfstring(lua_State *L, const char *fmt, va_list argp)
// 将全局环境入栈。
void lua_pushglobaltable(lua_State *L)
// 将值为"n"的整数入栈。
void lua_pushinteger(lua_State *L, lua_Integer n)
/* 将一个轻量用户数据"p"入栈。
* 用户数据是保留在Lua中的C值。轻量用户数据表示一个指针"void*"。
* 它像一个数值,你不需要专门创建它,它也没有独立的元表,
* 而且也不会被收集(因为从来不需要创建)。只要表示的C地址相同,两个轻量用户数据就相等。
*/
void lua_pushlightuserdata(lua_State *L, void *p)
// 将空值入栈。
void lua_pushnil(lua_State *L)
// 将一个值为"n"的浮点数入栈。
void lua_pushnumber(lua_State *L, lua_Number n)
// "L"表示的线程入栈。如果这个线程是当前状态机的主线程的话,返回1。
int lua_pushthread(lua_State *L)
// 将虚拟栈上索引"index"处的元素的副本入栈。
void lua_pushvalue(lua_State *L, int index)
对应的出栈函数就很简单了,只有一个lua_pop()
,
// 从虚拟栈中弹出"n"个元素。
void lua_pop(lua_State *L, int n)
Q:如何检查虚拟栈中元素的类型?
A:
// 如果栈中索引"index"处的元素为"bool"类型,则返回1,否则返回0。
int lua_isboolean(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个C函数,则返回1,否则返回0。
int lua_iscfunction(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个C函数或是一个Lua函数,则返回1,否则返回0。
int lua_isfunction(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个整数,则返回1,否则返回0。
int lua_isinteger(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个轻量级的"userdata",则返回1,否则返回0。
int lua_islightuserdata(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个"nil",则返回1,否则返回0。
int lua_isnil(lua_State *L, int index
// 如果"index"是一个无效索引时,返回1,否则返回0。
int lua_isnone(lua_State *L, int index)
// 如果"index"是一个无效索引或者"index"处的元素是一个"nil",则返回1,否则返回0。
int lua_isnoneornil(lua_State *L, int index)
/* 如果栈中索引"index"处的元素是一个数值或者是一个可以转换为数值的字符串,
* 则返回1,否则返回0。
*/
int lua_isnumber(lua_State *L, int index)
/* 如果栈中索引"index"处的元素是一个字符串或者是一个可以转换为字符串的数值,
* 则返回1,否则返回0。
*/
int lua_isstring(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个"table",则返回1,否则返回0。
int lua_istable(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个线程,则返回1,否则返回0。
int lua_isthread(lua_State *L, int index)
// 如果栈中索引"index"处的元素是一个"userdata",则返回1,否则返回0。
int lua_isuserdata (lua_State *L, int index)
// 如果栈中的"coroutine"可以被挂起,则返回1,否则返回0。
int lua_isyieldable(lua_State *L)
/* 返回栈中索引"index"处元素的类型。这些类型在"lua.h"中定义,如下:
* #define LUA_TNONE (-1) // 无效
* #define LUA_TNIL 0 // "nil"
* #define LUA_TBOOLEAN 1 // "bool"
* #define LUA_TLIGHTUSERDATA 2 // 轻量级"userdata"
* #define LUA_TNUMBER 3 // 数值
* #define LUA_TSTRING 4 // 字符串
* #define LUA_TTABLE 5 // "table"
* #define LUA_TFUNCTION 6 // 函数
* #define LUA_TUSERDATA 7 // "userdata"
* #define LUA_TTHREAD 8 // 线程
*/
int lua_type(lua_State *L, int index)
// 返回"tp"表示的类型的名字。"tp"是"lua_type()"的返回值之一。
const char *lua_typename(lua_State *L, int tp)
Q:如何转换虚拟栈中元素的类型?
A:
// 将栈中"index"处的元素转换为C中的"bool"值返回。
int lua_toboolean(lua_State *L, int index)
// 将栈中"index"处的元素转换为一个C函数返回。指定的元素必须是一个C函数,否则返回"NULL"。
lua_CFunction lua_tocfunction(lua_State *L, int index)
/* 将栈中"index"处的元素转换为一个整数返回。
* 指定的元素必须是一个整数或是一个可以被转换为整数的数字或字符串,否则返回0。
* 如果"isnum"非"NULL","*isnum"会被赋值为操作是否成功的"bool"值。
*/
lua_Integer lua_tointegerx(lua_State *L, int index, int *isnum)
// 内部调用"lua_tointegerx(L, index, NULL)"。
lua_Integer lua_tointeger(lua_State *L, int index)
/* 将栈中"index"处的元素转换为一个C字符串并将其指针返回。
* 如果"len"非"NULL","*len"将获得字符串的长度。
* 指定元素必须是一个字符串或是一个数字,否则返回返回"NULL"。
* 如果指定元素是一个数字,函数会将元素的类型转换为字符串。
* 返回的字符串结尾包含'\0',而在字符串中允许包含多个'\0'。
* 函数返回的字符串应立即转存,否则有可能被Lua垃圾回收器回收。
*/
const char *lua_tolstring(lua_State *L, int index, size_t *len)
/* 将栈中"index"处的元素转换为一个浮点数返回。
* 指定的元素必须是一个数字或是一个可被转换为数字的字符串,否则返回0。
* 如果"isnum"非"NULL","*isnum"会被赋值为操作是否成功的"bool"值。
*/
lua_Number lua_tonumberx(lua_State *L, int index, int *isnum)
// 内部调用"lua_tonumberx(L, index, NULL)"。
lua_Number lua_tonumber(lua_State *L, int index)
/* 将栈中"index"处的元素转换为一个C指针返回。
* 指定的元素必须是一个"userdata","table",线程或是一个函数,否则返回"NULL"。
*/
const void *lua_topointer(lua_State *L, int index)
// 内部调用"lua_tolstring(L, index, NULL)"。
const char *lua_tostring(lua_State *L, int index)
/* 将栈中"index"处的元素转换为一个Lua线程返回。
* 指定的元素必须是一个线程,否则返回"NULL"。
*/
lua_State *lua_tothread(lua_State *L, int index)
/* 栈中"index"处的元素如果是一个完全"userdata",则返回其内存地址的指针;
* 如果是一个轻量级"userdata",则返回其存储的指针。
*/
void *lua_touserdata(lua_State *L, int index)
Q:一些维护虚拟栈的方法?
A:
/* int lua_gettop(lua_State *L)
* 返回栈顶元素的索引。
* 因为栈中元素的索引是从1开始编号的,所以函数的返回值相当于栈中元素的个数。
* 返回值为0表示栈为空。
*/
lua_gettop(L); // 返回栈中元素的个数。
/* void lua_settop(lua_State *L, int index)
* 设置栈顶为索引"index"指向处。
* 如果"index"比"lua_gettop()"的值大,那么多出的新元素将被赋值为"nil"。
*/
lua_settop(L, 0); // 清空栈。
/* void lua_remove(lua_State *L, int index)
* 移除栈中索引"index"处的元素,该元素之上的所有元素下移。
*/
/* void lua_insert(lua_State *L, int index)
* 将栈顶元素移动到索引"index"处,索引"index"(含)之上的所有元素上移。
*/
/* void lua_replace(lua_State *L, int index)
* 将栈顶元素移动到索引"index"处。(相当于覆盖了索引"index"处的元素)
*/
一个”dump”整个堆栈内容的例子,
#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
static void stackDump(lua_State *L)
{
int i = 0;
int top = lua_gettop(L); // 获取栈中元素个数。
for (i = 1; i <= top; ++i) // 遍历栈中每一个元素。
{
int t = lua_type(L, i); // 获取元素的类型。
switch(t)
{
case LUA_TSTRING: // strings
printf("'%s'", lua_tostring(L, i));
break;
case LUA_TBOOLEAN: // bool
printf(lua_toboolean(L, i) ? "true" : "false");
break;
case LUA_TNUMBER: // number
printf("%g", lua_tonumber(L, i)); // %g,自动选择%e或%f表示数值。
break;
default: // other values
printf("%s", lua_typename(L, t)); // 将宏定义的类型码转换为类型名称。
break;
}
printf(" "); // put a separator
}
printf("\n");
}
int main(void)
{
lua_State *L = luaL_newstate(); // 创建Lua状态机。
// 向虚拟栈中压入值。
lua_pushboolean(L, 1); // true
lua_pushnumber(L, 10); // 10
lua_pushnil(L); // nil
lua_pushstring(L, "hello"); // "hello"
stackDump(L); // true 10 nil 'hello'
lua_pushvalue(L, -4); // 将索引-4处的值的副本入栈。
stackDump(L); // true 10 nil 'hello' true
lua_replace(L, 3); // 将栈顶元素移动到索引3处,并覆盖原先的元素。
stackDump(L); // true 10 true 'hello'
lua_settop(L, 6); // 将栈顶设置为索引6处,多出来的新元素被赋值为"nil"。
stackDump(L); // true 10 true 'hello' nil nil
lua_remove(L, -3); // 移除索引-3处的元素,其上所有元素下移。
stackDump(L); // true 10 true nil nil
lua_settop(L, -5); // 将栈顶设置为索引-5处。
stackDump(L); // true
lua_close(L); // 关闭Lua状态机。
return 0;
}
附加:
1、Lua库没有定义任何全局变量,它所有的状态都保存在动态结构lua_State
中,而且指向这个结构的指针作为所有Lua函数的一个参数。这样的实现方式使得Lua能够重入(reentrant),并且为在多线程中的使用中作好准备。
2、如果你是在C++的代码中使用Lua,别忘了,
extern "C" {
#include <lua.h>
}
3、Lua中的字符串可以不以'\0'
作为结束符。这样,字符串中可以包含任意的二进制(甚至是'\0'
),字符串的长度由明确的长度指定。
4、在lua_pushlstring()
、lua_pushliteral()
以及lua_pushstring()
中,Lua不保存字符串(变量)指针。因此当这些函数返回时,你就可以修改你的字符串了。
5、对于入栈是否有栈空间的情况,你需要自己判断,别忘了现在你是一个C程序员。当Lua启动或者任何Lua调用C的时候,虚拟栈中至少有20个空间(在”lua.h”中LUA_MINSTACK
定义),这对于一般情况下够用了,所以一般不用考虑。但有时候确实需要更多的栈空间(比如调用一个不定参数的函数),此时你需要使用lua_checkstack
检查栈空间的情况。
/* 确保堆栈上至少有"n"个额外空位。如果不能把堆栈扩展到相应的尺寸,函数返回"false"。
* 失败的原因包括将把栈扩展到比固定最大尺寸还大(至少是几千个元素)或分配内存失败。
* 这个函数永远不会缩小堆栈,如果堆栈已经比需要的大了,那么就保持原样。
*/
int lua_checkstack(lua_State *L, int sz)
6、遍历一个”table”时,不要将lua_tolstring()
作用在”key”上,这样会导致lua_next()
无法正常运行。
7、在平常的编码中,对于执行失败时会返回0
的lua_to*()
类别的函数,我们最好先使用lua_is*()
类别的函数判断参数的类型,之后再使用lua_to*()
类别的函数对参数进行转换;而对于执行失败时会返回NULL
的lua_to*()
类别的函数,我们可以直接使用lua_to*()
类别的函数直接对参数进行转换,判断函数的返回值非NULL
与否,就能判断转换是否成功。
8、lua_pop()
就是通过lua_settop()
实现的(在”lua.h”中定义),
#define lua_pop(L,n) lua_settop(L, -(n)-1)
9、以下操作对于虚拟栈没有任何影响,
lua_settop(L, -1); /* set top to its current value */
lua_insert(L, -1); /* move top element to the top */
lua_replace(L, -1); /* replace top element by the top element */