Java interoperability¶
JErlang runs on the JVM, so Erlang code calls Java through reflection and Java
code calls Erlang through org.jerlang.api.Bridge. This page defines the value
mapping and the handle rules. The tables are the 0.8 compatibility contract:
changing a row needs a version bump.
Erlang to Java¶
| Erlang term | Java type | Notes |
|---|---|---|
| integer that fits in 32 bits | java.lang.Integer |
|
| integer that fits in 64 bits | java.lang.Long |
|
| larger integer | java.math.BigInteger |
|
| float | java.lang.Double |
|
atom true / false |
java.lang.Boolean |
|
atom undefined |
null |
|
| any other atom | java.lang.String |
Returning a string becomes an Erlang string, not the original atom. |
| string (charlist) | java.lang.String |
Str and a list of character integers both convert. |
| binary | byte[] |
|
| list | java.util.List |
An ArrayList; the nil list becomes an empty list. |
| tuple | Object[] |
|
| map | java.util.Map |
A LinkedHashMap; keys and values are converted recursively. |
| object handle | the live Java object | Handles are references created by java:new, not values. |
Java to Erlang¶
| Java type | Erlang term | Notes |
|---|---|---|
null |
atom undefined |
|
java.lang.Boolean |
atom true / false |
|
java.lang.String |
string | |
java.lang.Character |
one-character string | |
Byte, Short, Integer, Long |
integer | |
java.math.BigInteger |
integer | |
Float, Double |
float | |
byte[] |
binary | |
Object[] |
tuple | |
java.util.Collection |
list | |
java.util.Map |
map | Keys and values are converted recursively. |
| any other object | object handle | Use java:release/1 when the Erlang side is done. |
Java does not distinguish integer widths in Erlang, so int, long and
BigInteger all become one Erlang integer type and the width is chosen again
on the way back.
Arguments and overload resolution¶
An argument is converted to the declared parameter type before a method or
constructor is chosen. For an Erlang integer the preference is int, then the
other integral types, then floating point. When several overloads accept the
arguments, the one with the lowest conversion distance wins; an Object
parameter gets the generic conversion above. A char parameter accepts a
one-character string or a code point, and an enum parameter accepts an atom
with the constant's name.
A distance tie is broken by parameter specificity and then by sorted signature,
so the same call picks the same method on every JVM instead of depending on the
order Class#getMethods returns, which the JDK does not specify. Varargs are
supported for a trailing Object... parameter only.
Java exceptions surface as an Erlang error with reason
{java_exception, ClassName, Message}. An Erlang exit does not stop the JVM.
Object handles¶
java:new/2 and any Java value that has no value mapping above are returned as
an object handle. One handle exists per Java object identity: asking again for
the same object returns the same reference. Handles are node-local and are not
Erlang terms that can be sent to another node.
The table belongs to the virtual machine, not to the bridge, so a handle lives
exactly as long as the node that created it. It starts empty when the node
starts and is drained when the node shuts down. It is bounded by
NodeOptions.maxHandles, default 100 000; creating a handle past the cap
raises system_limit, the reason Erlang already uses for an exhausted runtime
resource. {java_exception, _, _} stays reserved for exceptions thrown by
Java code.
Release policy¶
java:release/1releases early. A call on a released handle raises an error, and the reference is never reused for another object.Node.close(), and any VM shutdown, drains the whole table, so it is the catch-all for handles a caller forgot.- There is no finalizer and no
Cleaner: release happens explicitly or at shutdown, never because the garbage collector ran. An unreleased handle keeps its Java object reachable for the lifetime of the node. - Raising
NodeOptions.maxHandlesdoes not make forgetting a release safe; it only moves thesystem_limitfurther away.
Reflection is opt-in¶
NodeOptions.reflection(boolean) defaults to false. The java module is
registered on node start and unregistered on close, so a node that does not ask
for reflection has no java:new/2 and cannot reach arbitrary JVM classes.
Bridge and the value mapping are unaffected, because neither needs the module.
Embedding API¶
org.jerlang.api.Node implements AutoCloseable and owns the node:
Node node = Node.start(NodeOptions.builder()
.codePath(Path.of("target/ebin"))
.reflection(true)
.build());
node.registerFunction("my_mod", "echo", 1, args -> args[0]);
Term reply = node.apply("gen_server", "call", pid, request, Integer.of(5000));
Term out = node.apply(anErlangFun, Integer.of(41));
node.close();
apply takes both a module name and an Erlang fun. registerFunction makes a
Java lambda callable from Erlang through the normal module registry. Bridge
stays as it is: a static convenience over a default node.
Node.start resets the process-wide code path and module registry before it
applies NodeOptions, and close resets them again. Only one node may be open
in a JVM; close it before starting another. A failed start performs the same
cleanup and leaves no active node.
A Java callback that throws a RuntimeException reports an Erlang error with
reason {java_exception, ClassName, Message}, and the original exception is
available as the Java cause. To bound a call made from a Java thread, pass a
Duration:
A call that exceeds the deadline throws Module.ApplyTimeoutException after its
process is killed. A callback already running inside an Erlang process is not
interrupted by this timeout.
Compatibility gate¶
mvn verify runs japicmp 0.26.5 against a baseline jar checked in under
src/api-baseline/, with accessModifier public,
breakBuildOnSourceIncompatibleModifications and
breakBuildBasedOnSemanticVersioning. It compares the thin main artifact, not
the attached all jar, so bundled dependency classes are not treated as this
project's API. The breakBuildBasedOnSemanticVersioningForMajorVersionZero flag
remains because the baseline is a 0.x artifact; the 3.0.0 version is what makes
the deliberate packaging break a major release. The baseline is refreshed only
when the version is bumped.
Stability¶
Bridge.toTerm/1 and Bridge.toJava/1 are the conversion entry points for
embedding. TypeMappingTest has one test per row above, and a change to a row
must come with a version bump.