Java
Volt calls Java
Section titled “Volt calls Java”Java sources, jars and class directories are imported like a header. Nothing in the Java code is
written for Volt, and there are no signatures, start calls or VM values in the Volt code: bolt
compiles the sources with javac, asks the JVM what the classes export, and writes Volt that calls
them through JNI. The JVM starts the first time Java is called.
use std::io;use { "geo/Point.java", "geo/Color.java" } as geo; // or { "lib.jar" }, or use java { "classes" }
fn main() -> !void { var p = geo::Point::new(3.0, 4.0); // a constructor (overloads stay overloads) p.scale(2.0); // a method p.set_y(1.5); // a public field: y() and set_y(v) std::println("{} {}", p.x(), p.toString()); // 6 (6.0, 1.5) val q = try geo::Point::parse("1, 2"); // it declares an exception: java_error!Point std::println("{}", geo::Color::GREEN.next().lower()); // an enum and its methods: blue}| Java | Volt |
|---|---|
| a public class, interface or abstract class | a handle: a reference to the object (a copy refers to the same one); is_null() for a null one |
new T(...) |
T::new(...), one per public constructor |
| a public method | a method, or T::f(...) for a static one; overloads stay overloads |
| a public field | x() and, unless it’s final, set_x(v); T::x() for a static one |
an imported supertype S |
as_S(): the same object, as an S |
an enum |
a Volt enum with the same constants, and the enum’s methods |
boolean, byte, char, short, int, long, float, double |
bool, i8, u16, i16, i32, i64, f32, f64 |
String |
str in, std::string out (null is "") |
arrays of those and of String |
T[..] in (what Java changes in it comes back), std::vec<T> out |
a List, Set or other Collection of boxed numbers (Integer), String, an imported class or enum |
a copy: T[..] in (Java gets a new ArrayList, LinkedHashSet or TreeSet, as its parameter’s type asks), std::vec<T> out |
a Map of those |
a copy: std::map<K, V>& in (a new LinkedHashMap or TreeMap), std::map<K, V> out |
a generic method (<T> T first(List<T>)) |
a Volt generic: first(xs[..]) with numbers, strings or imported classes, boxed through Java (one function: Java erases the types). A type variable only in the result is given: make<i32>() |
a class the import doesn’t have (java.time.Duration, Object), and a generic class’s own type variable (Box<T>’s T, which Java erases to Object) |
a java_object: toString(), equals(&o) and is_null() |
a method that declares exceptions (throws) |
java_error!T; java_error::THROWN holds the exception’s toString() |
An exception a method doesn’t declare stops the program with its text, as a Volt panic does. So
does a null in a collection of numbers, or a value of another class where a generic’s type was given. A
method whose types aren’t in the table (an array of an imported class, a collection of another
library’s classes, a collection of collections, a wildcard such as List<? extends Shape>, a
Map keyed by anything but a String or an integer) is left out, listed in a comment of the generated
declarations (VOLT_SHOW_IMPORT=1 voltc check main.volt prints them). Each import loads its classes through
a class loader of its own, so a program can import several, and they share the process’s JVM.
bolt needs a JDK: $JAVA_HOME, else the one whose javac is on the PATH (for javac and its
jni.h). The program links its libjvm with an rpath to it.
The interop/java package
Section titled “The interop/java package”The interop/java package (in the repository) starts a Java VM inside a Volt program through JNI’s
invocation API, for calls made by hand. Depend on it and call Java methods with Volt values; its build file finds the JDK at
$JAVA_HOME (or the one whose java is on the PATH) and links libjvm with an rpath to it.
[dependencies]java = { path = "../volt/interop/java" }use std::io;
fn run(vm: java::vm&) -> java::java_error!void { val math = try vm.find_class("java/lang/Math"); std::println("{}", try math.call_static_int("max", "(II)I", 3, 9)); // 9
val counter = try vm.find_class("Counter"); // Counter.class on the class path val c = try counter.new_object("(Ljava/lang/String;I)V", "volt", 10); std::println("{}", try c.call_int("bump", "(I)I", 5)); // 15 std::println("{}", try c.to_string()); // volt=15
val bad = counter.call_static_int("divide", "(II)I", 1, 0); if (bad.err) { std::println("{}", bad.err); // THROWN(java.lang.ArithmeticException: / by zero) }}
fn main() -> !void { var vm = try java::start("classes:lib/gson.jar"); // the JVM stops when vm is deleted try run(&vm);}Methods are found by name and their JNI signature: (II)I takes two ints and returns one,
(Ljava/lang/String;)V takes a String and returns nothing. javap -s Counter prints them.
java::start(classpath) |
starts the JVM (a vm: deleting it stops it); once per process |
vm.find_class(name) |
a class: "java/util/ArrayList", "Counter", "com/example/Thing" |
vm.string(s) |
a new java.lang.String |
cls.call_static_void, _int, _long, _double, _bool, _object, _string |
a static method: (name, signature, args...) |
o.call_void, call_int, call_long, call_double, call_bool, call_object |
an instance method, the same way |
cls.new_object(signature, args...) |
a new instance: the constructor’s signature, like "(I)V" |
o.to_string() |
toString()’s text |
o.is_null() |
whether a returned object is null |
Arguments are integers (i8, i16, i32, i64), f32, f64, bool, str (a new Java
String) and java::object. An object argument is lent to the call; pass copy o to keep using
o afterwards. A java::object is a global reference: copying it adds one and deleting it drops
it. Every call that can fail returns java_error!T, and a Java exception becomes
java_error::THROWN with its toString().
The vm belongs to the thread that started it: call Java from that thread.
Java calls Volt
Section titled “Java calls Volt”A Volt library that lists java among its bindings gets one Java class from bolt build
(target/debug/bindings/NAME.java), for Java 22 or later. It calls the library through the FFM API
(java.lang.foreign), so there is no JNI and no C to compile.
The examples here use greet, the library every client in
examples/interop/calls-volt
calls: export fn add(a: i64, b: i64) -> i64, export fn hello(name: str) -> std::string, and an
export struct tally with tally_new, tally_add and tally_name. What export means, and what
crosses as what, is in They call Volt.
# bolt.toml of the Volt library[lib]kind = ["shared"]bindings = ["java"]public class Main { public static void main(String[] args) { System.out.println("add " + greet.add(2, 3)); // add 5 System.out.println(greet.hello("volt")); // hello, volt try (var c = new greet.tally("clicks")) { // AutoCloseable c.add(1); System.out.println(c.name() + " " + c.add(2)); // clicks 3 } }}javac -d classes target/debug/bindings/greet.java Main.javaLD_LIBRARY_PATH=target/debug java --enable-native-access=ALL-UNNAMED -cp classes MainThe class loads libNAME.so (libNAME.dylib on macOS, NAME.dll on Windows: Java’s
System.mapLibraryName), or the library -Dvolt.NAME.lib names. An error set becomes a
VoltException subclass, thrown with the error’s name (its code and name say which, and the
class has the codes as constants). Structs are mutable classes: what Volt changes through a T&
comes back to the object. Enums are Java enums, each with its value. Unsigned integers use the
Java type of the same size, as int for u32; slices and lists (std::vec) are arrays,
optionals the value or null, and a callback is a functional interface. A pointer to anything
but a struct, a number by reference (n: i32*) too, is a MemorySegment. An export struct a
program doesn’t close is freed by a Cleaner.
Java takes every shape:
- Owned values as parameters. A
std::stringparameter takes aString(Volt copies it); an export struct by value takes its object, which gives its handle up (closing it after does nothing). One lent to a call that hasn’t returned can’t be closed or given away until it does (a callback trying throwsIllegalStateException), nor lent and given in one call. - Traits. A Volt trait is a Java interface: any object implementing it can be lent (
s: shape&) or given (s: shape; Volt closes it when it’s done, when it’sAutoCloseable). A Volt value of the trait comes back asvolt_shape, which implements the interface and isAutoCloseable. - Callbacks taking and giving text and handles. A lambda gets
Strings and the objects (one Volt lends is never closed by Java) and gives them back; one for anE!Tcallback throwsVoltException.of(code)to give that error. - Closures given back are
AutoCloseableobjects with acallmethod. - Lists and arrays of text and handles. A
std::vec<T>comes back as an array (String[], or the objects, which the caller closes);String[]and object arrays go in forstd::string[..], slices of handles and lists; an optional text or handle is the value ornull.
For a library shapes with a trait shape (area, name, grow), describe(s: shape&) -> std::string, make_square(side: f64) -> shape and owners(xs: account&[..]) -> std::vec<std::string>:
class Circle implements shapes.shape, AutoCloseable { double r = 1; public double area() { return 3 * r * r; } public String name() { return "circle"; } public void grow(double by) { r += by; } public void close() {}}
System.out.println(shapes.describe(new Circle())); // circle of area 3try (var sq = shapes.make_square(2)) { // Volt's own shape System.out.println(sq.name() + " " + sq.area()); // square 4.0}String[] names = shapes.owners(new shapes.account[] {a, b}); // a std::vec<std::string>