Plasma GitLab Archive
Projects Blog Knowledge

Rpc_mapping_ref


RPC Language Mapping Reference

The OncRPC (alias SunRPC) standard consists of two parts, namely the external data representation (XDR) and the RPC protocol. They are defined in RFC 1831 and RFC 1832.

In this document we describe how the various parts of XDR and RPC are mapped to the Objective Caml language.

Representation Levels

The transformation of binary XDR messages to O'Caml values is done in several steps, corresponding to several ways of representing the values:

  • Binary level: The message is represented as byte string. In O'Caml, these byte strings are always string values.
  • Term level: The XDR type is given as Netxdr.xdr_type_term. The message is represented as structured Netxdr.xdr_value term. For example, an XDR struct with two components a and b with integer values 1 and 2 is represented as
    XV_struct [ "a", XV_int r1; "b" XV_int r2 ]
    where r1 = Netnumber.int4_of_int 1 and r2 = Netnumber.int4_of_int 2. There are sometimes several ways of representing a value on term level.
  • Fully-mapped level: The message is represented as a generated O'Caml type that closely corresponds to the XDR type. The struct example would use the type
    type name = { a : int; b : int }
    . Some details can be selected by the user, e.g. how integers are represented. The types are generated using ocamlrpcgen.
The tool ocamlrpcgen can be invoked on an input file name.x with different switches to create three modules: the type mapper Name_aux, the RPC client Name_clnt and the RPC server Name_srv. The type mapper module mainly contains the necessary definitions to convert values between the representation levels.

In particular, the type mapper module contains for every XDR type t several definitions:

  • The definition of the O'Caml type corresponding to t on the fully-mapped level.
  • The dynamic representation of the XDR type as Netxdr.xdr_type_term. This definition is named xdrt_t. The type term is required to convert a binary message to a value on term level. The conversion functions to do so are available in the Netxdr module.
  • The conversion function _of_t that turns a fully-mapped value into a term value represented as Netxdr.xdr_value.
  • The conversion function _to_t that turns a term value to a fully-mapped value.
In order to develop an RPC client or server it is usually not necessary to use these definitions. They are useful, however, to encode or decode binary XDR messages directly (e.g. outside an RPC context).

XDR: Simple Types

The following table shows:

  • how a variable x would be declared in the XDR file
  • what the corresponding term type is
  • how the type is mapped to a full OCaml type
XDR declaration for x Term-level mapping Full mapping Comment
void Netxdr.X_void unit (in some contexts omitted) -
int x Netxdr.X_int Netnumber.int4, int32, int64, or int [1] 32 bit signed integer
_abstract int x Netxdr.X_int Netnumber.int4 [6] 32 bit signed integer
_int32 int x Netxdr.X_int int32 32 bit signed integer
_int64 int x Netxdr.X_int int64 32 bit signed integer
_unboxed int x Netxdr.X_int int [2] 32 bit signed integer
unsigned int x Netxdr.X_uint Netnumber.uint4, int32, int64, or int [1] 32 bit unsigned integer
unsigned _abstract int x Netxdr.X_uint Netnumber.uint4 [6] 32 bit unsigned integer
unsigned _int32 int x Netxdr.X_uint int32 [4] 32 bit unsigned integer
unsigned _int64 int x Netxdr.X_uint int64 32 bit unsigned integer
unsigned _unboxed int x Netxdr.X_uint int [2] 32 bit unsigned integer
hyper x Netxdr.X_hyper Netnumber.int8, int64, or int [1] 64 bit signed integer
_abstract hyper x Netxdr.X_hyper Netnumber.int8 [6] 64 bit signed integer
_int64 hyper x Netxdr.X_hyper int64 64 bit signed integer
_unboxed hyper x Netxdr.X_hyper int [3] 64 bit signed integer
unsigned hyper x Netxdr.X_uhyper Netnumber.int8, int64, or int [1] 64 bit unsigned integer
unsigned _abstract hyper x Netxdr.X_uhyper Netnumber.uint8 [6] 64 bit unsigned integer
unsigned _int64 hyper x Netxdr.X_uhyper int64 [4] 64 bit unsigned integer
unsigned _unboxed hyper x Netxdr.X_uhyper int [3] 64 bit unsigned integer
bool x Netxdr.x_bool bool Boolean type
float x Netxdr.X_float float [5] 32 bit IEEE float
double x Netxdr.X_double float [5] 64 bit IEEE float
opaque x[n] Netxdr.X_opaque_fixed string Opaque data with exactly n bytes. The length n is dynamically checked
opaque x<n> Netxdr.X_opaque string Opaque data with up to n bytes. The length n is dynamically checked
string x<n> Netxdr.X_string string String consisting of up to n bytes. The length n is dynamically checked
_managed string x<n> Netxdr.X_mstring Netxdr_mstring.mstring Alternative runtime representation. See Netxdr_mstring.

