Lua和C语言的交互

本文转自http://www.grati.org/?p=662

Lua生来就是为了和C交互的,因此使用C扩展Lua或者将Lua嵌入到C当中都是非常流行的做法。要想理解C和Lua的交互方式,首先要回顾一下C语言是如何处理函数参数的。

C函数和参数
大家知道C语言是用汇编实现的,在汇编语言中可没有函数的概念,与函数对应的是叫做子过程的东西,子过程就是一段指令,一个子过程与它调用的子过程之间通过栈来进行参数的传递交互。在一个子过程在调用别的子过程之前,会按照约定的格式将要调用的子过程需要的参数入栈,在被调用的子过程中,可以按照约定的规则将参数从栈中取出。同理,对于返回值的传递也同样是通过堆栈进行的。C语言约定的参数放入栈中的格式,就是“调用惯例”。C语言的函数原型则决定了压入栈中的参数的数量和类型。

Lua的虚拟堆栈
Lua和C之间的交互巧妙的模拟了C语言的堆栈,Lua和C语言之间的相互调用和访问都通过堆栈来进行,巧妙的解决了不同类型之间变量相互访问的问题。具体的,我们想象如下一个图

  +-------+                      +-------+
  |       |                      |       |
  |       |      +-------+       |       |
  |   C   | <==> |       | <==>  |  Lua  |
  | Space |      |Virtual|       | Space |
  |       |      | Stack |       |       |
  |       |      |       |       |       |
  +-------+      +-------+       +-------+


由于C和Lua是不同层次的语言,因此C语言的变量和Lua中的变量以及函数不能直接的交互,我们假定C语言和Lua都有自己的“空间(C Space和Lua Space)”。而这两个空间之间的交互就通过上图中的这个虚拟堆栈来解决。为何采用虚拟堆栈的方式来进行交互呢?其目的是在提供强大的灵活性的同时避免交互时两种语言变量类型的组合爆炸。

C语言读写Lua全局变量(基本类型)
C语言读取Lua的全局变量是一种最简单的操作。通过上图我们可以猜测到,如果通过C语言读取Lua中的全局变量需要两步:1、将全局变量从Lua Space压入虚拟堆栈;2、从堆栈将全局变量读取到C语言Space中。在Lua和C的交互中,Lua无法看到和操作虚拟堆栈,仅在C语言中有操作堆栈的权利,因此前面说到的两步全都是在C语言中完成的。我们看一个简单的例子


Lua代码:
global_var1 = 5;
print("Print global varb from lua", global_var1);


C代码:
......
void get_global(lua_State *L)
{
int global_var1;
lua_getglobal(L, "global_var1"); /* 从lua的变量空间中将全局变量global_var1读取出来放入虚拟堆栈中 */
global_var1 = lua_tonumber(L, -1); /* 从虚拟堆栈中读取刚才压入堆栈的变量,-1表示读取堆栈最顶端的元素 */

printf("Read global var from C: %d\n", global_var1);
}
......

执行结果:

pi@raspberrypi ~/Programming/article_lua $ ./a.out global_var.lua
Print global var from lua  5
Read global_var: 5

Lua中对堆栈的操作都是通过索引来进行的,索引为1表示从栈底数第一个元素,索引为2表示从栈底数第二个元素;同样也可以使用负数从栈顶开始计算,-1表示从栈顶数第一个元素,-2表示从栈顶数第二个元素。更多堆栈的操作函数请参考lua的官方手册http://www.lua.org/manual/5.2/manual.html。 同样从堆栈中获取元素,除了我们使用的lua_tonumber之外,还有lua_tolstring,lua_toboolean等。
通常情况下在读取变量之前还需要对堆栈中元素的实际类型做出检查:

C代码:
......
void get_global(lua_State *L)
{
int global_var1;
lua_getglobal(L, "global_var1"); /* 从lua的变量空间中将全局变量global_var1读取出来放入虚拟堆栈中 */
if (!lua_isnumber(L, -1))        /* 检查堆栈中栈顶第一个元素是否是数字 */
error(L, "Is not number.");
global_var1 = lua_tonumber(L, -1); /* 从虚拟堆栈中读取刚才压入堆栈的变量,-1表示读取堆栈最顶端的元素 */
}
......

