Apache Thrift:当在列表前使用“optional”时,C++ 服务器似乎没有正确 return

Apache Thrift: When use "optional' before a list, C++ server seems not to return it correctly

大家好!大家好,我有一个类似的问题:

regarding thrift function return list

当列表具有 "optional" 修饰符时,thrift C++ 服务器将始终 return 它作为 null/undef。

此外,如果一个结构包含在可选列表中,则该结构的任何字段都不能设置为 "optional",或者,null/undef 值将是 return编辑

删除所有"optional"修饰符后,一切OK。

有人能告诉我为什么我不能在列表前使用 "optional" 吗?


这是节俭文件:

namespace cpp thrifttest
namespace perl thrifttest

struct Pair {
    1: optional string a, 
    2: optional string b
}

struct Result {
    1: optional list<string> strList,
    2: optional list<Pair> pairList
}

service Test {

    Result listTest()

}

这是 C++ 代码(服务器):

#include "gen-cpp/Test.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

#include <iostream>

using namespace std;

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

using namespace  ::thrifttest;

class TestHandler : virtual public TestIf {
    public:
        TestHandler() {
            // Your initialization goes here
        }

        void listTest(Result& _return) {

            _return.strList.push_back("Test");
            _return.strList.push_back("one level list");
            cout << "strList size: " << _return.strList.size() << endl;

            Pair pair;
            pair.a = "Test";
            pair.b = "two level list";
            _return.pairList.push_back(pair);
            cout << "pairList size: " << _return.pairList.size() << endl;

            printf("call listTest\n");
        }

};

int main(int argc, char **argv) {
    int port = 9595;
    shared_ptr<TestHandler> handler(new TestHandler());
    shared_ptr<TProcessor> processor(new TestProcessor(handler));
    shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
    shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
    shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

    TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
    server.serve();
    return 0;
}

这是 perl 代码(客户端):

#!/usr/bin/perl

use v5.12;
use warnings;
use autodie;

use utf8;
use Data::Dumper;

use lib 'gen-perl';

use thrifttest::Test;
use thrifttest::Constants;
use thrifttest::Types;

use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use Thrift::BufferedTransport;


my $socket    = new Thrift::Socket('localhost', 9595);
my $transport = new Thrift::BufferedTransport($socket, 1024, 1024);
my $protocol  = new Thrift::BinaryProtocol($transport);
my $client    = new thrifttest::TestClient($protocol);


eval {
    $transport->open();
    my $result = $client->listTest;
    say Dumper($result);
    $transport->close();
};
say $@ if $@;

C++ 服务器输出:

strList size: 2
pairList size: 1
call listTest

perl 客户端输出:

$VAR1 = bless( {
                 'pairList' => undef,
                 'strList' => undef
               }, 'thrifttest::Result' );

PS: 我的开发环境是CentOS 7, GCC 4.8.3, Perl 5.16, thrift 0.9.3

我错了,这不是真正的错误,它只是一些不幸的设计,并不是真正的万无一失,允许用户制作 Dumb Things™。问题在于 optional 运算符的语义以及它是如何为 C++ 实现的。

假设我们有这段 IDL:

struct Xtruct2
{
  1: i8     byte_thing,  
  2: Xtruct struct_thing,
  3: i32    i32_thing
}

生成的代码,特别是 write() 方法,如下所示:

uint32_t Xtruct2::write(::apache::thrift::protocol::TProtocol* oprot) const {
  //...
  xfer += oprot->writeFieldBegin("struct_thing", ::apache::thrift::protocol::T_STRUCT, 2);
  xfer += this->struct_thing.write(oprot);
  xfer += oprot->writeFieldEnd();
  //...
}

如果我们现在修改 IDL 并添加 optional 说明符:

struct Xtruct2
{
  1: i8     byte_thing,  
  2: optional Xtruct struct_thing,
  3: i32    i32_thing
}

生成的代码看起来略有不同:

uint32_t Xtruct2::write(::apache::thrift::protocol::TProtocol* oprot) const {
  //...
  if (this->__isset.struct_thing) {
    xfer += oprot->writeFieldBegin("struct_thing", ::apache::thrift::protocol::T_STRUCT, 2);
    xfer += this->struct_thing.write(oprot);
    xfer += oprot->writeFieldEnd();
  }
  //...
}

Thrift 有三种要求:requiredoptional 和默认。如果既没有指定 required 也没有指定 optional 则隐式假定后者(这就是默认要求没有特殊关键字的原因)。读取和写入这些字段的语义如下:

    requiredness        write field?        read field?         
    ----------------------------------------------------------------------
    required            always              always, must be present
    (default)           always              if present, may be missing
    optional            only if set         if present, may be missing

那么 optional 与默认值相比变化的是 write 方法的行为。虽然始终写入默认字段,但 optional 字段仅 有条件地 写入。这是通过 __isset 标志来检查的,它由一个位集组成,每个字段不是 required 的一位。如果设置了__isset中的相应位标志,则可以使用该字段值。如果位标志不存在,则字段值尚未初始化,因此不应使用。

到目前为止,这还不是什么大问题。但是有一个你设法击中的陷阱:默认和 optional 字段可以在 C++ 中访问和使用,即使未设置位标志 因为它们就在那里。在默认要求的情况下,这没什么大不了的:您分配您的值,并且由于该字段总是被写入,所以基本上没有什么不好的事情发生。反序列化字段时设置字段的位标志(参见生成的代码)。

但是,当您选择加入 optional 时,情况发生了变化:现在突然 负责正确设置标志,要么通过访问 __isset 直接或通过生成的 setter 方法,在我们的例子中是这个:

void Xtruct2::__set_struct_thing(const Xtruct& val) {
  this->struct_thing = val;
__isset.struct_thing = true;
}

我的假设是不应该访问未设置的可选字段,但事实证明这似乎是设计使然。我仍然认为这里的设计有点容易出错。