文章目录

  • 构建lua源码
  • C编写lua模块的两种形式
  • 直接修改lua源码
  • 动态链接库


官方lua是一门纯C实现的嵌入式语言。说是嵌入式语言,是指它可以作为辅助语言被主语言使用,比如Unity游戏中,lua就是作为一门嵌入C#的辅助语言为Unity游戏提供热更新,而帮助lua嵌入C#的框架可以是tolua,xlua或者ulua等。

然而“嵌入式”只是因为它可以上述这么用,lua也可以作为主体语言使用,与python类似但轻量级的解释性语言。例如:有时候出于很多种原因,需要使用C语言去编写一个lua可以用的模块。

在此之前,需要知道如何构建lua源码。

构建lua源码

构建lua源码主要是为了得到四个文件:lua解释器,lua字节码编译器,lua静态链接库,lua动态链接库。后两者是开发与lua相关的应用或者库所需要的。

首先在官网上下载lua源码(本篇文章下载lua5.4 release 2.0,不同版本之间可能有会有一点差别)。

下载下来的压缩文件解压之后,src目录下的就是所有lua的所有源码。在src目录下有一个makefile文件,这个文件是用来构建lua源码的。src目录外也有一个makefile文件,是用来安装lua的。

在linux下可以很方便地借助 gcc - make - makefile 一套工具链直接构建出所有东西:

cd src
make linux

在windows下可能麻烦一点,因为windows没有这么直接好用的工具链。在windows下的C语言开发工具链(或者集成开发环境)最好还是使用MSVC

起初我使用Cygwin提供的环境来编译lua源码,在这个环境中可以和在linux上一样使用gcc - make 工具链。然而,我始终无法得到正确的链接库,即pe格式.lib扩展名的静态链接库,得到的一直是elf格式的.a扩展名的静态链接库。而且得到的lua解释器也是linux下的解释器——如何知道呢?你可以简单地在一个.lua文件里面require一个不存在的模块,lua解释器会报错,说明在如下什么什么路径下找不到模块。这些路径如果形如/usr/开头的linux风格的路径,那说明你的解释器就是linux环境下的用的。如果是形如D:/software这样带驱动卷的路径,就是正确的windows环境的lua解释器。

MSVC有自己的C语言编译器——CL.exe。所以你可以为Make工具配置CL编译器,然后使用make一键构建。不过,单独获取MSVC,为Make配置编译器也是一件麻烦的事情。

没错,说了那么多废话,总结一句话就是:我太菜了,只会使用visual studio 2019…Orz…

visual studio需要有C++的相关开发组件才能正常编译。新建一个空项目,VS会同时在项目外套一层解决方案。解决方案和项目名建议不要重名。

lua可以编译成动态库吗 lua编译成c_lua可以编译成动态库吗


新建好之后,为项目添加源文件和头文件,即将lua源码中所有.c文件添加进源文件归类下,同时将所有.h文件添加到头文件归类下(lua 5.4有一个.hpp头文件,不需要)。

lua可以编译成动态库吗 lua编译成c_lua_02


这个项目是用来编译lua解释器的。点击项目的属性,查看:

lua可以编译成动态库吗 lua编译成c_lua可以编译成动态库吗_03


lua可以编译成动态库吗 lua编译成c_lua可以编译成动态库吗_04


可以看到配置类型属于应用程序,意思是这个项目将会被构建生成一个.exe。但是,源文件中有两个文件是带main函数的,一个lua.c,用来生成解释器,一个luac.c,用来生成字节码编译器。我们需要将源文件中的luac.c文件排除出项目外。最后 “生成 -> 生成lua”,不出意外应该会得到一个lua.exe。同理,右键解决方案,新建另外三个项目,将四个项目都用一个解决方案来管理。同理为新的三个项目导入lua源码,他们分别用来构建lua字节码编译器和两个链接库。

lua可以编译成动态库吗 lua编译成c_unity3d_05

  • luac项目的配置与lua相同,但将lua.c从项目中排除,保留luac.c
  • luadll和lualib的配置中,配置类型分别选择动态链接库和静态链接库,同时将lua.cluac.c从两个项目中排除。

最后 “生成 -> 生成解决方案”,我们可以同时构建四个项目。在生成目录下可以找到下面几个文件:

lua可以编译成动态库吗 lua编译成c_unity3d_06

C编写lua模块的两种形式

直接修改lua源码

