tolua编译集成lua-protobuf指南

一、前言

tolua_runtimetolua 的C源码,编译之后会输出tolua.dll。这个可以自己修改编译,为了给tolua集成lua-protobuf,需要自己编译tolua在各个平台的库。

tolua 的C代码部分是C#和lua的中间层,提供函数给C# DllImport,C#通过Marshal等与C代码交互。在和lua交互方面,它符合lua扩展库标准,一方面通过lua的C API与lua虚拟机交互,另一方面会提供接口给lua脚本使用。

同时 tolua 的C代码引入了一些lua扩展库,比如cjson、LuaSocket、sqlite3、lpeg、bit、pbc等手机游戏常用库,这些库扩展了lua的能力。

本文要介绍的就是将 lua-protobuf 和lua源码一起编译成 tolua 的native库,Windows平台叫做tolua.dll,Android叫做libtolua.so,Mac平台叫tolua.bundle,而iOS平台由于不允许使用动态库,所以会编译成静态库libtolua.a

为什么换成 lua-protobuf ?

一是 tolua 使用的protoc-gen-lua 很多年不更新了,存在bug且对 proto3 的支持堪忧;

二是protoc-gen-lua返回的不是lua table,看不到字段,不便于调试

三是服务端希望更新到proto3支持map特性;

四是C#的protobuf-net存在GC问题,升级proto3的同时更换为Google官方提供的版本;

五是使用.pb二进制文件代替lua协议定义文件,大小从2MB减小到180KB

还有,网上的文章都是几年前或者一年前了,随着 lua-protobuf 的更新,失去了时效性并且不够详细,这里再重新梳理一遍流程。

二、准备工作

tolua 源码

tolua 的源码 Github 地址: tolua_runtime

到releases下载最新的包即可。本文下载的是1.0.8.584版本。

lua-protobuf

lua-protobuf Github:lua-protobuf

主要是需要里面的pb.cpb.h文件。版本0.3.2。

protobuf-csharp

protobuf-csharp Github: protobuf-csharp

版本3.13.0,解决了C# GC问题,刚刚发布了3.14.0,修复了一点小bug,准备再升级一下。

本文不讨论protobuf-csharp的接入。

编译平台

tolua 作者使用的是 mingw 编译的,需要准备的编译平台是msys2

到官网下载之后安装即可。

有一说一,这个环境安装起来挺费劲的,很容易失败。

用 tolua 提供了配置好的 msys2 :https://pan.baidu.com/s/1c2JzvDQ 但是百度网盘下载实在是太慢了。并且这个版本的msys2比较老,所以我还是选择自己配置编译环境。

编译环境

有了编译平台,接着要在平台上安装 mingw 等环境。

打开 msys2 控制台输入运行:

1
pacman --needed -Sy bash pacman pacman-mirrors msys2-runtime

按照提示安装好 msys2 的运行环境。

安装好运行环境之后要把 msys2 的控制台关掉,进到msys2的文件夹中运行autorebase.bat。

为什么要这样做?因为如果不rebase的话 msys2 就没法更新其他软件包,这是 msys2 的问题。

接下来先更新软件包数据库和系统包:

1
pacman -Syu

如果报错重启msys2再执行下面命令继续更新(官网说的):

1
pacman -Su

更新好了数据库和本地系统包,下面才是真正安装环境。

依次输入回车安装:

1
2
3
4
5
pacman -S mingw-w64-i686-gcc
pacman -S mingw-w64-x86_64-gcc
pacman -S mingw-w64-i686-make
pacman -S mingw-w64-x86_64-make
pacman -S make

如果提示什么PGP签名失效,那么需要更新已知密钥:

1
pacman-key --refresh-keys

如果不行,更改一下key服务器地址。

配置文件在msys64\etc\pacman.d\gnupg\gpg.conf,增加一行

1
keyserver hkps://hkps.pool.sks-keyservers.net:443

或者:

1
keyserver hkp://ipv4.pool.sks-keyservers.net:11371

然后再refresh-keys。

上面弄好之后,再关掉 msysy2 然后rebase,然后再启动msys2。。。接着应该可以安装 mingw 和 make 了。

反正我这样之后成功了。

三、编译

下面就是正式的编译了,我们需要编译各个平台的 tolua 库,包括Windows、Android、Mac、iOS。

