Ocamlnet 3 Reference Manual : Rpc_mapping_ref

# 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 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 = Rtypes.int4_of_int 1` and `r2 = Rtypes.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 `Xdr.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 `Xdr` module.
- The conversion function `_of_`t that turns a fully-mapped value into a term value represented as `Xdr.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 simple XDR types are

- `void`: special type denoting that no data is passed
- signed `int` and `unsigned int`: 32 bit signed/unsigned integers
- signed `hyper` and `unsigned hyper`: 64 bit signed/unsigned integers
- `bool`: the boolean type with two values `FALSE` and `TRUE`
- `float`: single-precision IEEE floating point numbers
- `double`: double-precision IEEE floating point numbers
- `opaque[n]`: fixed-length opaque data containing n bytes, written

      opaque varname[n]

  in .x files
- `opaque<n>`: variable-length opaque data containing up to n bytes, written

      opaque varname<n>

  in .x files
- `string<n>`: string data containing up to n bytes, written

      string varname<n>

  in .x files

In O'Caml, these types are mapped as follows:

- `void`: on term level mapped to `Xdr.XV_void`. On the fully-mapped level sometimes mapped to `unit`, sometimes omitted
- `signed int`: on term level mapped to `Xdr.XV_int` which takes an `Rtypes.int4` as argument. On the fully-mapped level this type can be mapped either to `Rtypes.int4`, `int32` or `int`, depending on what the application needs. When mapping to `int` it can happen that not all values can be represented. The exception `Rtypes.Cannot_represent` is raised in this case.
- `unsigned int`: on term level mapped to `Xdr.XV_uint` which takes an `Rtypes.uint4` as argument. On the fully-mapped level this type can be mapped either to `Rtypes.uint4`, `int32` or `int`, depending on what the application needs. The mapping to `int32` (which is signed) is done bitwise (sign bit is ignored). When mapping to `int` it can happen that not all values can be represented. The exception `Rtypes.Cannot_represent` is raised in this case.
- `signed hyper`: on term level mapped to `Xdr.XV_hyper` which takes an `Rtypes.int8` as argument. On the fully-mapped level this type can be mapped either to `Rtypes.int8`, `int64` or `int`, depending on what the application needs. When mapping to `int` it can happen that not all values can be represented. The exception `Rtypes.Cannot_represent` is raised in this case.
- `unsigned hyper`: on term level mapped to `Xdr.XV_uhyper` which takes an `Rtypes.uint8` as argument. On the fully-mapped level this type can be mapped either to `Rtypes.uint8`, `int64` or `int`, depending on what the application needs. The mapping to `int64` (which is signed) is done bitwise (sign bit is ignored). When mapping to `int` it can happen that not all values can be represented. The exception `Rtypes.Cannot_represent` is raised in this case.
- `bool`: on term level mapped to `Xdr.xv_false` or `Xdr.xv_true` which are pre-defined constants using enumerations for booleans. Actually, `xv_false` is `XV_enum_fast 0` and `xv_true` is `XV_enum_fast 1`. On the fully-mapped level this type is mapped to `bool`
- `float`: on the term level mapped to `Xdr.XV_float` which takes an `Rtypes.fp4` as argument. On the fully-mapped level this type is mapped to `float` which, however, is double-precision. When converting an O'Caml `float` to an XDR `float` it may happen that precision is lost, and that very small or large numbers cannot represented at all. The nearest value is taken instead which may be `0` or `infinity`.
- `double`: on the term level mapped to `Xdr.XV_double` which takes an `Rtypes.fp8` as argument. On the fully-mapped level this type is mapped to `float` (loss-free)
- `opaque`: on the term level mapped to `Xdr.XV_opaque` which takes a string argument. On the fully-mapped level this type is mapped to `string`. The size constraint is dynamically checked in both cases when RPC message are analyzed or created.
- `string`: on the term level mapped to `Xdr.XV_string` which takes a string argument. On the fully-mapped level this type is mapped to `string`. The size constraint is dynamically checked in both cases when RPC message are analyzed or created.

Since Ocamlnet-3, there is also the variant of managed strings. A managed string (declared by the keyword `_managed` in the XDR file) is mapped to `Xdr.XV_mstring` on the term level. This constructor takes an argument of type `Xdr_mstring.mstring`, which is also the type on the fully-mapped level.

## 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.

## 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.

Fixed-size array are written

    t varname[n]

and variable-size arrays

    t varname<n>

in .x files (where n is the size). Arrays of any size are written

    t varname<>

in .x files.

On term level, array values are mapped to `Xdr.XV_array` `v` where `v` is an O'Caml array of the mapped element values.

On the fully-mapped level, arrays are mapped to the

    t' array

O'Caml type when `t'` is the mapped element type.

The size constraint is dynamically checked in both 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: 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`.

## 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 `Rtypes.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 O'Camlish) and whose value is `IntK`.

## 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 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 Rtypes.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 `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 {
         A = 5,
         B = 42,
         C = 7,
         D = 81
       }

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

is mapped to the O'Caml type

       [ 'A of int64     (* expanded default case *)
       | `B of int32
       | `C
       | `D of int64     (* expanded default case *)
       ]

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

## 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.