Lua 与 C/C++ 交互

绑定Lua和C/C++的库

Lua调用C/C++

简介

Lua(念“鲁啊”)作为一门发展成熟的脚本语言,正在变得越来越流行。它也可以作为和C/C++执行脚本交互的语言。并且Lua的整个库很小,Lua 5.1版本整个静态链接的lua.dll才164KB,所以Lua很轻量,特别适合轻量级脚本嵌入。

这节要讲Lua和C/C的交互——Lua通过C/C导出的dll来调用。

LUA调用C文件中的函数方法

  • C中注册函数

      lua_pushcfunction(l, l_sin); //注册在lua中使用的c函数l_sin
      lua_setglobal(l, "mysin");   //设定绑定到lua中的名字为mysin
    
  • C中提供的函数其定义要符合:

    typedef int function(lua_State *L)

准备工作

安装完Lua,需要在Visual Studio中配置Lua路径,使得你的编译器能搜寻到。关于VS2010的配置,见我的博文《VS2010 C++目录配置》一文。完成后新建一个Dll工程便可以了。

我们用一个在Lua中显示Windows对话框的程序来简要介绍一下,程序虽小,但五脏俱全。程序如下:

// 将一些有用的Win32特性导出
// 以便在Lua中使用
extern "C"  
{  
#include <lua.h>  
#include <lualib.h>  
#include <lauxlib.h>  
#pragma comment(lib, "lua.lib")  
};

#include <Windows.h>  
#include <iostream>  
using namespace std;

static const char* const ERROR_ARGUMENT_COUNT = "参数数目错误!";  
static const char* const ERROR_ARGUMENT_TYPE  = "参数类型错误!";  

// 发生错误,报告错误
void ErrorMsg(lua_State* luaEnv, const char* const pszErrorInfo)  
{  
    lua_pushstring(luaEnv, pszErrorInfo);  
    lua_error(luaEnv);  
}  
  
// 检测函数调用参数个数是否正常
void CheckParamCount(lua_State* luaEnv, int paramCount)  
{  
    // lua_gettop获取栈中元素个数.  
    if (lua_gettop(luaEnv) != paramCount)  
    {  
        ErrorMsg(luaEnv, ERROR_ARGUMENT_COUNT);  
    }  
}  
  
// 显示Windows对话框.  
// @param [in] pszMessage string 1  
// @param [in] pszCaption string 2  
extern "C" int ShowMsgBox(lua_State* luaEnv)  
{  
    const char* pszMessage = 0;  
    const char* pszCaption = 0;  
  
    // 检测参数个数是否正确.  
    CheckParamCount(luaEnv, 2);  
  
    // 提取参数.  
    pszMessage = luaL_checkstring(luaEnv, 1);  
    pszCaption = luaL_checkstring(luaEnv, 2);  
  
    if (pszCaption && pszMessage)  
    {  
        ::MessageBox(  
            NULL,  
            pszMessage,  
            pszCaption,  
            MB_OK | MB_ICONINFORMATION  
            );  
    }  
    else  
    {  
        ErrorMsg(luaEnv, ERROR_ARGUMENT_TYPE);  
    }  
  
    // 返回值个数为0个.  
    return 0;  
}  
  
// 导出函数列表.  
static luaL_Reg luaLibs[] =  
{  
    {"ShowMsgBox", ShowMsgBox},  
    {NULL, NULL}  
};  

// Dll入口函数,Lua调用此Dll的入口函数.  
extern "C" __declspec(dllexport)  
int luaopen_WinFeature(lua_State* luaEnv)  
{  
    const char* const LIBRARY_NAME = "WinFeature";  
    luaL_register(luaEnv, LIBRARY_NAME, luaLibs);  
  
    return 1;  
}  

程序解析

首先我们包含Lua的头文件,并链入库文件。注意:Lua的头文件为C风格,所以用external “C”来含入。在此例中,我们最终的导出函数为“ShowMsgBox”。

每一个导出函数的格式都为:

extern “C”int Export_Proc_Name(luaState* luaEnv);  

其中,luaState*所指的结构中包含了Lua调用此Dll时必备的Lua环境。那么Lua向函数传递参数该怎么办呢?实际上是用luaL_check[type]函数来完成的。如下:

const char* pHelloStr = luaL_checkstring(luaEnv, 1);  
double value = luaL_checknumber(luaEnv, 2);  
int ivalue = luaL_checkint(luaEnv, 3);  

luaL_check系列函数的第二个参数是Lua调用该函数时传递参数从坐到右的顺序(从1开始)。

然后我们看到,static的一个luaL_Reg的结构数组中包含了所有要导出的函数列表。最后通过luaopen_YourDllName的一个导出函数来完成一系列操作。YourDllName就是你最终的Dll的名字(不含扩展名)。因为你在Lua中调用此Dll时,Lua会根据此Dll名字找luaopen_YourDllName对应的函数,然后从此函数加载该Dll。

Dll入口函数格式如下:

extern "C" __declspec(dllexport)  
int luaopen_WinFeature(lua_State* luaEnv)  
{  
    const char* const LIBRARY_NAME = "WinFeature";  
    luaL_register(luaEnv, LIBRARY_NAME, luaLibs);  
  
    return 1;  
} 

我们通过luaL_register将LIBRARY_NAME对应的库名,以及luaL_Reg数组对应的导出列表来注册到lua_State*对应的Lua环境中。

Lua调用

那么我们要如何调用该Dll呢?首先,把该Dll放到你的Lua能搜寻到的目录——当前目录、Lua安装目录下的clibs目录,然后通过require函数导入。

因为Lua中如果你的函数调用参数只有一个,并且该参数为字符串的话,函数调用时的括号是可以省略的,所以:require(“YourLibName”)和requir“YourLibName”都是合法的。我们把刚刚生成的WinFeature.dll文件拷贝到C盘下,然后在C盘启动Lua。示例如下:

> require "WinFeature"
> for k, v in pairs(WinFeature) do
>>		print(k, v)
>> end
ShowMsgBox functon:0028AB90
>

可以看到,函数调用方式都是“包名.函数名”,而包名就是你的Dll的名字。我们可以用下面的方式查看一个包中的所有函数:

for k, v in pairs(PackageName) do  
    print(k, v)  
end  

然后我们调用WinFeature.ShowMsgBox函数:

> WinFeature.ShowMsgBox("Hello, this is a msgBox", "Tip")

会弹出对话框显示内容为"Hello, this is a msgBox"和标题为"Tip"。

Lua堆栈详解

唔,那么lua_State结构如何管理Lua运行环境的呢?Lua又是如何将参数传递到C/C函数的呢?C/C函数又如何返回值给Lua呢?……这一切,都得从Lua堆栈讲起。

Lua在和C/C交互时,Lua运行环境维护着一份堆栈——不是传统意义上的堆栈,而是Lua模拟出来的。Lua与C/C的数据传递都通过这份堆栈来完成,这份堆栈的代表就是lua_State*所指的那个结构。

堆栈结构解析

堆栈通过lua_push系列函数向堆栈中压入值,通过luaL_check系列从堆栈中获取值。而用luaL_check系列函数时传递的参数索引,比如我们调用WinFeature.ShowMsgBox(“Hello”, “Tip”)函数时,栈结构如下:

栈顶 "Tip" 2或者-1 "Hello" 1或者-2 栈底

其中,参数在栈中的索引为参数从左到右的索引(从1开始),栈顶元素索引也可以从-1记起。栈中元素个数可以用lua_gettop来获得,如果lua_gettop返回0,表示此栈为空。(lua_gettop这个函数名取得不怎么样!)

提取参数

luaL_check系列函数在获取值的同时,检测这个值是不是符合我们所期望的类型,如果不是,则抛出异常。所有这个系列函数如下:

luaL_checkany          —— 检测任何值(可以为nil)  
luaL_checkint          —— 检测一个值是否为number(double),并转换成int  
luaL_checkinteger      —— 检测一个值是否为number(double),并转换成lua_Integer(prtdiff_t),在我的机子上,ptrdiff_t被定义为int  
luaL_checklong         —— 检测一个值是否为number(double),并转换成long  
luaL_checklstring      —— 检测一个值是否为string,并将字符串长度传递在[out]参数中返回  
luaL_checknumber       —— 检测一个值是否为number(double)  
luaL_checkstring       —— 检测一个值是否为string并返回  
luaL_checkudata        —— 检测自定义类型  

传递返回值