Footnotes:

  1. The default is the first alternative. You can select with command-line switches of ocamlrpcgen one of the other options for the whole XDR file
  2. On 32 bit platforms only a subset of XDR values can be represented with this OCaml type. If a conversion fails, Netnumber.Cannot_represent is raised.
  3. On both 32 and 64 bit platforms only a subset of XDR values can be represented with this OCaml type. If a conversion fails, Netnumber.Cannot_represent is raised.
  4. This is a logical mapping: large XDR numbers where the MSB is set are mapped to negative OCaml numbers.
  5. The OCaml float type is a 64 bit IEEE floating point number.
  6. The module Netnumber is an extended version of the older (and now removed) Rtypes definition, and contains wrapper types for all XDR number types.

XDR: Options

The "pointer type" *t is considered as an option type in XDR corresponding to option in O'Caml, i.e. a variant with the two cases that an argument is missing or present. Option types are written

t *varname
in .x files.

On term level, the missing argument value is represented as Netxdr.xv_none. The present argument value is represented as Netxdr.xv_some v when v is the mapped argument value. Actually, xv_none and xv_some construct XDR terms that are unions over the boolean enumeration as discriminator.

On the fully-mapped level, the option type is mapped to

t' option
O'Caml type when t' is the mapped argument type.

XDR: Arrays

In XDR arrays are formed over an element type. Furthermore, there may be the size constraint that exactly or at most n elements are contained in the array. If the size constraint is missing, the array may have arbitrary many elements. However, due to the binary representation, the number is actually limited to 2 ^ 32 - 1.

XDR declaration for x Term-level mapping Full mapping Comment
t x[n] Netxdr.X_array t' array Arrays with exactly n elements
t x<n> Netxdr.X_array t' array Arrays with up to n elements
t x<> Netxdr.X_array t' array Arrays with any number of elements

The size constraints are dynamically checked in all cases when RPC message are analyzed or created.

XDR: Structs

Structs are products with named components, like record types in O'Caml. The components have, in addition to their name, a fixed order, because the order of the components determines the order in the binary message format. That means that the components can be accessed by two methods: by name and by index.

Struct are written as

 struct {
     t0 varname0;
     t1 varname1;
     ...
  }
in .x files.

For example, struct { int a; hyper b } means a struct with two components. At position 0 we find "a", and at position 1 we find "b". Of course, this type is different from struct { hyper b; int a } because the order of the components is essential.

On term level, there are two ways of representing structs Netxdr.X_struct: one identifies components by name, one by position. The latter is also called the "fast" representation (and the one used by ocamlrpcgen).

In the "by name" case, the struct value is represented as Netxdr.XV_struct components where components is an association list [(c0_name, c0_val); (c1_name, c1_val); ...] where cK_name are the names of the components and cK_val their actual values as terms. The order of the components can be arbitrary.

In the "by position" case, the struct value is represented as Netxdr.XV_struct_fast components where components is an array of terms such that components.(k) is the term value of the k-th component.

On the fully-mapped level, the struct is mapped to an O'Caml record. The order of the components remains the same, but the names of the components may be modified. First, the names are modified such that they are valid component names in O'Caml by ensuring that the first letter is lowercase. Second, the names may be changed because several structs use the same component names which is not possible in O'Caml. Thus, the generated O'Caml record type look like

   {
     mutable varname0' : t0';
     mutable varname1' : t1';
     ...
   }

