Chapter 6 Interfacing the Join-Calculus with Objective Caml
This chapter describes how to use types and values defined in
Objective Caml from the join-calculus and, to some extent, vice-versa.
Objective Caml types and values that can be used inside
join-calculus programs are called externals.
6.1 Overview and compilation information
To be seen as externals by the compiler, types and values from
Objective Caml must appear in the interface of a join-calculus module.
Usually, externals specifications are grouped into specific .ji
interfaces.
6.1.1 Declaring externals
User externals values and types written in Objective Caml are declared in a
join-calculus interface file mod.ji, using the type and
external keywords.
Declaring types
An external type declaration defines a join-calculus type constructor
with the same arity as in Objective Caml. Additionally, it may specify the
equivalent Objective Caml type constructor and an iterator. Such extra
information enables the automatic translation of values between the
two languages while using externals. In the following, this
translation process is described as ``type coercion''.
The most simple declaration of an external type concerns null-ary type
constructors:
type typeconstr-name = string-literal
This defines the type mod.typeconstr-name as a
join-calculus version of the Objective Caml type constructor given in
typeconstr-name.
For instance, here is how the type float
is declared in the standard library module ml.
type float = "Pervasives.float"
This declaration establishes an equivalence between the Caml type
Pervasives.float and the join-calculus type ml.float.
Type coercion between values of the two types is performed.
Another type declaration that applies to type constructors with
arguments is as follows:
type ( ' ident { , ' ident } ) typeconstr-name =
string-literal
Parenthesis are optional when only one type variable ' ident is
present.
The string-literal component specifies the Caml equivalent of
typeconstr-name . It may also provide an optional iterator that
is used for type coercion. There are two cases, depending on whether the
iterator is present or not.
For instance, here is how the type list is declared in the
standard library module ml.
type 'a list = "'a list %List.map"
This declaration establishes an equivalence between the Caml type
constructor list and the join-calculus type constructor ml.list.
Variable ``'a'' represents a join-calculus type on the left-hand side
and an equivalent Caml type on the right hand side. More precisely,
given a join-calculus type jc-type and and an equivalent Caml type
caml-type , the two types jc-type ml.list and caml-type list
are equivalent. Automatic type coercion between object of types
jc-type ml.list and caml-type list is performed by iterating the
coercion operators between jc-type and caml-type , using the list
iterator List.map.
Another example is the definition of external pairs in the library
module ml:
type ('a,'b) pair = "'a * 'b %(fun f g (x,y) -> f x,g y)"
Observe that the iterator takes two coercion operators ``f'' and ``g'' as
arguments. Argument order is defined by the type variable order on the
left-hand side of the type declaration. For instance, ``f'' applies to
values of type ``'a'' and ``g'' to values of type ``'b''.
Yet another example, where no iterator is specified, is the definition
of type constructor t from the library module hashtbl:
type ('a , 'b) t = "('a,'b) Hashtbl.t"
This declaration defines the type constructors hashtbl.t as the
join-calculus version of the Objective Caml type constructor
Hashtbl.t. Since no iterator is specified, type coercion is pruned
at the hashtbl.t level (see below).
Finally, the = string-literal component can be omitted in all
kinds of external type declarations. This results in an non-coerced
type, a technique that is reserved to system programming.
The complete type equivalence between join-calculus types and
Objective Caml types is defined as follows, along with type coercions.
-
The basic types int, string, and bool are
equivalent to the homonymous Objective Caml basic types. Values of
these types are translated using system provided type coercions.
- A type variable is equivalent to the same type variable. No type
coercion is performed.
- Constructed types are equivalent when the join calculus type
constructor is defined by an external type declaration that
establishes the equivalence of type constructors from both languages.
Additionally, their type arguments must be equivalent.
Arguments must match by
following the argument correspondence specified using type variables.
Individual argument equivalence is checked as follows:
-
If an iterator is specified, then unrestricted type equivalence
applies. Type coercion is performed, using the specified iterator.
- Otherwise, type variables arguments are equivalent to
themselves. All other types are equivalent to the Objective Caml type
word, the types of values in the join-calculus runtime.
A simple coercion is performed that does not extends to the
subcomponents of constructed values of these types.
- All other join-calculus types, including channel types and
non-coerced types, are equivalent to the Objective Caml type
word. No type coercion is performed.
Observe that, by rules 2. and 4. above, equivalence with the
Objective Caml type word disables type coercion. This allows
direct access to join-calculus values from within Objective Caml
programs, a technique that is reserved to system programming.
Declaring values
In some interface file mod.ji, external values are declared as
follows:
external name : type = string-literal
This defines the name mod.name as a join-calculus
value or primitive with type type.
The component string-literal is a string that contains an Objective
Caml expression caml-expr that defines name.
How this is done depends upon type .
If type is a synchronous channel type, then name is a
primitive, that ultimately calls caml-expr , which must be a function
of the appropriate type.
More precisely, let type be
< type 1 * type 2 * ... * type n >
-> < type n+1 * type n+2 * ... *
type n+m >
Then caml-expr if a function whose type is:
c-type 1 -> c-type 2 -> ... -> c-type n
-> ( c-type n+1 * c-type n+2 * ... *
c-type n+m )
where type i and c-type i are equivalent. Type coercion of
arguments and results is performed, following the rules of previous
section.
Otherwise, name is a value, which is computed by applying type
coercion to caml-expr . Of course, the type of caml-expr must be
equivalent to type .
For instance, here is how the string_of_float primitive is declared in the
standard library module ml:
external string_of_int : <int> -> <string> = "Pervasives.string_of_int"
Another example is the find primitive from the
standard library module hashtbl:
external find : <('a,'b) t * 'a> -> <'b> = "Hashtbl.find"
Remember that the definition of the external type constructor
hashtbl.t does not supply an iterator. Thus, no type coercion is
performed on either keys or contents of hash tables. In the case of
the polymorphic find, this does not hurt: keys and contents, whose
types are the variables ``'a'' and ``'b'' would not be converted
anyway. Generally speaking, no iterator is required in the definition
of an external type constructor when all the primitives that handles
external values of this type are polymorphic.
Finally here are the declarations for the external type in_channel and
for the standard input from the library module ml:
type in_channel = "Pervasive.in_channel"
external stdin : in_channel = " Pervasives.stdin"
From the join-calculus point of view ml.stdin is an external value
and automatic type coercion applies.
6.1.2 Implementing externals
User primitives are implemented by Objective Caml functions, whereas
external values are implemented by Objective Caml values. Thanks to
automatic type coercion, these are written in standard Objective Caml
code.
A noticeable exception is system code that belongs to the
implementation. Such code must know something about the implemention,
including type word. A purposely minimal set of declarations is given by
the Ocaml module Ext_imports from the distribution.
6.1.3 Linking Objective Caml code with join-calculus code
The join-calculus runtime system comprises two main parts: the
bytecode interpreter and a set of Objective Caml values that implement
externals. Some bytecode instructions are provided to access these
externals, and are designated by their offset in a table of Objective
Caml values (the table of externals).
In the default mode, the join-calculus linker produces bytecode for the
standard runtime system, with a standard set of externals. References
to externals that are not in this standard set result in the
``external mismatch'' code loading error.
In the ``custom runtime'' mode, the join-calculus linker scans
compiled interface files given as arguments
and determines the set of required externals. Then, it
builds a suitable runtime system, by calling the Objective Caml code
linker with:
-
the table of the required externals,
- a library that provides the bytecode interpreter
and the standard externals,
- Objective Caml compiled interfaces and object code files or
libraries (.cmi, .cmo, or .cma files in the limited
installation; .cmi, .cmx or .cmxa files in the complete installation)
mentioned on the
command line for the join-calculus linker, that provide implementations
for the user's external.
This builds a runtime system with the required externals. The name of
this runtime is given to the linker by the -custom runtime
option. The linker will later generate bytecode for this custom
runtime system, provided it was given the right -jcrun runtime
option on the command line. The bytecode includes the necessary
information to launch the right runtime.
Thus, to link in ``custom runtime'' mode, execute the jcc command
twice. First produce the runtime with:
-
the -custom runtime option,
- the names of the join-calculus interfaces (.ji or .jio files) that
specify the required externals.
- the names of the Objective Caml source files (.mli and .ml),
object files (.cmi and .cmo or .cmx) or libraries (.cmx or
.cmxa) that implement the required externals.
This should produce a file runtime. This file can be renamed or
moved, but this will affect the following
linking phase.
Second, produce the join-calculus executable with:
-
the -jcrun runtime option, where runtime is more
a command name than a file name: invoking runtime should launch
the custom runtime;
- the names of the join-calculus object files (.jo files) that
make your program.
These two steps can be taken together and there are some shortcuts,
see chapter 8 for details.
6.2 Sendbacks from Ocaml to the join-calculus
So far, we have described how to call Ocaml functions from the
join-calculus. In this section we show how Ocaml functions can send
some messages asynchronously. As a matter of fact, it is not possible
for Ocaml code to send synchronous messages (i.e., to wait for
answers), since the current implementation assumes that all primitives
are non-blocking.
Sendback is only possible using asynchronous channels of a restricted set of
types, once these channels have been wrapped into an external value
that contains an Ocaml function.
This operation is performed by various wrappers from the module
sendback of the standard library. For instance, we have:
external wrap_int: <<int>> -> <(int,ml.unit) ml.fun> = ...
Objects of type (int,ml.unit) ml.fun are then passed as arguments to Ocaml
primitives that perform the sendbacks as ordinary function calls.
Sendbacks defeat the rules of automatic type conversion.
Mostly, the conversion rules are well-suited to data-structures, they
become meaningless when functions are involved. As a consequence, a
``type correct'' user programs that perform sendbacks may produce runtime-type
errors, resulting in unpredictable program behavior.
However, there is a simple rule to remember: external values of type
(t,u) ml.fun are Ocaml functions of type t'
-> u', where both argument types t and t' and
result types u and u' are equivalent. In particular, this
means that the functions produced by wrappers from the sendback
module take Ocaml values as arguments.
They then explicitely perform the type
conversions that are needed to re-inject these Ocaml values into the
join-calculus runtime.
6.3 Examples
In this section we give two examples of interfacing Objective Caml
with the join-calculus. While building custom runtimes the
join-calculus driver (jcc) calls the Objective Caml compiler
(ocamlopt or ocamlc). When using jcc this way, it is useful to
pass it the verbose option -v to jcc, in order to figure out
what is happening.
6.3.1 Linking user Ocaml code
Consider the following Ocaml module interface file complex.mli, which
provides some operations on complex numbers:
type t
val unit_root : int -> t
val print : t -> unit
val mul : t -> t -> t
The function unit_root creates a complex number that is an nth root of one,
whereas mul is complex multiplication. Here is an implementation
file for the Ocaml module Complex:
type t = {re:float ; im:float}
let unit_root n =
let phi = 2.0 *. acos (-1.0) /. (float n) in
{re=cos phi ; im=sin phi}
let print {re=r ; im=i} =
print_string "{x=" ;
print_float r ;
print_string " ; y=" ;
print_float i ;
print_string "}"
let mul {re=r1 ; im=i1} {re=r2 ; im=i2} =
{re= (r1 *. r2) -. (i1 *.i2) ; im= (r1 *. i2) +. (r2 *. i1)}
Ocaml files are compiled
using the Ocaml native code compiler by ocamlopt -c complex.mli complex.ml (this applies to the joins calculus complete installation;
in the limited installation, use the bytecode compiler ocamlc).
Then, here is the interface file complex.ji that provides the
join-calculus with a view on the Ocaml module Complex
type t = "Complex.t"
external unit_root : < int > -> < t> = "Complex.unit_root"
external print : < t > -> <> = "Complex.print"
external mul : < t * t > -> < t> = "Complex.mul"
This interface file is compiled by the command jcc complex.ji.
Then, a new join-calculus runtime is built by the command
jcc -custom jcr-c complex.jio complex.cmx, where
complex.jio defines users externals and complex.cmx contains Ocaml
object code that implements them.
Complex numbers can now be used in some user program user.j:
let print_roots(n) =
let r = complex.unit_root(n) in
let next_root(n,c) =
complex.print(c) ; print_newline() ;
if n <= 0 then {reply}
else {
next_root(n-1,complex.mul(r,c)) ;
reply
} in
next_root(n-1,r) ;
reply
;;
do print_roots(3)
;;
Program user.j is compiled and linked by issuing the command
jcc -jcrun ./jcr-c user.j. This produces an executable file
a.out, whose execution yields:
{x=-0.5 ; y=0.866025403784}
{x=-0.5 ; y=-0.866025403784}
{x=1 ; y=-6.10622663544e-16}
All the compilation commands given above can be merged into a single
command:
jcc -custom jcr-c complex.ji complex.mli complex.ml -jcrun ./jcr-c user.j
However, separating custom-runtime production and join-calculus
compilations is a good idea, since the former takes a lot of time and
that the latter is performed more than once during program development.
6.3.2 Importing a library from the Ocaml distribution
The Objective Caml distribution includes many libraries. Not all of
them are integrated in the join-calculus default runtime. However,
this can be done quite easily. It suffices to write the appropriate
.ji file and then to supply the adequate Ocaml linking options.
Assume that we want arbitrary precision arithmetics.
The Objective Caml library module Num provides such operations, so that we
do not have to write any Ocaml code.
First, we write a num.ji interface file:
type t = "Num.num"
external add : <t * t> -> <t> = "Num.add_num"
....
Then, we produce our custom runtime myrun by issuing the command
jcc -custom myrun nums.cmxa -cclib -lnums num.ji, where nums.cmxa
is the Ocaml library that implements the Ocaml module Num, using
C-primitives from the C-library nums.a. A properly installed Ocaml
compiler knows where to find these libraries, when given the arguments
above.
Writting .ji files for external modules is a tedious task.
However, the join-calculus source distribution contains a far from perfect
and non-supported tool to translate .mli files into .ji files.
This tool is mli2ji and resides in the tools directory.