At the recent Erlang Factory I had a lot of fun pairing with Krzysiek Goj, extending Erjang with a new API for calling Java code. After the conference jetlag, I spent a night hacking on making this a bit more usable; and it looks quite good. Perhaps even nicer than Clojure's Java integration syntax?
-module(jsample).
test() ->
Map = 'java.util.HashMap':new(),
Map:put('x', "4"),
Map:put(1, 'foo'),
[io:format("key=~p, value=~p~n", [Key,Val]) || {Key,Val} <- Map].
1> jsample:test().
key=1, value=foo
key=x, value="4"
[ok,ok]
The above code works with the current head in the Erjang GitHub repository.
Many people have requested this; and so I've decided to add it even though such calls are of cause dangerous to use (you can easily violate the integrity of Erlangs immutability).
Accessing static members, and creating instances
The first syntax is
1> Map = 'java.util.HashMap':new().
[]
Which ... tada ... calls the corresponding constructor for java.util.HashMap. It's really cool that none of this requires changes to the normal Erlang compiler. It's just that if you try to run it on BEAM it won't work; in this case because there is no erlang module named 'java.util.HashMap'; Erjang just installs a synthetic module lazily if it fails to find a normal one.
The same syntax is also used to call static methods, so
2> 'java.lang.System':currentTimeMillis().
1276510012014
will give you the current time in milliseconds. The obervant reader will notice that there is no way then, to call a static member called new. You can of cause do:
3> Clazz = 'java.lang.Class':forName('foo.Bar').
4> Meth = Clazz:getMethod("new", []).
5> Value = Meth:invoke(null, []).
Also cool, but that is rather cumbersome, eh?
Matching on Java objects
Internally, objects coming from Java world are kept in an erjang.m.java.JavaObject, which has a set of built-in rules for what it can convert into. These conversions are tied to the corresponding BEAM instructions; and the same JavaObject may be converted into several different Erlang terms. In erlang there is a number of type tests, and for these JavaObjects, we'll simply allow an object to be more than one kind of thing.
- anything implementing
java.lang.Iterableincluding all arrays and all subclasses ofjava.util.Collectioncan be matched as a list. - anything implementing
java.util.Mapcan be matched as[{key, value}, ...]. - anything implementing
java.lang.CharSequence(includingjava.lang.String) can be matched as either a list of each individual character, or as a binary of the UTF-8 encoded bytes. - primitives, booleans, integers, etc. can be matched as the corresponding types.
In the code snippet above, we use the [{Key, Value}, ...] mapping to feed a map into a list comprehension:
[io:format("key=~p, value=~p~n", [Key,Val]) || {Key,Val} <- Map].
The right hand side of the <- operator is supposed to be a list, so the Map is then converted implicitly to what is essentially a lazy list based on calling Map.entrySet().iterator(). The Erlang compielr translates the list comprehension into an anonymous self-recursive function:
lc([]) -> [];
lc([{Key,Val}|Tail]) ->
OK = io:format("key=~p, value=~p~n", [Key,Val]),
[OK|lc(Tail)].
This ability of one value to turn into diffent kinds of things depending on context is really flexible; though we still have some challenges on then figuring out the semantics of equals, i.e. == and =:=; that's still being worked on.
Accessing virtual members
Once you have a java object, you can call virtual methods by using the same syntax as before, but simply using the JavaObject as the module:
Map:put(1, 'foo'),
Map:put('x', "4"),
This is using the same syntax as Erlang allows for module instances; so it all feels quite natural in context of erlang world.
Calling into Java land
When you call a method in Java, there can be multiple ones to choose from since Java has overloading.
- If an argument coming to Java is a JavaObject itself (i.e. it has come from a previous invocation of a java method), then it is simply unwrapped from the original value.
- Otherwise,
JavaObjecthas some built-in rules for converting values the other way.
For each combination of the above; each variant of argument types to see if a type-match can be reached.
For example, you can pass an Erlang list [97,98,99] (equivalent to the erlang term "abc") where a java.lang.String is expected. But that list could also be converted to int[], and if the same method is overloaded with both java.lang.String and int[] then Erjang will choose any one of them.
For the caution programmer, we also have a more verbose BIF that allows you to control overloading:
java:call(Receiver, Method::atom(), {Type::atom(), ...}, {Arg, ...})
Where you can specify exactly which variant of the method you want by specifying a tuple of atoms. But I expect most people will live with the far more convenient dynamic selection.
Implementing Java interfaces
This is not implemented, but this is the syntax I am thinking would be good. If you want to call a Java method that requires the client to implement a specific interface, then I'll do it as a single callback like this:
Component:addMouseListener(fun mouse_handler/3).
mouse_handler(mouseClicked, [_], [MouseEvent]) ->
io:format("mouse clicked at ~p,~p~n",
[MouseEvent:getX(), MouseEvent:getY()]);
mouse_handler(EventID, _, [MouseEvent]) ->
io:format("ignored event ~p:~p~n", [EventID, MouseEvent]).
I.e., any fun/3 can be used as an interface implementation which simply calls the given function with (1) an atom for the method name, (2) a list of atoms for the argument types, and (3) a list of the real arguments.
How does that look to you? If you like it, you're welcome to go ahead and try to implement it!
Status
This is not fully working yet but the basics is in place, ... it will probably need many iterations to flesh out; but it sure looks sweet, eh?
- As I said, we still have not decided on how to implement equals and equalsExactly in a context where either side of the equals may be a JavaObject that can (potentially) be converted to a number of different erlang terms.
- And we also need to make sure that all this is not at the expense of Erlang performance.
- It's still quite slow; being fully reflective. But that can be fixed.
- There is also significant future opportunity to capture more specific type information, so that some calls do not have to be reflective.
But I think this is the right direction. Let me know what you think.