where varnameK' is the component name after the mentioned renaming and tK' is the mapped component type, both for position K.

Equality constraints

Since Ocamlnet-3.6.7 the keyword _equals is understood and generates an equality constraint, e.g. struct _equals "M.t" { int a; hyper b }, meaning that this OCaml record type is the same as the one defined as M.t.

Controlling the name mapping

Ocamlnet-3.6.7 also introduces the name mapping directives for struct fields:

  • _lowercase: the XDR name is lowercased
  • _uppercase: the XDR name is uppercased
  • _capitalize: the XDR name is capitalized
  • _prefix "p": this prefix is prepended to the XDR name
For example, struct _lowercase _prefix "my_" { int A; hyper B } would generate a record type with fields my_a and my_b.

Tuples

Since Ocamlnet-3.6.7 it is possible to select an alternate mapping to OCaml tuples by specifying the _tuple keyword (e.g. struct _tuple { int a; hyper b } ).

XDR: Enumerations

In XDR it is possible to define enumerations which are considered as subtypes of int. These consist of a list of integers with associated symbolic names. In the .x file this is written as

   enum {
     Name0 = Int0,
     Name1 = Int1,
     ...
   }

where NameK are identifiers and IntK are literal numbers.

In this section we only consider the case that the enumerations are not used as discriminator for a union. (See below for the other case.)

On term level, there are again two representations. One uses the names to identify one of the enumerated values, and the other uses a positional method.

In the "by name" case, the value named NameK is represented as Netxdr.XV_enum "NameK", i.e. the name is the argument of XV_enum.

In the "by position" case, the value named NameK is represented as Netxdr.XV_enum_fast K, i.e. the position in the enum declaration is the argument of XV_enum.

On the fully-mapped level, the enumerated value named NameK is represented as O'Caml value of type Netnumber.int4 whose value is IntK, i.e. the number associated with the name. In the type mapper file generated by ocamlrpcgen there are additional definitions for every enum. In particular, there is a constant whose name is NameK (after makeing the name OCamlish) and whose value is IntK.

Controlling the name mapping

Ocamlnet-3.6.7 also introduces the name mapping directives for enum constants:

  • _lowercase: the XDR name is lowercased
  • _uppercase: the XDR name is uppercased
  • _capitalize: the XDR name is capitalized
  • _prefix "p": this prefix is prepended to the XDR name
For example, enum _lowercase _prefix "my_" { A=0, B=1 } would generate constants my_a and my_b (with the values 0 and 1, resp.).

XDR: Unions discriminated by integers

In XDR a union must always have disriminator. This can be an int, an unsigned int, or an enumeration. The latter case is described in the next section. In the integer case, the union declaration enumerates a number of arms and a default arm:

 union switch (d varname) {
     case Int0:
       t0 varname0;
     case Int1:
       t1 varname1;
     ...
     default:
       tD varnameD;
   }

Here, d is either int or unsigned int.

On term level, this is represented as Netxdr.XV_union_over_int(n,v) for the int case or Netxdr.XV_union_over_uint(n,v) for the unsigned int case. The number n is the selected arm of the union (it is not indicated whether the arm is one of the declared arms or the default arm). The value v is the mapped value of the arm.

On the fully-mapped level, the union is mapped to a polymorphic variant that corresponds to the original union declaration:

 
   [ `_Int0 of t0'
   | `_Int1 of t1'
   ...
   | `default of tD'
   ]

The labels of the variants are derived from the decimal literals of the numbers IntK associated with the arms. For example, the union

 union switch (int d) { 
     case -1: 
       hyper a;
     case 0:
       bool b;
     default:
       string s<>;
   }

is mapped to

 [ `__1 of int64 | `_0 of bool | `default of Netnumber.int4 * string ] 

Note that the default case consists of the value of the discriminant on the left and the value of the union on the right.

If an arm is simply void, the corresponding variant will not have an argument.

XDR: Unions discriminated by enumerations