代码修改

在编译之前需要对 luasocket 代码做一点修改,因为最新的 luasocket 删除了 LUASOCKET_INET_PTON 的定义,如果用最新的 msys2 编译会报错。

打开tolua源码中的luasocket/inet.h,删除如下三行代码:

1
2
3
#ifdef __MINGW32__
#define LUASOCKET_INET_PTON
#endif

修改的原因是新版的 msys2 中已经带有了inet_pton了,luasocket不再需要这个定义。

参考讨论:https://github.com/topameng/tolua_runtime/issues/29

集成lua-protobuf

网上有些文章说最新的lua-protobuf已经使用宏支持了lua5.1,不再需要修改代码,实际上还是需要的。

要先把 lua-protobuf 的 pb.cpb.h 文件复制到 tolua_runtime 文件夹中,然后进行下一步的修改。

lua-protobuf代码修改

lua-protobuf 的作者认为 tolua 的 OpenLibs 函数的实现方式应该符合Lua5.2+的 require 语义,不过tolua的作者并没有修改。这导致编译后的 lua-protobuf 不能在Lua中 require: local pb = require "pb",因此要对lua-protobuf 代码中的pb.c进行一点修改(lua-protobuf仓库放的第三方文章中说不需要了,但实际上还是需要的)。

pb.cluaL_newlib为lua5.2版本才支持的语法,而tolua是5.1,所以找到以下几个函数进行如下修改,判断Lua版本进行不同的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
LUALIB_API int luaopen_pb_io(lua_State *L)
{
//...
#if LUA_VERSION_NUM < 502
luaL_register(L, "pb.io", libs);
#else
luaL_newlib(L, libs);
#endif
return 1;
}

LUALIB_API int luaopen_pb_conv(lua_State *L)
{
//...
#if LUA_VERSION_NUM < 502
luaL_register(L, "pb.io", libs);
#else
luaL_newlib(L, libs);
#endif
return 1;
}

LUALIB_API int luaopen_pb(lua_State *L)
{
//...
#if LUA_VERSION_NUM < 502
luaL_register(L, "pb", libs);
#else
luaL_newlib(L, libs);
#endif
return 1;
}

如果不进行这样的修改,还有另外一种方法,在tolua的LuaClient.cs中添加以下内容:

img

不过我没有使用,不如直接支持在Lua中require方便。

Github讨论地址:https://github.com/topameng/tolua/issues/168

Windows

  • 32位编译

进入 msys2文件夹,打开mingw32.exe。cd 进入到 tolua 代码所在的文件夹(我的在D盘):

1
2
cd d:
cd WorkSpace/tolua_runtime

然后执行:

1
./build_win32.sh

编译应该没有问题,有一个warning可以忽略。

然后在 Plugins\x86 目录下看见 tolua.dll 文件便编译成功

  • 64位编译

跟上面一样,只不过是打开mingw64.exe执行相同操作,执行的脚本改成:

1
./build_win64.sh

然后在 Plugins\x86_64 目录下看见 tolua.dll 文件便编译成功

Android

  • 要先准备NDK10环境 https://dl.google.com/android/repository/android-ndk-r10e-windows-x86_64.zip
  • 下载完成后解压到不包含中文和空格的目录下
  • build_arm.sh , build_x86.sh , build_arm64.sh .文件中的 NDK 路径改为自己本地存储的路径
    • 我的是D:/android-ndk-r10e
  • link_arm64.bat 文件中的 ndkPath 修改为上面的NDK解压路径下。只需要修改上面文件中的根路径。不要修改 NDK 的版本

android平台用得最多的cpu架构体系是Acorn公司的arm和Intel公司x86,由于arm市场占有率最高,大多android的app也就只编译了arm版本,所以Intel也专门针对arm体系架构做了一个转换程序,也就是说,arm程序在x86机子上也可以跑起来。所以,一般来说,只要编译arm就可以了(最常用的CPU和ABI是ARMv7a),当然,将x86也编译起来是极好的,据以往分析闪退的经验,在x86机子上闪退的一大元凶就是那个转换程序出了问题,代价就是会增加包体的大小(每多支持一个CPU架构,就是多编译一个动态库so)。

注意:经过测试,NDK版本必须是 android-ndk-r10e 才可以编译,更新的版本生成文件的位置不一样,编译脚本会失效。