写入全局变量也一样简单:
首先将数据压入堆栈,然后再将堆栈中的数据存入全局变量。

C代码:
void set_global(lua_State *L)
{
lua_pushinteger(L, 9);
lua_setglobal(L, "global_var1");
printf("set global var from C:9\n");
}

执行结果:

pi@raspberrypi ~/Programming/article_lua $ ./a.out global_var.lua
set global var from C:9
Print global var from lua       9

C调用Lua函数 不要怀疑,对Lua函数的调用也是通过栈来进行的。请看如下代码:

Lua代码: function lua_func (x, y) print("Parameters are: ", x, y) return (x^2 * math.sin(y))/(1-x) end

C代码: double c_func(lua_State *L, double x, double y){ double z;

lua_getglobal(L, "lua_func");    /* 首先将lua函数从Lua Space放入虚拟堆栈中 */ lua_pushnumber(L, x);            /* 然后再把所需的参数入栈 */ lua_pushnumber(L, y);

if (lua_pcall(L, 2, 1, 0) != 0){ /* 使用pcall调用刚才入栈的函数,pcall的参数的含义为:pcall(Lua_state, 参数个数, 返回值个数, 错误处理函数所在的索引),最后一个参数暂时先忽略 */ error(L, "error running lua function: $s", lua_tostring(L, -1)); }

z = lua_tonumber(L, -1);         /* 将函数的返回值读取出来 */ lua_pop(L, 1);                   /* 将返回值弹出堆栈,将堆栈恢复到调用前的样子 */

printf("Return from lua:%f\n", z);

 

return z; }

执行结果: pi@raspberrypi ~/Programming/article_lua $ ./a.out lua_func.lua Parameters are: 9 2 Return from lua:-9.206636

Lua调用C函数 Lua调用C函数其实就是用C编写Lua的扩展,使用C为Lua编写扩展也非常简单。所有C扩展的函数都有一个固定的函数原型,如下所示: C代码: static int l_sin (lua_State *L) { double d = lua_tonumber(L, 1);      /* 不出意外,Lua中的参数也是通过虚拟堆栈传递的。因此C函数必须自己从堆栈中读取参数。注意在Lua中调用函数时是不会做原型检查的,Lua代码调用C函数时传递几个参数,虚拟堆栈中就会有几个参数,因此C代码在从堆栈中读取参数的时候最好自己检查一下堆栈的大小和参数类型是否符合预期。这里为了简化起见我们就不做类型检查了 */ d = sin(d); /* 这里是C函数实现自己功能的代码 */ lua_pushnumber(L, d);              /* 在完成计算后,只需将结果重新写入虚拟堆栈即可(写入的这个值就是函数的返回值) */ return 1; /* 函数的返回值是函数返回参数的个数。没错,Lua函数可以有多个返回值。 */ }

static void regist_func(lua_State *l) /* 这个函数将C函数写入Lua的命名空间中。 */ { lua_pushcfunction(l, l_sin); lua_setglobal(l, "mysin"); }

 

将函数写入Lua全局命令空间的代码很简单,和写入全局变量的代码一样,都是先将C函数压入堆栈,然后再将虚拟堆栈中的函数指针写入Lua全局命名空间并将其命名为”mysin”。之后在Lua中就可以使用”ret = mysin(30)”这样的形式调用我们的C函数了。

C语言读取Lua中的表 C语言读取Lua table会稍微复杂一点,不过Lua的table是一种重要的数据结构,因此对table的读写也是很重要的内容。读取Table基本需要如下几步: 1、使用lua_getglobal将表从Lua命名空间读取到虚拟堆栈中; 2、使用lua_pushstring将要读取的字段的名称压入堆栈; 3、使用函数lua_gettable,这个函数会将table和key出栈,然后把对应字段的值入栈; 4、最后使用lua_toXXXX从堆栈中读取值并使用lua_pop将数值出栈将堆栈恢复到调用前的样子; 不要纠结,我也觉得复杂,而且十分怀疑性能问题,但是Lua作者说Lua是一门快速的语言。好吧,暂且听他的等,回来理解深入之后再读下代码一探究竟。

