# 把C++静态库lib封装到Lua解释器中
本文介绍了Lua和C/C++交互的三种方式:
1. C代码中调用执行Lua脚本
2. Lua脚本调用C编写的dll库
3. 把C/C++编写的静态lib库封装进Lua解释器,由Lua脚本调用
前两种方式网上已经有很多现成的文章了,只做粗略的介绍,由于此次我的需求是把代码封装成lib库,再编译链接进lua解释器,最终由lua脚本直接调用,因此本文重点对第三种方式做介绍,并尽量详细记录此过程遇到过的坑。
## 前置相关知识总结
Lua是一个很小巧的脚本语言,由C编写,源码开放([带注释的源码](https://github.com/lichuang/Lua-5.1.4-codedump)),主要应用于游戏编程领域,上手容易,可以轻松实现热更新。Lua解释器开源,在原生解释器的基础上可以进行扩展编程,如比较有名的luajit、TINN,都是对Lua解释器做封装,赋予了新的可能。既然是要扩展,就免不了需要在Lua和C/C++之间做交互。
Lua与C相互调用的首要问题是如何交换数据,Lua API使用了一个抽象的栈与C语言交换数据,提供了压入元素,查询元素和弹出元素等功能的API操作栈,[这里](https://blog.csdn.net/CoderAldrich/article/details/87723488)可以查看Lua的C api接口,栈中的元素可以通过索引访问,从栈底向上是从1开始递增的正整数,从栈顶向下是从-1开始递减的负整数,栈的元素按照FIFO的规则进出。
### C调用Lua
在C中嵌入Lua脚本既可以让用户在不重新编译代码的情况下修改Lua代码更新程序,也可以给用户提供一个自由定制的接口,这种方法遵循了机制与策略分离的原则。
执行Lua脚本的C代码如下:
```cpp
lua_State* state = luaL_newstate();
luaL_openlibs(state);
if (luaL_dostring(state, "print([[lua env is ready]])") != 0)
{
printf("lua env is bad!\n");
}
lua_close(state);
```
- luaL_newstate创建了一个新的lua_State,我称之为Lua的一个会话空间,之后C和Lua的操作都依赖于这个会话空间;
- luaL_openlibs把默认模块加载到会话空间,以便执行Lua时使用;
- luaL_dostring执行指定的Lua代码,此处也可以是luaL_dofile加载Lua脚本文件并执行。
### Lua调用C
Lua可以调用C函数的能力将极大的提高Lua的可扩展性和可用性。对于有些和操作系统相关的功能,或者是对效率要求较高的模块,我们完全可以通过C函数来实现,之后再通过Lua调用指定的C函数。对于那些可被Lua调用的C函数而言,其接口必须遵循Lua要求的形式,即`typedef int (*lua_CFunction)(lua_State* L)`。简单说明一下,该函数类型仅仅包含一个表示Lua环境的指针作为其唯一的参数,实现者可以通过该指针进一步获取Lua代码中实际传入的参数。返回值是整型,表示该C函数将返回给Lua代码的返回值数量,如果没有返回值,则return 0即可。需要说明的是,C函数无法直接将真正的返回值返回给Lua代码,而是通过虚拟栈来传递Lua代码和C函数之间的调用参数和返回值的。这里我们将介绍两种Lua调用C函数的规则。
#### C函数作为应用程序的一部分
```cpp
#include
#include
#include
#include
#include
//待Lua调用的C注册函数。
static int add2(lua_State* L)
{
//检查栈中的参数是否合法,1表示Lua调用时的第一个参数(从左到右),依此类推。
//如果Lua代码在调用时传递的参数不为number,该函数将报错并终止程序的执行。
double op1 = luaL_checknumber(L,1);
double op2 = luaL_checknumber(L,2);
//将函数的结果压入栈中。如果有多个返回值,可以在这里多次压入栈中。
lua_pushnumber(L,op1 + op2);
//返回值用于提示该C函数的返回值数量,即压入栈中的返回值数量。
return 1;
}
//另一个待Lua调用的C注册函数。
static int sub2(lua_State* L)
{
double op1 = luaL_checknumber(L,1);
double op2 = luaL_checknumber(L,2);
lua_pushnumber(L,op1 - op2);
return 1;
}
const char* testfunc = "print(add2(1.0,2.0)) print(sub2(20.1,19))";
int main()
{
lua_State* L = luaL_newstate();
luaL_openlibs(L);
//将指定的函数注册为Lua的全局函数变量,其中第一个字符串参数为Lua代码
//在调用C函数时使用的全局函数名,第二个参数为实际C函数的指针。
lua_register(L, "add2", add2);
lua_register(L, "sub2", sub2);
//在注册完所有的C函数之后,即可在Lua的代码块中使用这些已经注册的C函数了。
if (luaL_dostring(L,testfunc))
printf("Failed to invoke.\n");
lua_close(L);
return 0;
}
```
#### C函数dll成为Lua的模块
```cpp
#include
#include
#include
#include
#include
//待注册的C函数,该函数的声明形式在上面的例子中已经给出。
//需要说明的是,该函数必须以C的形式被导出,因此extern "C"是必须的。
//函数代码和上例相同,这里不再赘述。
extern "C" int add(lua_State* L)
{
double op1 = luaL_checknumber(L,1);
double op2 = luaL_checknumber(L,2);
lua_pushnumber(L,op1 + op2);
return 1;
}
extern "C" int sub(lua_State* L)
{
double op1 = luaL_checknumber(L,1);
double op2 = luaL_checknumber(L,2);
lua_pushnumber(L,op1 - op2);
return 1;
}
//luaL_Reg结构体的第一个字段为字符串,在注册时用于通知Lua该函数的名字。
//第一个字段为C函数指针。
//结构体数组中的最后一个元素的两个字段均为NULL,用于提示Lua注册函数已经到达数组的末尾。
static luaL_Reg mylibs[] = {
{"add", add},
{"sub", sub},
{NULL, NULL}
};
//该C库的唯一入口函数。其函数签名等同于上面的注册函数。见如下几点说明:
//1. 我们可以将该函数简单的理解为模块的工厂函数。
//2. 其函数名必须为luaopen_xxx,其中xxx表示library名称。Lua代码require "xxx"需要与之对应。
//3. 在luaL_register的调用中,其第一个字符串参数为模块名"xxx",第二个参数为待注册函数的数组。
//4. 需要强调的是,所有需要用到"xxx"的代码,不论C还是Lua,都必须保持一致,这是Lua的约定,
// 否则将无法调用。
extern "C" __declspec(dllexport)
int luaopen_mytestlib(lua_State* L)
{
const char* libName = "mytestlib";
luaL_register(L,libName,mylibs);
return 1;
}
```
Lua代码:
```lua
require "mytestlib" --指定包名称
--在调用时,必须是package.function
print(mytestlib.add(1.0,2.0))
print(mytestlib.sub(20.1,19))
```
## Lua使用C++静态库
Lua使用C++静态库本质上与上一节的C函数作为应用程序的一部分由Lua调用一样,关键点是编写静态库以及链接符号识别问题,原因是Lua是纯粹的C代码,而我们想要写的lib库也不希望被限制只能用纯C,C++又会由于生成的符号命名规则不同导致连接时报`无法解析的外部符号`这个错误,可谓一步一个坑,接下来一步一步对过程进行介绍。
### 用C++为Lua编写静态库
1. 首先正常编写静态库,导出函数声明到头文件中,如下:
```cpp
#pragma once
#include
#include
BOOL fun1();
BOOL fun2();
BOOL fun3(char* str1, char* str2);
```
编写好lib之后确保能正常被C调用。
2. 使用[tolua++](https://github.com/LuaDist/toluapp)自动生成符合Lua形式的cpp源码
关于tolua++使用的介绍可以移步[这里](https://blog.csdn.net/xingxinmanong/article/details/78137514)。
生成的cpp源码文件添加进静态库的工程,重新生成,有错误的话检查一下生成的cpp源码头文件包含的时候齐全,写package的时候容易漏掉一些头文件,还要注意把该去掉的信息去掉,如`using namespace std;`这句就不该被写入package文件。
3. 生成lib时记得把“链接库依赖项”设置为“是”
lib成功生成之后,接下来就要把lib静态库编译进Lua解释器源码中。
### 把C++静态库编译到Lua解释器
1. 把上一步生成的lib静态库文件复制到Lua代码包的src目录,找到msvcbuild.bat,定位到link命令那行,把静态库的全名添加进去;
2. 观察发现Lua解释器源码后缀名都是.c,是不能直接调用cpp生成的lib库的,因此把luajit.c后缀名改为cpp,luajit.cpp内部包含的头文件用extern “C"{}包住,意思是这些依旧用C的编译风格进行编译;
3. 如果只是到此为止,直接把lib编译链接进luajit.exe的话,会导致lua脚本代码调用lib中接口时报无法找到标识符的错误,因此需要调用注册函数。用tolua++生成的cpp源文件中有一个Open function,作用就是向当前lua_State会话空间注册函数符号,并把函数符号跟对应的C++函数关联起来,此函数需要在调用完`luaL_newstate()`之后调用,因此我们打开luajit.cpp源码,找到main函数,在`luaL_newstate()`后面添加如下代码:
```cpp
int tolua_cppfilename_open(lua_State*);// 因为lib库没有带头文件,所以需要声明函数原型,否则会报找不到函数的错误
tolua_cppfilename_open(L);
```
4. 再次运行msvcbuild.bat,此时生成的luajit.exe就是包含了lib库的程序。
### Lua调用C++静态库中的接口
由于代码是以静态库的方式编译链接进exe中的,所以不存在模块的说法,因此根本不需要`require()`操作,可以把lib中的接口当做内置函数直接调用:
```lua
-- 此lua程序用来测试lua脚本能否成功调用lib中的导出接口
if fun3("str1", "str2") then
-- body
print("fun3successed");
else
-- body
print("fun3failed");
end
if fun2() then
-- body
print("fun2 is reachable");
else
print("fun2 is un reachable");
end
if fun1() then
-- body
print("fun1 successed");
else
-- body
print("fun1 failed");
end
```
## 总结遇到的坑
因为之前对lua完全不了解,所以在开始这个任务的时候绕了很大的弯子,搜到最多的资料是为lua编写dll,现在回头开会觉得很简单。容易出问题的点有以下两个:
1. 编写lib库时要提供注册函数,把函数注册到lua_state中;
2. c和cpp混用问题,由于c和cpp编译方式有区别,而现在写代码一般都是用的cpp后缀,因此命名规则的改变会导致找不到符号;
本文参考了:
1. [https://www.cnblogs.com/chenny7/p/3993456.html](https://www.cnblogs.com/chenny7/p/3993456.html)
2. [https://blog.csdn.net/xingxinmanong/article/details/78137514](https://blog.csdn.net/xingxinmanong/article/details/78137514)
如果想要进一步把stl、C++类封装进lib使用,可以参考这篇内容:
- [FFLIB之FFLUA——C++嵌入Lua&扩展Lua利器](https://cloud.tencent.com/developer/article/1056709)
0 Comments latest
No comments.