Mayan Moudgill wrote:


I was working on getting a C port of the thrift backend working, but after looking at some of the issues involved, I decided that it made more sense to ditch the Thrift IDL, and use a different approach to access the Thrift marshalling and RPC infrastructure.

I've descibed the approach in the attatched document.

I wrote a prototype that implements about 1/2 the features described in the document; I can post the source code and some examples a little later if there is interest.

I'm focusing on the client side stubs. I suspect that the stubs generated by the prototype are several factors and may even be faster by orders of magintude than the equivalent stubs generated by the Thrift IDL.

Hmmm... looks like mailing-lists don't like attatchments. OK, so I've inlined the document below (it's been exported as a MediaWiki .txt file).

= Using Thrift with C =
= Mayan Moudgill =
= Introduction =
Thrift is a RPC frame-work developed at Facebook, and then open-sourced as a project in the Apache Software Foundation Incubator. It consists of a code generation engine and libraries to allow services to be built that can work seamlessly across a variety of different languages, including C++ & Python. One of the languages that is conspicuously not supported was C.

During an attempt to port the Thrift framework to C, it became fairly clear that, while Thrift allowed services to be specified and built quickly by auto-generating the inter-process communication code and most of the other code needed for building services, the resulting code was not very efficient.

In this paper, we describe an alternative approach that leverages the existing Thrift infrastructure using C as the specification and implementation language. This allows us to develop client stubs that are extremely low overhead.

== Overview ==
Thrift is a framework primarily for developing client-server style services. It has several components, including:

* A method for describing data-structures that are to be communicated between the client & server. * A method for descriptions the (RPC) functions that are to be called from the client side and executed on the server side.

Note that these descriptions are type declarations; in the case of the RPCs, the behavior of the function is not defined, and it is up to the server-programmer to write the actual body.

Thrift also provides a method for specifying other behaviors, including the auto-generating a sever template, but for now we shall ignore those components.

Also, the framework does not necessarily need to be for traditional client-server applications; one application is to store & retrieve the “RPC messages� on disk.

== Client/Server Communication ==
The Thrift compiler takes the specification of the RPCs and the data structures, and generates code that will execute the RPC. Thus, give a specification for a RPC <tt>foo()</tt>, the Thrift framework will generate code so that a client can call a function <tt>foo()</tt> with the some arguments and have the function <tt>foo()</tt> evaluated on the server with those arguments. The generated code will generate code for stubs on the client & sever that:

* On the client side, package the function & the arguments,
* Sends it over some transport link to the server side
* On the server side, un-package the function and the arguments
* Invoke the function in the context of some server
* Package up the results of the function
* Send it over the transport link to the client side
* On the client-side, un-package the function result and return it as the result of the client-side stub.

The Thrift framework provides several layers of abstraction, including:

* The stub: generated by the compiler, it contains calls to the protocol layer to package up the primitive elements of the arguments being passed. * The protocol: the methods for packaging & un-packaging the primitives to be sent over the transport layer * The transport: the means for shipping the bytes from the client to the server and back.

This layering allows for a fairly large amount of flexibility – in particular, it is possible at run-time to pick different combinations protocols (i.e. different ways of packaging the data) and different transports (i.e. different ways of shipping the data). For instance given a Binary protocol and a Text protocol and a TCP-based transport and a UDP-base transport, it is possible to select 4 combinations – {Binary over TCP, Text over TCP, Binary over UDP, Text over UDP} – and to do this selection at run-time.

== Monolithic Implementation ==
Having multiple layers of abstraction adds flexibility; however, as usual, flexibility comes at the price of performance. If we picked a particular protocol & transport, then the Thrift compiler could generate code for the stubs that would package the arguments and invoke the transport layer without going through the intervening abstraction layers.

In the specific case of using the Thrift TBinaryProtocol over TSocket transport, the client stub could, for instance, in certain circumstance be generated to use a fixed sized buffer with most of the values already filled in. The client stub would then insert arguments where necessary, and issue a single write call to send it over a socket.

For an extreme example, consider the case of the <tt>void ping(void)</tt> RPC defined in tutorial.thrift. An optimized implementation<ref name="ftn1">This is just an example; the actual implementation would have additional error checking code, as well as support for retry on EINTR etc.</ref> of the client side stub in C would be:

 void
 ping( int socket_fd )
 {
   <nowiki>static unsigned char send_buf[] = { </nowiki>
     0x80, 0x01, 0x00, 0x01,
     0x00, 0x00, 0x00, 0x04, 'p', 'i', 'n', 'g',
     0x00, 0x00, 0x00, 0x00, 0x00
   };
   <nowiki>unsigned char recv_buf[17];</nowiki>

   write(socket_fd, send_buf, sizeof(send_buf));
   read(socket_fd, recv_buf, sizeof(recv_buf));
 }

As might be expected, this has considerably lower overhead than the C++ implementation currently being generated by the cpp option of the Thrift compiler.

== C-compatible specification ==
The Thrift compiler starts off with a description of the data and RPCs written in a Thrift-specific IDL syntax, and converts them into data-structure & stub headers and files that can be used by the called by the rest of the client program. However, there is no reason why it has to be that particular syntax. In fact, one could just as easily use C syntax, with Thrift specific syntax buried in comments.

Hypothetically, one could rewrite the data-structure defined in tutorial.thrift using the Thrift IDL syntax:

 struct Work {
   1: i32 num1,
   2: i32 num2,
   3: Operation op,
   4: optional string comment,
 }

using a C compatible description syntax:

 struct Work {
   int num1; /* @thrift: 1 */
   int num2; /* @thrift: 2 */
   enum Operation op; /* @thrift: 3 */
   char * comment; /* @thrift: 4 optional */
 };

= C-Based Definition =
In this section, we shall describe a proposed set of annotations to C structure and function declarations that will allow a stub-compiler to produce stubs similar to those produced by the existing Thrift infrastructure. The initial focus shall be on producing efficient code for the TbinaryProtocol over TSocket transport, but at first glance, it appears likely that the same set of annotations will allow a stub-compiler to generate code for other protocol/transport combinations.

== Preliminaries ==
The Thrift annotations shall be embedded inside comments in the C code. The thrift annotations will use the syntax:

 /* @thrift: … */

The section of code containing declarations for the stub-compiler to process shall be demarcated with <tt>begin</tt> and <tt>end</tt> annotations, shown below. The stub-compiler shall skip all other parts of the input. There shall be no variable definitions or declarations between the begin and end.

 /* stub-compiler ignores this code */
 /* '''@thrift: begin''' */
   /* code for the stub-compiler to process */
   …
 /* '''@thrift: end''' */

Annotations can be global, or associated with structures, fields, function or arguments. The annoatations shall appear after the closing '<nowiki>;</nowiki>' or '<tt>,</tt>' for that syntax element<ref name="ftn2">For the last argument in a function, it shall appear between the name of the argument and the closing '<tt>)</tt>'.</ref>. Thus,

 /* @thrift: begin */
   /* @thrift: default-field-init 1 */ /* global annotation */
   struct Work {
      …
      char * comment; /* @thrift: optional */ /*field annotation */
   };

   struct InvalidOperation {
      …
   }; /* @thrift: exception */ /* structure annotation */

   int add(
         int socket_fd,
         int num1, /* @thrift: 1 */ /* argument annotations */
         int num2  /* @thrift: 2 */
         );
   void zip(int sfd); /* @thrift: oneway */
 /* @thrift: end */

Multiple annotations can be grouped in the same comment.

The first argument to a function will be the socket file descriptor. It shall be used only for reading & writing, and obviously is not counted as an argument field.

== Basic types ==
There is a straight-forward equivalence between C types, the base types defined by the Thrift IDL and the Ttype defined in TProtocol . They are summarized by the table below:


{| class="prettytable"
! <center>C</center>
! <center>IDL</center>
! <center>TType</center>

|-
| bool<ref name="ftn3"><tt>bool</tt> is introduced as a type in C99. If the stub-compiler is supporting C99, then it should probably also recognize all the stdint.h types as well.</ref>
| bool
| T_BOOL<ref name="ftn4">In version 0.2.0, the python libraries generate T_I08 instead of T_BOOL.</ref>

|-
| signed char, char, unsigned char
| byte
| T_I08

|-
| signed short, short, unsigned short
| i16
| T_I16

|-
| signed int, int, unsigned int signed long, signed long, unsigned long
| i32
| T_I32

|-
| signed long long, long long, unsigned long long
| I64
| T_I64

|-
| float, double
| double
| T_DOUBLE

|}
Annotations bool,byte, i16, i32, i64 allow this equivalence to be overridden. Thus, to declare a function as returning a boolean, use the following annotation:

 int foo(...); /* @thrift: bool */

== String ==
C implements strings as NULL-terminated arrays of <tt>char</tt>s. Unless overridden, <tt>char *</tt> fields, arguments, and function returns will be treated as equivalent to the Thrift <tt>string</tt> type. They shall be transmitted using the corresponding TBinaryProtocol; i.e. using <tt>T_STRING</tt> followed by the 4-byte length of the string followed by the characters not including the terminating NULL.

The stub for a RPC function returning a string shall allocate space for the string plus the terminating NULL, copy the received bytes into the allocated memory and add the terminating NULL.

== Structures ==
There is, again, a fairly obvious equivalence between C structs and Thrift structs. We shall not allow anonymous C structs in the code the stub-compiler will process.

The C-struct fields can be annotated with a field identifier, using syntax <tt>/* @thrift: 1 */</tt>. However, the stub-compiler shall automatically number the fields in a structure, starting at 1, and incrementing by 1 for each field. These values can be overridden by the global annotations <tt>default-field-init </tt>and <tt>default-field-incr</tt>. If a field identifier is specified, then the current field is set to that value, and the next field will be the result of incrementing the specified identifier.

Fields may be annotated as optional, with equivalent behavior to the Thrift optional. If the type of the optional field is a pointer, then a NULL pointer indicates that the field is not to be transmitted. The isset interface may also be used to indicate whether the field is to be transmitted or not.

Fields may be annotated as skip, which means that the field is not to be transmitted or received.

There are three isset interfaces defined.

* Bit-vector: A field in the struct is annotated as the isset field. It will be used as a bit vector to indexed by the field identifiers, where 1 means field is present and 0 means not present. Bit 0 of the bit vector is the smallest bit field. There must be one bit for every value between the smallest and largest field identifier, even if some identifiers are not used. The field can either be a scalar or an array. The isset field will not be transmitted. * Compressed bit-vector: Similar to bit-vector, except that there only needs to be a bit for each field, and unused identifiers do not use up bits. * Functional: the user provides functions (or macros) to clear all the issets, set individual fields, and query individual fields. These will have the formats:

 <nowiki>void clear_<struct>(<strcut> *);</nowiki>
 <nowiki>void set_<field>_<struct>(<struct> *);</nowiki>
 int  <nowiki>isset_<field>_<struct>(<struct> *);</nowiki>

The bit-vector interfaces are specified by annotating the isset field as isset or isset-compressed. The functional interface is specified by annotating the structure as isset-functional.

If one of the bit-vector based approaches is specified, then the stub-compiler can generate the functional macros as a convenience for the client program.

== Pointers ==
In C structures are generally passed by address, not by value. Further, in Thrift, we pass around values, not references to objects. Therefore, when serializing values, all pointers will be dereferenced, and the objects that they point to will be serialized in turn. Certain pointers, will be treated specially:

* string pointers (i.e. char * pointers that are not otherwise annotated) will be transmitted as strings, as described above
* NULL pointers may be treated as optional pointers and not transmitted.
* Array pointers (i.e. pointers to a sequence of objects) will be treated as lists and transmitted as described below

A pointer that is part of the type being returned by a function implies that the stub is responsible for allocating memory, setting the pointer to point to this allocated memory, and using the allocated memory as part of the deserialization process.

== Other types ==
It is possible to describe types as being equivalent to basic types, either by having the stub-compiler parse C typedefs, or by using annotations. Thus, one could use either of:

 typedef int MyInteger;
 /* @thrift: typedef i32 MyInteger */

Another use is to introduce a type as opaque. In this case, the stub-compiler will accept it as a type. This is generally useful only for specifying types for skipped fields and arguments. For example,

 /* @thrift: typedef opaque FILE */

== List as array ==
The list collection type at the TBinaryProtocol consists of a T_LIST<ref name="ftn5">And the argument/field identifier, of course</ref> and another TType byte depending on the type of list. This is followed by the number of elements in the collection and the serialized representations of the elements of the collection.

There are several different ways of converting between a list and its C representation. For instance, one could represent a list of 4 integers as

 <nowiki>int x[4];</nowiki>

Because of the interchangeability between pointers and arrays in C, it might be represented as:

 int * x;
 int x_count;

where<tt> x_count</tt> is the number of elements in the array pointed to by <tt>x</tt>.

Note that an array declared with some particular size may not have all of it in use, and so one may not want to send the entire array. Consider:

 <nowiki>int stack[MAX_STACK];</nowiki>
 int stack_count;

In this example, we may wish to transmit only a list of the first <tt>stack_count</tt> elements of the array <tt>stack</tt>.

Note that a pointer in C does not necessarily need to point to a collection of elements; it could be pointing to a singleton element. Our annotations need to be able to distinguish between those two cases.

Currently, a pointer or array appearing as a field in a structure or an argument to a function can be annotated by the dimension annotation to indicate that it is to be treated as a list. The dimension annotation consists of the <tt>dim</tt> keyword followed by a token indicating the field/argument containing the size. Thus,

 struct stack {
   <nowiki>int stack[MAX_STACK]; /* @thrift: dim stack_count */</nowiki>
   int stack_count; /* @thrift: skip */
 };

 int sum(
    int socket_fd,
    int nvals, /* @thrift: skip */
    int * vals /* @thrift: dim nvals */
 );

Note the use of the skip annotation for the variable providing the size of the array. In general, we do not want to also transmit the size of the array.

When returning a list, the value returned may be a structure with a pointer/array and a dimension variable. In that case the dimension variable will get set to the list count determined during the deserialization process. If the function is set up to return multiple values (described below), then the list count may be assigned to one of the multiple values.

== List as lists ==
An alternative implementation of the list type in C would be as a true list; i.e. as a data-structure with a next pointer. The value of the each list cell could be inlined or we could use a pointer to point to the llist element. Consider:

 struct Foo_cell {
   struct Foo_cell * foo_in_next; /* @thrift: list-next */
   struct Foo value;
 };
 struct Foo_cell {
   struct Foo_cell * next; /* @thrift: list-next */
   struct Foo * value;
 }

In both of these examples, a (pointer to a) Foo_cell will be serialized as lists of Foo.

If the structure contains more than one non-skipped field, then it is assumed that (unless otherwise annotated) there is no separate data-structure singleton data structure type, and that the type of the structure is synonymous with the type of the list of the structure. Consider:

 struct hash_string {
   struct hash_string * next; /* @thrift: list-next */
   char * str;
   int hash;
 };

This is equivalent to the (illegal) Thrift IDL declaration:

 struct hash_string' {
   1: string str,
   2: i32 hash
 }
 <nowiki>typedef list<hash_string'> hash_string;</nowiki>

