OCaml-Java: creating and manipulating Java instances from OCaml code


This page contains the information about the typer extensions that allows to access Java elements from OCaml sources, as shipped with the distribution since version 2.0-early-access8.

Warning! these extensions, as well as the associated library, are still at the experimental stage and subject to changes.

Warning! in order to enable the extensions, it is necessary to pass the -java-extensions switch to the ocamljava compiler, and to link with the javalib library. Thus resulting in the following command-line for a one-file application:

ocamljava -I + javalib javalib.cmja -java-extensions source.ml

The same command-line switch should be passed to ocamldoc when generating documentation. This command-line switch can be requested in an ocamlbuild _tags file through the java-extensions tag.

Types

In order to be able to manipulate Java elements from OCaml sources, it is necessary to choose a mapping from Java types to OCaml types. The following table shows how Java primitive types are mapped to OCaml predefined types.

Java type OCaml type
boolean bool
byte int
char int
double float
float float
int int32
long int64
short int

Java reference types are mapped to two OCaml types, namely java_instance and java_extends. Both are abstract data types accepting one type parameter used to denote a class name. The difference between the two types is that java_instance designates exactly an instance of a given class, while java_extends designates an instance of either that class or any of its subclasses.

A special notation is introduced to specify the class name: the type parameter of either java_instance or java_extends can be:

  • a classical type variable, e.g. 'a;
  • a Java class name, e.g. java'lang'String.

When writing a Java class name, single quotes have to be used instead of dots to abide the OCaml syntactic rules. Thus leading to the type expression java'lang'String java_instance to designate in OCaml an instance of the java.lang.String class.

Java pervasives

When Java extensions are enabled, the JavaPervasives module is automatically opened at the beginning of each file (just like the Pervasives module in vanilla OCaml). This module defines the following functions:

  • !@, a shorthand for JavaString.of_string, thus allowing the write Java string literals !@"a string value";
  • ^^^, an equivalent of Pervasives.(^) (concatenation over string values) for JavaString.t values;
  • |., akin to Pervasives.(@@) with a different priority.

The idiomatic use of the |. operator is to chain calls to Java methods, like in the following code:

Java.make "..."
|> Java.call "..." |. p1 |. ... |. pn
|> ...

Functions

Once the mapping from Java types to OCaml types is defined, we need mechanisms to create new instances, call methods, and access fields. This is done through functions from the Java module.

A new instance is built by calling Java.make with a first parameter describing the constructor to be used (as a string literal), the other parameters being the parameters actually passed to the constructor. For example, the following code builds a bare object and an Integer:

let obj = Java.make "java.lang.Object()"
let itg = Java.make "java.lang.Integer(int)" 123l

A similar mechanism is used to invoke methods, through the Java.call function. The first parameter to this function is a descriptor to the method to be called. The other parameters are the parameters passed to the function, including the instance on which the method should be invoked (if the method is not static). For example, the following code retrieves the hash code of the previously-created object and then tests whether the two instances are equal:

let obj_hash = Java.call "java.lang.Object.hashCode():int" obj
let eq = Java.call "java.lang.Object.equals(java.lang.Object):boolean" obj itg

It is noteworthy that the subtyping relationship over Java instances is preserved, so that it is possible to define a function retrieving the hash code, and then apply it to Object and Integer instances:

let hash_code x = Java.call "java.lang.Object.hashCode():int" x
let obj_hash = hash_code obj
let itg_hash = hash_code itg

When calling a Java method with a variable number of arguments, it is possible to choose how arguments will be passed: through a Java array, or through an OCaml literal array. The choice is made by using one of the two allowed variants for the method (or constructor descriptor):

  • "C.m(T[])" implies the use of a Java array;
  • "C.m(T...)" implies the use of an OCaml literal array.

The following example applies these notations to the Arrays.asList method:

let a1 =
  let res = Java.make_array "Object[]" 5l in
  ...
  res