一种是直接更改lua源码,重新编译lua解释器。事实上,原生的lua解释器本身已经自带了一些模块,例如你无须导入任何文件和代码就可以使用lua自带的数学模块:

require "math"
print(math.sin(3.1415926/2))

这个数学模块对应lua源码中的lmathlib.c,这个模块体现在lua中就是一张表,表中所有的函数和常量都在这个C源文件中注册了:

lua可以编译成动态库吗 lua编译成c_lua可以编译成动态库吗_07


这个文件只是定义了一个模块,并在模块内部注册了一些函数或者常量。但是我们还需要为整个lua注册这个模块,这样我们才能使用这个模块,从而使用模块内的函数和常量。这个工作是在linit.c中完成的:

lua可以编译成动态库吗 lua编译成c_unity3d_08


现在我们可以仿照lmathlib.c自己写一个模块,新建一个lmytestlib.c,为项目的源文件导入这个新的源文件。

#include "lua.h" // lua最主要的变量和API都在这里声明
#include "lauxlib.h" // 这个头文件声明了与构建模块有关的辅助API,包括 luaL_Reg 结构体
#include "lualib.h" // 所有的内置模块都需要在这里面声明

#define MY_NUM 233

// 所有需要导出到外部使用的函数的签名都形如 static int func(lua_State *)
static int 
myadd(lua_State *L){
	lua_Number a = luaL_checknumber(L, 1); // 获取从lua传入的第一个参数
	lua_Number b = luaL_checknumber(L, 2); // 获取从lua传入的第二个参数
	lua_pushnumber(L, a + b); // 将结果压入虚拟栈
	return 1; // 返回值的个数
}

// 就需要导出到外部使用的API都注册在这个结构体数组中
static const luaL_Reg
mytestlib[] = {
	{"myadd",   myadd}, // 为函数注册它外部的名字
	{NULL, NULL} // 比如以这个结尾,告诉lua注册结束了
};

// 这个函数真正地向lua表明这是一个库,lua在加载这个库的时候其实就是调用这个函数
// 函数签名一律为 LUAMOD_API int luaopen_[库名](lua_State *)
LUAMOD_API int 
luaopen_mytest(lua_State* L) {
	luaL_newlib(L, mytestlib); // 加载所有注册的函数
	lua_pushnumber(L, MY_NUM); // 下面两行设置这个表的变量
	lua_setfield(L, -2, "my_num");
	return 1;
}

然后我们将luaopen_mytest函数在lualib.h中声明一下,这样linit.c才能找到这个函数。

lua可以编译成动态库吗 lua编译成c_unity3d_09

然后在linit.c注册这个模块:

lua可以编译成动态库吗 lua编译成c_unity3d_10


最后重新编译lua源码。

我们书写以下lua来验证一下新的lua解释器是不是自带了新的模块:

require "mytest"

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

print(mytest.my_num)
print(mytest.myadd(12, 13))

运行正常!!

lua可以编译成动态库吗 lua编译成c_linux_11

动态链接库

上面那种修改lua源码,重新编译解释器的方式显然是不健壮的。如果用官方的解释器运行如下lua代码,尝试加载一个不存在的库:

require "wahfdfhuioweh"

会得到如下结果:

lua可以编译成动态库吗 lua编译成c_源文件_12


这告诉了我们当lua需要加载一个模块的时候,它会去哪里找它,其中就包括动态链接库(.dll for windows and .so for linux)。

新建一个项目,为其新建一个源文件,例如mytest.c,同时导入lua源码的所有头文件。mytest.c代码如lmytestlib.c,但是需要在开头多定义两个宏:

// 注意之前luaopen函数前面的修饰宏,LUAMOD_LIB,这个宏在上面的例子中被扩展成了 extern,表明该函数可供外部调用
// 声明了下面的宏之后,LUAMODE_LIB 被扩展成了 __declspec(dllexport),它表明如果当前函数被打包进一个dll,那它将被暴露出来以供其他程序使用
#define LUA_BUILD_AS_DLL
#define LUA_LIB

然后编译它成一个dll,编译的时候记得为其指定一个依赖项,依赖上面构建出来的lua的静态链接库(具体的,在项目属性 -> VC++ 目录 -> 库目录 中添加lib所在的目录,然后在项目属性 -> 链接器 -> 输入 -> 附加依赖项 中输入静态链接库的名称)。

之后就可以像上面一个正常地require了。需要注意的是,这里的require需要将结果保存到一个变量中,它不像require内置模块的时候lua会自动将模块保存到一个本地变量里。

local mytest = require "mytest"