当我们要传递返回值给Lua时,可以用lua_push系列函数来完成。每一个导出函数都要返回一个int型整数,这个整数是你的导出函数的返回值的个数。而返回值通过lua_push系列函数压入栈中。比如一个Add函数:

extern “C” int Add(lua_State* luaEnv)  
{  
    CheckParamCount(luaEnv, 2);  
  
    double left = luaL_checknumber(luaEnv, 1);  
    double right = luaL_checknumber(luaEnv, 2);  
  
    double result = left + right;  
    lua_pushnumber(luaEnv, result);  
      
    return 1;  
}

可以看出,我们用lua_pushnumber把返回值压入栈,最后返回1——1代表返回值的个数。lua_push系列函数如下:

lua_pushboolean        —— 压入一个bool值  
lua_pushcfunction      —— 压入一个lua_CFunction类型的C函数指针  
lua_pushfstring        —— 格式化一个string并返回,类似于sprintf  
lua_pushinteger        —— 压入一个int  
lua_pushlightuserdata  —— 压入自定义数据类型  
lua_pushliteral        —— 压入一个字面值字符串  
lua_pushlstring        —— 压入一个规定长度内的string  
lua_pushnil            —— 压入nil值  
lua_pushnumber         —— 压入lua_Number(double)值  
lua_pushstring         —— 压入一个string  
lua_pushthread         —— 压入一个所传递lua_State所对应的线程,如果此线程是主线程,则返回1  
lua_pushvalue          —— 将所传递索引处的值复制一份压入栈顶  
lua_pushvfstring       —— 类似lua_pushfstring  

通过这些函数,我们可以灵活的使用C/C++的高性能特性,来导出函数供Lua调用。

C/C++调用Lua脚本

简介

C调用LUA文件中的函数方法

lua_getglobal(L, <function name>) //获取lua中的函数
lua_push*() //调用lua_push系列函数,把输入参数压栈。例如lua_pushnumber(L, x)
lua_pcall(L, <arguments nums>, <return nums>, <错误处理函数地址>)

例如:

lua_settop(m_pLua,0);
lua_getglobal(m_pLua,"mainlogic");
lua_pushlstring(m_pLua,(char*)msg.getBuf(),msg.size());
int ret = 0;
ret = lua_pcall(m_pLua,1,4,0);

上一节介绍了如何在Lua中调用C/C代码,本节介绍如何在C/C中调用Lua脚本。本节介绍一个例子,通过Lua来生成一个XML格式的便签。便签格式如下:

<?xml version="1.0" encoding="utf-8" ?>  
<note>  
    <fromName>发送方姓名</fromName>  
    <toName>接收方姓名</toName>  
    <sendTime>发送时间</sendTime>  
    <msgContent>便签内容</msgContent>  
</note>  

我们通过C/C++来输入这些信息,然后让Lua来生成这样一个便签文件。

Lua代码

xmlHead = '<?xml version="1.0" encoding="utf-8" ?>\n'  
  
-- Open note file to wriet.  
function openNoteFile(fileName)  
    return io.open(fileName, "w")  
end  

-- Close writed note file.  
function closeNoteFile(noteFile)  
    noteFile:close()  
end  

function writeNestedLabel(ioChanel, label, nestCnt)  
    if nestCnt == 0 then  
        ioChanel:write(label)  
        return  
    end  
  
    for i = 1, nestCnt do  
        ioChanel:write("\t")  
    end  
  
    ioChanel:write(label)  
end  

function generateNoteXML(fromName, toName, msgContent)  
    local noteFile = openNoteFile(fromName .. "_" .. toName .. ".xml")  
    if not noteFile then  
        return false  
    end  
  
    noteFile:write(xmlHead)  
    noteFile:write("<note>\n")  
  
    local currNestCnt = 1  
    writeNestedLabel(noteFile, "<fromName>", currNestCnt)  
    noteFile:write(fromName)  
    writeNestedLabel(noteFile, "</fromName>\n", 0)  
  
    writeNestedLabel(noteFile, "<toName>", currNestCnt)  
    noteFile:write(toName)  
    writeNestedLabel(noteFile, "</toName>\n", 0)  
  
    local sendTime = os.time()  
    writeNestedLabel(noteFile, "<sendTime>", currNestCnt)  
    noteFile:write(sendTime)  
    writeNestedLabel(noteFile, "</sendTime>\n", 0)  
  
    writeNestedLabel(noteFile, "<msgContent>", currNestCnt)  
    noteFile:write(msgContent)  
    writeNestedLabel(noteFile, "</msgContent>\n", 0)  
  
    noteFile:write("</note>\n")  
    closeNoteFile(noteFile)  
  
    return true  
