Plasma GitLab Archive
Projects Blog Knowledge

{1 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.

{2 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 {!Xdr.xdr_type_term}.
  The message is represented as structured {!Xdr.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 {i t}
several definitions:

- The definition of the O'Caml type corresponding to {i t} on the
  fully-mapped level.
- The dynamic representation of the XDR type as {!Xdr.xdr_type_term}.
  This definition is named [xdrt_]{i 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 {!Xdr} module.
- The conversion function [_of_]{i t} that turns a fully-mapped
  value into a term value represented as {!Xdr.xdr_value}.
- The conversion function [_to_]{i 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).


{2 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

{table t1}
{tr t1head}
{td t1}XDR declaration for [x]{tdend t1}
{td t1}Term-level mapping{tdend t1}
{td t1}Full mapping{tdend t1}
{td t1}Comment{tdend t1}
{trend t1}
{tr t1}
{td t1}[void]{tdend t1}
{td t1}{!Xdr.X_void}{tdend t1}
{td t1}[unit] (in some contexts omitted){tdend t1}
{td t1}-{tdend t1}
{trend t1}
{tr t1}
{td t1}[int x]{tdend t1}
{td t1}{!Xdr.X_int}{tdend t1}
{td t1}{!Netnumber.int4}, [int32], [int64], or [int] \[1\]{tdend t1}
{td t1}32 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_abstract int x]{tdend t1}
{td t1}{!Xdr.X_int}{tdend t1}
{td t1}{!Netnumber.int4} \[6\]{tdend t1}
{td t1}32 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_int32 int x]{tdend t1}
{td t1}{!Xdr.X_int}{tdend t1}
{td t1}[int32]{tdend t1}
{td t1}32 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_int64 int x]{tdend t1}
{td t1}{!Xdr.X_int}{tdend t1}
{td t1}[int64]{tdend t1}
{td t1}32 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_unboxed int x]{tdend t1}
{td t1}{!Xdr.X_int}{tdend t1}
{td t1}[int] \[2\]{tdend t1}
{td t1}32 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned int x]{tdend t1}
{td t1}{!Xdr.X_uint}{tdend t1}
{td t1}{!Netnumber.uint4}, [int32], [int64], or [int] \[1\]{tdend t1}
{td t1}32 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _abstract int x]{tdend t1}
{td t1}{!Xdr.X_uint}{tdend t1}
{td t1}{!Netnumber.uint4} \[6\]{tdend t1}
{td t1}32 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _int32 int x]{tdend t1}
{td t1}{!Xdr.X_uint}{tdend t1}
{td t1}[int32] \[4\]{tdend t1}
{td t1}32 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _int64 int x]{tdend t1}
{td t1}{!Xdr.X_uint}{tdend t1}
{td t1}[int64]{tdend t1}
{td t1}32 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _unboxed int x]{tdend t1}
{td t1}{!Xdr.X_uint}{tdend t1}
{td t1}[int] \[2\]{tdend t1}
{td t1}32 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[hyper x]{tdend t1}
{td t1}{!Xdr.X_hyper}{tdend t1}
{td t1}{!Netnumber.int8}, [int64], or [int] \[1\]{tdend t1}
{td t1}64 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_abstract hyper x]{tdend t1}
{td t1}{!Xdr.X_hyper}{tdend t1}
{td t1}{!Netnumber.int8} \[6\]{tdend t1}
{td t1}64 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_int64 hyper x]{tdend t1}
{td t1}{!Xdr.X_hyper}{tdend t1}
{td t1}[int64]{tdend t1}
{td t1}64 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[_unboxed hyper x]{tdend t1}
{td t1}{!Xdr.X_hyper}{tdend t1}
{td t1}[int] \[3\]{tdend t1}
{td t1}64 bit signed integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned hyper x]{tdend t1}
{td t1}{!Xdr.X_uhyper}{tdend t1}
{td t1}{!Netnumber.int8}, [int64], or [int] \[1\]{tdend t1}
{td t1}64 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _abstract hyper x]{tdend t1}
{td t1}{!Xdr.X_uhyper}{tdend t1}
{td t1}{!Netnumber.uint8} \[6\]{tdend t1}
{td t1}64 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _int64 hyper x]{tdend t1}
{td t1}{!Xdr.X_uhyper}{tdend t1}
{td t1}[int64] \[4\]{tdend t1}
{td t1}64 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[unsigned _unboxed hyper x]{tdend t1}
{td t1}{!Xdr.X_uhyper}{tdend t1}
{td t1}[int] \[3\]{tdend t1}
{td t1}64 bit unsigned integer{tdend t1}
{trend t1}
{tr t1}
{td t1}[bool x]{tdend t1}
{td t1}{!Xdr.x_bool}
{td t1}[bool]{tdend t1}
{td t1}Boolean type{tdend t1}
{trend t1}
{tr t1}
{td t1}[float x]{tdend t1}
{td t1}{!Xdr.X_float}{tdend t1}
{td t1}[float] \[5\]{tdend t1}
{td t1}32 bit IEEE float{tdend t1}
{trend t1}
{tr t1}
{td t1}[double x]{tdend t1}
{td t1}{!Xdr.X_double}{tdend t1}
{td t1}[float] \[5\]{tdend t1}
{td t1}64 bit IEEE float{tdend t1}
{trend t1}
{tr t1}
{td t1}[opaque x[n]]{tdend t1}
{td t1}{!Xdr.X_opaque_fixed}{tdend t1}
{td t1}[string]{tdend t1}
{td t1}Opaque data with exactly [n] bytes.
The length [n] is dynamically checked{tdend t1}
{trend t1}
{tr t1}
{td t1}[opaque x<n>]{tdend t1}
{td t1}{!Xdr.X_opaque}{tdend t1}
{td t1}[string]{tdend t1}
{td t1}Opaque data with up to [n] bytes.
The length [n] is dynamically checked{tdend t1}
{trend t1}
{tr t1}
{td t1}[string x<n>]{tdend t1}
{td t1}{!Xdr.X_string}{tdend t1}
{td t1}[string]{tdend t1}
{td t1}String consisting of up to [n] bytes.
The length [n] is dynamically checked{tdend t1}
{tdend t1}
{trend t1}
{tr t1}
{td t1}[_managed string x<n>]{tdend t1}
{td t1}{!Xdr.X_mstring}{tdend t1}
{td t1}{!Xdr_mstring.mstring}{tdend t1}
{td t1}Alternative runtime representation. See {!Xdr_mstring}.{tdend t1}
{trend t1}
{tableend t1}