Lua代码中定义如下的table: BLUE = {r=0, g=0, b=1} background = BLUE

C语言中使用如下方法读取table: ... static void read_table(lua_State *L) { double resault;

lua_getglobal(L, "background");     /* 将表从lua空间复制到虚拟堆栈(应该是仅拷贝索引,否则速度无法保证) */ lua_pushstring(L, "b");             /* 将要读取的键压入虚拟堆栈 */ lua_gettable(L, -2);                /* 使用lua_gettable读取table,其第二个参数为table在虚拟堆栈中的索引(-1为key,所以-2为table) */ resault = lua_tonumber(L, -1);      /* 将读取出的结果复制到C空间 */ lua_pop(L, 1);                      /* 将结果出栈,将堆栈恢复成调用前的样子 */

 

printf("Read from lua table: %f\n", resault); } ... 运行结果: pi@raspberrypi ~/Programming/article_lua $ ./a.out table.lua Read from lua table: 1.000000

C语言写入Lua中的表: 1、将要写入的table放入堆栈,可以新建也可以写入现有table; 2、将要写入的键压入堆栈; 3、将要写入的值压入堆栈; 4、调用lua_settable执行table的写入 5、如果是新建table的话,最后需要使用lua_setglobal,将修改后的table写会lua全局变量。 Lua代码: print ("Read talbe.r", background.r) print ("Read talbe.g", background.g) print ("Read talbe.b", background.b) C代码: static void write_table(lua_State *L) { lua_newtable(L);              /* 新建table并放入堆栈。对于lua空间中没有table的情况可以使用lua_newtable新建一个table;如果是写入已有table,则应该使用lua_getglobal将数据从lua空间读入虚拟堆栈 */

lua_pushstring(L, "r");       /* 将要写入的键压入堆栈 */ lua_pushnumber(L, (double)0); /* 将要写入的值压入堆栈 */ lua_settable(L, -3);          /* 执行table的写入,函数的第二个参数是table在虚拟堆栈中的位置 */

lua_pushstring(L, "b");       /* 重复三次,一共写入了"r", "g", "b" 三个成员 */ lua_pushnumber(L, (double)1); lua_settable(L, -3);

lua_pushstring(L, "g"); lua_pushnumber(L, (double)0); lua_settable(L, -3);

 

lua_setglobal(L, "background"); /* 最后将新table写入lua全局命名空间 */ } 运行结果: pi@raspberrypi ~/Programming/article_lua $ ./a.out print_table.lua Read talbe.r 0 Read talbe.g 0 Read talbe.b 1

自定义数据类型:

我们通过使用C语言实现一个Lua数组来演示Lua实现自定义用户数据。数组的结构如下所示: typedef struct NumArray{ int size; //表示数组的大小 double values[]; //此处的values仅代表一个double*类型的指针,values指向NumArray结构后部紧跟的数据的地址 } NumArray; 我们实现四个函数new,get,set和size,分别用来完成数组的新建,读取,写入和获取大小。在Lua中用来实现自定义数据结构的类型叫做userdata。Lua提供了如下接口用来创建userdata: void *lua_newuserdata(lua_State *L, size_t size),该函数分配size大小的内存作为userdata并将其压入栈,函数的返回值为新建立的userdata,可以自由转换为所需的数据结构。 实现自定义数据结构的C代码如下: /* 新建array */ static int newarray (lua_State *L){ int n = luaL_checkint(L, -1); //检查参数,数组的个数必须是整数 size_t nbytes = sizeof(NumArray) + n*sizeof(double); //计算C语言结构所需的内存空间,nbytes的长度包括数组结构头和其后部的数据 NumArray *a = (NumArray *)lua_newuserdata(L, nbytes); //新建一个大小为nbytes的userdata并压入堆栈。 a->size = n; //初始化NumArray的大小 return 1; }

/* 设置array中的数值 */ static int setarray(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -3); //将堆栈中的userdata读取出来 int index = luaL_checkint(L, -2); //读取索引 double value = luaL_checknumber(L, -1); //读取数值

luaL_argcheck(L, NULL != a, 1, "'array' expected"); //检查参数的返回,如果第二个表达式为假,则抛出最后一个参数指定的错误信息 luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

a->values[index - 1] = value; //将lua中写入的数值设置到C数组中

return 0; }