end  

我们通过openNoteFile和closeNoteFile来打开/关闭XML文件。generateNoteXML全局函数接受发送方姓名、接收方姓名、便签内容,生成一个XML便签文件。便签发送时间通过Lua标准库os.time()函数来获取。writeNestedLabel函数根据当前XML的缩进数目来规范XML输出格式。此文件很好理解,不再赘述。

C++调用Lua脚本

extern "C"  
{  
#include <lua.h>  
#include <lualib.h>  
#include <lauxlib.h>  
#pragma comment(lib, "lua.lib")  
};  

#include <iostream>  
#include <string>  
using namespace std;  

// 初始化Lua环境.  
lua_State* initLuaEnv()  
{  
    lua_State* luaEnv = lua_open();  
    luaopen_base(luaEnv);  
    luaL_openlibs(luaEnv);  
  
    return luaEnv;  
}  

// 加载Lua文件到Lua运行时环境中
bool loadLuaFile(lua_State* luaEnv, const string& fileName)  
{  
    int result = luaL_loadfile(luaEnv, fileName.c_str());  
    if (result)  
    {  
        return false;  
    }  
      
    result = lua_pcall(luaEnv, 0, 0, 0);  
    return result == 0;  
}  

// 获取全局函数
lua_CFunction getGlobalProc(lua_State* luaEnv, const string& procName)  
{  
    lua_getglobal(luaEnv, procName.c_str());  
    if (!lua_iscfunction(luaEnv, 1))  
    {  
        return 0;  
    }  
  
    return lua_tocfunction(luaEnv, 1);  
}  

int main()  
{  
    // 初始化Lua运行时环境.  
    lua_State* luaEnv = initLuaEnv();  
    if (!luaEnv)  
    {  
        return -1;  
    }  
  
    // 加载脚本到Lua环境中.  
    if (!loadLuaFile(luaEnv, ".\\GenerateNoteXML.lua"))  
    {  
        cout<<"Load Lua File FAILED!"<<endl;  
        return -1;  
    }  
  
    // 获取Note信息.  
    string fromName;  
    string toName;  
    string msgContent;  
  
    cout<<"Enter your name:"<<endl;  
    cin>>fromName;  
  
    cout<<"\nEnter destination name:"<<endl;  
    cin>>toName;  
  
    cout<<"\nEnter message content:"<<endl;  
    getline(cin, msgContent);  
    getline(cin, msgContent);  
  
    // 将要调用的函数和函数调用参数入栈.  
    lua_getglobal(luaEnv, "generateNoteXML");  
    lua_pushstring(luaEnv, fromName.c_str());  
    lua_pushstring(luaEnv, toName.c_str());  
    lua_pushstring(luaEnv, msgContent.c_str());  
  
    // 调用Lua函数(3个参数,一个返回值).  
    lua_pcall(luaEnv, 3, 1, 0);  
  
    // 获取返回值.  
    if (lua_isboolean(luaEnv, -1))  
    {  
        int success = lua_toboolean(luaEnv, -1);  
        if (success)  
        {  
            cout<<"\nGenerate Note File Successful!"<<endl;  
        }  
    }  
  
    // 将返回值出栈.  
    lua_pop(luaEnv, 1);  
  
    // 释放Lua运行时环境.  
    lua_close(luaEnv);  
  
    system("pause");  
    return 0;  
}  

代码解析

初始化Lua运行时环境

lua_State*所指向的结构中封装了Lua的运行时环境。我们用initLuaEnv这个函数来初始化。lua_open函数用来获取一个新环境,然后我们用luaopen_base打开Lua的基础库,然后用luaL_openlibs打开Lua的io、string、math、table等高级库。

加载Lua文件

然后我们用luaL_loadfile和lua_pcall来将一个Lua脚本加载到对应的Lua运行时环境中——注意:并不自动执行,只是加载。这两个函数如果返回非0,表示加载失败——你的Lua脚本文件可能未找到或Lua脚本有语法错误……

加载Lua函数

我们用lua_getglobal函数将Lua脚本中的全局函数、全局变量等入栈,放在栈顶。