1. armeabi-v7a

  1. 提前需要保证当前目录下存在 Plugins\Android\libs\armeabi-v7a 目录,不然没有文件输出
  2. msys232位编译环境中执行 ./build_arm.sh.
  3. 然后在 Plugins\Android\libs\armeabi-v7a 目录下看见 libtolua.so 文件便编译成功

2.x86

  1. 提前需要保证当前目录下存在 Plugins\Android\libs\x86 目录,不然没有文件输出
  2. msys232位编译环境中执行 ./build_x86.sh.
  3. 然后在 Plugins\Android\libs\x86 目录下看见 libtolua.so 文件便编译成功

3. arm64-v8a

  1. 提前需要保证当前目录下存在 Plugins\Android\libs\arm64-v8a 目录,不然没有文件输出
  2. msys264位编译环境中执行 ./build_arm64.sh.
  3. 然后在 Plugins\Android\libs\arm64-v8a 目录下看见 libtolua.so 文件便编译成功

iOS

必须在Mac机器上编译

环境:macOS Mojav 10.14.5,Xcode 9.4.1

arm64 : 必选项,支持iphone5s及以上;最低支持版本:iOS5.1.1
armv7s:支持iPhone5及以上
armv7:支持iPhone4及以上

我把 build_ios.sh中的armv7和armv7s那段指令删除了,没必要支持那么低的iPhone版本。

  1. 打开终端.切换到 tolua_runtime 目录下
  2. 在终端中运行 build_ios.sh .如果遇见权限不足,用chmod +x命令提升权限
  3. 然后在 Plugins\iOS 目录下看见 libtolua.a 文件便编译成功

Mac

必须在Mac机器上编译

  1. 打开终端.切换到 tolua_runtime 目录下
  2. 在终端中运行 build_osx.sh .如果遇见权限不足,用chmod +x命令提升权限
  3. 然后在 Plugins 目录下看见 tolua.bundle 库文件便编译成功

四、集成到Unity

修改tolua

首先要在tolua C#部分 LuaDLL.cs中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int luaopen_pb(IntPtr L);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int luaopen_pb_io(IntPtr L);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int luaopen_pb_conv(IntPtr L);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int luaopen_pb_buffer(IntPtr L);

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int luaopen_pb_slice(IntPtr L);

并且在LuaClient.cs的OpenLibs函数中,将上述模块导入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected virtual void OpenLibs()
{
//lua-protobuf
luaState.OpenLibs(LuaDLL.luaopen_pb);
luaState.OpenLibs(LuaDLL.luaopen_pb_io);
luaState.OpenLibs(LuaDLL.luaopen_pb_conv);
luaState.OpenLibs(LuaDLL.luaopen_pb_buffer);
luaState.OpenLibs(LuaDLL.luaopen_pb_slice);
luaState.OpenLibs(LuaDLL.luaopen_struct);
luaState.OpenLibs(LuaDLL.luaopen_lpeg);
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
luaState.OpenLibs(LuaDLL.luaopen_bit);
#endif

if (LuaConst.openLuaSocket)
{
OpenLuaSocket();
}

if (LuaConst.openLuaDebugger)
{
OpenZbsDebugger();
}
}

读取proto

tolua 集成到 Unity 还需要额外做一些处理,除了替换Plugins目录下的所有内容外,其中主要是修改读取lua和proto的二进制.pb文件的逻辑。因为Unity的AB包是只支持txt和bytes的,lua的部分我们项目中已经做好了处理逻辑,要增加的就是.pb的读取。

.pb是二进制文件,打AB包的时候加上.bytes后缀再打包,然后读取的时候从对应的AB包中加载TextAsset即可。

在tolua中,读取lua文件是在 LuaResLoader.cs 中,要注意lua层使用的protobuf的 require "pb" 中的 pb 不是一个lua文件,所以在移动平台走LuaResLoader的时候从AB包中读不到,要对ReadDownLoadFile函数做保护,AB包中没有直接返回null即可,最终lua层会在全局库中找到pb库。

输出.pb文件

开发过程中编写的.proto文件是Protobuf的Scheme描述文件,经过protoc.exe编译可以输出各种语言的数据格式代码文件和编解码文件。前提当然是语言需要是官方支持的,但很可惜lua官方并不支持。这也是我们使用第三方protobuf库给lua用的原因。

