tolua编译集成lua-protobuf指南
一、前言
tolua_runtime 是 tolua 的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.c
和pb.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 | pacman -S mingw-w64-i686-gcc |
如果提示什么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 |
修改的原因是新版的 msys2 中已经带有了inet_pton了,luasocket不再需要这个定义。
参考讨论:https://github.com/topameng/tolua_runtime/issues/29
集成lua-protobuf
网上有些文章说最新的lua-protobuf已经使用宏支持了lua5.1,不再需要修改代码,实际上还是需要的。
要先把 lua-protobuf 的 pb.c
和 pb.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.c
中luaL_newlib
为lua5.2版本才支持的语法,而tolua是5.1,所以找到以下几个函数进行如下修改,判断Lua版本进行不同的调用:
1 | LUALIB_API int luaopen_pb_io(lua_State *L) |
如果不进行这样的修改,还有另外一种方法,在tolua的LuaClient.cs中添加以下内容:
不过我没有使用,不如直接支持在Lua中require方便。
Github讨论地址:https://github.com/topameng/tolua/issues/168
Windows
- 32位编译
进入 msys2文件夹,打开mingw32.exe。cd 进入到 tolua 代码所在的文件夹(我的在D盘):
1 | cd d: |
然后执行:
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
- 提前需要保证当前目录下存在
Plugins\Android\libs\armeabi-v7a
目录,不然没有文件输出 - 在
msys2
的32位编译环境中执行./build_arm.sh
. - 然后在
Plugins\Android\libs\armeabi-v7a
目录下看见libtolua.so
文件便编译成功
2.x86
- 提前需要保证当前目录下存在
Plugins\Android\libs\x86
目录,不然没有文件输出 - 在
msys2
的32位编译环境中执行./build_x86.sh
. - 然后在
Plugins\Android\libs\x86
目录下看见libtolua.so
文件便编译成功
3. arm64-v8a
- 提前需要保证当前目录下存在
Plugins\Android\libs\arm64-v8a
目录,不然没有文件输出 - 在
msys2
的64位编译环境中执行./build_arm64.sh
. - 然后在
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版本。
- 打开终端.切换到
tolua_runtime
目录下 - 在终端中运行
build_ios.sh
.如果遇见权限不足,用chmod +x
命令提升权限 - 然后在
Plugins\iOS
目录下看见libtolua.a
文件便编译成功
Mac
必须在Mac机器上编译
- 打开终端.切换到
tolua_runtime
目录下 - 在终端中运行
build_osx.sh
.如果遇见权限不足,用chmod +x
命令提升权限 - 然后在
Plugins
目录下看见tolua.bundle
库文件便编译成功
四、集成到Unity
修改tolua
首先要在tolua C#部分 LuaDLL.cs中添加如下代码:
1 | [ ] |
并且在LuaClient.cs的OpenLibs函数中,将上述模块导入即可:
1 | protected virtual void OpenLibs() |
读取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 | local protoBytes |
Editor下从本地读取二进制文件,移动端从AB包读取,并且要用LuaInterface.LuaByteBuffer
封装一下才是标准的二进制格式。
pb.load
是加载proto数据的方法,成功了则返回true。
序列化反序列化
之前用 protoc-gen-lua
有个很麻烦的地方,就是根据message id去初始化一个proto message数据结构:
1 | local req = ProtoGame.Cmd_Team_FetchList_Req() |
更换 lua-protobuf
之后就不需要这样了,只要写一个table即可:
1 | local req = {} |
然后传递给 LuaNetwork
时带上协议ID:
1 | LuaNetwork.SendMsg(ProtoGame.IDCmd_Team_FetchList_Req, req) |
LuaNetWork
自己会根据消息ID去找该序列化成哪个消息,省去了一个初始化的步骤。
序列化方法如下,把table序列化为Person:
1 | -- lua 表数据 |
反序列化:
1 | -- 再解码回Lua表 |
反序列化为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