Previous Contents Next

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.

  1. 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.
  2. A type variable is equivalent to the same type variable. No type coercion is performed.
  3. 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:
    1. If an iterator is specified, then unrestricted type equivalence applies. Type coercion is performed, using the specified iterator.
    2. 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.
  4. 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: 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: 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: 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.


Previous Contents Next