Metaprogramming API
TeaVM Metaprogramming is a compile-time code generation API that lets you write Java code that runs during TeaVM compilation to generate the actual runtime code. Think of it as a type-safe, Java-based alternative to annotation processors or bytecode instrumentation — but tightly integrated into the TeaVM compilation pipeline.
Core concepts
The metaprogramming API lets a meta-method (compile-time Java code) generate the body of a target method (runtime Java code). The meta-method runs at TeaVM compilation time, inspects its arguments as compile-time descriptors, and uses the Metaprogramming API to emit the code that will actually execute at runtime.
All methods on Metaprogramming throw UnsupportedOperationException when called outside a TeaVM compile-time environment; the class exists only to give you a typed API.
The two worlds
There are two separate execution contexts, and it is essential to keep them apart:
| Compile-time world | Runtime world | |
|---|---|---|
| When it runs | During TeaVM compilation | In the browser / on the JVM after transpilation |
| What you can do | Inspect types, iterate fields/methods, call emit/lazy/exit |
Run the generated code |
| Java values | Ordinary Java objects, ReflectClass, ReflectMethod, … |
Ordinary Java objects |
| Represented as | Regular variables in the meta-method | Value<T> handles in the meta-method |
The meta-method lives entirely in the compile-time world. Lambdas you pass to emit(), lazy(), and exit() are snippets of runtime code, not closures executed at compile time.
@Meta — declaring a meta-method
Annotate a native method with @Meta to tell the compiler that a meta-method exists alongside it in the same class. The meta-method has the same name, returns void, and is static. Its parameters are a remapped version of the target method's parameters:
Class<X>parameters becomeReflectClass<X>— the class argument that was passed at the call-site is resolved at compile time.- All other parameters become
Value<T>— a handle to the runtime value that the caller will pass. - For instance methods, an implicit first parameter
Value<TheClass>representing the receiver is prepended.
// Target method — called from normal code
@Meta
static native int classNameLength(Class<?> cls, int add);
// Meta-method — runs at compile time to generate the body of the above
static void classNameLength(ReflectClass<?> cls, Value<Integer> add) {
int length = cls.getName().length(); // compile-time computation
exit(() -> length + add.get()); // emit the return expression
}
When TeaVM encounters a call to classNameLength(String.class, 3), it runs the meta-method with cls = ReflectClass<String> and add = Value<Integer> (bound to the runtime argument 3). The meta-method computes length at compile time and emits code equivalent to return 14 + add;.
Overloading
Multiple @Meta-annotated methods can share the same name as long as they have different signatures. Each gets its own meta-method:
@Meta private static native String callDebug(Class<?> cls, Object obj);
static void callDebug(ReflectClass<?> cls, Value<Object> obj) { … }
@Meta private static native String callDebug(Class<?> cls, Object obj, String a, int b);
static void callDebug(ReflectClass<?> cls, Value<Object> obj, Value<String> a, Value<Integer> b) { … }
Value<T> — the bridge between worlds
Value<T> is an opaque handle to a runtime value. You obtain Value<T> instances in two ways:
- As parameters to a meta-method — the framework wraps the callee's arguments.
- From
emit()— callingemit(() -> expr)executesexprat runtime and returns aValue<T>that represents the result.
The only method on Value<T> is T get(). Its semantics depend on where you call it:
- Inside a lambda passed to
emit()orlazy()— this is the emitter domain.get()splices the held runtime value into the generated code. This is the only valid place to call it. - Outside the emitter domain — calling
get()throwsIllegalStateExceptionat compile time. You cannot observe runtime values at compile time.
Capturing compile-time variables
Inside an emitter lambda, you can freely reference compile-time-local variables. They are captured by value and become compile-time constants in the generated code:
static void classNameLength(ReflectClass<?> cls, Value<Integer> add) {
int length = cls.getName().length(); // computed at compile time
exit(() -> length + add.get()); // `length` is a constant; `add` is a runtime value
}
Passing Value handles around
Value<T> handles are plain Java objects and can be stored in arrays, local variables, or passed to helper methods (that are @CompileTime or called from within a meta-method). The lambda you give to emit() / lazy() can then call .get() on any Value<T> it can reach:
static void captureArray(Value<Integer> a, Value<String> b) {
Value<?>[] parts = { a, emit(() -> ":"), b };
exit(() -> String.valueOf(parts[0].get()) + parts[1].get() + parts[2].get());
}
emit() — producing runtime code
static <T> Value<T> emit(Computation<T> computation)
static void emit(Action action)
emit() is the primary tool for generating runtime code. The lambda body you provide becomes a fragment of the generated method body.
emit(Computation<T>)— the lambda returns a value;emitreturns aValue<T>handle to that result.emit(Action)— the lambda returns nothing; use this for side-effecting statements.
Multiple emit() calls build up the generated method body sequentially:
static <T> void createProxyWithBoxedParameters(ReflectClass<T> proxyType) {
Value<T> proxy = proxy(proxyType, (instance, method, args) -> {
Value<StringBuilder> sb = emit(() -> new StringBuilder()); // allocate at runtime
String name = method.getName(); // compile-time constant
emit(() -> sb.get().append(name).append('(')); // append name
for (int i = 0; i < args.length; ++i) {
Value<Object> arg = args[i];
emit(() -> sb.get().append(',').append(arg.get()));
}
emit(() -> sb.get().append(')'));
exit(() -> sb.get().toString());
});
exit(() -> proxy.get());
}
Compile-time variables (name, arg) referenced inside lambdas become inlined constants in the generated code. Runtime values are accessed via .get().
Emitting a Value as a computation
You can pass an existing Value<T> directly to emit() as a Computation<T> (because Value<T> is a supertype):
Value<String> v = emit(() -> "hello");
Value<String> v2 = emit(v); // emits code that reads the same runtime variable
lazy() — conditional code emission
static <T> Value<T> lazy(Computation<T> computation)
lazy() creates a Value<T> whose computation is deferred — the lambda body is only emitted into the generated code when (and each time) the resulting Value<T> is read via .get() inside another emitter.
This is the primary tool for conditional dispatch and building chains of decisions. The key insight is that lazy() never unconditionally emits code; the code is spliced in wherever the value is consumed.
Idiom: conditional field lookup
static void fieldType(ReflectClass<Object> cls, Value<String> name) {
// Start with a default: "not found"
Value<String> result = lazy(() -> null);
for (ReflectField field : cls.getDeclaredFields()) {
String type = field.getType().getName(); // compile-time constant
String fieldName = field.getName(); // compile-time constant
Value<String> previous = result; // capture current chain
// Each iteration wraps the previous chain: if this field matches, return its type;
// otherwise delegate to the rest of the chain.
result = lazy(() -> fieldName.equals(name.get()) ? type : previous.get());
}
Value<String> type = result;
exit(() -> type.get());
}
At runtime this generates a cascade of if/else checks without any reflection, because all field names and type strings are compile-time constants inlined by the code generator.
Idiom: short-circuit evaluation
static void withLazy(Value<WithSideEffect> a, Value<WithSideEffect> b) {
Value<Boolean> first = lazy(() -> a.get().getValue() > 0);
Value<Boolean> second = lazy(() -> b.get().getValue() > 0);
// `second` is only evaluated if `first` is false — standard || semantics
exit(() -> first.get() || second.get() ? 1 : 2);
}
If first.get() is true at runtime, the second computation is never executed.
lazyFragment()
static <T> Value<T> lazyFragment(LazyComputation<T> computation)
A lower-level variant where the lambda itself returns a Value<T> (i.e. it calls emit() / lazy() internally). Prefer lazy() in most cases.
exit() — returning values
static void exit(Computation<?> returnValue)
static void exit()
exit() terminates the generated method by emitting a return statement.
exit(() -> expr)— emitreturn expr;and stop generating.exit()— emitreturn;(for void methods).
exit() can be called conditionally in an if/else to generate different return paths:
static void callDebug(ReflectClass<?> cls, Value<Object> obj) {
ReflectMethod method = cls.getMethod("debug");
if (method == null) {
exit(() -> "missing"); // generates: return "missing";
} else {
exit(() -> method.invoke(obj.get())); // generates: return obj.debug();
}
}
After exit() returns to the meta-method, control flow in the meta-method continues normally, but further emit() calls after exit() extend unreachable branches in the generated IR; prefer not to emit anything meaningful after exit().
unsupportedCase()
static void unsupportedCase()
Signals that the current set of compile-time arguments cannot be handled by this meta-method. When called, the compiler skips this usage and reports a compilation error (the specific call-site is flagged as unsupported). Use it as a guard when your meta-method only handles a known finite set of types:
static void classNameLength(ReflectClass<?> cls, Value<Integer> add) {
if (cls != findClass(Object.class) && cls != findClass(Integer.class)) {
unsupportedCase();
return;
}
// … handle the supported cases
}
proxy() — generating anonymous implementations
static <T> Value<T> proxy(Class<T> type, InvocationHandler<T> handler)
static <T> Value<T> proxy(ReflectClass<T> type, InvocationHandler<T> handler)
Creates a new anonymous class at compile time that implements or extends type, generates a method body for each abstract method by calling handler, and returns a Value<T> holding an instance of the new class.
InvocationHandler
interface InvocationHandler<T> {
void invoke(Value<T> proxy, ReflectMethod method, Value<Object>[] args);
}
handler.invoke is called at compile time, once per abstract method. It receives:
proxy— aValue<T>representingthisinside the generated method.method— compile-time reflection of the method being generated.args—Value<Object>[]for each parameter, boxed toObject. Primitive arguments are auto-boxed.
Inside invoke, use emit(), lazy(), and exit() exactly as in a normal meta-method. You must call exit() (or let the compiler insert a default return via falling off the end).
Example: interface proxy
@Meta
private static native <T> T createProxy(Class<T> proxyType, String add);
private static <T> void createProxy(ReflectClass<T> proxyType, Value<String> add) {
Value<T> proxy = proxy(proxyType, (instance, method, args) -> {
String name = method.getName(); // compile-time: method name becomes a constant
exit(() -> name + add.get()); // runtime: concatenate the name with `add`
});
exit(() -> proxy.get());
}
Calling createProxy(MyInterface.class, "!") generates a class whose every method returns "<methodName>!".
Default return values
If invoke does not call exit(), the compiler inserts a default return: null for object types, 0 for numeric types, false for booleans, void for void methods. This makes it easy to log method calls without caring about return types:
Value<T> proxy = proxy(proxyType, (instance, method, args) -> {
String name = method.getName();
emit(() -> log.append(name + ";")); // side effect only, no exit() call
});
Boxing of primitive arguments
All args are Value<Object>. When the real parameter type is a primitive, the value is auto-boxed before being handed to the handler. The handler is responsible for unboxing if needed (e.g. by calling a typed method on the boxed value inside the emitter lambda).
MetaprogrammingProvider — annotation-driven generators
As an alternative to @Meta, you can register a MetaprogrammingProvider via the Java SPI. This allows you to attach code generation to arbitrary annotations without touching the target class.
Interfaces
public interface MetaprogrammingProvider {
MethodGenerator provide(ReflectMethodDescriptor method);
}
public interface MethodGenerator {
void generate(MethodGeneratorContext context);
}
public interface MethodGeneratorContext {
ReflectMethodDescriptor method();
List<? extends Value<?>> parameters();
Value<?> callReceiver(); // null for static methods
}
Registration
Create a file META-INF/services/org.teavm.metaprogramming.MetaprogrammingProvider listing your implementation class, just like any Java SPI:
com.example.MyMetaprogrammingProvider
How it works
- TeaVM discovers all
MetaprogrammingProviderimplementations via SPI. - For every native method encountered during compilation, it calls
provider.provide(method)on each registered provider. - If
providereturns a non-nullMethodGenerator, that generator'sgeneratemethod is called with the context, and the generator usesemit(),lazy(),exit()to produce the method body.
Example
// Annotation that triggers generation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ArgWithType { }
// Provider (registered via SPI)
@CompileTime
public class MyProvider implements MetaprogrammingProvider {
@Override
public MethodGenerator provide(ReflectMethodDescriptor method) {
if (method.getAnnotation(ArgWithType.class) != null) {
return this::generate;
}
return null;
}
private void generate(MethodGeneratorContext ctx) {
Value<StringBuilder> sb = emit(() -> new StringBuilder());
for (int i = 0; i < ctx.method().getParameterCount(); i++) {
String typeName = ctx.method().getParameterType(i).toString();
Value<?> param = ctx.parameters().get(i);
emit(() -> sb.get().append(typeName).append(": ").append(param.get()).append("\n"));
}
exit(() -> sb.get().toString());
}
}
// Usage — just annotate a native method; no @Meta pairing needed
@ArgWithType
private static native String describe(int a, String b);
Calling describe(1, "hello") at runtime produces "int: 1\njava.lang.String: hello\n".
The provider approach is suitable when the same generation strategy applies across many methods, or when you cannot modify the class that declares the target methods.
@CompileTime — compile-time-only classes
@Target({ ElementType.TYPE, ElementType.PACKAGE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CompileTime
Mark a class or a whole package with @CompileTime to indicate that this class should be picked by Metaprogramming API. Without it, you'll get error trying to call methods of the class from Metaprogramming API.
Use @CompileTime for:
- Helper classes that only call
emit(),lazy(), etc. MetaprogrammingProviderimplementations.- Utility classes shared among multiple meta-methods.
Without @CompileTime, a class used from meta-methods may accidentally be pulled into the compiled output.
Package-level annotation
Put @CompileTime in package-info.java to mark an entire package:
// package-info.java
@CompileTime
package com.example.generators;
import org.teavm.metaprogramming.CompileTime;
All classes in the package inherit the annotation.
Sharing generators across classes
A @CompileTime helper class can expose methods that call emit() / lazy(), and these methods can be called from any meta-method in the same compilation unit:
@CompileTime
public class WrapperGenerator {
public Value<String> wrap(String prefix, String suffix, String value) {
return emit(() -> prefix + value + suffix);
}
}
// In a meta-method:
static void compileTimeClass(Value<Boolean> ignored) {
Value<String> result = new WrapperGenerator().wrap("[", "]", "foo");
exit(() -> result.get());
}
Reflection API
The compile-time reflection API (ReflectClass, ReflectMethod, ReflectField) mirrors java.lang.reflect but operates on compile-time class data. Instances are obtained via Metaprogramming.findClass() or received as meta-method parameters.
static ReflectClass<?> findClass(String name) // by fully-qualified name
static <T> ReflectClass<T> findClass(Class<T> cls)
static <T> ReflectClass<T[]> arrayClass(ReflectClass<T> componentType)
ReflectClass<T>
Mirrors java.lang.Class. Key additions over standard reflection:
| Method | Description |
|---|---|
asJavaClass() |
Convert to Class<T> for use inside emitter lambdas |
createArray(Value<Integer> size) |
Emit array creation |
getArrayElement(Value<Object> array, Value<Integer> index) |
Emit array element access |
getArrayLength(Value<Object> array) |
Emit array length read |
Use isAssignableFrom(Class<?>) and isAssignableFrom(ReflectClass<?>) to check type compatibility at compile time.
ReflectMethod
Object invoke(Object obj, Object... args) // emit an instance/static method call
Object construct(Object... args) // emit a constructor call
Both methods are called inside an emitter lambda. Their arguments are either Value<?> handles (unwrapped by the framework) or plain objects (treated as compile-time constants). The return value is a Value<T> when returned from an emitter lambda.
// Call an instance method
exit(() -> method.invoke(obj.get(), a.get(), b.get()));
// Invoke a constructor
exit(() -> ctor.construct(a.get(), b.get()));
Look up constructors by the special name "<init>":
ReflectMethod ctor = type.getMethod("<init>", stringClass, intClass);
getMethods() / getDeclaredMethods() return all methods visible/declared on the class. getMethod() searches the entire hierarchy; getDeclaredMethod() looks only in the current class. The J variants (getDeclaredJMethod, getJMethod) accept Class<?> instead of ReflectClass<?> for convenience.
ReflectField
Object get(Object target) // emit a field read
void set(Object target, Object value) // emit a field write
Again, called inside emitter lambdas:
exit(() -> field.get(obj.get())); // emit: return obj.fieldName;
emit(() -> field.set(obj.get(), val)); // emit: obj.fieldName = val;
Annotations
All reflection types implement ReflectAnnotatedElement:
<T extends Annotation> T getAnnotation(Class<T> type)
Annotation instances returned at compile time are real Java annotation proxy objects that you can read normally:
TestAnnotation ann = method.getAnnotation(TestAnnotation.class);
if (ann != null) {
String value = ann.a(); // read annotation attribute at compile time
}
Diagnostics
static Diagnostics getDiagnostics()
interface Diagnostics {
void error(SourceLocation location, String message, Object... params)
void warning(SourceLocation location, String message, Object... params)
}
Report compile-time errors and warnings. Errors cause compilation to fail; warnings are informational. params may include ReflectClass, ReflectMethod, ReflectField, or Class<?> instances — they are formatted as their respective names.
In the message you should put placeholders for arguments. Arguments have form: {{<specifier><index>}},
where specifier is one of:
t- matchesReflectClass;m- matchesReflectMethod;f- matchesReflectField;
and
Metaprogramming.getDiagnostics().error(
new SourceLocation(callerMethod),
"Unsupported type: {{t0}}",
cls
);
Source locations
static void location(String fileName, int lineNumber)
static void defaultLocation()
static SourceLocation getLocation()
By default, emitted instructions carry no debug source location. Use location() to associate subsequent emit() / lazy() / exit() calls with a specific source position (for source-maps and error messages). defaultLocation() removes the override.
getLocation() returns the current forced location (or the location of the original call-site if no override is set), useful for passing to Diagnostics.
Utilities
getClassLoader()
static ClassLoader getClassLoader()
Returns the class-loader used during compilation. Useful for loading resources or classes by name using standard Java APIs.
getResources()
static Iterator<Resource> getResources(String name)
Iterates over classpath resources with the given name. Useful for processing data files at compile time and embedding results as constants in the generated code.
static void initFromConfig(ReflectClass<?> cls) {
Iterator<Resource> resources = getResources("config.properties");
while (resources.hasNext()) {
try (InputStream is = resources.next().open()) {
Properties props = new Properties();
props.load(is);
String val = props.getProperty("key");
emit(() -> System.out.println(val)); // val is a compile-time constant
}
}
}
createClass()
static ReflectClass<?> createClass(byte[] bytecode)
Submits a dynamically-generated class (as raw bytecode) into the compilation unit and returns a ReflectClass<?> for it. The class is treated like any other class — it can be used with proxy(), methods can be invoked via ReflectMethod, etc.