Skip to content

Commit a7a14ae

Browse files
committed
TeaVM 0.15.0-dev-3 with workarounds
1 parent 6bce9a6 commit a7a14ae

8 files changed

Lines changed: 233 additions & 68 deletions

File tree

bridge/build.gradle.kts

Lines changed: 19 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -371,59 +371,17 @@ val stubs = listOf(
371371

372372
val stubsClassesDir = layout.buildDirectory.dir("generated/teavm-stubs/classes")
373373

374-
// TeaVM's classlib jar ships `META-INF/teavm.properties` with the directive
375-
// `includePackageHierarchy|java=false` — meaning every classpath entry whose class names start
376-
// with `java.` is *excluded* by default (TeaVM expects to provide them itself, via the
377-
// `org.teavm.classlib.java.*` → `java.*` renaming). Our stubs jar carries an additional
378-
// `META-INF/teavm.properties` that *re-includes* the specific java.* / jakarta.* / javax.*
379-
// sub-packages we ship stubs for, so the renamer doesn't drop them on the floor.
380-
val teavmStubsIncludes = listOf(
381-
// Mapping rules so my generated `org.teavm.classlib.<pkg>.T<Class>` stubs surface as the
382-
// requested `<pkg>.<Class>` classes. teavm-classlib already ships these for `java.*`; the
383-
// jakarta.* and javax.* lines below add equivalent prefixes for the packages it doesn't
384-
// cover. Package-hierarchy rules cascade to sub-packages.
385-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.jakarta=T",
386-
"mapPackageHierarchy|org.teavm.classlib.jakarta=jakarta",
387-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.javax=T",
388-
"mapPackageHierarchy|org.teavm.classlib.javax=javax",
389-
// org.w3c.* and org.xml.* — same shape, distinct package roots.
390-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.w3c=T",
391-
"mapPackageHierarchy|org.teavm.classlib.org.w3c=org.w3c",
392-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.xml=T",
393-
"mapPackageHierarchy|org.teavm.classlib.org.xml=org.xml",
394-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.glassfish=T",
395-
"mapPackageHierarchy|org.teavm.classlib.org.glassfish=org.glassfish",
396-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.jakarta.xml=T",
397-
"mapPackageHierarchy|org.teavm.classlib.jakarta.xml=jakarta.xml",
398-
// com.beanit, com.google.gson, io.r2dbc — leaf libraries we excluded from the dep graph;
399-
// these prefixes let the stubs surface where TeaVM expects to find the original classes.
400-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.com.beanit=T",
401-
"mapPackageHierarchy|org.teavm.classlib.com.beanit=com.beanit",
402-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.com.google=T",
403-
"mapPackageHierarchy|org.teavm.classlib.com.google=com.google",
404-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.io.r2dbc=T",
405-
"mapPackageHierarchy|org.teavm.classlib.io.r2dbc=io.r2dbc",
406-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.jooq=T",
407-
"mapPackageHierarchy|org.teavm.classlib.org.jooq=org.jooq",
408-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.reactivestreams=T",
409-
"mapPackageHierarchy|org.teavm.classlib.org.reactivestreams=org.reactivestreams",
410-
// SLF4J shim: org.teavm.classlib.org.slf4j.TLoggerFactory → org.slf4j.LoggerFactory.
411-
// Replaces SLF4J's real LoggerFactory whose provider-discovery static init would
412-
// otherwise abort the moment any rell-base `companion object: KLogging()` resolves.
413-
"stripPrefixFromPackageHierarchyClasses|org.teavm.classlib.org.slf4j=T",
414-
"mapPackageHierarchy|org.teavm.classlib.org.slf4j=org.slf4j",
415-
// teavm-classlib excludes javax.xml from project-classpath lookups by default
416-
// (`includePackageHierarchy|javax.xml=false`); the rename above re-enables it for the
417-
// sub-tree we ship stubs for.
418-
"includePackageHierarchy|javax.xml=true",
419-
)
374+
// The `org.teavm.classlib.<pkg>.T<Class>` stubs below are surfaced as the requested
375+
// `<pkg>.<Class>` classes by RellPlaygroundSubstitutionPolicy (a SubstitutionPolicy SPI). Up to
376+
// 0.14 this remapping was driven by `mapPackageHierarchy` / `stripPrefixFromPackageHierarchyClasses`
377+
// directives in a `META-INF/teavm.properties` we shipped in this jar; 0.15.0-dev-3 deleted the
378+
// classpath property-renamer from teavm-core, so the rules now live in that policy class instead.
420379

421380
val generateTeavmStubs by tasks.registering {
422381
description = "Emits empty stub bytecode for JDK packages TeaVM's classlib omits."
423382
group = LifecycleBasePlugin.BUILD_GROUP
424383

425384
inputs.property("stubs", stubs.joinToString { "${it.name}|${it.isInterface}" })
426-
inputs.property("includes", teavmStubsIncludes.joinToString("\n"))
427385
outputs.dir(stubsClassesDir)
428386

429387
doLast {
@@ -465,11 +423,6 @@ val generateTeavmStubs by tasks.registering {
465423
classFile.parentFile.mkdirs()
466424
classFile.writeBytes(cw.toByteArray())
467425
}
468-
469-
// teavm.properties: re-includes the java.* / jakarta.* / javax.* sub-packages we ship.
470-
val propsFile = outDir.resolve("META-INF/teavm.properties")
471-
propsFile.parentFile.mkdirs()
472-
propsFile.writeText(teavmStubsIncludes.joinToString("\n", postfix = "\n"))
473426
}
474427
}
475428

@@ -521,27 +474,26 @@ dependencies {
521474

522475
implementation(libs.rell.api.base)
523476

524-
// teavm-jso brings the @JSExport annotation used to expose static Kotlin methods to JS.
525-
// The annotation has no runtime semantics on JVM; TeaVM consumes it at compile time.
526477
compileOnly(libs.teavm.jso)
527-
528-
// teavm-core: needed to compile the TeaVMPlugin / ClassHolderTransformer at
529-
// com.chromia.rellplayground.teavm.*. Marked `compileOnly` because the plugin classes are
530-
// loaded by TeaVM's own daemon (which already has teavm-core on its classpath) — shipping
531-
// teavm-core itself into the compiled output would bloat the JS bundle with classes TeaVM
532-
// never reaches at runtime.
533478
compileOnly(libs.teavm.core)
479+
compileOnly(libs.teavm.extension.spi)
534480

535-
// TeaVM classpath. teavm(...) adds the entry to the configuration TeaVM compiles from
536-
// (in addition to runtimeClasspath). jsoApis provides DOM + JSON types — we use a small
537-
// subset (e.g. nothing yet) but it's required for the JS frontend's helpers.
538481
teavm(libs.teavm.jso.apis)
539482
}
540483

541-
// Tensor-probe parameters. Override on the CLI with -Prell.teavm.opt=BALANCED, -Prell.teavm.fast=true.
484+
// TeaVM codegen config. Overridable on the CLI (-Prell.teavm.opt=..., -Prell.teavm.fast=...,
485+
// -Prell.teavm.obf=...) but the defaults below are the only combination that produces a *working*
486+
// bridge on 0.15.0-dev-3:
487+
// - fastGlobalAnalysis = true is mandatory: precise analysis (fast=false) over-prunes
488+
// reflectively-reached init and emits JS that references eliminated symbols, so every
489+
// fast=false build crashes at runtime (C_LibBridge.instance uninitialized / ReferenceError).
490+
// - With fastGlobalAnalysis on, the optimizer's size/codegen passes are bypassed, so NONE /
491+
// BALANCED / AGGRESSIVE yield a byte-identical artifact — the opt level only changes build
492+
// time (AGGRESSIVE is just slower for the same output). BALANCED is the fastest viable build.
542493
val probeOpt: OptimizationLevel = (project.findProperty("rell.teavm.opt") as String?)
543-
?.let { OptimizationLevel.valueOf(it) } ?: OptimizationLevel.NONE
544-
val probeFast: Boolean = (project.findProperty("rell.teavm.fast") as String?)?.toBoolean() ?: false
494+
?.let { OptimizationLevel.valueOf(it) } ?: OptimizationLevel.BALANCED
495+
val probeFast: Boolean = (project.findProperty("rell.teavm.fast") as String?)?.toBoolean() ?: true
496+
val probeObfuscated: Boolean = (project.findProperty("rell.teavm.obf") as String?)?.toBoolean() ?: true
545497

546498
teavm {
547499
all {
@@ -578,7 +530,7 @@ teavm {
578530
optimization = probeOpt
579531
outOfProcess = true
580532
processMemory = 8192
581-
obfuscated = true
533+
obfuscated = probeObfuscated
582534
sourceMap = false
583535
targetFileName = "rell-playground-bridge.wasm"
584536
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (C) 2026 ChromaWay AB. See LICENSE for license information.
3+
*/
4+
5+
package com.chromia.rellplayground.teavm;
6+
7+
import java.util.Set;
8+
import org.teavm.model.ClassHolder;
9+
import org.teavm.model.ClassHolderTransformer;
10+
import org.teavm.model.ClassHolderTransformerContext;
11+
import org.teavm.model.ElementModifier;
12+
import org.teavm.model.MethodHolder;
13+
import org.teavm.model.Program;
14+
import org.teavm.model.emit.ProgramEmitter;
15+
16+
/**
17+
* Works around a TeaVM 0.15 WasmGC codegen crash. {@code WasmGCMethodGenerator.createInstanceFunction}
18+
* / {@code generateMethodBody} guard against {@code STATIC} and {@code NATIVE} methods but not
19+
* {@code ABSTRACT} ones, so when an abstract method becomes a virtual-dispatch target the generator
20+
* calls {@code generateRegularMethodBody}, which does {@code Objects.requireNonNull(getProgram())}
21+
* and NPEs. TeaVM catches the NPE and emits an {@code unreachable} body (so the {@code .wasm} is
22+
* fine), but it also calls {@code diagnostics.error}, which fails the build.
23+
*
24+
* <p>Giving the offending abstract methods a real (throwing) body removes the null program, so no
25+
* exception fires. The bodies are dead: every concrete implementor of these interfaces overrides
26+
* them, so virtual dispatch never lands on the synthetic body. {@code throw} (rather than a silent
27+
* return) preserves TeaVM's trap semantics and surfaces a clear error if one is ever reached.
28+
*
29+
* <p>Scoped to the specific interfaces TeaVM trips on (extend {@link #DEABSTRACT_CLASSES} as new
30+
* ones surface) — de-abstracting the whole hierarchy would be wrong. The underlying bug should be
31+
* fixed upstream (konsoletyper/teavm — skip abstract methods in the WasmGC method generator).
32+
*/
33+
final class AbstractMethodBodyStubber implements ClassHolderTransformer {
34+
private static final Set<String> DEABSTRACT_CLASSES = Set.of(
35+
// Reached via Rell's INSERT path: TableImpl.field -> Tools.tableField -> Fields.field.
36+
"org.jooq.Fields");
37+
38+
@Override
39+
public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) {
40+
if (!DEABSTRACT_CLASSES.contains(cls.getName())) {
41+
return;
42+
}
43+
for (MethodHolder method : cls.getMethods()) {
44+
if (method.hasModifier(ElementModifier.ABSTRACT)
45+
&& !method.hasModifier(ElementModifier.NATIVE)
46+
&& method.getProgram() == null) {
47+
method.getModifiers().remove(ElementModifier.ABSTRACT);
48+
method.setProgram(buildThrowStub(method, context));
49+
}
50+
}
51+
}
52+
53+
private static Program buildThrowStub(MethodHolder method, ClassHolderTransformerContext context) {
54+
var emitter = ProgramEmitter.create(method, context.getHierarchy());
55+
emitter.construct(UnsupportedOperationException.class,
56+
emitter.constant("not in TeaVM: " + method.getOwnerName() + "." + method.getName()))
57+
.raise();
58+
return emitter.getProgram();
59+
}
60+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2026 ChromaWay AB. See LICENSE for license information.
3+
*/
4+
5+
package com.chromia.rellplayground.teavm;
6+
7+
import org.teavm.model.ClassHolder;
8+
import org.teavm.model.ClassHolderTransformer;
9+
import org.teavm.model.ClassHolderTransformerContext;
10+
import org.teavm.model.GenericTypeParameter;
11+
12+
/**
13+
* Works around a second null-handling bug in TeaVM 0.15's WasmGC reflection metadata generator.
14+
* {@code ClassReader.getGenericParameters()} returns {@code null} for any class without a generic
15+
* signature, but {@code ReflectionMetadataGenerator.generateClassMetadata} forwards that null
16+
* straight into {@code generateTypeParameters}, which does {@code for (var p : params)} and NPEs —
17+
* for any class that has annotations / reflectable members / inner classes (so it isn't skipped)
18+
* yet no type variables.
19+
*
20+
* <p>Normalising the model so every class reports an empty array instead of null sidesteps it.
21+
* Like the rest of this package this is a TeaVM workaround that should be fixed upstream
22+
* (konsoletyper/teavm — the generator should treat null as "no type parameters").
23+
*/
24+
final class GenericParametersNormalizer implements ClassHolderTransformer {
25+
private static final GenericTypeParameter[] EMPTY = new GenericTypeParameter[0];
26+
27+
@Override
28+
public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) {
29+
if (cls.getGenericParameters() == null) {
30+
cls.setGenericParameters(EMPTY);
31+
}
32+
}
33+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2026 ChromaWay AB. See LICENSE for license information.
3+
*/
4+
5+
package com.chromia.rellplayground.teavm;
6+
7+
import java.util.HashSet;
8+
import java.util.Set;
9+
import org.teavm.dependency.AbstractDependencyListener;
10+
import org.teavm.dependency.DependencyAgent;
11+
import org.teavm.dependency.DependencyNode;
12+
import org.teavm.dependency.MethodDependency;
13+
import org.teavm.model.MethodReference;
14+
import org.teavm.model.ValueType;
15+
16+
/**
17+
* Works around a TeaVM 0.15 reflection-emit crash. When {@code Class.newInstance},
18+
* {@code Class.getDeclaredAnnotations} or {@code Class.getDeclaredClasses} is reachable, the
19+
* dependency analyser records every class that flows into them as a reflection target — including
20+
* the inner classes ({@code $DefaultImpls}, {@code $WhenMappings}, …) of every reachable class.
21+
* Many of those targets are never otherwise reached, so {@code TeaVM.link()} (which copies only
22+
* {@code getReachableClasses()} into the emit source) drops them. The JS {@code ClassInfoGenerator}
23+
* and the WasmGC {@code ReflectionMetadataGenerator} then iterate the same target set and
24+
* dereference each one's now-null {@code ClassReader}, NPEing the whole emit phase with no hint of
25+
* which class is to blame.
26+
*
27+
* <p>The fix pulls every reflection target that has bytecode back into the reachable set, so it
28+
* survives into {@code cutClasses} and the emit phase finds a non-null {@code ClassReader}. Linking
29+
* is only legal while the analyser is propagating (not in {@code completing}), so we attach a
30+
* consumer to each sink's class-value node in {@link #methodReached} and link targets as they
31+
* arrive. The set converges because the targets are overwhelmingly leaf inner classes.
32+
*
33+
* <p>Installed by {@link RellPlaygroundPlugin}. The TeaVM bug should also be reported upstream
34+
* (konsoletyper/teavm) — the generators ought to null-check or skip pruned targets.
35+
*/
36+
public final class ReflectionTargetPreserver extends AbstractDependencyListener {
37+
private static final Set<MethodReference> REFLECTIVE_SINKS = Set.of(
38+
new MethodReference(Class.class, "newInstance", Object.class),
39+
new MethodReference(Class.class, "getDeclaredAnnotations", java.lang.annotation.Annotation[].class),
40+
new MethodReference(Class.class, "getDeclaredClasses", Class[].class));
41+
42+
private final Set<String> linked = new HashSet<>();
43+
44+
@Override
45+
public void methodReached(DependencyAgent agent, MethodDependency method) {
46+
if (!REFLECTIVE_SINKS.contains(method.getReference())) {
47+
return;
48+
}
49+
DependencyNode classValues = method.getVariable(0).getClassValueNode();
50+
classValues.addConsumer(type -> {
51+
ValueType valueType = type.getValueType();
52+
if (!(valueType instanceof ValueType.Object)) {
53+
return;
54+
}
55+
var className = ((ValueType.Object) valueType).getClassName();
56+
// Only pull in targets that actually have bytecode; `linked` keeps this idempotent so
57+
// repeated propagation doesn't re-link and the analysis still converges.
58+
if (linked.add(className) && agent.getClassSource().get(className) != null) {
59+
agent.linkClass(className).initClass(null);
60+
}
61+
});
62+
}
63+
}

bridge/src/main/java/com/chromia/rellplayground/teavm/RellPlaygroundPlugin.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ public final class RellPlaygroundPlugin implements TeaVMPlugin {
2626
@Override
2727
public void install(TeaVMHost host) {
2828
host.add(new MissingMethodInjector());
29+
host.add(new GenericParametersNormalizer());
30+
host.add(new AbstractMethodBodyStubber());
31+
host.add(new ReflectionTargetPreserver());
2932
}
3033
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.chromia.rellplayground.teavm;
2+
3+
import java.util.function.Predicate;
4+
import org.teavm.extension.spi.substitution.SimpleSubstitutionPolicy;
5+
import org.teavm.extension.spi.substitution.SubstitutionSink;
6+
7+
/**
8+
* Redirects the handful of non-{@code java.*} packages we ship empty stubs for to our
9+
* {@code org.teavm.classlib.<pkg>.T<Class>} overlay classes — the exact transformation
10+
* teavm-classlib applies to {@code java.*} (see {@code ClasslibSubstitutionPolicy}).
11+
*
12+
* <p>This replaces the {@code mapPackageHierarchy} / {@code stripPrefixFromPackageHierarchyClasses}
13+
* directives our stubs jar used to carry in {@code META-INF/teavm.properties}. TeaVM 0.15.0-dev-3
14+
* deleted the classpath property-renamer from teavm-core (konsoletyper/teavm#1191) and moved the
15+
* whole mechanism to this {@link org.teavm.extension.spi.substitution.SubstitutionPolicy} SPI, so
16+
* without this class every stubbed class surfaces as "was not found" at link time.
17+
*
18+
* <p>Registered via {@code META-INF/services/org.teavm.extension.spi.substitution.SubstitutionPolicy};
19+
* TeaVM loads it with a {@code ServiceLoader} over the program classpath, alongside
20+
* {@link RellPlaygroundPlugin}.
21+
*/
22+
public class RellPlaygroundSubstitutionPolicy extends SimpleSubstitutionPolicy {
23+
// Package roots whose `<root>.….<Class>` references resolve to our
24+
// `org.teavm.classlib.<root>.….T<Class>` stubs. `java.*` is owned by teavm-classlib.
25+
private static final String[] ROOTS = {
26+
"jakarta",
27+
"javax",
28+
"org.w3c",
29+
"org.xml",
30+
"org.glassfish",
31+
"com.beanit",
32+
"com.google",
33+
"io.r2dbc",
34+
"org.jooq",
35+
"org.reactivestreams",
36+
// SLF4J shim: only `org.teavm.classlib.org.slf4j.TLoggerFactory` exists, so LoggerFactory
37+
// resolves to our no-op (sidestepping SLF4J's provider-discovery static init); every other
38+
// org.slf4j.* class has no T-stub and falls back to the real slf4j-api type.
39+
"org.slf4j",
40+
};
41+
42+
@Override
43+
public void contribute(SubstitutionSink sink) {
44+
Predicate<String> inAnyRoot = inPackage(ROOTS[0], true);
45+
for (int i = 1; i < ROOTS.length; i++) {
46+
inAnyRoot = inAnyRoot.or(inPackage(ROOTS[i], true));
47+
}
48+
sink.selectClasses(inAnyRoot)
49+
.packagePrefix("org.teavm.classlib.")
50+
.simpleNamePrefix("T");
51+
}
52+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.chromia.rellplayground.teavm.RellPlaygroundSubstitutionPolicy

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[versions]
22
kotlin = "2.3.0"
33
rell = "0.16.0"
4-
teavm = "0.15.0-dev-2"
4+
teavm = "0.15.0-dev-3"
55
node-gradle = "7.1.0"
66

77
[libraries]
@@ -14,6 +14,7 @@ rell-gtx = { module = "net.postchain.rell:rell-gtx", version.ref = "rell" }
1414
teavm-jso = { module = "org.teavm:teavm-jso", version.ref = "teavm" }
1515
teavm-jso-apis = { module = "org.teavm:teavm-jso-apis", version.ref = "teavm" }
1616
teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" }
17+
teavm-extension-spi = { module = "org.teavm:teavm-extension-spi", version.ref = "teavm" }
1718

1819
[plugins]
1920
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

0 commit comments

Comments
 (0)