目录
1、Lua语言介绍
2、下载Lua
3、在VS中配置Lua
4、C++调用Lua脚本的简单示例
5、C++与Lua的数据交换 -- Lua的堆栈
5.1、压入元素
5.2、查询元素
5.3、其他堆栈操作
5.4、示例代码
6、C++调用Lua函数
7、用C++来扩展Lua
7.1、扩展Lua的简单步骤
7.2、使用C函数库扩展Lua
7.3、使用库扩展Lua的步骤
8、最后
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开发,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。很多项目中都使用到了Lua脚本,今天我们就来介绍一下如何在C++中使用Lua的一些基础知识。
Lua 是一个小巧的脚本语言。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言。
Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。
可以到Lua官网的下载页面:http://www.lua.org/download.html
去下载Lua源码或者编译好的二进制文件。目前最新的lua脚本是lua5.4.2。
在windows平台下我们必须配置Visual C++,以便让编译器和连接器能找到Lua文件。在打开源码工程的VS中,右键点击使用Lua脚本的工程,弹出菜单中选择“属性”,打开属性页面:(以安装版Lua配置为例)
1)点击左边列表中的"VC++ 目录";
2)在右边的“包含目录”中,添加一个新路径 "lua安装路径/include";
3)在右边的“库目录”中,添加路径" lua安装路径/lib/dll"(这里假设你下载的库为dll,你也可以下载静态链接库)。
现在你就可以在你的工程中编译你的第一个Lua应用了。
C++调用Lua脚本的示例代码如下:
#include
// 包含Lua的头文件
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
int main ( int argc, char *argv[] )
{
lua_State *L = luaL_newstate() // 返回一个指向Lua解释器的指针
luaL_openlibs(L); // 加载lua库
luaL_dofile(L, "test.lua"); // 执行lua脚本
lua_close(L); // 关闭Lua
printf( "pause" );
return 0;
}
Lua使用一个抽象的栈在lua与C之间进行数据交换。栈中的每一条记录都可以保存任何lua值。无论何时你想要从Lua请求一个值(比如一个全局变量的值),调用Lua,被请求的值会被压入栈。无论何时你想要传递一个值给Lua,首先将这个值压入栈,然后调用Lua(这个值将被弹出)。
另外栈是由Lua来管理的,垃圾回收器知道哪个值正在被C使用。几乎所有的API函数都用到了栈。Lua以一个严格的LIFO规则(后进先出,也就是说始终存取栈顶)来操作栈,当你调用Lua时,它只会改变栈顶部分。你的C代码却有更多的自由,你可以查询栈上的任何元素,甚至在任何一个位置插入和删除元素。
Lua提供的API有一系列压栈函数,它将每种可以用C来描述的Lua类型压栈。
void lua_pushnil ( lua_state *L);
void lua_pushboolean (lua_state *L, int bool );void lua_pushnumber (lua_state *L,double n ) ;
void lua_pushlstring ( lua_state *L,const char * s,size_t length ) ;
void lua_pushstring (lua_State *L,const char *s );
Lua中的字符串不是以0为结束符的,他们依赖于一个明确的长度,因此可以包含任意的二进制数据。
将字符串压入栈的函数是lua_pushlstring,它要求一个明确的长度作为参数。对于以0结束的字符串,你可以用lua_pushstring(它用strlen来计算字符串长度)。Lua从来不保持一个指向外部字符串的指针,对于它保持的所有字符串,Lua要么做一份内部的拷贝,要么重新利用已经存在的字符串。因此,一旦这些函数返回之后,你可以自由的修改或是释放你的缓冲区。
使用API用索引来访问栈中的元素。在栈中的第一个元素(也就是第一个被压入栈的)的索引是1,下一个被压入栈的元素索引是2,依次类推。我们也可以用栈顶作为参照来存取元素,利用负索引。在这种情况下,-1指栈顶元素(也就是最后被压入栈的元素),-2指出栈顶元素的前一个元素,依次类推。
API提供了一套lua_is*函数来检查一个元素是否是一个指定的类型,*可以是任何Lua类型。因此有lua_isnumber, lua_isstring, lua_istable以及类似的函数。所有这些函数都有同样的原型:
int lua_is. .. ( lua_state *L,int index ) ;
lua_isnumber和lua_isstring函数不检查这个值是否是指定的类型,而是看它是否能被转换成指定的那种类型。例如任何数字类型都满足lua_isstring。
还有一个lua_type函数,它返回栈中元素的类型。在lua.h头文件中,每种类型都被定义为一个常量:LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA以及LUA_TTHREAD。当我们需要真正检查字符串和数字类型时,我们可以用这个函数。
为了从栈中获得值,可以使用lua_to*函数:
int lua_toboolean (lua_state *L,int index ) ;
double lua_tonumber (lua_state *L,int index );
const char * lua_tostring (lua_state *L, int index ) ;
size_t lua_strlen (lua_state *L,int index) ;
即使给定的元素类型不明确,调用上面这些函数也没有什么问题。在这种情况下,lua_toboolean, lua_tonumber和lua_strlen返回0,其他函数返回NULL。
lua_tostring函数返回一个指向字符串内部拷贝的指针,你不能修改它。只要这个指针对应的值还在栈内,Lue会保证这个指针一直有效。
当一个C函数返回后,Lua会清理它的栈,所以,有一个原则:永远不要将指向Lua字符串的指针保存到访问他们的外部函数中。
Lua_string返回的字符串结尾总会有一个字符串结束标志0,但是字符串中间也可能包含0,lua_strlen返回字符串的实际长度。特殊情况下,假定栈顶的值是一个字符串,下面的断言总是有效的:
const char *s = lua_tostring(L,-1); /* any Lua string *+ /
size_t l = lua_strlen(L,-1); /*its length * /
assert(s[1] == '\0');
assert (strlen (s) <= l) ;
除了上面所提到的C与堆栈交换值的函数外,API也提供了下列函数来完成通常的堆栈维护工作:
int lua_gettop ( lua_state *L);
void lua_settop ( lua_state *L, int index );
void lua_pushvalue (lua_state *L,int index );
void lua_remove ( lua_state *L, int index );
void lua_insert ( lua_state *L, int index );
void lua_replace (lua_state *L, int index );
函数lua_gettop返回堆栈中的元素个数,它也是栈顶元素的索引。lua_settop设置栈顶(也就是堆栈中的元素个数)为一个指定的值。如果之前的栈顶高于新的栈顶,顶部的值被丢弃,否则,为了得到指定的大小,这个函数压入相应个数的空值(nil)到栈上。特别的,lua_settop(L,0)清空堆栈。你也可以用负数索引作为调用lua_settop的参数,那将会设置栈顶到指定的索引,利用这种技巧,API提供了下面这个宏,它从堆栈中弹出n个元素:
#define lua_pop(L,n) lua_settop (L, -(n)-1)
函数lua_pushvalue压入堆栈上指定索引的一个拷贝到栈顶;lua_remove移除指定索引位置的元素,并将其上面所有的元素下移来填补这个位置的空白;lua_insert移动栈顶元素到指定索引的位置,并将这个索引位置上面的元素全部上移至栈顶被移动留下的空隔;最后,lua_replace从栈顶弹出元素值,并将其设置到指定索引位置,没有任何移动操作。
#include
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
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_TNUMBER:
printf("%g",lua_tonumber(L,i));
break;
default:
printf("%s",lua_typename(L,t));
break;
}
printf(" ");
}
printf("\n");
}
int main ( int argc, char *argv[] )
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushboolean( L,1 );
lua_pushnumber( L,10 );
lua_pushnil( L );
lua_pushstring( L, "hello" );
stackDump(L);
lua_pushvalue( L, 4 );
stackDump(L);
lua_replace( L, 3 );
stackDump(L);
lua_settop( L, 6 );
stackDump(L);
lua_remove( L, -3 );
stackDump(L);
lua_settop( L, -5 );
stackDump(L);
lua_close(L);
getchar();
return 0;
}
Lua作为配置文件的一个最大的长处在于它可以定义一个被应用调用的函数。比如,你可以写一个应用程序来绘制一个函数的图像,使用Lua来定义这个函数。
使用API调用函数的方法很简单:
1)将被调用的函数入栈;
2)依次将所有参数入栈;
3)使用lua_pcall调用函数;最后:从栈中获取函数执行返回的结果。
示例代码如下:
#include "stdafx.h"
#include "string.h"
#include "lua.hpp"
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_TNUMBER:
printf("%g",lua_tonumber(L,i));
break;
default:
printf("%s",lua_typename(L,t));
break;
}
printf(" ");
}
printf("\n");
}
int main ( int argc, char *argv[] )
{
//初始化Lua
lua_State *L = luaL_newstate();
//载入Lua基本库
luaL_openlibs(L);
//调用lua函数
if( luaL_loadfile(L,"script/test.lua") || lua_pcall(L,0,0,0) )
printf("can not running the script file : %s",lua_tostring(L,-1));
stackDump(L);
double z;
lua_getglobal(L,"f");
stackDump(L);
lua_pushnumber(L,3);
stackDump(L);
lua_pushnumber(L,4);
stackDump(L);
if( lua_pcall(L,2,1,0) != 0 )
{
printf("error running function f : %s!\n",lua_tostring(L,-1));
}
stackDump(L);
if( !lua_isnumber(L,-1) )
{
printf("function f must return a number!\n");
}
stackDump(L);
z = lua_tonumber(L,-1);
printf("the call result is : %f\n",z);
lua_pop(L,1);
getchar();
// 清除Lua
lua_close(L);
return 0;
}
在调用lua_pcall时,可以指定参数的个数和返回结果的个数,第四个参数可以指定一个错误处理函数,这个例子里面没有使用。
和Lua中赋值操作一样,lua_pcall会根据你的要求调整返回结果的个数,多余的丢弃,少的用nil补足。在将结果入栈之前,lua_pcall会将栈内的函数和参数移除。如果函数返回多个结果,第一个结果被第一个入栈,因此如果有n个结果,第一个返回结果在栈中的位置为-n,最后一个返回结果在栈中的位置为-1。
如果lua_pcall运行时出现错误,lua_pcall会返回一个非0的结果。另外,它将错误信息入栈(仍然会先将函数和参数从栈中移除)。在将错误信息入栈之前,如果指定了错误处理函数,lua_pcall会先调用错误处理函数。使用lua_pcall的最后一个参数来指定错误处理函数,0代表没有错误处理函数,也就是说最终的错误信息就是原始的错误信息。否则那个参数应该是一个错误函数被加载时,在栈中的索引。因此,在这种情况下,错误处理函数必须在被调用的函数和其他参数入栈之前入栈。
对于一般的错误,lua_pcall返回错误代码LUA_ERRRUN。有两种特殊情况,会返回特殊错误代码。第一种情况是内存分配错误,对于这种错误,lua_pcall总是返回LUA_ERRMEM。第二种情况是当Lua正在运行错误处理函数时发生错误,这种情况下,再次调用错误处理函数没有意义,所以lua_pcall立即返回错误代码LUA_ERRERR。
扩展Lua的基本方法之一就是为应用程序注册新的C函数到Lua中去。
当我们提到Lua可以调用C函数,不是指Lua可以调用任何类型的C函数。正如我们前面所看到的,当C调用Lua函数的时候,必须遵循一些协议来传递参数和获得返回结果。同样的,从Lua中调用C函数,也必须遵循一些协议来传递参数和获得返回结果。另外,从Lua中调用C函数,我们必须先注册函数,也就是说,我们必须把C函数的地址以一个适当的方式传递给Lua解释器。
当Lua调用C函数的时候,使用和C调用Lua相同类型的栈来交互。C函数从栈中获取他的参数,调用结束后将返回结果放到栈中。为了区分返回结果和栈中其他的值,每个C函数还会返回结果的个数。这里有一个重要的概念:用于交互的栈不是全局变量,每一个函数都有他自己的私有栈。当Lua调用C函数的时候,第一个参数总是在这个私有栈的index=1的位置。甚至当一个C函数调用Lua代码(Lua代码调用同一个C函数或者其他的C函数),每一个C函数都有自己的独立的私有栈,并且第一个参数在index=1的位置。
1)实现一个lua_CFunction类型的函数。任何在Lua中注册的函数必须有同样的原型,这个原型就是定义在lua.h中的lua_CFunction:
typedef int( *lua_CFunction)( lua_state *L);
2)实现这个函数:
static int l_sin (lua_state *L) { double d = luaL_checknumber (L,1 ); lua_pushnumber(L,sin (d) ) ; return l;/ * number of results */ }
3)注册这个函数。我们使用lua_pushfunction来完成这个任务:它获取指向C函数的指针,并在Lua中创建一个function类型的值来表示这个函数。一个方便快捷但是不专业的解决方案是将上面的函数定义代码直接放到lua,c文件中,并且在调用luaL_newstate()后面适当的位置加上下面两行:
lua_pushcfunction ( l, l_sin); lua_setglobal( l,"mysin" );
第一行将类型为function的值入栈,第二行将function赋值给全局变量mysin。这样修改之后,重新编译Lua,你就可以在你的Lua程序中使用新的mysin函数了。
一个Lua库实际上是一个定义了一系列Lua函数的chunk,并将这些函数保存在适当的地方。Lua的C库就是这样实现的。除了定义C函数之外,还必须定义一个特殊的用了和Lua库的主chunk通信的特殊函数。一旦调用,这个函数就会注册库中的所有C函数,并将它们保存到适当的位置。像一个Lua主chunk一样,它也会初始化其他一些在库中需要初始化的东西。
Lua通过这个注册过程,就可以看到库中的C函数。一旦一个C函数被注册之后并保存在Lua中,在Lua程序中就可以直接引用它的地址(注册这个函数的时候传给Lua的地址)来访问这个函数了。换句话说,一旦C函数被注册之后,Lua调用这个函数并不依赖于函数名,包的位置,或者调用函数的可见规则。通常C库都有一个外部的用来打开库的函数。其他的函数可能都是私有的,在C中被声明为static。
当你打算使用C函数来扩展Lua的时候,即使你仅仅只想注册一个C函数,将你的C代码设计为一个库是个比较好的思想,不久的将来你就会发现你需要其他的函数。一般情况下,辅助库对这种实现提供了帮助。LuaL_openLib函数接受一个C函数的列表和他们对应的函数名,并且作为一个库在一个table中注册所有这些函数。
1)新建一个mylib.c文件。
2)在mylib.c文件里面定义库函数。static int l_sin(lua_State *L){ double d = luaL_checknumber(L,1); lua_pushnumber(L, sin(d)); return 1; /* number of results */ }
3)在mylib.c文件里面声明一个数组,保存所有的函数和他们对应的名字。这个数组的元素类型为luaL_reg:是一个带有两个域的结构体,一个字符串和一个函数指针。
static const struct luaL_reg mylib [] = { {“mysin”,l_sin}, {NULL,NULL} /* sentinel */ };
在我们的例子中,只有一个函数l_sin需要声明。注意数组中最后一对必须是{NULL,NULL},用来表示结束。
4)定义库名称和打开库的API接口。在lualib.h里面添加下面两行代码#define LUA_MYLIBNAME "my" /* 库的名称 */ LUAMOD_API int (luaopen_mylib) (lua_State *L);
5)在mylib.c文件里面使用luaL_newlib实现库打开函数。
LUAMOD_API int luaopen_mylib (lua_State *L) { luaL_newlib(L, mylib); return 1; }
6)在linit.c文件里面的loadedlibs数组里面添加一行,如下所示:
static const 1uaL_Reg loadedlibs[] =i i"_G",luaopen_base}, {LUA_LOADLIBNAME,luaopen_package}, {LUA_COLIBNAME,1uaopen_coroutine}, {LUA_TABLIBNAME,luaopen_table}, {LUA_IOLIBNAME,1uaopen_io}, {LUA_0SLIBNAME,luaopen_os}, {LUA_STRLIBNAME,1uaopen_string}, {LUA_MATHLI BNANE,1uaopen_nath}, {LUA_UTF8LI BNANE,luaopen_utf8}, {LUADBLIBNAME, luaopen debuq}, {iLUA_MYLIBNAME, 1uaopen_mylib}, if defined(LuA_cOMPAT_BITLIB) {LUA_BITLIBNAME,1uaopen_bit32}, #endif {NULL,NULL} };
7)重新编译lua。
在目前所有脚本引擎中,Lua的速度是最快的,这一切都决定了Lua是作为嵌入式脚本的最佳选择。关于编写Lua脚本的基础知识,可以参见:
脚本语言Lua的基础知识总结https://blog.csdn.net/chenlycly/article/details/126083179