let l1 = Java.call "Arrays.asList(Object[])" a1

let l2 =
  Java.call "Arrays.asList(Object...)"
  [| Java.null ;
     Java.make "Object()" () ;
     (JavaString.of_string "xyz" :> java'lang'Object java_instance) |]

Accessing fields to read (respectively write) their values is done through the Java.get function (respectively Java.set). The first parameter to the function is a descriptor of the Java field to access, and the second parameter the instance to use (or unit if the field is static). For example, we can retrieve the maximum integer value and and increment the width of a dimension by:

let max_int = Java.get "java.lang.Integer.MAX_VALUE:int" ()
let incr_width dim =
  let w = Java.get "java.awt.Dimension.width:int" dim in
  Java.set "java.awt.Dimension.width:int" dim (Int32.succ w)

Finally, it is possible to implement a Java interface with OCaml code through the Java.proxy function. The first parameter to the function is a descriptor designating a Java interface, while the second parameter is an OCaml object implementing the methods specified by the Java interface. The Java.proxy function then returns a fully-functional instance of the interface. For example, the following code implements an action listener and registers it with a previously created button:

let button = Java.make "java.awt.Button()"

let listener =
  Java.proxy "java.awt.event.ActionListener" (object
    method actionPerformed _ =
      print_endline "clicked!"
  end)
  
let () =
  Java.call "java.awt.Button.addActionListener(java.awt.event.ActionListener):void"
    button listener

Sugar

So far, we have seen how to create and manipulate Java instances from purely OCaml code. However, the resulting code is quite verbose. We thus introduce some syntactic sugar to allow terser programs. Firstly, it is possible to remove the return type of a method or the type of a field as long as there is no ambiguity. Secondly, the type of a method parameter can be replaced by an underscore provided there is no ambiguity. The combination of these rules already allows us to switch from

let eq = Java.call "java.lang.Object.equals(java.lang.Object):boolean" obj itg

to

let eq = Java.call "java.lang.Object.equals(_)" obj itg

Another notation, (-) allows to match any number of parameters, allowing to write

let x = Java.call "Integer.rotateLeft(-)" y z

It is noteworthy that the types are not affected by these shorthand notations, and that the compiler will issue an error if there is an ambiguity.

Lastly, we introduce a mechanism akin to the Java import pack.* directive through a special form of the OCaml open directive. Importing all the classes of the Java package pack is done by writing open Package'pack (note that, as in Java, the java.lang package is always opened). Thus leading to a revised version of our proxy example:

open Package'java'awt
open Package'java'awt'event

let button = Java.make "Button()"

let listener =
  Java.proxy "ActionListener" (object
    method actionPerformed _ =
      print_endline "clicked!"
  end)

let () = Java.call "Button.addActionListener(_)" button listener

Opened packages also allow to reduce the verbosity of type expressions, allowing to replace the package name by an underscore. The type of the aforementioned hash_code function can then be written:

val hash_code : _'Object java_extends -> int32rather than:
val hash_code : java'lang'Object java_extends -> int32

Example

The following code builds a simple Celsius-Fahrenheit converter based on a Swing GUI. The code is derived from the Clojure code sample available here. The code uses the JavaString module for conversion between Java and OCaml strings.

open Package'java'awt
open Package'java'awt'event
open Package'javax'swing
    