Footnotes:
 +  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
 +  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.
 +  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.
 +  This is a logical mapping: large XDR numbers where the MSB is set
    are mapped to negative OCaml numbers.
 +  The OCaml [float] type is a 64 bit IEEE floating point number.
 +  The module {!Netnumber} is an extended version of the older
    {!Rtypes} definition, and contains wrapper types for all XDR
    number types.

{2 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 
{!Xdr.xv_none}. The present argument value is represented as
{!Xdr.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.

{2 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.

{table t1}
{tr t1head}
{td t1}XDR declaration for [x][  ]{tdend t1}
{td t1}Term-level mapping[  ]{tdend t1}
{td t1}Full mapping[  ]{tdend t1}
{td t1}Comment{tdend t1}
{trend t1}
{tr t1}
{td t1}[t x[n]]{tdend t1}
{td t1}{!Xdr.X_array}{tdend t1}
{td t1}[t' array]{tdend t1}
{td t1}Arrays with exactly [n] elements{tdend t1}
{trend t1}
{tr t1}
{td t1}[t x<n>]{tdend t1}
{td t1}{!Xdr.X_array}{tdend t1}
{td t1}[t' array]{tdend t1}
{td t1}Arrays with up to [n] elements{tdend t1}
{trend t1}
{tr t1}
{td t1}[t x<>]{tdend t1}
{td t1}{!Xdr.X_array}{tdend t1}
{td t1}[t' array]{tdend t1}
{td t1}Arrays with any number of elements{tdend t1}
{trend t1}
{tableend t1}

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

{2 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 {!Xdr.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
{!Xdr.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
{!Xdr.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].

{3 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].

{3 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].

{3 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 } ]).



{2 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
[Xdr.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
[Xdr.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].

{3 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.).

{2 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 [Xdr.XV_union_over_int(n,v)] for
the [int] case or [Xdr.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 {b 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.


{2 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
[Xdr.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
[Xdr.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].


{3 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.


{2 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_]{i P}[']{i V} where {i P} is the name of
the program and {i V} is the version of the program.


{2 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.


{2 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