压入Lua函数调用参数

我们用lua_push系列函数来将要调用函数所需参数全部入栈,入栈顺序为Lua函数对应参数从左到右的顺序。

调用Lua函数

最后用lua_pcall来调用函数。Lua_pcall函数原型如下:

int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc);  

其中,L为此函数执行的Lua环境,nargs为此函数所需的参数个数,nresults为此函数的返回值个数,errfunc为发生错误时错误处理函数在堆栈中的索引。

获取Lua函数返回值

然后,我们可以通过检测栈顶的位置(从-1开始),来获取返回值。获取返回值后,用lua_pop将栈顶元素出栈——出栈个数为返回值个数。

释放Lua环境

最后,通过lua_close函数来关闭Lua环境并释放资源。

运行结果

我们将GenerateNoteXML.lua脚本和最终的C++生成的GenerateNoteXML.exe放在同一路径下,并运行GenerateNoteXML.exe,在此目录下会生成一个XML文件。如下:

Enter your name:
Jack

Enter destnation name:
Joe

Enter message content:
Hello, Can you help me?

Generate Note File Successful!

生成的arnozhang_YaFengZhang.xml文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<note>
  <fromName>Jack</fromName>
  <toName>Joe</toName>
  <sendTime>1317971623</sendTime>
  <msgContent>Hello, Can you help me?</msgContent>
</note>

C 作为动态库文件被 Lua 调用

C/C++中的入口函数定义

一定是要定义成: luaopen_(dll或so文件的文件名称),(dll或so文件的文件名称)必须和dll或so文件名称保持一致。

例如(C++ windows情况):

#ifdef _WIN32
#define _EXPORT extern "C" __declspec(dllexport)
#else //unix/linux
#define _EXPORT extern "C"
#endif
_EXPORT int luaopen_capi_mytestlib(lua_State *L)
{
    struct luaL_reg driver[] = {
        {"average", average1},
        {NULL, NULL},};
    luaL_register(L, "mylib", driver);
    //luaL_openlib(L, "mylib", driver, 0);
    return 1;
}

动态库要供LUA调用的function

其定义要符合:

typedef int function(lua_State *L)

在动态库调用LUA注册

将要调用的函数放到这个结构体里:

struct luaL_Reg lib[] ={}

在动态库的入口函数里调用luaL_register将这个结构体注册,在这个入口函数注册结构体时,要注册成:

luaL_register(L,"XXX",lib);

在写脚本的时候,使用require("XXX")

就是入口函数的luaopen_后面的XXX,注意大小写敏感

编译生成的动态库命令成XXX.so或XXX.dll(win)

同入口函数的luaopen_后面的XXX一致

示例:

C文件如下:

#include <stdio.h>
#include "lua/lua.h"
#include "lua/lualib.h"
#include "lua/lauxlib.h"
static int add(lua_State *L)
{
	int a,b,c;
	a = lua_tonumber(L,1);
	b = lua_tonumber(L,2);
	c = a+b;
	lua_pushnumber(L,c);
	printf("test hello!!!\r\n");
	return 1;
}
static const struct luaL_Reg lib[] =
{
	{"testadd",add},
	{NULL,NULL}
};

int luaopen_testlib(lua_State *L)
{
	luaL_register(L,"testlib",lib);
	return 1;
}

编译: gcc test.c -fPIC -shared -o testlib.so

lua脚本编写:

require("testlib")
c = testlib.testadd(15,25)
print("The result is ",c);

示例:

int lua_createmeta (lua_State *L, const char *name, const luaL_reg *methods) {
	if (!luaL_newmetatable (L, name))
		return 0;
	
	luaL_openlib (L, NULL, methods, 0);
	
	lua_pushliteral (L, "__gc");
	lua_pushcfunction (L, methods->func);
	lua_settable (L, -3);
	lua_pushliteral (L, "__index");
	lua_pushvalue (L, -2);
	lua_settable (L, -3);
	lua_pushliteral (L, "__metatable");
	lua_pushliteral (L, "you're not allowed to get this metatable");
	lua_settable (L, -3);
	return 1;
}

示例中的luaopen_testlib函数替换为:

int luaopen_testlib(lua_State *L)
{
	lua_createmeta(L,"testlib",lib);
	return 1;
}

导航


书籍推荐