let () =
  let str = JavaString.of_string in
  let open Java in
  let title = str "Celsius Converter" in
  let frame = make "JFrame(String)" title in
  let temp_text = make "JTextField()" () in
  let celsius_label = make "JLabel(String)" (str "Celsius") in
  let convert_button = make "JButton(String)" (str "Convert") in
  let farenheit_label = make "JLabel(String)" (str "Farenheit") in
  let handler = proxy "ActionListener" (object
    method actionPerformed _ =
      try
        let c = call "JTextField.getText()" temp_text in
        let c = call "Double.parseDouble(_)" c in
        let f = (c *. 1.8) +. 32.0 in
        let f = Printf.sprintf "%f Farenheit" f in
        call "JLabel.setText(_)" farenheit_label (str f)
      with Java_exception je ->
        let je_msg = call "Throwable.getMessage()" je in
        let je_msg = JavaString.to_string je_msg in
        let msg = str (Printf.sprintf "invalid float value (%s)" je_msg) in
        let error = get "JOptionPane.ERROR_MESSAGE" () in
        call "JOptionPane.showMessageDialog(_,_,_,_)"
          frame msg title error
  end) in
  let () = call "JButton.addActionListener(_)" convert_button handler in
  let layout = make "GridLayout(_,_,_,_)" 2l 2l 3l 3l in
  call "JFrame.setLayout(_)" frame layout;
  ignore (call "JFrame.add(Component)" frame temp_text);
  ignore (call "JFrame.add(Component)" frame celsius_label);
  ignore (call "JFrame.add(Component)" frame convert_button);
  ignore (call "JFrame.add(Component)" frame farenheit_label);
  call "JFrame.setSize(_,_)" frame 300l 80l;
  let exit = get "WindowConstants.EXIT_ON_CLOSE" () in
  call "JFrame.setDefaultCloseOperation(int)" frame exit;
  call "JFrame.setVisible(_)" frame true

Exceptions

Warning! Up to version 2.0-early-access11, all exceptions where encapsulated into Java_exception.

On the OCaml side, Java exceptions are encapsulated into one of the following exceptions:

  • Java_exception for instances of java.lang.Exception;
  • Java_error for instances of java.lang.Error.

These exceptions are defined as follows:

exception Java_exception of java'lang'Exception java_instance
exception Java_error of java'lang'Error java_instance

The Java.instanceof and Java.cast functions can be used to respectively test whether a given object is an instance of a given class, and to cast it to a given class. Both functions accept as their first parameter the name of the class as a string literal. The following code illustrates how OCaml and Java exceptions can be mixed:

open Package'java'io

let f ... =
  try
    ...
  with
  | Not_found ->
    (* predefined OCaml Not_found exception *)
    ...
  | Java_exception t when Java.instanceof "IOException" t ->
    (* Java exception that is a subclass of java.io.IOException *)
    ...
  | Java_exception _ ->
    (* any other Java exception inheriting from java.lang.Exception *)
    ...
  | _ ->
    (* any other OCaml exception *)
    ...

Java exceptions are raised from OCaml code by calling the Java.throw function with a parameter of type java'lang'Throwable java_extends.

Arrays

For effiency reasons, Java arrays are mapped to specialized implementations, as shown by the following table:

Java type OCaml type
boolean[] bool java_boolean_array = bool JavaBooleanArray.t
byte[] int java_byte_array = int JavaByteArray.t
char[] int java_char_array = int JavaCharArray.t
double[] float java_double_array = float JavaDoubleArray.t
float[] float java_float_array = float JavaFloatArray.t
int[] int32 java_int_array = int32 JavaIntArray.t
long[] int64 java_long_array = int64 JavaLongArray.t
short[] int java_short_array = int JavaShortArray.t
reference[] reference java_reference_array = reference JavaReferenceArray.t

note 1: each array type has a type parameter representing the OCaml type of array elements.
note 2: all primitive array types share a common signature, namely JavaArraySignature.T.

Array instances are created through either the Java.make_array function, or any of the make functions from the various modules. The Java.make_array function accepts as its first parameter an array descriptor, and as additional parameters int32 values for the various dimensions. For example, a 2x3 byte matrix can be created through:

let m = Java.make_array "byte[][]" 2l 3l

It is also possible to use a uniform representation for arrays by wrapping array instances into JavaArray.t values. The JavaArray.t type is a GADT that unifies all specialized arrays types into one common type. This allows to write generic code over arrays, at the price of an indirection. The JavaArray modules provides the functions to wrap the various kinds of arrays into JavaArray.t values.