之前protoc-gen-lua库使用的方式是模仿官方其他语言的形式——通过Scheme描述文件编译生成lua的数据结构,然后在运行时使用。

lua-protobuf更换了一种方式,不再使用代码生成,而是通过Scheme描述文件编译生成二进制的.pb文件,这个文件相当于二进制的 Protobuf 数据结构,在lua层加载一下就拥有了所有的Protobuf消息数据结构,这个加载方式和云风的pbc是一致的。

另外,我们项目中使用的ProtoGame_pb.lua等定义文件大小超过2MB,换成二进制之后减小到180KB

为了方便,我们可以把多个proto文件编译输出为一个.pb文件:

1
protoc -o ProtoAll.pb *.proto

C#是这样输出的,会为每一个。proto文件生成一个cs文件:

1
protoc --csharp_out=. *.proto

读取.pb文件

这里需要知道的前提是,非AB模式直接从lua层用io.read读取二进制文件是不行的,lua读取二进制文件之后的格式会是userdata,这样是没法给 lua-protobuf 使用的。

lua-protobuf 提供了pb.io.read 来代替非移动平台在非AB模式下的读取。下面是个例子:

1
2
3
4
5
6
7
8
9
local protoBytes
if LuaProtoLoader.IsEditor() == 1 then
protoBytes = pb.io.read("Assets/ToLua/Lua/protobuf/ProtoTest.pb")
else
protoBytes = LuaProtoLoader.Load("ProtoTest.pb")
end

local load = assert(pb.load(protoBytes))
print(load)

Editor下从本地读取二进制文件,移动端从AB包读取,并且要用LuaInterface.LuaByteBuffer封装一下才是标准的二进制格式。

pb.load 是加载proto数据的方法,成功了则返回true。

序列化反序列化

之前用 protoc-gen-lua 有个很麻烦的地方,就是根据message id去初始化一个proto message数据结构:

1
2
3
local req = ProtoGame.Cmd_Team_FetchList_Req()
req.modeid = modeid
req.stageid = stageid

更换 lua-protobuf 之后就不需要这样了,只要写一个table即可:

1
2
3
local req = {}
req.modeid = modeid
req.stageid = stageid

然后传递给 LuaNetwork 时带上协议ID:

1
LuaNetwork.SendMsg(ProtoGame.IDCmd_Team_FetchList_Req, req)

LuaNetWork 自己会根据消息ID去找该序列化成哪个消息,省去了一个初始化的步骤。

序列化方法如下,把table序列化为Person:

1
2
3
4
5
6
7
8
9
10
11
12
-- lua 表数据
local data = {
name = "ilse",
age = 18,
contacts = {
{ name = "alice", phonenumber = 12312341234 },
{ name = "bob", phonenumber = 45645674567 }
}
}

-- 将Lua表编码为二进制数据
local bytes = assert(pb.encode("Proto.Person", data))

反序列化:

1
2
-- 再解码回Lua表
local data2 = assert(pb.decode("Proto.Person", bytes))

反序列化为table,可以看到每个字段和值,不再像 protoc-gen-lua 那样看不到字段、调试费劲、而且遍历容易出问题。

网络层修改

到这里还没结束,由于没有了协议定义文件,之前我们使用的ProtoGame.IDXXX的方式都不能再使用了,为了兼容之前的逻辑,我们需要自己生成两个协议映射文件。

一个是消息到id的映射ProtoId.lua,如:

1
ProtoGame.IDCmd_Team_FetchList_Req = 1001

一个是消息id到消息名称的映射ProtoName.lua,如:

1
ProtoName[1001] = "Cmd_Team_FetchList_Req"

这样Lua层的protobuf接入就基本上完成了。不过C#更换为官方的 Protobuf 也是一个漫长的过程。

编译结果

如果是不希望这么麻烦,可以直接用我编译好的tolua with protobuf https://github.com/jozhn/tolua_pb

参考文章

https://www.jianshu.com/p/5a35602adef8

https://zhuanlan.zhihu.com/p/82093496

http://changxianjie.gitee.io/unitypartner/2019/10/01/tolua%E4%B8%AD%E4%BD%BF%E7%94%A8protobuf3%E2%80%94%E9%9B%86%E6%88%90lua-protobuf/