/* 读取array中的数值 */ static int getarray(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -2); //前面的步骤和setarray中的相同 int index = luaL_checkint(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 1 && index <= a->size, 1, "index out of range");

lua_pushnumber(L, a->values[index - 1]); //将C数组中的数值压入堆栈

return 1; }

/* 获取array的大小 */ static int getsize(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -1); luaL_argcheck(L, NULL != a, 1, "'array' expected");

lua_pushnumber(L, a->size);

return 1; }

/* 将我们定义的四个函数写成数组的形式,在主函数中可以使用luaL_openlib将四个函数一口气注册到lua空间 */ static const struct luaL_Reg arraylib[] = { {"new", newarray}, {"set", setarray}, {"get", getarray}, {"size", getsize}, {NULL, NULL} };

int main(int argc, char* argv[]) { ...... luaL_openlib(L, "array", arraylib, 0); //注册刚才实现的四个函数到全局变量array下,其名称分别为new,set,get和size。 ...... return 0; }

对应的Lua程序为: a = array.new(1000) print(a) print(array.size(a)) for i=1,999 do array.set(a, i, 1/i) end

print ("Print first 10 elements") for i=1,10 do print(array.get(a, i) ) end

运行结果是: pi@raspberrypi ~/Programming/article_lua $ ./a.out userdata3.lua userdata: 0x20237b0 1000 Print first 10 elements 1 0.5 0.33333333333333 0.25 0.2 0.16666666666667 0.14285714285714 0.125 0.11111111111111 0.1 但是,上述程序有两个问题:1、参数中仅检查了用户的输入参数是否是userdata,并没有区分实际的类型,如果用户传递array.get(io.stdin, 200),就会造成Sagement fault,这样的行为是不可接受的。2、用户仅能使用array.size(a),和array.get(a,40)的形式访问内容,能否使用类似于面向对象特性的方式如a:size()和a:get(40)的形式呢? 答案是肯定的,因此必须引入metatable的机制。 我们假定读者已经对lua语言本身和在Lua中使用metatable有一定的了解,这里仅介绍在C语言中如何为userdata添加metatable。在对于userdata的metatable有如下几点需要注意: 1、metatable在lua中仅是一个普通的table,但是在lua提供给C语言的接口中,metabtable需要使用专门的接口来创建和读取。(原因暂时不详) 2、lua的metatable和javascript的prototype不同。如果访问对象缺失的方法,javascript会直接从prototype指向的对象中查找缺失的方法,但lua不同,他不会直接从metatable中查找,而是会从metatable的__index域所指向的对象中查找。如果我们设置object.metatable.__index = object.metatable,这样就和javascript类似了。对于对象缺失的方法,会直接从metatable中查找。 3、对于userdata而言其什么方法都没有,因此对他的成员的访问都是通过访问metatable.__index来实现的(如果设置metatable.__index = metatable,那么访问userdata的成员就相当于访问userdata的metatable)。

使用metatable修改后的程序如下所示: /* 将要注册的函数拆分为两部分 arraylib_f注册给全局变量array arraylib_m注册给metatable,作为methord */ static const struct luaL_Reg arraylib_f[] = { {"new", newarray}, {NULL, NULL} };

static const struct luaL_Reg arraylib_m[] = { {"set", setarray}, {"get", getarray}, {"size", getsize}, {NULL, NULL} };

/* C函数的安装使用如下函数 整个函数中我们创建了一个匿名的metatable,后续注释中的metatable一词代指这个新建的metatable的实例 */ static void install_func(lua_State *L) { luaL_newmetatable(L, "LuaBook.array"); //新建一个metatable,一方面压入堆栈,另一方面将metatable以"LuaBook.array"为key放入register中(register类似于一个全局哈希表)

lua_pushstring(L, "__index"); lua_pushvalue(L, -2); lua_settable(L, -3); //这三步的作用是将新建的metatable的__index字段赋值为他自己,相当于metatable.__index = metatable,尽是为了方便,否则还需要另外建立一个table2,并设置metatable.__index = table2 luaL_openlib(L, NULL, arraylib_m, 0); //将set、get、size这三个函数注册给metatable(给luaL_openlib函数传递NULL时,表示要操作的表已经放在堆栈中)

luaL_openlib(L, "array", arraylib_f, 0); //将new这个函数注册给全局变量array }

static int newarray (lua_State *L){ int n = luaL_checkint(L, 1); size_t nbytes = sizeof(NumArray) + n*sizeof(double); NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);

luaL_getmetatable(L, "LuaBook.array"); lua_setmetatable(L, -2); //新建userdata的时候将register中名为LuaBook.array中metatable设置给新建的array对象,从此所有array共用一个metatable

a->size = n; return 1; }

static int setarray(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -3, "LuaBook.array"); //调用函数的时候,除了检查参数是否为userdata之外,还要判断该userdata的metatable是否是"LuaBook.array",这样在使用io.stdin调用我们的函数时就可以检查出错误,不至造成Sagement fault int index = luaL_checkint(L, -2); double value = luaL_checknumber(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

a->values[index - 1] = value;

return 0; }