By default, the stub-compiler will treat instances of hash_string as though they were declared as lists; appropriate annotations will cause them to be serialized as though they were singletons.

 void send_1(
         int socket_fd,
         struct hash_string * val1 /* @thrift: single */
         );
 void send_n(
         int socket_fd,
         struct hash_string * valn
         );

In the first example, the annotation overrides the default list interpretation to transmit only one element, while in the second example, the transmit stub generated will walk the list using the next pointer.

== Multiple returns ==
C does not permit more than one value to be returned from a function. This causes problems when multiple values are expected to be returned. One case that we have identified above is the ability to return the number of elements returned and the elements themselves as two separate entities. The traditional method of doing this in C is to pass multiple addresses of variables that will then be filled in with the multiple returned values. We shall use the return annotation to indicate such variables. Thus,

 void return_array_and_count(
         …
         int * x, /* @thrift: return dim x_count*/
         int * x_count /* @thrift: return */
         );

In this declaration, x and x_count are treated as two return parameters.

Note that specifying a dimension that is not a return value specifies:

* the array is already allocated
* at most count values can be received; if more are returned, then it is an error.

It is possible to specify both a return and a non-return dim variable for a return pointer variable. In that case the return dim is set to the actual count of values returned.

We can use the multiple return mechanism to deal with exception returns as well. In this case, we assume that functions return an integer to distinguish between normal (T_REPLY) returns and exceptional (T_EXCEPTION) returns. The returned value will be 0 for normal returns, and the specified field for exceptional returns.

