Metaprogramming API
TeaVM was designed to create web applications. One important requirement for web applications is their size. Nobody will use a compiler which produces tens of megabytes of JS. That's why tools like TeaVM and GWT perform advanced optimizations to reduce code size. Unfortunately, it's impossible to make these optimizations reflection-friendly. Even if TeaVM implemented reflection, any attempt to use it could lead to "dependency explosion", i.e. huge JS file. That's why TeaVM does not provide reflection support. Instead, it comes with its own replacement, called metaprogramming API.
Essence
Unlike Java reflection, TeaVM metaprogramming does not work in runtime. It allows you to write a code that runs in the compile-time. TeaVM can query from compiler much about classes, methods and fields and generate JS code that will be executed at run-time.
One, familiar with GWT can find this approach very close to deferred binding (aka generators). However, TeaVM's approach is slightly more powerful, as you can see below.
Quick start
Let's start by writing a method which reads field called foo
or returns null
if such field is not available.
public static Object getFoo(Object obj) {
return getFooImpl(obj.getClass(), obj);
}
@Meta
private static native Object getFooImpl(Class<?> cls, Object obj);
private static void getFooImpl(ReflectClass<Object> cls, Value<Object> obj) {
if (!whitelist(cls)) {// TODO important to whitelist classes you are
unsupportedCase();// using in metaprogramming, otherwise it will
return; // increase size of generated javascript dramatically
} // or lead to long compilation time
var field = cls.getField("foo");
if (field != null) {
exit(() -> field.get(obj));
} else {
exit(() -> null);
}
}
Here, our method simply delegates all work to getFooImpl
, which is a native method marked with @Meta
annotation.
This annotation does all magic. It tells compiler to generate body of getFooImpl
by invoking another getFooImpl
method with slightly different signature. It's quite easy to find the difference: returning value must be void
,
the ReflectClass
argument must correspond to Class
and Value
argument must correspond to any other argument.
Note that only one argument can be of ReflectClass
type.
GWT's entry point to generators are special classes created by
GWT.create
method, which requires single class literal argument (class variable are not allowed). TeaVM's approach looks similar, however, it does not restrict developer to the literal argument. You can get class from anywhere, for example, by calling Object.getClass().
The second getFooImpl
which has actual Java code is executed in run-time. It provides access to classes available
to compiler via API which resembles Java reflection, with Reflect
prefix added to each class. We use it
to find a field called "foo".
To get field's value and return it from method, we use Metaprogramming.exit
(or just exit
, since there's
a convention to always statically import entire Metaprogramming
class). This method takes lambda which will be
again executed in run-time and causes original getFoo
method to return evaluated expression.
We can now test this method:
class A {
private String foo;
A(String foo) { this.foo = foo; }
}
class B {
}
public static void main(String[] args) {
System.out.println(getFoo(new A("barbaz"));
System.out.println(getFoo(new B()));
}
So, we can freely switch between compile-time and run-time. To switch from runtime to compile-time, we
declare a pair of methods with equal names and similar signatures, mark first one with @Meta
annotation.
To switch back, we call special method like exit
or emit
.
GWT requires you to generate Java code in compile-time which it further compiles to JavaScript. TeaVM works with bytecode so it would be hard to write the code that generates byte-code. Mataprogramming API does byte-code generation for you using lambdas as templates.
@Meta
annotation
@Meta
annotation must be put on a static native method. Another static non-abstract non-native method with the
same name must exist in the same class. Its signature must correspond to methods of the original method:
- It should be
void
no matter which type is returned by the original method. - Any
Class
argument can be mapped toReflectClass
argument. - Any other argument must be mapped to
Value
argument. - There can be at most one
ReflectClass
argument.
@Meta
annotation causes the first method's body to be generated by the second method which runs in compile-time.
Emitting bytecode
There are several methods which can be called from compile-time to generate code. All they take lambda as an argument, and simply write the lambda's body to the generated code. Unlike normal lambdas, template lambdas have some restrictions over variables they can capture.
- It's allowed to capture primitive values (numbers, strings).
- It's allowed to capture
Value
,ReflectClass
,ReflectField
,ReflectMethod
. - Any other value is disallowed.
These captured variables act as template parameters.
The main (and the simplest) one is Metaprogramming.emit
. It simply writes template as-is. exit
method
writes template and additional return
statement which returns expression evaluated by lambda.
lazy
does not write template immediately. Instead, it produces Value
which is written as soon as
accessed by another lambda.
Passing data between template lambdas
In the real world templates can't be isolated. Value produced by one template may be needed to another template.
TeaVM uses Value
for this purpose. Methods like emit
and lazy
produce Value
. Value can't be read at
compile-time. The only way to read Value
is to capture it by another lambda and call get
method there.
For example:
Value a = emit(() -> 2);
Value b = emit(() -> a.get() + 3);
exit(() -> b.get());
Reflection restrictions
You can use metaprogramming reflection much like usual Java reflection. However, it imposes several restrictions:
- You can't search and enumerate fields and methods of a class in template lambdas.
- You can't get and set value of
ReflectField
in compile-time. - You can't do
ReflectMethod.invoke
methods in compile-time.
Proxies
Of course, there is an alternative to reflection proxies. You can call Metaprogramming.proxy
method
which accepts interface and InvocationHandler
.