Java Interop
The java package lets rontolisp drive arbitrary Java APIs by reflection —
construct objects, call instance and static methods, read fields, and turn a
rontolisp lambda into a Java interface instance. It is how the Swing demos in
examples/ (java-interop.lisp, swing.lisp, life-gui.lisp) put a window on
the screen without any bespoke Java glue.
JVM only (interpreter and compiled
.class). Interop values are opaque host-object references, so the feature needs a real JVM: it works under the JVM-hosted interpreter (java -jar rontolisp.jar program.lisp) and in a JVM-compiled program (-o Prog.class, run withjava Prog) — a call the compiler resolves becomes a direct call in the generated class, ajava:reifyorjava:proxya class generated for it, and for the calls left to run time the compiler writes a small reflection bridge beside it (Prog$JavaBridge.class, or an entry inside-o prog.jar), which the program then needs on its class path (see Compiling against a Java release or a class path for the JRE it needs). The WASM backend cannot lower host references, so compilingjava:to.wasmremains aCannot compile: java:...error. The GraalVM native binary (rontolisp program.lisp) can compile ajava:program to a.class, but cannot interpret one: a native image only contains the classes and members its build registered for reflection, and rontolisp's build registers none for interop, so there even(java:static "java.lang.Math" "max" 3 7)fails withNo such class.
The functions
The package is not part of Common Lisp, so its functions are referenced with the
java: qualifier (or unqualified after (in-package java)).
| Function | Purpose |
|---|---|
java:new | Construct a host object: (java:new "fqcn" args...) |
java:call | Invoke an instance method: (java:call obj "method" args...) |
java:static | Invoke a static method: (java:static "fqcn" "method" args...) |
java:field | Read a static or instance field: (java:field class-or-obj "name") |
java:proxy | Adapt a callable to one or more interfaces: (java:proxy "iface"... callable) |
java:subclass | Extend a class with a callable: (java:subclass "super" '("iface"...) '("method"...) args... callable) |
java:reify | Implement interfaces one method at a time: (java:reify "iface" "method" function ...) |
java:handle | Stand for a Lisp value Java has no value of: (java:handle value "text") |
java:view | Stand for a Lisp collection as a read-only Java one: (java:view value items :list) |
A constructed or returned object prints opaquely as #<java <class-name>> and
can be passed back into java:call/java:field:
A Lisp value is a java:call receiver too, called as the object it becomes for an Object
parameter: a string as a String, an integer as an Integer (a Long when it does not fit
one), a float as a Double, a bignum as a BigInteger, a character as a Character (a
supplementary one as the Integer of its code point), t as Boolean.TRUE, the symbol
|false| as Boolean.FALSE. nil, a function, any other symbol, a list, an array and a hash
table are no receiver.
Value marshalling
Arguments and results are converted between rontolisp and Java automatically:
| rontolisp | Java (in) | Java (out) |
|---|---|---|
| integer | int/long/short/byte/float/double (and their boxes), BigInteger | int/long/.../BigInteger → integer |
| bignum | BigInteger (or a supertype: Number, Object, ...) | BigInteger → integer |
| float | double/float (and boxes) | double/float → float |
| string | String, or char if length 1 | String → string |
| character | char/Character | Character → character |
t / nil | boolean (nil also → any null reference) | boolean → t/nil (after :java-false, t/\|false\|) |
the symbol named false | boolean false, Boolean.FALSE for any reference | after :java-false, Java's false |
a java object | the wrapped host object | any other object → a java object |
| a function/lambda | a java:proxy over the matching interface, or after :functional an implementation of its abstract methods (an argument only) | — |
| a proper list / a vector (specialized too) | T[] (element-wise, incl. primitives), or List/Collection/Iterable | any Java array → a list (after :octets, a byte[] → an (unsigned-byte 8) vector) |
| a hash table | a fresh java.util.LinkedHashMap (Map, HashMap, Object, ...) | — |
a java:handle | an object Java sees as its text | the value it stands for |
a java:view | a read-only List, Set or Map of its items (a List view: an array of them where nothing takes it whole; a :bytes view: the byte[] of its octets) | the value it stands for |
A Java null (and a void method) comes back as nil. A proper list — or a
rank-1 array made with make-array, a specialized one included (double-float,
single-float, bfloat16, (unsigned-byte 8|16|32)) — passed where a Java array is expected is
converted element-wise to the component type (including primitive arrays like
int[]), and where a List/Collection/Iterable is expected it becomes a
java.util.List; nested lists convert recursively. In the other direction a
Java array result becomes a Lisp list, while a returned java.util.List
stays an opaque java object whose methods you call:
A bignum is passed as a java.math.BigInteger where one (or a supertype such as
Number or Object) is expected, and nowhere narrower: not as a long, and not
as a double -- convert it with float first. A fixnum reaches a BigInteger
parameter too, when no primitive overload takes it. In the other direction a
java.math.BigInteger result is a Lisp integer, not a java object: compute with
it in Lisp rather than through java:call.
nil is Java's null wherever a reference is expected, so '|false| -- the symbol
spelled as Java spells false -- is the way to pass Boolean.FALSE to an Object
parameter, and what a callback answers for a boolean or Boolean result. A hash
table becomes a fresh java.util.LinkedHashMap of its entries in insertion order,
each key and value converted as an Object argument is (an equalp table's keys as
first stored):
Any other symbol, ratios, dotted (improper) lists and multidimensional (rank-2+) arrays
are not bridged; a java:handle stands for one.
Java's false back: :java-false
Java's false comes back as nil, Common Lisp's only false. A java:new, java:call,
java:static or java:field ending in :java-false answers it as |false| instead -- a
boolean result, a Boolean.FALSE, an array's elements. The marker goes after the
arguments, before or after :functional:
A java:proxy, java:reify or java:subclass ending in it hands its functions Java's
false as |false|, as does a function passed where an interface is expected at a call
ending in it. A function passed where a java.util.Comparator is expected, at a call ending
in both markers, answers compare as Clojure's AFunction.compare reads a function: t is
-1, |false| is 1 when the function answers true for the two arguments swapped and 0
otherwise, a float or ratio is truncated, an integer is its low 32 bits; nil throws the
NullPointerException and any other answer the ClassCastException that AFunction.compare
throws, which the call reports as the method's failure:
Octets back: :octets
A Java array comes back as a list, so a byte[] is a list of signed bytes. A java:new,
java:call, java:static or java:field ending in :octets answers a byte[] -- the
result, a field's value, an element of an array it answers -- as an (unsigned-byte 8) vector
of its octets instead, beside the other markers in any order. The vector is Java's array
itself, on the interpreter and in a compiled program alike: what Java stores into the array
later -- a ByteBuffer's array(), which the buffer writes -- the vector holds, and two answers
of one array are eq. A :bytes view hands one to Java:
A function the call converts, and every function of a java:proxy, java:reify or
java:subclass ending in :octets, is handed a byte[] -- an argument, or an element of
one -- as such a vector too: Java's array itself, so what the function stores into it Java
reads, and what Java stores while the function runs the function reads. An array Java hands
twice is one vector:
Handles: java:handle
(java:handle value "text") makes a Java object that stands for a Lisp value Java has no
value of. Java sees the text as its toString, and two handles of one text are equals. A
handle hashes as its text and orders by it, or, as (java:handle value "text" hash "order"), hashes as the integer's low 32 bits and orders by the order text: a handle keys a
HashMap and sorts in a TreeSet as the value would in its own language. Wherever Java
hands a handle back -- a result, an array's element, a callback's argument -- java: answers
the value:
The full form is (java:handle value text hash order class). A nil hash makes a handle
equal only to a handle of the very same value, and a nil text then spells it as Object
does, the class and the hash. An order that is a function compares the value with the
object Java compares the handle with, by the sign of its answer, and a nil order makes the
handle order nothing. class splits equality and order: handles of two classes are never
equal and never compare, a ClassCastException naming both. A handle of a real number is a
java.lang.Number of it (java:handle). An object
that stands for a value and implements interfaces too is a java:reify given :value
(Implementing interfaces with java:reify).
Views: java:view
(java:view value items shape) makes a read-only Java collection that stands for a Lisp one:
its elements are the items converted as Object arguments are, once; shape is :list,
:vector (a List that is also RandomAccess and Comparable), :set or :map (of a hash
table or a plist). Java reads it through the java.util interfaces -- equals and hashCode
are theirs, every write an UnsupportedOperationException -- its toString is a printer's
answer for the value, and Java hands it back as the value:
A view is passed itself wherever its class fits. A List view where a Java array is expected
is an array of its items, but only after every way to pass it whole, a varargs array holding
it as one element included: a Java array a call answers is a list here, and its view
converts back to the array a later call expects.
(java:view value octets :bytes) makes no collection: wherever a byte[] fits (Object
included) Java is handed the byte[] of an (unsigned-byte 8) vector -- the vector's own
storage, so what Java stores there the vector holds, during the call and later through an
object that keeps the array (ByteBuffer.wrap):
The Clojure front end hands Java every value this way: a vector, list, set or map as a view
printed as Clojure prints it, a byte array as its byte[], a keyword, symbol or ratio as a
handle hashed and ordered as Clojure's Keyword, Symbol and Ratio are, and any other
value as a handle equal only to itself (java:view).
A java object is eq and eql only to itself: the same object answered by two
calls is eq, while two objects that are equals are not. equal and equalp
ask the object's equals. So an eq or eql hash table keys a java object by
identity (a key mutated after it was stored is still found), and an equal or
equalp table by equals and hashCode:
With a Lisp value on the right, equal hands the object's equals the value as
a Java method's Object parameter receives it (a string a String, a character
a Character, nil null); a symbol, list, vector or ratio is equal to no
java object. A Lisp value on the left is never equal to a java object, as in
Clojure, whose = asks its left operand.
Overload resolution
When a class has several constructors or methods of the same name and arity,
java picks the overload whose arguments convert at the lowest total cost —
an exact match beats a widening conversion, which beats a lossy/boxed one — with
ties broken by a stable signature ordering. So an integer argument prefers an
int parameter over long/double, and the choice never depends on the order
reflection happens to return methods:
When no integer overload exists the integer is converted to the available type:
A function costs less for a functional interface (one abstract method) than for another
interface, so TreeSet(Comparator) wins over TreeSet(Collection), as a Java lambda's
target does:
Resolving calls before they run
The interpreter and a compiled class resolve every call by that one rule. A call whose
class is known from the program text -- the class java:new or java:static names, the type
of a java:call receiver -- is resolved once, before it first runs, among that class's
methods: to exactly one method when the argument kinds are known too, otherwise to the
overloads the arguments can select, among which the call chooses by the kinds its arguments
have each time it runs. The model is Clojure's type-hinted interop, applied by the
interpreter too. A call whose receiver class is not known is resolved when it runs, from the
receiver's class and the arguments' kinds. The method chosen is the same either way, with
the one exception below.
What the program text says about a value:
- a literal's kind:
3,2.5,"x",#\a,t,nil, alambda; (java:new "C" ...)is exactly aC;- a resolved call's value has the type its method declares:
appendon aStringBuilderanswers aStringBuilder, so a chain resolves link by link; a method declared to returnObjectsays nothing; (the (java:object "C") x)and(declare (type (java:object "C") v))say that the value is aC(ornil).Cis a binary class name, as forjava:new(java.util.Map$Entry).(java:object "C" :exact)says it is exactly aC, nevernil, asjava:newanswers;- a
letorlet*variable has its initializer's type, unless it is special or something in its scope assigns it (setq,setf,incf, ..., in a closure too); (declaim (type (java:object "C") v))types the globalvin the forms after it. Adefvar's initial value does not: any form may assign the variable.(java:reify "I" ...)and(java:proxy "I" ...)with a literal interface make an object of a class that implementsIand nothing else a program can name: a call on it resolves amongI's methods, and one that passes it resolves as its argument. Aletvariable keeps that type, which(java:object "I" :exact)spells -- no object's class is exactly an interface, so for one:exactmeans this. A(java:proxy "I" "J" ...)of several literal interfaces implements each: a call passing it resolves, a call on it is resolved by its class when it runs, and no specifier spells its type.
A call on a value whose known class is an interface also resolves to Object's public
methods the interface does not declare (toString, getClass, ...), as Java's own call
list.toString() does.
A call on a value known to be a string, float, character, bignum or t resolves among the
methods of the class it is called as (String, Double, ...). An integer's box depends on
its size, so a call on one is resolved when it runs.
A declared type is trusted: a value that is not a C is an error where it meets the call,
whether it is the receiver or an argument -- never converted for a method it was not chosen
for.
(defun parse (s)
(declare (type (java:object "java.lang.String") s))
(java:static "java.lang.Integer" "parseInt" s))
(parse 42) ; error: java:static: argument 1 is not a java.lang.String, got 42
Both calls below are resolved before they run: sb is exactly a StringBuilder.
A compiled class makes a call resolved to one method a direct call of it, and a call resolved to overloads a comparison of its arguments' kinds followed by a direct call of the overload they select -- no reflection either way -- and writes the reflection bridge only for the calls left to run time. The interpreter runs a resolved call the same way: the method chosen, the arguments checked and converted.
a and b may be anything, so each call of bigger chooses among max(int,int),
max(long,long), max(float,float) and max(double,double) by the cost rule.
An argument decides the method before the call runs only when every kind it can have
selects the same one. A String answer may be nil, which selects append(boolean), so
(java:call sb "append" (java:call sb "toString")) chooses between append(String) and
append(boolean) when it runs.
The declared receiver class decides the candidates
A call on a receiver of declared class C resolves among C's methods, as in Java -- so
does one on a let variable whose initializer is typed C. A public overload of the same
name that only the run-time class adds is not a candidate, whether the argument kinds are
known before the call runs or only when it does -- the one place where resolving early
chooses differently from resolving at run time:
Parameter tags
A method name, or the class name of java:new, may carry the parameter types, which names
the overload directly: "max(long,long)", "java.lang.StringBuilder(int)". _ matches any
type and leaves that parameter to the cost rule; a type without a package means java.lang;
T[] or T... is an array.
Reflection warnings
(setq java:*warn-on-reflection* t) reports each call in the forms that follow which is
left to run-time resolution, with the reason: the interpreter when it loads a form (with
the form's line), the compiler at compile time (with the call's position).
--warn-java-reflection turns it on from the start:
$ rontolisp --warn-java-reflection len.lisp -o Len.class
len.lisp:1:16: warning: java:call "length" is resolved by reflection at run time: the receiver's class is not known
Compiling against a Java release or a class path
The interpreter resolves against the JDK it runs on and the program's class path
(Java libraries). The JVM compiler reads class files instead: the
JDK's lib/ct.sym -- the running JDK's, else JAVA_HOME's, else that of the java on
PATH -- for the newest release it holds or for --java-release N, followed by the
class path. A compiled class calls the
methods chosen at compile time, so it is stamped for that release (class version 44 + N, at
least Java 17's 61): a JRE older than the release refuses to load it. A call that names a
class the compile cannot see is resolved when it runs, and the reflection bridge such a call
uses needs a JRE at least as new as the one rontolisp was built with.
$ rontolisp app.lisp -o app.jar --java-release 21 --java-classpath lib/guava.jar
Compiling without reflection
--java-static makes every call that needs reflection a compile error: one left to run
time, and a java:reify or java:proxy whose interface is named at run time or not found
when compiling (it becomes a java.lang.reflect.Proxy). The compile lists them all at once.
A java:reify, a java:proxy of a literal interface and a function passed where an
interface is expected -- including an argument whose kind is known only when the call runs,
where an overload expects an interface, as String.join(CharSequence, Iterable) does -- are
classes generated at compile time and need no reflection. What compiles has no reflection in
it, so GraalVM native-image builds the jar into an executable with no
reachability metadata -- no reflect-config.json, no agent run:
$ rontolisp app.lisp --java-static -o app.jar
$ native-image --no-fallback -jar app.jar -o app
$ ./app
$ rontolisp len.lisp --java-static -o len.jar
error: --java-static: 1 java: call cannot be compiled without reflection:
len.lisp:1:16: java:call "length": it is resolved by reflection at run time: the receiver's class is not known
A (declare (type (java:object "C") v)) or a (the (java:object "C") x) is what makes such a
call resolve.
Varargs
A varargs method (e.g. String.format(String, Object...)) accepts any number
of trailing arguments; they are packed into the varargs array automatically. A
fixed-arity overload is preferred when both match, and a list/vector passed in
the varargs position can also supply the whole array itself:
Implementing interfaces with java:reify
java:reify implements a host interface one method at a time: each method name is
followed by the function that implements it, called with the method's arguments. The
object it makes can be passed wherever the interface is expected, and a Java caller calls
it like any other implementation:
The methods are chosen before the form runs, by the same rule in the interpreter and a compiled program:
- A name designates one method. One several methods share is tagged with the parameter
types, as a
java:callname is ("append(char)"); a name that matches more than one method, or none, is an error. - An abstract method no name designates throws
UnsupportedOperationExceptionwhen it is called; a default method keeps the interface's body;toString,equalsandhashCodemay be named, and are otherwise#<java-reify I>and identity. - A function's value is converted to the method's return type as an argument is, except
that a function is not made a proxy on the way back: return a
java:reifyorjava:proxyobject where an interface is expected. - A quoted list of interface names,
(java:reify '("I" "J") ...), makes one object implementing each, a name designating a method of any of them. :value vafter the interfaces makes the object stand forv, as a handle does: Java hands it back asv, and unless named, itsequals,hashCodeandtoStringare those of a handle with a nil hash, of the class:classnames.
A compiled program implements each java:reify whose names are literal strings with a
class generated for it (Prog$Reify0.class), so it needs no reflection: see Compiling
without reflection. The reference
page has more examples.
Callbacks via java:proxy
java:proxy makes a host interface instance backed by a rontolisp callable. The
callable is applied as (callable "method-name" arg...) for every interface
method, so a single lambda can implement the whole interface and dispatch on the
method name. Its return value is marshalled back to the method's return type
(void methods ignore it, and a function it returns is not made a proxy):
A callable passed directly where an interface is expected is wrapped in a proxy
automatically, which is what lets a Swing ActionListener be a plain lambda:
(java:call button "addActionListener"
(lambda (method event) (handle-click)))
A java:new, java:call or java:static ending in :functional, after its arguments
(a java:subclass after its callable, for its constructor arguments), converts a function
the way Java converts a lambda instead: each abstract method of the
interface calls it with the method's arguments alone, and default methods keep their
bodies. The Clojure front end ends its calls in it, so a Clojure fn takes no method name:
Class proxies via java:subclass
java:subclass makes a host class instance backed by a rontolisp callable --
what java:proxy cannot do, since a java.lang.reflect.Proxy implements
interfaces only. The form names the superclass, the extra interfaces, the
overridden methods and the constructor arguments:
The callable is applied as (callable this "method-name" arg...) for every
named method: this first, then the name. The constructor arguments choose the
superclass constructor by the shared overload rule. A named method runs its body
(toString/equals/hashCode included); a method left out is inherited when
the class implements it, and throws UnsupportedOperationException with the
method's name when it is called and nothing implements it. A proxy-super
(written in Clojure) reaches the superclass implementation through the generated
super$ accessor, called as an ordinary method:
A java:subclass whose names are literal strings is a class generated at
compile time, so the construction compiles under --java-static. One left to
run time -- a name computed at run time, or a class the compile cannot see (a
project class needs --java-classpath) -- is refused by name; the interpreter
resolves it when it runs. The reference
page has more examples.
Errors and non-local exits
An exception a Java member throws is signalled as a java:java-exception, a simple-error
that reports the member and the exception and carries the exception itself, which
java:java-exception-cause answers:
Handed back to Java -- as an argument of a member, or as the receiver of a java:call
whose class is not known before it runs -- a java:java-exception is the exception it
carries:
A condition a rontolisp function signals while Java calls it back, and a return-from,
throw or go out of that function, propagates through the Java frames in between as it
does through Lisp frames, to the code that made the Java call:
To the Java code in between it is an ordinary exception, and only what that code lets
propagate arrives: one it catches and ignores never does, and one it wraps, or rethrows on
another thread, arrives as the failure of the Java call (FutureTask.get wraps it in an
ExecutionException).
A Swing example
examples/jvm/java-interop.lisp builds a small window directly through the package
(interpret it -- or compile it to a .class -- on a machine with a display):
(defvar *frame* (java:new "javax.swing.JFrame" "java interop"))
(defvar *label* (java:new "javax.swing.JLabel" "click count: 0"))
(defvar *button* (java:new "javax.swing.JButton" "Increment"))
(defvar *panel* (java:new "javax.swing.JPanel" (java:new "java.awt.BorderLayout" 12 12)))
(defvar *count* 0)
(java:call *button* "addActionListener"
(java:proxy "java.awt.event.ActionListener"
(lambda (method event)
(setq *count* (+ *count* 1))
(java:call *label* "setText"
(concatenate 'string "click count: " (princ-to-string *count*))))))
(java:call *panel* "add" *label* (java:field "java.awt.BorderLayout" "CENTER"))
(java:call *panel* "add" *button* (java:field "java.awt.BorderLayout" "SOUTH"))
(java:call *frame* "setContentPane" *panel*)
(java:call *frame* "setDefaultCloseOperation"
(java:field "javax.swing.WindowConstants" "DISPOSE_ON_CLOSE"))
(java:call *frame* "setSize" 360 180)
(java:call *frame* "setVisible" t)
examples/jvm/swing.lisp builds a reusable grid-window helper on top of these five
functions -- wrapped in a swing package of its own,
spliced in with (require :swing "swing.lisp") -- and examples/jvm/life-gui.lisp
animates Conway's Game of Life with it (swing:grid-window, swing:paint, ...).
Java libraries
A program's Java class path holds the libraries its java: calls reach beyond the
JDK. --java-classpath names directories and jars, separated as for java -cp;
--java-dep names a library by its Maven coordinates (groupId:artifactId:version,
repeatable), together with what it depends on. The interpreter loads classes from the
class path, a JVM compile resolves calls against it, and a Clojure program's host forms
see its classes as they see the JDK's.
$ rontolisp app.lisp --java-dep com.google.guava:guava:33.4.0-jre
$ rontolisp app.lisp -o app.jar --java-dep com.google.guava:guava:33.4.0-jre
$ java -jar app.jar
--java-dep resolves as Maven resolves a project's dependencies: where two versions of
a library meet, the one nearest the requested coordinates wins, and the jars join the
class path after the --java-classpath entries, in Maven's class path order. They come
from Maven Central through the local repository mvn uses -- ~/.m2/repository, or
the localRepository of settings.xml. A SNAPSHOT, LATEST, RELEASE or version range,
given or in a dependency's POM, resolves through Central's maven-metadata.xml as Maven
resolves it. The local repository keeps that metadata and asks Central again once a day,
as it does for a file Central did not have. Central serves no SNAPSHOT, as in Maven: a
SNAPSHOT is looked up only in the repositories named with --java-repository.
A library Central does not hold -- Clojars, a company repository, a file: directory --
is named with --java-repository [ID=]URL (repeatable; https:, http: or file:).
Those repositories are searched after Central, in the order given. The ID (default
java-repository-N) is what settings.xml matches: its <server> supplies the
credentials and a <mirror> whose mirrorOf names the id replaces the URL. An id of
central replaces Central's URL instead of adding a repository.
$ rontolisp app.lisp --java-dep clj-http:clj-http:3.12.3 \
--java-repository clojars=https://repo.clojars.org/
settings.xml applies as it does for mvn: ~/.m2/settings.xml, merged over
$MAVEN_HOME/conf/settings.xml when MAVEN_HOME is set. Its offline is honored, a
mirror covering Central is contacted in Central's place (a blocked one fails), a proxy
carries the requests, and the <server> of the repository contacted supplies Basic
credentials, httpHeaders and timeouts. A password encrypted with
mvn --encrypt-password is decrypted with the master password in
~/.m2/settings-security.xml.
The <repositories> of the active settings.xml profiles (named in <activeProfiles>,
or holding to their <activation>; the global and the user's file together) are searched
as mvn searches them: ahead of Central and the --java-repository ones, the profile
defined last first, a profile's own repositories in order. A profile repository with the
id of one of those replaces it.
A Clojure program's deps.edn dependencies that hold classes join the class path
after these (Projects: deps.edn).
What each output carries:
- A program jar (
-o app.jar) copies the class path intoapp-lib/beside it and names the copies in its manifest'sClass-Path, sojava -jar app.jarandnative-image -jar app.jarfind them. Ship the jar with itsapp-lib/. - A war (
-o app.war) packs the jars intoWEB-INF/lib/and a directory's files intoWEB-INF/classes/. - A class (
-o Prog.class) carries nothing: run it with the class path, asjava -cp .:lib/guava.jar Prog. - A library jar (
--no-main) carries no class path either: its pom (--maven-coordinates,--emit-pom) lists the--java-depcoordinates as its dependencies, for the consumer's Maven to resolve, and a--java-classpathentry is the consumer's to provide.
The GraalVM native binary cannot load a class at run time, so there the class path reaches only what a compile resolves calls against and what its output carries.
Native image
A compiled java: program builds into a GraalVM native image. One whose calls
all resolve before they run needs nothing more: compile it with --java-static
(Compiling without reflection) and build the
jar as it is -- its java:reify and java:proxy objects and the functions it
passes where an interface is expected are classes generated at compile time,
so they need nothing either. The calls left to run time are reflective and need reachability
metadata, which the tracing agent records from a run:
rontolisp prog.lisp -o prog.jar
java -agentlib:native-image-agent=config-output-dir=config -jar prog.jar
native-image -jar prog.jar -H:ConfigurationFileDirectories=config
The metadata covers only the calls the traced run made. A call that selects an
overload the run never selected fails in the image with
MissingReflectionRegistrationError -- for example (java:call sb "append" x)
on an sb whose class is not known, passed a float after a run that only
passed integers. Trace runs that exercise every call
shape the program uses, or declare the types so the calls resolve.
Limitations
- JVM only: the interpreter (
java -jar rontolisp.jar) and JVM-compiled classes (java Prog). Not on the WASM backend, and not when interpreting in the GraalVM native binary, whose image carries no reflection metadata for the interop classes (the native binary can still compile ajava:program to a.class). - In a compiled class the
java:functions work in call position only: they have no first-class value, so#'java:callor(funcall 'java:new ...)is a compile error (wrap them in your owndefuninstead), and the embeddedevalruntime does not know them either. A compiled program that usesjava:needs a JRE of the release its calls were resolved against, and one that leaves a call to run time a JRE at least as new as the one rontolisp was built with. - Symbols other than
|false|, dotted (improper) lists and multidimensional (rank-2+) arrays are not marshalled — pass them as Java collections you build withjava:new/java:call, or as ajava:handleorjava:view, instead. - A returned
java.util.List(unlike a Java array) stays an opaquejavaobject: it keeps its identity and mutability, so read it withjava:call("get","size", ...) rather than list functions. - Overload resolution is by argument cost, not the full Java type-inference rules; an ambiguous call resolves to the lowest-cost (then lowest-signature) candidate rather than signalling an ambiguity error. A parameter tag names an overload explicitly.
- It is a full host-reflection bridge, so it can run arbitrary Java code: treat a
program that uses
java:with the same trust as any other JVM program.