Functions that return exceptions are defined as returning a value and having at least one exception return parameter. From the example in tutorial.thrift,

 int calculate(
         int socket_fd,
         int logid,
         struct Work * w,
         int * result /* @thrift: return */
       struct InvalidOperation * ouch /* @thrift: exception 1 */
       );

If a normal return (T_REPLY) is received, then the function returns a 0, if the value returned is the exception (T_EXCEPTION) then it returns a 1.

== Internal Errors ==
We can extend this notion to deal with internal errors as well, returning an error number for various forms of internal errors as well. The internal errors can be:

* out of memory (i.e. malloc returns 0)
* read errors (non-0 return)
* write errors (non-byte count return)

Other error codes can include:

* All fields of return structure not set
* Unspecified structure field returned
* Unspecified exception returned
* Return type mismatch
* Too large a count on for a pre-allocated memory

Each of these situations can be set to:

* cause an assert failure
* return a particular error code
* be ignored

The special case of EINTR error on a read should be handled by retrying; however, it may also be specified to be an error.

The relevant annotations are:

* <nowiki>error write <number></nowiki>
* <nowiki>error read <number></nowiki>
* <nowiki>error malloc <number></nowiki>
* <nowiki>error decode <number></nowiki>

We can additionally define a return value to be the errno using the annotation return-errno. This shall capture the errno on an internal failure, and be set to a mask of error codes based on the kinds of decode errors.

== Other issues ==
There is no reason that the name of the C function has to be the same as the name of the function on the server. In particular, there may be multiple client side functions which are invoked with different kinds of parameters that end up calling the same server side function. The default name is, of course, the name of the C function. However, it is possible to override the name of the function called by using the calls function annotation:

 int ping_with_error_code(int sfd); /* @thrift: calls ping */

== Sockets & other transports ==
The current interface was designed to support blocking sockets. It uses write, writev, read and readv on the sockets to perform the actual transport.

Porting it to other transports may be fairly straight-forward. There has been one parameter that we have passed to every function – the socket file descriptor. This mayu be replaced by an alternate type. Alternate read & write functions will need to be provided. Either readv & writev equivalents will be provided, or they must be replaced with sequences of reads and writes. The error return codes will have to be modified to handle the kinds of errors specific to the new transport.




----
<references/>


Reply via email to