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.
The transformation of binary XDR messages to O'Caml values is done in several steps, corresponding to several ways of representing the values:
string
values.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.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
.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:
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._of_
t that turns a fully-mapped
value into a term value represented as Xdr.xdr_value
._to_
t that turns a term value
to a fully-mapped value.The simple XDR types are
void
: special type denoting that no data is passedint
and unsigned int
: 32 bit signed/unsigned integershyper
and unsigned hyper
: 64 bit signed/unsigned integersbool
: 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 filesopaque<n>
: variable-length opaque data containing up to n bytes,
written opaque varname<n>
in .x filesstring<n>
: string data containing up to n bytes,
written string varname<n>
in .x files
void
: on term level mapped to Xdr.XV_void
. On the fully-mapped level
sometimes mapped to unit
, sometimes omittedsigned 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.
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.
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.
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
.
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
.
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.
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.
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.
To write
Rpc_client
as basisRpc_client
ocamlrpcgen
generates an enhanced client module containing
procedure stubs. These stubs are on the fully-mapped level.To write
Rpc_server
as basisRpc_server
ocamlrpcgen
generates an enhanced server module containing a
converter to/from the fully-mapped level.