If the discriminator is an enumeration, different O'Caml types are used, as a much nicer mapping is possible.

As for integer-discriminated unions, the arms are enumerated. The default arm, however, is now optional. The whole construct looks like:

 enum e {
     Name0 = Int0,
     Name1 = Int1,
     ...
   }

   union switch (e varname) {
     case Name0:
       t0 varname0;
     case Name1:
       t1 varname1;
     ...
     default:          /* optional! */
       tD varnameD;
   }

On the term level, there are again two different ways of representing a union value, namely by referring to the arm symbolically or by position.

In the first case, the value is represented as Netxdr.XV_union_over_enum(n,v) where n is the string name of the value of the discriminator (i.e. "NameK"), and v is the mapped value of the selected arm.

In the second case, the value is represented as Netxdr.XV_union_over_enum_fast(K,v) where K is the position of the value of the discriminator in the enumeration, and v is the mapped value of the selected arm.

On the fully-mapped level, the union is again mapped to a polymorphic variant:

  [ `Name0 of t0'
  | `Name1 of t1'
  | ...
  ]

Every label of an enumerated value is turned into the label of the variant. The argument is the mapped value of the corresponding arm. Note that default values do not occur in this representation as such.

For example, the union

 enum e {
     CASEA = 5,
     CASEB = 42,
     CASEC = 7,
     CASED = 81
   }

  union switch (e d) {
    case CASEB:
      int b;
    case CASEC:
      void;
    default:
      hyper ad;
  }

is mapped to the O'Caml type (the tags are all lowercase by default):

 
   [ 'casea of int64     (* expanded default case *)
   | `caseb of int32
   | `casec
   | `cased of int64     (* expanded default case *)
   ]

If an arm is simply void like for CASEC, the corresponding variant will not have an argument like `casec.

Controlling the name mapping

Ocamlnet-3.6.7 also introduces the name mapping directives for union tags:

  • _lowercase: the XDR name is lowercased
  • _uppercase: the XDR name is uppercased
  • _capitalize: the XDR name is capitalized
  • _prefix "p": this prefix is prepended to the XDR name
An example: The XDR type

  union _capitalize switch (e d) {
    case CASEB:
      int b;
    case CASEC:
      void;
    default:
      hyper ad;
  }

is mapped to

 
   [ 'Casea of int64
   | `Caseb of int32
   | `Casec
   | `Cased of int64
   ]

If there are name mapping directives in the definition of the enumeration e, these directives will be ignored. Only the directives in the union definition are used for generating the OCaml tag names.

RPC: Programs

In an .x file one can declare programs. A program consists of a number of program versions, and every version consists of a number of procedures. Every procedure takes a (possibly empty) list of arguments and yields exactly one result (which may be void, however). This is written as:

   /* type definitions come first */
   ...

   /* Now the programs: */
   program P1 {
       version V1 {
           r1 name1(arg11, arg12, ...) = L1;
           r2 name2(arg21, arg22, ...) = L2;
           ...
       } = M1;

       version V2 {
          ...
       } = M2;

       ...

   } = N1;

   program P2 {
     ...
   } = N2;

   ...

Here, P1, P2, ..., V1, V2, ...,name1, name2, ... are identifiers. r1, r2, arg11, ... are type expressions. N1, N2, ..., M1, M2, ..., L1, L2, ... are unsigned numbers.

Programs are dynamically represented using the Rpc_program module. Every Rpc_program.t value contains the full signature of exactly one version of one program.

In the generated type mapper module, the definitions for the programs are available as constants program_P'V where P is the name of the program and V is the version of the program.

RPC: Clients

To write

  • Point to Rpc_client as basis
  • Clients can be used on term level by directly calling functions of Rpc_client
  • ocamlrpcgen generates an enhanced client module containing procedure stubs. These stubs are on the fully-mapped level.

RPC: Servers

To write

  • Point to Rpc_server as basis
  • Servers can be created on term level by using functions of Rpc_server
  • ocamlrpcgen generates an enhanced server module containing a converter to/from the fully-mapped level.

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml