protobuf-lua有个BUG:
当import其他proto的消息类型时,会报错
假如有两个proto:reward和mail。
其中mail的proto导入了reward的消息类型
reward.proto如下:
package reward;
message Reward
{
optional uint32 money = 1;
}
mail.proto如下:
import "reward.proto";
package mail;
message Mail
{
optional uint32 id = 1;
optional reward.Reward reward = 2;
}
运行的代码,如下:
--optional运行代码
-- 发送 --
local sendMail = mail_pb.Mail()
sendMail.id = 12
local reward = sendMail.reward
reward.money = 30
printf(sendMail)
-- 模拟接收 --
local recvMail = mail_pb.Mail()
recvMail:ParseFromString(sendMail:SerializeToString())
printf(recvMail)
非常简单的代码,但很不幸报错。
[string “protobuf.lua”]:363: attempt to index upvalue ‘message_type’ (a nil value)
Reward这个消息类型如果定义在Mail中是不会报错的,但是一旦通过其他proto的方式导入时,就会出现上述错误。
同样地,将字段的optional类型改成repeated类型,也会报错。代码和示例如下:
mail.proto:
import "reward.proto";
package mail;
message Mail
{
optional uint32 id = 1;
repeated reward.Reward reward = 2;
}
运行代码:
--repeated运行代码
-- 发送 --
local sendMail = mail_pb.Mail()
sendMail.id = 12
for i=1,10 do
local reward = sendMail.reward:add()
reward.money = 30+i
end
printf(sendMail)
-- 模拟接收 --
local recvMail = mail_pb.Mail()
recvMail:ParseFromString(sendMail:SerializeToString())
printf(recvMail)
报错:
[string “containers.lua”]:27: attempt to call field ‘_concrete_class’ (a nil value)
检查了一圈,发现其实proto-lua根本就没有考虑过导入其他proto情况;他默认的消息类型都是在本地。
查看他自动生成的lua文件就能看出。
通过
protoc.exe --plugin=protoc-gen-lua="XXX\protoc-gen-lua.bat" --lua_out=XX\YourOutDir -I=XXX\InputDir XX\InputDir\reward.proto XX\InputDir\mail.proto
(windows平台下的生成方式)
生成 mail_pb.lua 和 reward_pb.lua文件。
检查发现,REWARD_PB_REWARD
这个消息类型根本就不存在!怪不得会报nil错误。
想到的解决方案有两种:
1.将reward_pb里的REWARD类型挂到reward_pb下,而不是作为局部变量
2.将REWARD_PB_REWARD
改成reward_pb.Reward,并修改相应代码
github上已经有第一种方法的实现,链接如下“:
https://github.com/sean-lin/protoc-gen-lua/pull/7
此种方法虽然能够解决问题,但个人觉得太过暴力。原作者可能并不希望MAIL、REWARD等等这种大写的消息类型暴露到外面,此外,也会增加初学者的困惑:我到底是用Mail呢还是MAIL呢?
为了保留作者的初衷,我用了第二种方法实现。
需要改三个文件:
1.protoc-gen-lua (改变自动生成的规则,目的将
REWARD_PB_REWARD
改成reward_pb.Reward)
2.protobuf.lua (修改optional的发送和接受)
3.containers.lua (修改repeated的add函数)
下面分别讲解每个文件的主要的修改内容,具体的细节实现我放到了github上,链接如下:
https://github.com/sean-lin/protoc-gen-lua/pull/22
github里的几位大神好久没维护了,那几个pullrequest都没处理。所以可能还得你们自己动手改。
判断field_desc是否来自别的包,是则不变;不是则换成大写。
代码如下:
type_name = env.get_ref_name(field_desc.type_name)
if not type_name.split('.')[0] in [filename+"_pb" for filename in includes]:
type_name = type_name.upper().replace('.', '_')
protobuf-lua在实现中分了两种对象类型来区分消息和域,分别是Message和Descriptor。
像MAIL和REWARD属于Descriptor,但Mail和Reward属于Message。
如果REWARD类型换成Reward类型,可想而知他的对象类型也发生了变化,从Descriptor变成了Message。
研究源码可以发现如果某个消息的域用了其他消息类型,比如MAIL用了REWARD。那么他会去调用REWARD._concrete_class来创建消息对象,调用形式类似:REWARD._concrete_class()
那REWARD._concrete_class是个什么东东呢?
检查发现他居然是Reward!
Reward是Message类型,是可以直接调用的。因为他在元表里写了__call函数,所以可以直接调用。这也是为什么我们可以用mail_pb.Mail()创建Mail消息对象的原因。
另外Message类型是没有_concrete_class这个字段的。
所以修改方案很明确,只要将REWARD._concrete_class() 换成 Reward()即可。
找到报错的地方(有两处,分别在发送和接收的代码里):message_type._concrete_class(),换成如下:
(message_type._concrete_class and message_type._concrete_class()) or message_type()
optional需要对消息接发收都要修改,但对于repeated,只需要修改一处,即在containers.lua里的add函数里。这是因为repeated的域不直接参与到消息的接收发送过程,只要保证创建的对象正确即可。
修改方式也是类似,将message_descriptor._concrete_class(),换成:
(message_descriptor._concrete_class and message_descriptor._concrete_class()) or message_descriptor()
经过这三点修改,又跑了一遍那两个例子,完美通过。结果就不贴了。如果有错的地方,欢迎指正。