一、通信原理详解
1.1、堆栈概念
要理解Lua和C++交互,首先要理解Lua堆栈。简单来说,Lua和C/C++语言通信的主要方法是一个无处不在的虚拟栈,栈的特点是先进后出。
在Lua中,Lua堆栈就是一个struct,堆栈索引的方式可是正数也可以是负数,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶,如下图示:
Lua的栈类似于以下的定义,它是在创建lua_State时创建的:
TValue stack[max_stack_len] --可查lstate.c中的stack_init函数
存入栈的数据类型包括数值、字符串、指针、table、闭包等
lua_pushcclosure(L, func, 0) -- 创建并压入一个闭包
lua_createtable(L, 0, 0) -- 新建并压入一个表
lua_pushnumber(L, 3) -- 压入一个数字
lua_pushstring(L, "str") -- 压入一个字符串
压入到栈中的类型有数值、字符串、表和闭包(在C中看来是不同类型的值,但是最后都是统一用TValue这种数据结构来保存的),如下图示:
TValue结构对应于Lua中的所有数据类型,是一个{值,类型}结构,这就是Lua中动态类型的实现,它把值和类型绑定到一起,用tt记录value的类型,value是一个联合结构,由Value定义,可看到这个联合有四个域:
- p :可以存一个指针,实际上时Lua中的light userdata结构
- n :所有的数值存在这里
- b : Boolean值存在这里
- gc : 其他诸如table、thread、closure、string,userdata需要内存管理垃圾回收的类型都存在这里
1.2、堆栈操作
因为Lua与C/C++是通过栈来通信的,Lua提供了C API来对栈进行操作。
API
| 作用
|
int lua_gettop(lua_State *L);
| 返回栈顶索引(即栈长度)
|
void lua_settop(lua_State *L, int idx);
| 设置栈顶的位置,即修改栈中元素的数量
|
void lua_pushvalue(lua_State *L, int idx);
| 将idx索引上的值的副本压入栈顶
|
void lua_remove(lua_State *L, int idx);
| 移除idx索引上的值
|
void lua_insert(lua_State *L, int idx);
| 弹出栈顶元素,并插入索引idx位置
|
void lua_replace(lua_State *L, int idx);
| 弹出栈顶元素,并替换索引idx位置的值
|
lua_settop将栈顶设置为一个指定的位置,如果值比原栈顶高,则高的部分用nil补;如果值比原栈底,则原栈高出的部分舍弃,所以可以用lua_settop(0)来清空栈。
使用Lua作为C语言项目的配置文件实例
Lua和c通信时有这样的约定: 所有的lua中的值由Lua来管理, c++中产生的值Lua不知道, 类似表达了这样一种意思: “如果你(c/c++)想要什么, 你告诉我(Lua), 我来产生, 然后放到栈上, 你只能通过api来操作这个值, 我只管我的世界”, 这个很重要, 因为:
“如果你想要什么, 你告诉我, 我来产生”,就可以保证, 凡是Lua中的变量, Lua要负责这些变量的生命周期和垃圾回收, 所以, 必须由Lua来创建这些值(在创建时就加入了生命周期管理要用到的簿记信息)
“然后放到栈上, 你只能通过api来操作这个值”, Lua api给c提供了一套完备的操作界面, 这个就相当于约定的通信协议, 如果Lua客户使用这个操作界面, 那么Lua本身不会出现任何"意料之外"的错误.
"我只管我的世界"这句话体现了Lua和c/c++作为两个不同系统的分界, c/c++中的值, Lua是不知道的, Lua只负责它的世界
例如:
data.lua
str = "I am so cool"
tbl = {name = "shun", id = 20114442}
function add(a,b)
return a + b
end
通过test.cpp
来读取它:
#include <iostream>
#include <string.h>
using namespace std;
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
void main()
{
//1.创建Lua状态
lua_State *L = luaL_newstate();
if (L == NULL)
return ;
//2.加载Lua文件
int bRet = luaL_loadfile(L,"hello.lua");
if(bRet)
{
cout<<"load file error"<<endl;
return ;
}
//3.运行Lua文件
bRet = lua_pcall(L,0,0,0);
if(bRet)
{
cout<<"pcall error"<<endl;
return ;
}
//4.读取变量
lua_getglobal(L,"str");
string str = lua_tostring(L,-1);
cout<<"str = "<<str.c_str()<<endl; //str = I am so cool~
//5.读取table
lua_getglobal(L,"tbl");
lua_getfield(L,-1,"name");
str = lua_tostring(L,-1);
cout<<"tbl:name = "<<str.c_str()<<endl; //tbl:name = shun
//6.读取函数
lua_getglobal(L, "add"); // 获取函数,压入栈中
lua_pushnumber(L, 10); // 压入第一个参数
lua_pushnumber(L, 20); // 压入第二个参数
int iRet= lua_pcall(L, 2, 1, 0);// 调用函数,调用完成以后,会将返回值压入栈中,2表示参数个数,1表示返回结果个数。
if (iRet) // 调用出错
{
const char *pErrorMsg = lua_tostring(L, -1);
cout << pErrorMsg << endl;
lua_close(L);
return ;
}
if (lua_isnumber(L, -1)) //取值输出
{
double fValue = lua_tonumber(L, -1);
cout << "Result is " << fValue << endl;
}
//至此,栈中的情况是:
//=================== 栈顶 ===================
// 索引 类型 值
// 4 int: 30
// 3 string: shun
// 2 table: tbl
// 1 string: I am so cool~
//=================== 栈底 ===================
//7.关闭state
lua_close(L);
return ;
}
知道怎么读取后,我们来看下如何修改上面代码中table的值:
// 将需要设置的值设置到栈中
lua_pushstring(L, "我是一个大帅锅~");
// 将这个值设置到table中(此时tbl在栈的位置为2)
lua_setfield(L, 2, "name");
我们还可以新建一个table:
// 创建一个新的table,并压入栈
lua_newtable(L);
// 往table中设置值
lua_pushstring(L, "Give me a girl friend !"); //将值压入栈
lua_setfield(L, -2, "str"); //将值设置到table中,并将Give me a girl friend 出栈
需要注意的是:堆栈操作是基于栈顶的,就是说它只会去操作栈顶的值。
举个比较简单的例子,函数调用流程是先将函数入栈,参数入栈,然后用lua_pcall调用函数,此时栈顶为参数,栈底为函数,所以栈过程大致会是:参数出栈->保存参数->参数出栈->保存参数->函数出栈->调用函数->返回结果入栈。
类似的还有lua_setfield,设置一个表的值,肯定要先将值出栈,保存,再去找表的位置。
再不理解可看如下例子:
lua_getglobal(L, "add"); // 获取函数,压入栈中
lua_pushnumber(L, 10); // 压入第一个参数
lua_pushnumber(L, 20); // 压入第二个参数
int iRet= lua_pcall(L, 2, 1, 0);// 将2个参数出栈,函数出栈,压入函数返回结果
lua_pushstring(L, "我是一个大帅锅~"); //
lua_setfield(L, 2, "name"); // 会将"我是一个大帅锅~"出栈
lua_getglobal(L,“var”)会执行两步操作:1.将var放入栈中,2.由Lua去寻找变量var的值,并将变量var的值返回栈顶(替换var)。
lua_getfield(L,-1,“name”)的作用等价于 lua_pushstring(L,“name”) + lua_gettable(L,-2)
二、C/C++中调用Lua脚本
C/C++文件
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <QCoreApplication>
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
lua_State *L; //脚本上下文
int lua_add(int x, int y)
{
int sum;
lua_getglobal(L,"add"); //获取函数名
lua_pushnumber(L,x); //传入第一个参数
lua_pushnumber(L,y); //传入第二个参数
lua_call(L,2,1); //调用函数,两个参数,一个返回值,返回值会被压入栈中
sum = (int)lua_tonumber(L,-1); //获取结果,结果位于栈顶(-1)
lua_pop(L,1); //清空返回值
return sum;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
L = luaL_newstate(); //新建Lua解释器
luaL_openlibs(L); //载入Lua基础库
//QString转char*
QString str = QCoreApplication::applicationDirPath()+"/Add.lua";
QByteArray ba = str.toLatin1();
char* file_path=ba.data();
luaL_dofile(L,file_path); //执行脚本
qDebug()<<"sum = "<<lua_add(10,15); //调用Lua脚本中的函数
lua_close(L); //清空Lua
return a.exec();
}
Add.lua
function add(x,y)
return x+y
end
三、Lua脚本中调用C/C++函数
C/C++中供Lua脚本调用的函数必须满足如下形式:
typedef int (*lua_CFunction) (lua_State *L);
也就是说,C++中定义函数时必须以lua_State*
为参数, 以int为返回值才能被Lua所调用。
但是,lua_State是支持栈的, 所以通过栈可以传递无穷个参数, 大小只受内存大小限制;而返回的int值也只是指返回值的个数,真正的返回值都存储在lua_State的栈中。
C/C++文件
#include "mainwindow.h"
#include <QApplication>
#include <QCoreApplication>
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
lua_State* L;
static int Average(lua_State *L)
{
int n = lua_gettop(L);
double sum = 0;
for(int i = 1; i <= n; ++i)
{
sum += lua_tonumber(L, i);
}
lua_pushnumber(L, sum / n);
lua_pushnumber(L, sum);
return 2;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
L = luaL_newstate(); //新建Lua解释器
luaL_openlibs(L); //载入Lua基础库
//QString转char*
QString str = QCoreApplication::applicationDirPath()+"/Test.lua";
QByteArray ba = str.toLatin1();
char* file_path=ba.data();
lua_register(L,"average",Average); //注册函数
luaL_dofile(L,file_path); //执行Lua脚本
lua_close(L); //清空Lua
return a.exec();
}
Test.lua
print("Hello Lua!")
avg,sum = average(10,20,30,40,50)
print("The average is: ",avg)
print("The sum is: ",sum)