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.
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:
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.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
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 itgto
let eq = Java.call "java.lang.Object.equals(_)" obj itgAnother 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 listenerOpened 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
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
On the OCaml side, Java exceptions are encapsulated into one of the following exceptions:
exception Java_exception of java'lang'Exception java_instance exception Java_error of java'lang'Error java_instanceThe 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.
For effiency reasons, Java arrays are mapped to specialized implementations, as shown by the following table:
Java type | OCaml type |
---|---|
boolean[] | int java_boolean_array = int 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 = int 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 |
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.