static int getarray(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -2, "LuaBook.array"); //同样增加参数类型和metatable的检查 int index = luaL_checkint(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

lua_pushnumber(L, a->values[index - 1]);

return 1; }

static int getsize(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -1, "LuaBook.array"); //同样增加参数类型和metatable的检查

luaL_argcheck(L, NULL != a, 1, "'array' expected"); lua_pushnumber(L, a->size);

return 1; }

int main(int argc, char* argv[]) { ...... install_func(L); //使用install_func将函数注册到lua空间 ...... }

对应Lua的例子变为: a = array.new(1000) print(a) print(a:size()) for i=1,999 do a:set(i, 1/i) end

print ("Print first 10 elements") for i=1,10 do print(a:get(i)) end

文章中用到的示例程序:

最后给出一个main函数的例子,在这个函数中我们可以添加前面说到的示例代码从而组合出完整的示例程序。 int main(int argc, char* argv[]) { char* filename; double ret = 0;

//新建一个lua state lua_State *L = luaL_newstate();

if (argc >=2 ){ filename = argv[1]; } else { printf("usage: %s filename\n", argv[0]); return 1; }

//这个函数加载lua标准库 luaL_openlibs(L);

/* 在这里添加代码注册C语言实现的函数 */

/* 从指定的文件名加载lua代码(实际上代码被编译成chunk压入栈中) */ if (luaL_loadfile(L, filename)){ error(L, "cannot run file: %s", lua_tostring(L, -1)); }

/* 执行刚才读取的lua代码 */ if (lua_pcall(L, 0, 0, 0)){ error(L, "cannot run file: %s", lua_tostring(L, -1)); }

/* 如果调用lua函数,要放在这里 */

return 0; } 写在最后: 本文假定读者对Lua的基本语法已经有了一定的了解。由于Lua是原型继承语言,和我们之前使用的基于类型的语言有些区别(倒是和Javascript类似,Javascript也是原型继承语言)。因此在开始学习的过程一定要跳出类和对象的思维才能真正理解Lua。最后推荐两个学习Lua的优秀材料: 官方手册:http://www.lua.org/manual/查起来很方便,如果用它来学习会很困难。 作者的著作:Programming in Lua,第一版免费基于Lua5.0的http://www.lua.org/pil/contents.html,网上可以搜到一个中译的版本,个人感觉学习足够了,当然也可以在amazon上购买英文第三版。


                
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值