把C++静态库lib封装到Lua解释器中

把C++静态库lib封装到Lua解释器中

本文介绍了Lua和C/C++交互的三种方式:

  1. C代码中调用执行Lua脚本
  2. Lua脚本调用C编写的dll库
  3. 把C/C++编写的静态lib库封装进Lua解释器,由Lua脚本调用

前两种方式网上已经有很多现成的文章了,只做粗略的介绍,由于此次我的需求是把代码封装成lib库,再编译链接进lua解释器,最终由lua脚本直接调用,因此本文重点对第三种方式做介绍,并尽量详细记录此过程遇到过的坑。

前置相关知识总结

Lua是一个很小巧的脚本语言,由C编写,源码开放(带注释的源码),主要应用于游戏编程领域,上手容易,可以轻松实现热更新。Lua解释器开源,在原生解释器的基础上可以进行扩展编程,如比较有名的luajit、TINN,都是对Lua解释器做封装,赋予了新的可能。既然是要扩展,就免不了需要在Lua和C/C++之间做交互。
Lua与C相互调用的首要问题是如何交换数据,Lua API使用了一个抽象的栈与C语言交换数据,提供了压入元素,查询元素和弹出元素等功能的API操作栈,这里可以查看Lua的C api接口,栈中的元素可以通过索引访问,从栈底向上是从1开始递增的正整数,从栈顶向下是从-1开始递减的负整数,栈的元素按照FIFO的规则进出。

C调用Lua

在C中嵌入Lua脚本既可以让用户在不重新编译代码的情况下修改Lua代码更新程序,也可以给用户提供一个自由定制的接口,这种方法遵循了机制与策略分离的原则。
执行Lua脚本的C代码如下:

 	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函数作为应用程序的一部分
#include <stdio.h>
#include <string.h>
#include <lua.hpp>
#include <lauxlib.h>
#include <lualib.h>

//待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的模块
#include <stdio.h>
#include <string.h>
#include <lua.hpp>
#include <lauxlib.h>
#include <lualib.h>

//待注册的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代码:

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. 首先正常编写静态库,导出函数声明到头文件中,如下:
#pragma once
#include <Windows.h>
#include <string>

BOOL fun1();

BOOL fun2();

BOOL fun3(char* str1, char* str2);

编写好lib之后确保能正常被C调用。
2. 使用tolua++自动生成符合Lua形式的cpp源码
关于tolua++使用的介绍可以移步这里
生成的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()后面添加如下代码:
  int tolua_cppfilename_open(lua_State*);// 因为lib库没有带头文件,所以需要声明函数原型,否则会报找不到函数的错误
  tolua_cppfilename_open(L);
  1. 再次运行msvcbuild.bat,此时生成的luajit.exe就是包含了lib库的程序。

Lua调用C++静态库中的接口

由于代码是以静态库的方式编译链接进exe中的,所以不存在模块的说法,因此根本不需要require()操作,可以把lib中的接口当做内置函数直接调用:

-- 此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
  2. https://blog.csdn.net/xingxinmanong/article/details/78137514

如果想要进一步把stl、C++类封装进lib使用,可以参考这篇内容:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值