jso: implement exporting Java methods to JS

Fix #785
This commit is contained in:
Alexey Andreev 2024-01-09 20:07:08 +01:00
parent cf850157f0
commit 8db406c603
58 changed files with 2115 additions and 831 deletions

View File

@ -27,7 +27,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -139,7 +138,6 @@ import org.teavm.runtime.RuntimeArray;
import org.teavm.runtime.RuntimeClass;
import org.teavm.runtime.RuntimeObject;
import org.teavm.vm.BuildTarget;
import org.teavm.vm.TeaVMEntryPoint;
import org.teavm.vm.TeaVMTarget;
import org.teavm.vm.TeaVMTargetController;
import org.teavm.vm.spi.TeaVMHostExtension;
@ -817,8 +815,7 @@ public class CTarget implements TeaVMTarget, TeaVMCHost {
private void generateMain(GenerationContext context, CodeWriter writer, IncludeManager includes,
ListableClassHolderSource classes, List<? extends ValueType> types) {
Iterator<? extends TeaVMEntryPoint> entryPointIter = controller.getEntryPoints().values().iterator();
String mainFunctionName = entryPointIter.hasNext() ? entryPointIter.next().getPublicName() : null;
var mainFunctionName = controller.getEntryPointName();
if (mainFunctionName == null) {
mainFunctionName = "main";
}
@ -936,15 +933,13 @@ public class CTarget implements TeaVMTarget, TeaVMCHost {
private void generateCallToMainMethod(IntrinsicContext context, InvocationExpr invocation) {
NameProvider names = context.names();
Iterator<? extends TeaVMEntryPoint> entryPointIter = controller.getEntryPoints().values().iterator();
if (entryPointIter.hasNext()) {
TeaVMEntryPoint entryPoint = entryPointIter.next();
context.importMethod(entryPoint.getMethod(), true);
String mainMethod = names.forMethod(entryPoint.getMethod());
context.writer().print(mainMethod + "(");
context.emit(invocation.getArguments().get(0));
context.writer().print(")");
}
var method = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class),
ValueType.parse(void.class));
context.importMethod(method, true);
String mainMethod = names.forMethod(method);
context.writer().print(mainMethod + "(");
context.emit(invocation.getArguments().get(0));
context.writer().print(")");
}
@Override

View File

@ -0,0 +1,32 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.backend.javascript;
import java.util.function.Consumer;
import org.teavm.backend.javascript.codegen.NamingStrategy;
import org.teavm.backend.javascript.codegen.SourceWriter;
public class ExportedDeclaration {
final Consumer<SourceWriter> name;
final Consumer<NamingStrategy> nameFreq;
final String alias;
public ExportedDeclaration(Consumer<SourceWriter> name, Consumer<NamingStrategy> nameFreq, String alias) {
this.name = name;
this.nameFreq = nameFreq;
this.alias = alias;
}
}

View File

@ -129,6 +129,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
private final Map<String, String> importedModules = new LinkedHashMap<>();
private JavaScriptTemplateFactory templateFactory;
private JSModuleType moduleType = JSModuleType.UMD;
private List<ExportedDeclaration> exports = new ArrayList<>();
private int maxTopLevelNames = 80_000;
@Override
@ -398,7 +399,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
var rememberingWriter = new RememberingSourceWriter(debugEmitter != null);
var renderer = new Renderer(rememberingWriter, asyncMethods, renderingContext, controller.getDiagnostics(),
methodGenerators, astCache, controller.getCacheStatus(), templateFactory);
methodGenerators, astCache, controller.getCacheStatus(), templateFactory, exports,
controller.getEntryPoint());
renderer.setProperties(controller.getProperties());
renderer.setProgressConsumer(controller::reportProgress);
@ -414,15 +416,19 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
renderer.renderStringPool();
renderer.renderStringConstants();
renderer.renderCompatibilityStubs();
for (var entry : controller.getEntryPoints().entrySet()) {
var alias = "$rt_export_" + entry.getKey();
var ref = entry.getValue().getMethod();
var alias = "$rt_export_main";
var ref = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class),
ValueType.parse(void.class));
if (classes.resolve(ref) != null) {
rememberingWriter.startVariableDeclaration().appendFunction(alias)
.appendFunction("$rt_mainStarter").append("(").appendMethod(ref);
rememberingWriter.append(")").endDeclaration();
rememberingWriter.appendFunction(alias).append(".")
.append("javaException").ws().append("=").ws().appendFunction("$rt_javaException")
.append(";").newLine();
exports.add(new ExportedDeclaration(w -> w.appendFunction(alias),
n -> n.functionName(alias), controller.getEntryPointName()));
}
for (var listener : rendererListeners) {
@ -448,8 +454,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
for (var module : importedModules.values()) {
naming.functionName(module);
}
for (var exportedName : controller.getEntryPoints().keySet()) {
naming.functionName("$rt_export_" + exportedName);
for (var export : exports) {
export.nameFreq.accept(naming);
}
var frequencyEstimator = new NameFrequencyEstimator();
runtime.replay(frequencyEstimator, RememberedSource.FILTER_REF);
@ -553,8 +559,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
}
private void printIIFStart(SourceWriter writer) {
for (var exportedName : controller.getEntryPoints().keySet()) {
writer.append("var ").appendGlobal(exportedName).append(";").softNewLine();
for (var export : exports) {
writer.append("var ").appendGlobal(export.alias).append(";").softNewLine();
}
writer.append("(function()").appendBlockStart();
@ -603,40 +609,42 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
}
private void printUmdEnd(SourceWriter writer) {
for (var export : controller.getEntryPoints().keySet()) {
writer.appendFunction("$rt_exports").append(".").append(export).ws().append("=").ws()
.appendFunction("$rt_export_" + export).append(";").softNewLine();
for (var export : exports) {
writer.appendFunction("$rt_exports").append(".").append(export.alias).ws()
.append("=").ws();
export.name.accept(writer);
writer.append(";").softNewLine();
}
writer.outdent().append("}));").newLine();
}
private void printCommonJsEnd(SourceWriter writer) {
for (var export : controller.getEntryPoints().keySet()) {
writer.appendFunction("exports.").append(export).ws().append("=").ws()
.appendFunction("$rt_export_" + export).append(";").softNewLine();
for (var export : exports) {
writer.append("exports.").append(export.alias).ws().append("=").ws();
export.name.accept(writer);
writer.append(";").softNewLine();
}
}
private void printIFFEnd(SourceWriter writer) {
for (var exportedName : controller.getEntryPoints().keySet()) {
writer.appendGlobal(exportedName).ws().append("=").ws().appendFunction("$rt_export_" + exportedName)
.append(";").softNewLine();
for (var export : exports) {
writer.appendGlobal(export.alias).ws().append("=").ws();
export.name.accept(writer);
writer.append(";").softNewLine();
}
writer.outdent().append("})();");
}
private void printES2015End(SourceWriter writer) {
if (controller.getEntryPoints().isEmpty()) {
return;
}
writer.append("export").ws().append("{").ws();
var first = true;
for (var exportedName : controller.getEntryPoints().keySet()) {
for (var export : exports) {
if (!first) {
writer.append(",").ws();
}
first = false;
writer.appendFunction("$rt_export_" + exportedName).append(" as ").append(exportedName);
export.name.accept(writer);
writer.append(" as ").append(export.alias);
}
writer.ws().append("};").softNewLine();
}

View File

@ -35,6 +35,7 @@ import org.teavm.ast.RegularMethodNode;
import org.teavm.ast.analysis.LocationGraphBuilder;
import org.teavm.ast.decompilation.DecompilationException;
import org.teavm.ast.decompilation.Decompiler;
import org.teavm.backend.javascript.ExportedDeclaration;
import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.backend.javascript.spi.GeneratedBy;
import org.teavm.backend.javascript.spi.Generator;
@ -89,11 +90,15 @@ public class Renderer implements RenderingManager {
private JavaScriptTemplateFactory templateFactory;
private boolean threadLibraryUsed;
private AstDependencyExtractor dependencyExtractor = new AstDependencyExtractor();
private List<ExportedDeclaration> exports;
private String entryPoint;
public static final MethodDescriptor CLINIT_METHOD = new MethodDescriptor("<clinit>", ValueType.VOID);
public Renderer(SourceWriter writer, Set<MethodReference> asyncMethods, RenderingContext context,
Diagnostics diagnostics, Map<MethodReference, Generator> generators,
MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory) {
MethodNodeCache astCache, CacheStatus cacheStatus, JavaScriptTemplateFactory templateFactory,
List<ExportedDeclaration> exports, String entryPoint) {
this.writer = writer;
this.classSource = context.getClassSource();
this.classLoader = context.getClassLoader();
@ -106,6 +111,8 @@ public class Renderer implements RenderingManager {
this.astCache = astCache;
this.cacheStatus = cacheStatus;
this.templateFactory = templateFactory;
this.exports = exports;
this.entryPoint = entryPoint;
}
@Override
@ -113,6 +120,27 @@ public class Renderer implements RenderingManager {
return writer;
}
@Override
public String getEntryPoint() {
return entryPoint;
}
@Override
public void exportMethod(MethodReference method, String alias) {
exports.add(new ExportedDeclaration(w -> w.appendMethod(method), n -> n.methodName(method), alias));
}
@Override
public void exportClass(String className, String alias) {
exports.add(new ExportedDeclaration(w -> w.appendClass(className), n -> n.className(className), alias));
}
@Override
public void exportFunction(String functionName, String alias) {
exports.add(new ExportedDeclaration(w -> w.appendFunction(functionName),
n -> n.functionName(functionName), alias));
}
public boolean isThreadLibraryUsed() {
return threadLibraryUsed;
}

View File

@ -19,13 +19,22 @@ import java.util.Properties;
import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.common.ServiceRepository;
import org.teavm.model.ListableClassReaderSource;
import org.teavm.model.MethodReference;
public interface RenderingManager extends ServiceRepository {
SourceWriter getWriter();
void exportMethod(MethodReference method, String alias);
void exportClass(String className, String alias);
void exportFunction(String functionName, String alias);
ListableClassReaderSource getClassSource();
ClassLoader getClassLoader();
Properties getProperties();
String getEntryPoint();
}

View File

@ -105,7 +105,6 @@ import org.teavm.backend.wasm.model.expression.WasmLoadInt32;
import org.teavm.backend.wasm.model.expression.WasmReturn;
import org.teavm.backend.wasm.model.expression.WasmSetLocal;
import org.teavm.backend.wasm.model.expression.WasmStoreInt32;
import org.teavm.backend.wasm.model.expression.WasmUnreachable;
import org.teavm.backend.wasm.optimization.UnusedFunctionElimination;
import org.teavm.backend.wasm.render.ReportingWasmBinaryStatsCollector;
import org.teavm.backend.wasm.render.WasmBinaryRenderer;
@ -176,7 +175,6 @@ import org.teavm.runtime.RuntimeClass;
import org.teavm.runtime.RuntimeObject;
import org.teavm.runtime.ShadowStack;
import org.teavm.vm.BuildTarget;
import org.teavm.vm.TeaVMEntryPoint;
import org.teavm.vm.TeaVMTarget;
import org.teavm.vm.TeaVMTargetController;
import org.teavm.vm.spi.TeaVMHostExtension;
@ -1197,29 +1195,23 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
public WasmExpression apply(InvocationExpr invocation, WasmIntrinsicManager manager) {
switch (invocation.getMethod().getName()) {
case "runMain": {
var entryPointIter = controller.getEntryPoints().values().iterator();
if (entryPointIter.hasNext()) {
TeaVMEntryPoint entryPoint = entryPointIter.next();
String name = manager.getNames().forMethod(entryPoint.getMethod());
WasmCall call = new WasmCall(name);
var arg = manager.generate(invocation.getArguments().get(0));
if (manager.isManagedMethodCall(entryPoint.getMethod())) {
var block = new WasmBlock(false);
block.setType(WasmType.INT32);
var callSiteId = manager.generateCallSiteId(invocation.getLocation());
block.getBody().add(manager.generateRegisterCallSite(callSiteId,
invocation.getLocation()));
block.getBody().add(arg);
arg = block;
}
call.getArguments().add(arg);
call.setLocation(invocation.getLocation());
return call;
} else {
var unreachable = new WasmUnreachable();
unreachable.setLocation(invocation.getLocation());
return unreachable;
var entryPoint = new MethodReference(controller.getEntryPoint(),
"main", ValueType.parse(String[].class), ValueType.parse(void.class));
String name = manager.getNames().forMethod(entryPoint);
WasmCall call = new WasmCall(name);
var arg = manager.generate(invocation.getArguments().get(0));
if (manager.isManagedMethodCall(entryPoint)) {
var block = new WasmBlock(false);
block.setType(WasmType.INT32);
var callSiteId = manager.generateCallSiteId(invocation.getLocation());
block.getBody().add(manager.generateRegisterCallSite(callSiteId,
invocation.getLocation()));
block.getBody().add(arg);
arg = block;
}
call.getArguments().add(arg);
call.setLocation(invocation.getLocation());
return call;
}
case "setCurrentThread": {
String name = manager.getNames().forMethod(new MethodReference(Thread.class,

View File

@ -24,11 +24,20 @@ import org.teavm.model.*;
public class DependencyAgent implements DependencyInfo, ServiceRepository {
private DependencyAnalyzer analyzer;
private String entryPoint;
DependencyAgent(DependencyAnalyzer analyzer) {
this.analyzer = analyzer;
}
public String getEntryPoint() {
return entryPoint;
}
void setEntryPoint(String entryPoint) {
this.entryPoint = entryPoint;
}
public DependencyNode createNode() {
return analyzer.createNode();
}

View File

@ -142,6 +142,11 @@ public abstract class DependencyAnalyzer implements DependencyInfo {
classType = getType("java.lang.Class");
}
public void setEntryPoint(String entryPoint) {
classSource.setEntryPoint(entryPoint);
agent.setEntryPoint(entryPoint);
}
public void setObfuscated(boolean obfuscated) {
classSource.obfuscated = obfuscated;
}
@ -290,20 +295,6 @@ public abstract class DependencyAnalyzer implements DependencyInfo {
classSource.addTransformer(transformer);
}
public void addEntryPoint(MethodReference methodRef, String... argumentTypes) {
ValueType[] parameters = methodRef.getDescriptor().getParameterTypes();
if (parameters.length + 1 != argumentTypes.length) {
throw new IllegalArgumentException("argumentTypes length does not match the number of method's arguments");
}
MethodDependency method = linkMethod(methodRef);
method.use();
DependencyNode[] varNodes = method.getVariables();
varNodes[0].propagate(getType(methodRef.getClassName()));
for (int i = 0; i < argumentTypes.length; ++i) {
varNodes[i + 1].propagate(getType(argumentTypes[i]));
}
}
private int propagationDepth;
void schedulePropagation(DependencyConsumer consumer, DependencyType type) {

View File

@ -47,6 +47,7 @@ class DependencyClassSource implements ClassHolderSource {
Map<String, Optional<ClassHolder>> cache = new LinkedHashMap<>(1000, 0.5f);
private ReferenceResolver referenceResolver;
private ClassInitInsertion classInitInsertion;
private String entryPoint;
DependencyClassSource(ClassReaderSource innerSource, Diagnostics diagnostics,
IncrementalDependencyRegistration dependencyRegistration, String[] platformTags) {
@ -117,10 +118,6 @@ class DependencyClassSource implements ClassHolderSource {
return generatedClasses.keySet();
}
public Collection<ClassHolder> getGeneratedClasses() {
return generatedClasses.values();
}
public boolean isGeneratedClass(String className) {
return generatedClasses.containsKey(className);
}
@ -129,6 +126,10 @@ class DependencyClassSource implements ClassHolderSource {
transformers.add(transformer);
}
void setEntryPoint(String entryPoint) {
this.entryPoint = entryPoint;
}
public void dispose() {
transformers.clear();
}
@ -163,5 +164,10 @@ class DependencyClassSource implements ClassHolderSource {
public void submit(ClassHolder cls) {
DependencyClassSource.this.submit(cls);
}
@Override
public String getEntryPoint() {
return entryPoint;
}
};
}

View File

@ -29,5 +29,7 @@ public interface ClassHolderTransformerContext {
boolean isStrict();
String getEntryPoint();
void submit(ClassHolder cls);
}

View File

@ -50,14 +50,17 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo {
private Map<MethodReference, MethodInfo> methodInfoMap = new HashMap<>();
private ListableClassReaderSource classes;
private ClassHierarchy hierarchy;
private String entryPoint;
private List<String> order = new ArrayList<>();
private List<? extends String> readonlyOrder = Collections.unmodifiableList(order);
private String currentAnalyzedClass;
private DependencyInfo dependencyInfo;
public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy) {
public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy,
String entryPoint) {
this.classes = classes;
this.hierarchy = hierarchy;
this.entryPoint = entryPoint;
}
public void analyze(DependencyInfo dependencyInfo) {
@ -99,6 +102,11 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo {
return;
}
if (className.equals(entryPoint)) {
classStatuses.put(className, DYNAMIC);
return;
}
var cls = classes.get(className);
if (cls == null || cls.getAnnotations().get(StaticInit.class.getName()) != null) {

View File

@ -23,7 +23,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -73,6 +72,7 @@ import org.teavm.model.ProgramCache;
import org.teavm.model.ValueType;
import org.teavm.model.analysis.ClassInitializerAnalysis;
import org.teavm.model.analysis.ClassInitializerInfo;
import org.teavm.model.instructions.ExitInstruction;
import org.teavm.model.instructions.InitClassInstruction;
import org.teavm.model.instructions.InvokeInstruction;
import org.teavm.model.optimization.ArrayUnwrapMotion;
@ -134,12 +134,13 @@ import org.teavm.vm.spi.TeaVMPlugin;
public class TeaVM implements TeaVMHost, ServiceRepository {
private static final MethodDescriptor MAIN_METHOD_DESC = new MethodDescriptor("main",
ValueType.arrayOf(ValueType.object("java.lang.String")), ValueType.VOID);
private static final MethodDescriptor CLINIT_DESC = new MethodDescriptor("<clinit>", ValueType.VOID);
private final DependencyAnalyzer dependencyAnalyzer;
private final AccumulationDiagnostics diagnostics = new AccumulationDiagnostics();
private final ClassLoader classLoader;
private final Map<String, TeaVMEntryPoint> entryPoints = new LinkedHashMap<>();
private final Map<String, TeaVMEntryPoint> readonlyEntryPoints = Collections.unmodifiableMap(entryPoints);
private String entryPoint;
private String entryPointName = "main";
private final Set<String> preservedClasses = new HashSet<>();
private final Set<String> readonlyPreservedClasses = Collections.unmodifiableSet(preservedClasses);
private final Map<Class<?>, Object> services = new HashMap<>();
@ -284,39 +285,50 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
return target.getPlatformTags();
}
public void entryPoint(String className, String name) {
if (entryPoints.containsKey(name)) {
throw new IllegalArgumentException("Entry point with public name `" + name + "' already defined "
+ "for class " + className);
}
var cls = dependencyAnalyzer.getClassSource().get(className);
if (cls == null) {
diagnostics.error(null, "There's no main class: '{{c0}}'", className);
return;
}
if (cls.getMethod(MAIN_METHOD_DESC) == null) {
diagnostics.error(null, "Specified main class '{{c0}}' does not have method '" + MAIN_METHOD_DESC + "'",
cls.getName());
return;
}
var mainMethod = dependencyAnalyzer.linkMethod(new MethodReference(className,
"main", ValueType.parse(String[].class), ValueType.VOID));
var entryPoint = new TeaVMEntryPoint(name, mainMethod);
dependencyAnalyzer.defer(() -> {
dependencyAnalyzer.linkClass(className).initClass(null);
mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;"));
mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String"));
mainMethod.use();
});
entryPoints.put(name, entryPoint);
public void setEntryPoint(String entryPoint) {
this.entryPoint = entryPoint;
}
public void entryPoint(String className) {
entryPoint(className, "main");
public void setEntryPointName(String entryPointName) {
this.entryPointName = entryPointName;
}
private void processEntryPoint() {
dependencyAnalyzer.setEntryPoint(entryPoint);
dependencyAnalyzer.addClassTransformer((c, context) -> {
if (c.getName().equals(entryPoint)) {
var clinit = c.getMethod(CLINIT_DESC);
if (clinit == null) {
clinit = new MethodHolder(CLINIT_DESC);
clinit.getModifiers().add(ElementModifier.STATIC);
var clinitProg = new Program();
clinitProg.createVariable();
var block = clinitProg.createBasicBlock();
block.add(new ExitInstruction());
clinit.setProgram(clinitProg);
c.addMethod(clinit);
}
}
});
var cls = dependencyAnalyzer.getClassSource().get(entryPoint);
if (cls == null) {
diagnostics.error(null, "There's no main class: '{{c0}}'", entryPoint);
return;
}
var mainMethod = cls.getMethod(MAIN_METHOD_DESC) != null
? dependencyAnalyzer.linkMethod(new MethodReference(entryPoint,
"main", ValueType.parse(String[].class), ValueType.VOID))
: null;
dependencyAnalyzer.defer(() -> {
dependencyAnalyzer.linkClass(entryPoint).initClass(null);
if (mainMethod != null) {
mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;"));
mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String"));
mainMethod.use();
}
});
}
public void preserveType(String className) {
@ -368,6 +380,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
return;
}
processEntryPoint();
dependencyAnalyzer.setAsyncSupported(target.isAsyncSupported());
dependencyAnalyzer.setInterruptor(() -> {
int progress = dependencyAnalyzer.getReachableClasses().size();
@ -453,7 +466,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
}
var classInitializerAnalysis = new ClassInitializerAnalysis(classSet,
dependencyAnalyzer.getClassHierarchy());
dependencyAnalyzer.getClassHierarchy(), entryPoint);
classInitializerAnalysis.analyze(dependencyAnalyzer);
classInitializerInfo = classInitializerAnalysis;
insertClassInit(classSet);
@ -535,13 +548,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
}
}
var initializers = target.getInitializerMethods();
if (initializers == null) {
initializers = entryPoints.values().stream().map(ep -> ep.getMethod()).collect(Collectors.toList());
}
for (var initMethod : initializers) {
addInitializersToEntryPoint(classes, initMethod);
}
addInitializersToEntryPoint(classes, new MethodReference(entryPoint, CLINIT_DESC));
}
private void addInitializersToEntryPoint(ClassHolderSource classes, MethodReference methodRef) {
@ -560,7 +567,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
Instruction first = block.getFirstInstruction();
for (String className : classInitializerInfo.getInitializationOrder()) {
var invoke = new InvokeInstruction();
invoke.setMethod(new MethodReference(className, "<clinit>", ValueType.VOID));
invoke.setMethod(new MethodReference(className, CLINIT_DESC));
first.insertPrevious(invoke);
}
}
@ -914,8 +921,13 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
}
@Override
public Map<String, TeaVMEntryPoint> getEntryPoints() {
return readonlyEntryPoints;
public String getEntryPoint() {
return entryPoint;
}
@Override
public String getEntryPointName() {
return entryPointName;
}
@Override
@ -1032,9 +1044,9 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
ListableClassReaderSourceAdapter(ClassReaderSource classSource, Set<String> classes) {
this.classSource = classSource;
this.classes = Collections.unmodifiableSet(classes.stream()
this.classes = classes.stream()
.filter(className -> classSource.get(className) != null)
.collect(Collectors.toSet()));
.collect(Collectors.toUnmodifiableSet());
}
@Override

View File

@ -15,7 +15,6 @@
*/
package org.teavm.vm;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
@ -48,7 +47,9 @@ public interface TeaVMTargetController {
boolean isFriendlyToDebugger();
Map<? extends String, ? extends TeaVMEntryPoint> getEntryPoints();
String getEntryPoint();
String getEntryPointName();
Set<? extends String> getPreservedClasses();

View File

@ -0,0 +1,27 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JSClass {
String name() default "";
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JSExport {
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface JSExportClasses {
Class<?>[] value();
}

View File

@ -16,12 +16,16 @@
package org.teavm.jso.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.backend.javascript.rendering.RenderingManager;
import org.teavm.backend.javascript.spi.VirtualMethodContributor;
import org.teavm.backend.javascript.spi.VirtualMethodContributorContext;
import org.teavm.jso.JSClass;
import org.teavm.model.AnnotationReader;
import org.teavm.model.ClassReader;
import org.teavm.model.ElementModifier;
import org.teavm.model.FieldReader;
import org.teavm.model.FieldReference;
import org.teavm.model.ListableClassReaderSource;
@ -36,104 +40,209 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
private SourceWriter writer;
private ListableClassReaderSource classSource;
private JSTypeHelper typeHelper;
private RenderingManager context;
@Override
public void begin(RenderingManager context, BuildTarget buildTarget) {
writer = context.getWriter();
classSource = context.getClassSource();
typeHelper = new JSTypeHelper(context.getClassSource());
this.context = context;
}
@Override
public void complete() {
exportClasses();
exportModule();
}
private void exportClasses() {
if (!hasClassesToExpose()) {
return;
}
writer.startVariableDeclaration().appendFunction("$rt_jso_marker")
.appendGlobal("Symbol").append("('jsoClass')").endDeclaration();
writer.append("(function()").ws().append("{").softNewLine().indent();
writer.append("var c;").softNewLine();
for (String className : classSource.getClassNames()) {
ClassReader classReader = classSource.get(className);
var methods = new HashMap<String, MethodDescriptor>();
var properties = new HashMap<String, PropertyInfo>();
for (var method : classReader.getMethods()) {
var methodAlias = getPublicAlias(method);
if (methodAlias != null) {
switch (methodAlias.kind) {
case METHOD:
methods.put(methodAlias.name, method.getDescriptor());
break;
case GETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.getter = method.getDescriptor();
break;
}
case SETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.setter = method.getDescriptor();
break;
}
}
writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent();
writer.append("let c;").softNewLine();
for (var className : classSource.getClassNames()) {
var classReader = classSource.get(className);
var hasExportedMembers = false;
hasExportedMembers |= exportClassInstanceMembers(classReader);
if (!className.equals(context.getEntryPoint())) {
hasExportedMembers |= exportClassStaticMembers(classReader);
if (hasExportedMembers && !typeHelper.isJavaScriptClass(className)
&& !typeHelper.isJavaScriptImplementation(className)) {
exportClassFromModule(classReader);
}
}
var isJsClassImpl = typeHelper.isJavaScriptImplementation(className);
if (methods.isEmpty() && properties.isEmpty() && !isJsClassImpl) {
continue;
}
writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;")
.softNewLine();
if (isJsClassImpl) {
writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;")
.softNewLine();
}
for (var aliasEntry : methods.entrySet()) {
if (classReader.getMethod(aliasEntry.getValue()) == null) {
continue;
}
if (isKeyword(aliasEntry.getKey())) {
writer.append("c[\"").append(aliasEntry.getKey()).append("\"]");
} else {
writer.append("c.").append(aliasEntry.getKey());
}
writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue())
.append(";").softNewLine();
}
for (var aliasEntry : properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) {
continue;
}
writer.append("Object.defineProperty(c,")
.ws().append("\"").append(aliasEntry.getKey()).append("\",")
.ws().append("{").indent().softNewLine();
writer.append("get:").ws().append("c.").appendVirtualMethod(propInfo.getter);
if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) {
writer.append(",").softNewLine();
writer.append("set:").ws().append("c.").appendVirtualMethod(propInfo.setter);
}
writer.softNewLine().outdent().append("});").softNewLine();
}
FieldReader functorField = getFunctorField(classReader);
if (functorField != null) {
writeFunctor(classReader, functorField.getReference());
}
}
writer.outdent().append("})();").newLine();
}
private boolean exportClassInstanceMembers(ClassReader classReader) {
var members = collectMembers(classReader, method -> !method.hasModifier(ElementModifier.STATIC));
var isJsClassImpl = typeHelper.isJavaScriptImplementation(classReader.getName());
if (members.methods.isEmpty() && members.properties.isEmpty() && !isJsClassImpl) {
return false;
}
writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(".prototype;")
.softNewLine();
if (isJsClassImpl) {
writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;")
.softNewLine();
}
for (var aliasEntry : members.methods.entrySet()) {
if (classReader.getMethod(aliasEntry.getValue()) == null) {
continue;
}
appendMethodAlias(aliasEntry.getKey());
writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue())
.append(";").softNewLine();
}
for (var aliasEntry : members.properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null || classReader.getMethod(propInfo.getter) == null) {
continue;
}
appendPropertyAlias(aliasEntry.getKey());
writer.append("get:").ws().append("c.").appendVirtualMethod(propInfo.getter);
if (propInfo.setter != null && classReader.getMethod(propInfo.setter) != null) {
writer.append(",").softNewLine();
writer.append("set:").ws().append("c.").appendVirtualMethod(propInfo.setter);
}
writer.softNewLine().outdent().append("});").softNewLine();
}
var functorField = getFunctorField(classReader);
if (functorField != null) {
writeFunctor(classReader, functorField.getReference());
}
return true;
}
private boolean exportClassStaticMembers(ClassReader classReader) {
var members = collectMembers(classReader, c -> c.hasModifier(ElementModifier.STATIC));
if (members.methods.isEmpty() && members.properties.isEmpty()) {
return false;
}
writer.append("c").ws().append("=").ws().appendClass(classReader.getName()).append(";").softNewLine();
for (var aliasEntry : members.methods.entrySet()) {
appendMethodAlias(aliasEntry.getKey());
var fullRef = new MethodReference(classReader.getName(), aliasEntry.getValue());
writer.ws().append("=").ws().appendMethod(fullRef).append(";").softNewLine();
}
for (var aliasEntry : members.properties.entrySet()) {
var propInfo = aliasEntry.getValue();
if (propInfo.getter == null) {
continue;
}
appendPropertyAlias(aliasEntry.getKey());
var fullGetter = new MethodReference(classReader.getName(), propInfo.getter);
writer.append("get:").ws().appendMethod(fullGetter);
if (propInfo.setter != null) {
writer.append(",").softNewLine();
var fullSetter = new MethodReference(classReader.getName(), propInfo.setter);
writer.append("set:").ws().appendMethod(fullSetter);
}
writer.softNewLine().outdent().append("});").softNewLine();
}
return true;
}
private void appendMethodAlias(String name) {
if (isKeyword(name)) {
writer.append("c[\"").append(name).append("\"]");
} else {
writer.append("c.").append(name);
}
}
private void appendPropertyAlias(String name) {
writer.append("Object.defineProperty(c,")
.ws().append("\"").append(name).append("\",")
.ws().append("{").indent().softNewLine();
}
private Members collectMembers(ClassReader classReader, Predicate<MethodReader> filter) {
var methods = new HashMap<String, MethodDescriptor>();
var properties = new HashMap<String, PropertyInfo>();
for (var method : classReader.getMethods()) {
if (!filter.test(method)) {
continue;
}
var methodAlias = getPublicAlias(method);
if (methodAlias != null) {
switch (methodAlias.kind) {
case METHOD:
methods.put(methodAlias.name, method.getDescriptor());
break;
case GETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.getter = method.getDescriptor();
break;
}
case SETTER: {
var propInfo = properties.computeIfAbsent(methodAlias.name, k -> new PropertyInfo());
propInfo.setter = method.getDescriptor();
break;
}
}
}
}
return new Members(methods, properties);
}
private void exportModule() {
var cls = classSource.get(context.getEntryPoint());
for (var method : cls.getMethods()) {
if (!method.hasModifier(ElementModifier.STATIC)) {
continue;
}
var methodAlias = getPublicAlias(method);
if (methodAlias != null && methodAlias.kind == AliasKind.METHOD) {
context.exportMethod(method.getReference(), methodAlias.name);
}
}
}
private void exportClassFromModule(ClassReader cls) {
var name = cls.getSimpleName();
if (name == null) {
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
}
var jsExport = cls.getAnnotations().get(JSClass.class.getName());
if (jsExport != null) {
var nameValue = jsExport.getValue("name");
if (nameValue != null) {
var nameValueString = nameValue.getString();
if (!nameValueString.isEmpty()) {
name = nameValueString;
}
}
}
context.exportClass(cls.getName(), name);
}
private boolean hasClassesToExpose() {
for (String className : classSource.getClassNames()) {
ClassReader cls = classSource.get(className);
if (cls.getMethods().stream().anyMatch(method -> getPublicAlias(method) != null)
|| typeHelper.isJavaScriptImplementation(className)) {
if (typeHelper.isJavaScriptImplementation(className)) {
return true;
}
for (var method : cls.getMethods()) {
if (!method.hasModifier(ElementModifier.STATIC) && getPublicAlias(method) != null) {
return true;
}
}
}
return false;
}
@ -242,12 +351,22 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
return methodReader != null && getPublicAlias(methodReader) != null;
}
static class PropertyInfo {
private static class Members {
final Map<String, MethodDescriptor> methods;
final Map<String, PropertyInfo> properties;
Members(Map<String, MethodDescriptor> methods, Map<String, PropertyInfo> properties) {
this.methods = methods;
this.properties = properties;
}
}
private static class PropertyInfo {
MethodDescriptor getter;
MethodDescriptor setter;
}
static class Alias {
private static class Alias {
final String name;
final AliasKind kind;
@ -257,7 +376,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
}
}
enum AliasKind {
private enum AliasKind {
METHOD,
GETTER,
SETTER

View File

@ -18,11 +18,14 @@ package org.teavm.jso.impl;
import org.teavm.dependency.AbstractDependencyListener;
import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.MethodDependency;
import org.teavm.jso.JSExportClasses;
import org.teavm.model.AnnotationReader;
import org.teavm.model.CallLocation;
import org.teavm.model.ClassReader;
import org.teavm.model.ElementModifier;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;
class JSDependencyListener extends AbstractDependencyListener {
private JSBodyRepository repository;
@ -55,8 +58,22 @@ class JSDependencyListener extends AbstractDependencyListener {
}
if (exposeAnnot != null) {
MethodDependency methodDep = agent.linkMethod(method.getReference());
methodDep.getVariable(0).propagate(agent.getType(className));
methodDep.use();
if (methodDep.getMethod() != null) {
if (!methodDep.getMethod().hasModifier(ElementModifier.STATIC)) {
methodDep.getVariable(0).propagate(agent.getType(className));
}
methodDep.use();
}
}
}
var exportClassesAnnot = cls.getAnnotations().get(JSExportClasses.class.getName());
if (exportClassesAnnot != null) {
for (var classRef : exportClassesAnnot.getValue("value").getList()) {
if (classRef.getJavaClass() instanceof ValueType.Object) {
var classRefName = ((ValueType.Object) classRef.getJavaClass()).getClassName();
agent.linkClass(classRefName);
}
}
}
}

View File

@ -23,12 +23,12 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teavm.diagnostics.Diagnostics;
import org.teavm.jso.JSExport;
import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.model.AccessLevel;
import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader;
import org.teavm.model.AnnotationValue;
import org.teavm.model.BasicBlock;
import org.teavm.model.CallLocation;
@ -99,6 +99,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
}
exposeMethods(cls, exposedClass, context.getDiagnostics(), functorMethod);
exportStaticMethods(cls, context.getDiagnostics());
}
private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics,
@ -156,21 +157,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
classHolder.addMethod(exportedMethod);
var export = classToExpose.methods.get(method);
String annotationName;
switch (export.kind) {
case GETTER:
annotationName = JSGetterToExpose.class.getName();
break;
case SETTER:
annotationName = JSSetterToExpose.class.getName();
break;
default:
annotationName = JSMethodToExpose.class.getName();
break;
}
AnnotationHolder annot = new AnnotationHolder(annotationName);
annot.getValues().put("name", new AnnotationValue(export.alias));
exportedMethod.getAnnotations().add(annot);
exportedMethod.getAnnotations().add(createExportAnnotation(export));
if (methodRef.equals(functorMethod)) {
addFunctorField(classHolder, exportedMethod.getReference());
@ -178,6 +165,85 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
}
}
private void exportStaticMethods(ClassHolder classHolder, Diagnostics diagnostics) {
int index = 0;
for (var method : classHolder.getMethods().toArray(new MethodHolder[0])) {
if (!method.hasModifier(ElementModifier.STATIC)
|| method.getAnnotations().get(JSExport.class.getName()) == null) {
continue;
}
var callLocation = new CallLocation(method.getReference());
var exportedMethodSignature = Arrays.stream(method.getSignature())
.map(type -> ValueType.object(JSObject.class.getName()))
.toArray(ValueType[]::new);
var exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++,
exportedMethodSignature);
var exportedMethod = new MethodHolder(exportedMethodDesc);
exportedMethod.getModifiers().add(ElementModifier.STATIC);
var program = new Program();
program.createVariable();
exportedMethod.setProgram(program);
var basicBlock = program.createBasicBlock();
var marshallInstructions = new ArrayList<Instruction>();
var marshaller = new JSValueMarshaller(diagnostics, typeHelper, hierarchy.getClassSource(),
program, marshallInstructions);
var variablesToPass = new Variable[method.parameterCount()];
for (int i = 0; i < method.parameterCount(); ++i) {
variablesToPass[i] = program.createVariable();
}
for (int i = 0; i < method.parameterCount(); ++i) {
variablesToPass[i] = marshaller.unwrapReturnValue(callLocation, variablesToPass[i],
method.parameterType(i), false, true);
}
basicBlock.addAll(marshallInstructions);
marshallInstructions.clear();
var invocation = new InvokeInstruction();
invocation.setType(InvocationType.SPECIAL);
invocation.setMethod(method.getReference());
invocation.setArguments(variablesToPass);
basicBlock.add(invocation);
var exit = new ExitInstruction();
if (method.getResultType() != ValueType.VOID) {
invocation.setReceiver(program.createVariable());
exit.setValueToReturn(marshaller.wrapArgument(callLocation, invocation.getReceiver(),
method.getResultType(), JSType.MIXED, false));
basicBlock.addAll(marshallInstructions);
marshallInstructions.clear();
}
basicBlock.add(exit);
classHolder.addMethod(exportedMethod);
var export = createMethodExport(method);
exportedMethod.getAnnotations().add(createExportAnnotation(export));
}
}
private AnnotationHolder createExportAnnotation(MethodExport export) {
String annotationName;
switch (export.kind) {
case GETTER:
annotationName = JSGetterToExpose.class.getName();
break;
case SETTER:
annotationName = JSSetterToExpose.class.getName();
break;
default:
annotationName = JSMethodToExpose.class.getName();
break;
}
var annot = new AnnotationHolder(annotationName);
annot.getValues().put("name", new AnnotationValue(export.alias));
return annot;
}
private ExposedClass getExposedClass(String name) {
ExposedClass cls = exposedClasses.get(name);
if (cls == null) {
@ -206,7 +272,9 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
exposedCls.inheritedMethods.addAll(parent.methods.keySet());
exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces);
}
addInterfaces(exposedCls, cls);
if (!addInterfaces(exposedCls, cls)) {
addExportedMethods(exposedCls, cls);
}
}
private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) {
@ -226,58 +294,10 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|| (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) {
continue;
}
if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) {
String name = null;
MethodKind kind = MethodKind.METHOD;
AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
if (methodAnnot != null) {
name = method.getName();
AnnotationValue nameVal = methodAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
} else {
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
if (propertyAnnot != null) {
AnnotationValue nameVal = propertyAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
String expectedPrefix;
if (method.parameterCount() == 0) {
if (method.getResultType() == ValueType.BOOLEAN) {
expectedPrefix = "is";
} else {
expectedPrefix = "get";
}
kind = MethodKind.GETTER;
} else {
expectedPrefix = "set";
kind = MethodKind.SETTER;
}
if (name == null) {
name = method.getName();
if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length()
&& Character.isUpperCase(name.charAt(expectedPrefix.length()))) {
name = Character.toLowerCase(name.charAt(expectedPrefix.length()))
+ name.substring(expectedPrefix.length() + 1);
}
}
}
}
if (name == null) {
name = method.getName();
}
exposedCls.methods.put(method.getDescriptor(), new MethodExport(name, kind));
}
addExportedMethod(exposedCls, method);
}
} else {
addExportedMethods(exposedCls, iface);
}
}
return added;
@ -290,6 +310,75 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
return addInterfaces(exposedCls, cls);
}
private void addExportedMethods(ExposedClass exposedCls, ClassReader cls) {
for (var method : cls.getMethods()) {
if (method.hasModifier(ElementModifier.STATIC)) {
continue;
}
if (method.getAnnotations().get(JSExport.class.getName()) != null) {
addExportedMethod(exposedCls, method);
}
}
}
private void addExportedMethod(ExposedClass exposedCls, MethodReader method) {
if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) {
exposedCls.methods.put(method.getDescriptor(), createMethodExport(method));
}
}
private MethodExport createMethodExport(MethodReader method) {
String name = null;
MethodKind kind = MethodKind.METHOD;
var methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
if (methodAnnot != null) {
name = method.getName();
var nameVal = methodAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
} else {
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
if (propertyAnnot != null) {
var nameVal = propertyAnnot.getValue("value");
if (nameVal != null) {
String nameStr = nameVal.getString();
if (!nameStr.isEmpty()) {
name = nameStr;
}
}
String expectedPrefix;
if (method.parameterCount() == 0) {
if (method.getResultType() == ValueType.BOOLEAN) {
expectedPrefix = "is";
} else {
expectedPrefix = "get";
}
kind = MethodKind.GETTER;
} else {
expectedPrefix = "set";
kind = MethodKind.SETTER;
}
if (name == null) {
name = method.getName();
if (name.startsWith(expectedPrefix) && name.length() > expectedPrefix.length()
&& Character.isUpperCase(name.charAt(expectedPrefix.length()))) {
name = Character.toLowerCase(name.charAt(expectedPrefix.length()))
+ name.substring(expectedPrefix.length() + 1);
}
}
}
}
if (name == null) {
name = method.getName();
}
return new MethodExport(name, kind);
}
private void addFunctorField(ClassHolder cls, MethodReference method) {
if (cls.getAnnotations().get(FunctorImpl.class.getName()) != null) {
return;

View File

@ -127,6 +127,17 @@ class JSValueMarshaller {
}
}
if (!className.equals("java.lang.String")) {
if (!typeHelper.isJavaScriptClass(className) && !typeHelper.isJavaScriptImplementation(className)) {
var unwrapNative = new InvokeInstruction();
unwrapNative.setLocation(location);
unwrapNative.setType(InvocationType.SPECIAL);
unwrapNative.setMethod(new MethodReference(JSWrapper.class,
"dependencyJavaToJs", Object.class, JSObject.class));
unwrapNative.setArguments(var);
unwrapNative.setReceiver(program.createVariable());
replacement.add(unwrapNative);
return unwrapNative.getReceiver();
}
return var;
}
}
@ -317,6 +328,15 @@ class JSValueMarshaller {
return unwrap(var, "unwrapString", JSMethods.JS_OBJECT, stringType, location.getSourceLocation());
} else if (typeHelper.isJavaScriptClass(className)) {
return var;
} else {
var wrapNative = new InvokeInstruction();
wrapNative.setLocation(location.getSourceLocation());
wrapNative.setType(InvocationType.SPECIAL);
wrapNative.setMethod(LIGHTWEIGHT_JS_TO_JAVA);
wrapNative.setArguments(var);
wrapNative.setReceiver(program.createVariable());
replacement.add(wrapNative);
return wrapNative.getReceiver();
}
} else if (type instanceof ValueType.Array) {
return unwrapArray(location, var, (ValueType.Array) type);

View File

@ -0,0 +1,36 @@
import org.teavm.gradle.api.JSModuleType
import org.teavm.gradle.api.OptimizationLevel
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
java
war
id("org.teavm")
}
dependencies {
teavm(teavm.libs.jsoApis)
}
teavm.js {
addedToWebApp = true
mainClass = "org.teavm.samples.modules.SimpleModule"
moduleType = JSModuleType.ES2015
obfuscated = false
outOfProcess = true
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.modules;
import org.teavm.jso.JSExport;
public class SimpleModule {
static {
System.out.println("Module initialized");
}
@JSExport
public static void foo() {
System.out.println("Hello, world");
}
@JSExport
public static String bar(int a) {
return "bar: " + a + Integer.TYPE;
}
}

View File

@ -60,6 +60,7 @@ include("kotlin")
include("scala")
include("web-apis")
include("software3d")
include("module-test")
gradle.allprojects {
apply<WarPlugin>()

View File

@ -36,6 +36,7 @@ include("jso:core", "jso:apis", "jso:impl")
include("platform")
include("classlib")
include("tools:core")
include("tools:browser-runner")
include("tools:deobfuscator-js")
include("tools:junit")
include("tools:devserver")

View File

@ -38,6 +38,7 @@ dependencies {
testImplementation(project(":metaprogramming:impl"))
testImplementation(project(":tools:core"))
testImplementation(project(":tools:junit"))
testImplementation(project(":tools:browser-runner"))
testImplementation(libs.hppc)
testImplementation(libs.rhino)
testImplementation(libs.junit)

View File

@ -79,7 +79,7 @@ public class ClassValueTest {
TeaVM vm = new TeaVMBuilder(target).build();
vm.add(new DependencyTestPatcher(getClass().getName(), methodName));
vm.installPlugins();
vm.entryPoint(getClass().getName());
vm.setEntryPoint(getClass().getName());
vm.build(fileName -> new ByteArrayOutputStream(), "tmp");
if (!vm.getProblemProvider().getSevereProblems().isEmpty()) {
fail("Code compiled with errors:\n" + describeProblems(vm));

View File

@ -136,7 +136,7 @@ public class DependencyTest {
MethodReference testMethod = new MethodReference(DependencyTestData.class,
testName.getMethodName(), void.class);
vm.entryPoint(DependencyTestData.class.getName());
vm.setEntryPoint(DependencyTestData.class.getName());
vm.build(fileName -> new ByteArrayOutputStream(), "out");
List<Problem> problems = vm.getProblemProvider().getSevereProblems();

View File

@ -204,7 +204,7 @@ public class IncrementalTest {
target.setObfuscated(false);
target.setStrict(true);
vm.add(new EntryPointTransformer(entryPoint));
vm.entryPoint(EntryPoint.class.getName());
vm.setEntryPoint(EntryPoint.class.getName());
vm.installPlugins();
vm.build(buildTarget, name);
List<Problem> problems = vm.getProblemProvider().getSevereProblems();

View File

@ -0,0 +1,120 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.teavm.backend.javascript.JSModuleType;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.browserrunner.BrowserRunDescriptor;
import org.teavm.browserrunner.BrowserRunner;
import org.teavm.tooling.ConsoleTeaVMToolLog;
import org.teavm.tooling.TeaVMProblemRenderer;
import org.teavm.vm.TeaVMBuilder;
import org.teavm.vm.TeaVMOptimizationLevel;
public class ExportTest {
private static File targetFile = new File(new File(System.getProperty("teavm.junit.target")), "jso-export");
private static BrowserRunner runner = new BrowserRunner(
targetFile,
"JAVASCRIPT",
BrowserRunner.pickBrowser(System.getProperty("teavm.junit.js.runner")),
false
);
@BeforeClass
public static void start() {
runner.start();
}
@AfterClass
public static void stop() {
runner.stop();
}
@Test
public void simple() {
testExport("simple", SimpleModule.class);
}
@Test
public void initializer() {
testExport("initializer", ModuleWithInitializer.class);
}
@Test
public void primitives() {
testExport("primitives", ModuleWithPrimitiveTypes.class);
}
@Test
public void exportClassMembers() {
testExport("exportClassMembers", ModuleWithExportedClassMembers.class);
}
@Test
public void importClassMembers() {
testExport("importClassMembers", ModuleWithConsumedObject.class);
}
@Test
public void exportClasses() {
testExport("exportClasses", ModuleWithExportedClasses.class);
}
private void testExport(String name, Class<?> moduleClass) {
if (!Boolean.parseBoolean(System.getProperty("teavm.junit.js", "true"))) {
return;
}
try {
var jsTarget = new JavaScriptTarget();
jsTarget.setModuleType(JSModuleType.ES2015);
var teavm = new TeaVMBuilder(jsTarget).build();
var outputDir = new File(targetFile, name);
teavm.installPlugins();
teavm.setEntryPoint(moduleClass.getName());
teavm.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED);
outputDir.mkdirs();
teavm.build(outputDir, "test.js");
if (!teavm.getProblemProvider().getSevereProblems().isEmpty()) {
var log = new ConsoleTeaVMToolLog(false);
TeaVMProblemRenderer.describeProblems(teavm, log);
throw new RuntimeException("TeaVM compilation error");
}
var testRunnerFile = new File(outputDir, "runner.js");
try (var writer = new OutputStreamWriter(new FileOutputStream(testRunnerFile), StandardCharsets.UTF_8)) {
writer.write("import { test } from '/resources/org/teavm/jso/export/" + name + ".js';\n");
writer.write("export function main(args, callback) {\n");
writer.write(" test().then(() => callback()).catch(e => callback(e));\n");
writer.write("}\n");
}
var descriptor = new BrowserRunDescriptor(name, "tests/" + name + "/runner.js", true,
List.of("resources/org/teavm/jso/export/assert.js"), null);
runner.runTest(descriptor);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013 Alexey Andreev.
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,25 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.vm;
package org.teavm.jso.export;
import org.teavm.dependency.MethodDependency;
import org.teavm.model.MethodReference;
import org.teavm.jso.JSExport;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
public class TeaVMEntryPoint {
String publicName;
MethodDependency methodDep;
TeaVMEntryPoint(String publicName, MethodDependency methodDep) {
this.publicName = publicName;
this.methodDep = methodDep;
public final class ModuleWithConsumedObject {
private ModuleWithConsumedObject() {
}
public String getPublicName() {
return publicName;
@JSExport
public static String takeObject(I o) {
return "object taken: foo = " + o.foo() + ", bar = " + o.getBar();
}
public MethodReference getMethod() {
return methodDep.getReference();
public interface I extends JSObject {
int foo();
@JSProperty
String getBar();
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import org.teavm.jso.JSExport;
import org.teavm.jso.JSProperty;
public final class ModuleWithExportedClassMembers {
private ModuleWithExportedClassMembers() {
}
@JSExport
public static C createObject(String prefix) {
return new C(prefix);
}
@JSExport
public static String consumeObject(C c) {
return "consumeObject:" + c.bar();
}
public static class C {
private String prefix;
public C(String prefix) {
this.prefix = prefix;
}
@JSExport
@JSProperty
public int getFoo() {
return 23;
}
@JSExport
public String bar() {
return prefix + ":" + 42;
}
@JSExport
public static int baz() {
return 99;
}
@JSExport
@JSProperty
public static String staticProp() {
return "I'm static";
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSExport;
import org.teavm.jso.JSExportClasses;
import org.teavm.jso.JSProperty;
@JSExportClasses({ ModuleWithExportedClasses.A.class, ModuleWithExportedClasses.B.class })
public class ModuleWithExportedClasses {
public static class A {
@JSExport
public static int foo() {
return 23;
}
}
@JSClass(name = "BB")
public static class B {
private int bar;
public B(int bar) {
this.bar = bar;
}
@JSExport
@JSProperty
public int getBar() {
return bar;
}
@JSExport
public static B create(int bar) {
return new B(bar);
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import org.teavm.jso.JSExport;
public final class ModuleWithInitializer {
private static int count;
static {
count += AnotherInitialier.count * 10;
}
private ModuleWithInitializer() {
}
@JSExport
public static String foo() {
return "foo";
}
@JSExport
public static String bar() {
return "bar";
}
@JSExport
public static int getCount() {
return count;
}
@JSExport
public static int getAnotherCount() {
return AnotherInitialier.count;
}
static class AnotherInitialier {
private static int count;
static {
count += 1;
}
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import org.teavm.jso.JSExport;
public final class ModuleWithPrimitiveTypes {
private ModuleWithPrimitiveTypes() {
}
@JSExport
public static boolean boolResult() {
return true;
}
@JSExport
public static byte byteResult() {
return 1;
}
@JSExport
public static short shortResult() {
return 2;
}
@JSExport
public static int intResult() {
return 3;
}
@JSExport
public static float floatResult() {
return 4.1f;
}
@JSExport
public static double doubleResult() {
return 5.2f;
}
@JSExport
public static String stringResult() {
return "q";
}
@JSExport
public static boolean[] boolArrayResult() {
return new boolean[] { true, false };
}
@JSExport
public static byte[] byteArrayResult() {
return new byte[] { 1, 2 };
}
@JSExport
public static short[] shortArrayResult() {
return new short[] { 2, 3 };
}
@JSExport
public static int[] intArrayResult() {
return new int[] { 3, 4 };
}
@JSExport
public static float[] floatArrayResult() {
return new float[] { 4f, 5f };
}
@JSExport
public static double[] doubleArrayResult() {
return new double[] { 5, 6 };
}
@JSExport
public static String[] stringArrayResult() {
return new String[] { "q", "w" };
}
@JSExport
public static String boolParam(boolean param) {
return "bool:" + param;
}
@JSExport
public static String byteParam(byte param) {
return "byte:" + param;
}
@JSExport
public static String shortParam(short param) {
return "short:" + param;
}
@JSExport
public static String intParam(int param) {
return "int:" + param;
}
@JSExport
public static String floatParam(float param) {
return "float:" + param;
}
@JSExport
public static String doubleParam(double param) {
return "double:" + param;
}
@JSExport
public static String stringParam(String param) {
return "string:" + param;
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.export;
import org.teavm.jso.JSExport;
public final class SimpleModule {
private SimpleModule() {
}
@JSExport
public static int foo() {
return 23;
}
}

View File

@ -97,7 +97,7 @@ public class JSOTest {
TeaVM vm = new TeaVMBuilder(new JavaScriptTarget()).build();
vm.add(new DependencyTestPatcher(JSOTest.class.getName(), methodName));
vm.installPlugins();
vm.entryPoint(JSOTest.class.getName());
vm.setEntryPoint(JSOTest.class.getName());
vm.build(name -> new ByteArrayOutputStream(), "tmp");
return vm.getProblemProvider().getSevereProblems();
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function assertEquals(a, b) {
if (a == b) {
return
}
if (a instanceof Array && b instanceof Array && a.length === b.length) {
let allEqual = true;
for (let i = 0; i < a.length; ++i) {
if (a[i] != b[i]) {
allEqual = false;
}
}
if (allEqual) {
return;
}
}
throw Error(`Assertion failed: ${a} != ${b}`);
}
function assertApproxEquals(a, b) {
if (Math.abs(a - b) > 0.01) {
throw Error(`Assertion failed: ${a} != ${b}`);
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createObject, consumeObject, C } from '/tests/exportClassMembers/test.js';
export async function test() {
let o = createObject("qwe");
assertEquals(23, o.foo);
assertEquals("qwe:42", o.bar());
assertEquals("consumeObject:qwe:42", consumeObject(o));
assertEquals(99, C.baz());
assertEquals("I'm static", C.staticProp);
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { A, BB } from '/tests/exportClasses/test.js';
export async function test() {
assertEquals(23, A.foo());
let o = BB.create(42);
assertEquals(true, o instanceof BB);
assertEquals(false, o instanceof A);
assertEquals(42, o.bar);
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { takeObject } from '/tests/importClassMembers/test.js';
export async function test() {
assertEquals("object taken: foo = 23, bar = qw", takeObject({
foo: () => 23,
bar: "qw"
}));
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { foo, bar, getCount, getAnotherCount } from '/tests/initializer/test.js';
export async function test() {
assertEquals("foo", foo());
assertEquals(1, getAnotherCount());
assertEquals(10, getCount());
assertEquals("bar", bar());
assertEquals(1, getAnotherCount());
assertEquals(10, getCount());
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as java from '/tests/primitives/test.js';
function testReturnPrimitives() {
assertEquals(true, java.boolResult());
assertEquals(1, java.byteResult());
assertEquals(2, java.shortResult());
assertEquals(3, java.intResult());
assertApproxEquals(4.1, java.floatResult());
assertApproxEquals(5.2, java.doubleResult());
assertEquals("q", java.stringResult());
}
function testReturnArrays() {
assertEquals([true, false], java.boolArrayResult());
assertEquals([1, 2], java.byteArrayResult());
assertEquals([2, 3], java.shortArrayResult());
assertEquals([3, 4], java.intArrayResult());
assertEquals([4, 5], java.floatArrayResult());
assertEquals([5, 6], java.doubleArrayResult());
assertEquals(["q", "w"], java.stringArrayResult());
}
function testConsumePrimitives() {
assertEquals("bool:true", java.boolParam(true));
assertEquals("byte:1", java.byteParam(1));
assertEquals("short:2", java.shortParam(2));
assertEquals("int:3", java.intParam(3));
assertEquals("float:4.0", java.floatParam(4));
assertEquals("double:5.0", java.doubleParam(5));
assertEquals("string:q", java.stringParam("q"));
}
export async function test() {
testReturnPrimitives();
testReturnArrays();
testConsumePrimitives();
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { foo } from '/tests/simple/test.js';
export async function test() {
assertEquals(23, foo());
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
`java-library`
`teavm-publish`
}
description = "Runs JS tests in the browser"
configurations {
create("js")
}
dependencies {
implementation(libs.jackson.annotations)
implementation(libs.jackson.databind)
implementation(libs.javax.servlet)
implementation(libs.jetty.server)
implementation(libs.jetty.websocket.server)
implementation(libs.jetty.websocket.client)
implementation(libs.jetty.websocket.client)
"js"(project(":tools:deobfuscator-js", "js"))
}
tasks.withType<Jar>().configureEach {
if (name == "relocateJar") {
dependsOn(configurations["js"])
from(project.provider { configurations["js"].map { zipTree(it) } }) {
include("deobfuscator-lib.js")
into("test-server")
rename { "deobfuscator.js" }
}
}
}
teavmPublish {
artifactId = "teavm-browser-runner"
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.browserrunner;
import java.util.Collection;
public class BrowserRunDescriptor {
private final String name;
private final String testPath;
private final boolean module;
private final Collection<String> additionalFiles;
private final String argument;
public BrowserRunDescriptor(String name, String testPath, boolean module, Collection<String> additionalFiles,
String argument) {
this.name = name;
this.testPath = testPath;
this.module = module;
this.additionalFiles = additionalFiles;
this.argument = argument;
}
public String getName() {
return name;
}
public String getTestPath() {
return testPath;
}
public boolean isModule() {
return module;
}
public Collection<String> getAdditionalFiles() {
return additionalFiles;
}
public String getArgument() {
return argument;
}
}

View File

@ -0,0 +1,526 @@
/*
* Copyright 2021 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.browserrunner;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
public class BrowserRunner {
private boolean decodeStack;
private final File baseDir;
private final String type;
private final Function<String, Process> browserRunner;
private Process browserProcess;
private Server server;
private int port;
private AtomicInteger idGenerator = new AtomicInteger(0);
private BlockingQueue<Session> wsSessionQueue = new LinkedBlockingQueue<>();
private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>();
private ObjectMapper objectMapper = new ObjectMapper();
public BrowserRunner(File baseDir, String type, Function<String, Process> browserRunner, boolean decodeStack) {
this.baseDir = baseDir;
this.type = type;
this.browserRunner = browserRunner;
this.decodeStack = decodeStack;
}
public static Function<String, Process> pickBrowser(String name) {
switch (name) {
case "browser":
return BrowserRunner::customBrowser;
case "browser-chrome":
return BrowserRunner::chromeBrowser;
case "browser-firefox":
return BrowserRunner::firefoxBrowser;
case "none":
return null;
default:
throw new RuntimeException("Unknown run strategy: " + name);
}
}
public void start() {
runServer();
browserProcess = browserRunner.apply("http://localhost:" + port + "/index.html");
}
public void stop() {
try {
server.stop();
} catch (Exception e) {
e.printStackTrace();
}
if (browserProcess != null) {
browserProcess.destroy();
}
}
private void runServer() {
server = new Server();
var connector = new ServerConnector(server);
server.addConnector(connector);
var context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
var servlet = new TestCodeServlet();
var servletHolder = new ServletHolder(servlet);
servletHolder.setAsyncSupported(true);
context.addServlet(servletHolder, "/*");
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
port = connector.getLocalPort();
}
static class CallbackWrapper {
private final CountDownLatch latch;
volatile Throwable error;
volatile boolean shouldRepeat;
CallbackWrapper(CountDownLatch latch) {
this.latch = latch;
}
void complete() {
latch.countDown();
}
void error(Throwable e) {
error = e;
latch.countDown();
}
void repeat() {
latch.countDown();
shouldRepeat = true;
}
}
public void runTest(BrowserRunDescriptor run) throws IOException {
while (!runTestOnce(run)) {
// repeat
}
}
private boolean runTestOnce(BrowserRunDescriptor run) {
Session ws;
try {
do {
ws = wsSessionQueue.poll(1, TimeUnit.SECONDS);
} while (ws == null || !ws.isOpen());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return true;
}
int id = idGenerator.incrementAndGet();
var latch = new CountDownLatch(1);
var callbackWrapper = new CallbackWrapper(latch);
awaitingRuns.put(id, callbackWrapper);
var nf = objectMapper.getNodeFactory();
var node = nf.objectNode();
node.set("id", nf.numberNode(id));
var array = nf.arrayNode();
node.set("tests", array);
var testNode = nf.objectNode();
testNode.set("type", nf.textNode(type));
testNode.set("name", nf.textNode(run.getName()));
var fileNode = nf.objectNode();
fileNode.set("path", nf.textNode(run.getTestPath()));
fileNode.set("type", nf.textNode(run.isModule() ? "module" : "regular"));
testNode.set("file", fileNode);
if (!run.getAdditionalFiles().isEmpty()) {
var additionalJsJson = nf.arrayNode();
for (var additionalFile : run.getAdditionalFiles()) {
var additionFileObj = nf.objectNode();
additionFileObj.set("path", nf.textNode(additionalFile));
additionFileObj.set("type", nf.textNode("regular"));
additionalJsJson.add(additionFileObj);
}
testNode.set("additionalFiles", additionalJsJson);
}
if (run.getArgument() != null) {
testNode.set("argument", nf.textNode(run.getArgument()));
}
array.add(testNode);
var message = node.toString();
ws.getRemote().sendStringByFuture(message);
try {
latch.await();
} catch (InterruptedException e) {
// do nothing
}
if (ws.isOpen()) {
wsSessionQueue.offer(ws);
}
if (callbackWrapper.error != null) {
var err = callbackWrapper.error;
if (err instanceof RuntimeException) {
throw (RuntimeException) err;
} else {
throw new RuntimeException(err);
}
}
return !callbackWrapper.shouldRepeat;
}
class TestCodeServlet extends HttpServlet {
private WebSocketServletFactory wsFactory;
private Map<String, String> contentCache = new ConcurrentHashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
var wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER);
wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy);
wsFactory.setCreator((req, resp) -> new TestCodeSocket());
try {
wsFactory.start();
} catch (Exception e) {
throw new ServletException(e);
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
var path = req.getRequestURI();
if (path != null) {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (req.getMethod().equals("GET")) {
switch (path) {
case "/index.html":
case "/frame.html": {
var content = getFromCache(path, "true".equals(req.getParameter("logging")));
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/html");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
break;
}
case "/client.js":
case "/frame.js":
case "/deobfuscator.js": {
var content = getFromCache(path, false);
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("application/javascript");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
break;
}
}
if (path.startsWith("/tests/")) {
var relPath = path.substring("/tests/".length());
var file = new File(baseDir, relPath);
if (file.isFile()) {
resp.setStatus(HttpServletResponse.SC_OK);
if (file.getName().endsWith(".js")) {
resp.setContentType("application/javascript");
} else if (file.getName().endsWith(".wasm")) {
resp.setContentType("application/wasm");
}
try (var input = new FileInputStream(file)) {
input.transferTo(resp.getOutputStream());
}
resp.getOutputStream().flush();
}
}
if (path.startsWith("/resources/")) {
var relPath = path.substring("/resources/".length());
var classLoader = BrowserRunner.class.getClassLoader();
try (var input = classLoader.getResourceAsStream(relPath)) {
if (input != null) {
if (relPath.endsWith(".js")) {
resp.setContentType("application/javascript");
}
resp.setStatus(HttpServletResponse.SC_OK);
input.transferTo(resp.getOutputStream());
} else {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
resp.getOutputStream().flush();
}
}
}
if (path.equals("/ws") && wsFactory.isUpgradeRequest(req, resp)
&& (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted())) {
return;
}
}
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
private String getFromCache(String fileName, boolean logging) {
return contentCache.computeIfAbsent(fileName, fn -> {
var loader = BrowserRunner.class.getClassLoader();
try (var input = loader.getResourceAsStream("test-server" + fn);
var reader = new InputStreamReader(input)) {
var sb = new StringBuilder();
var buffer = new char[2048];
while (true) {
int charsRead = reader.read(buffer);
if (charsRead < 0) {
break;
}
sb.append(buffer, 0, charsRead);
}
return sb.toString()
.replace("{{PORT}}", String.valueOf(port))
.replace("\"{{LOGGING}}\"", String.valueOf(logging))
.replace("\"{{DEOBFUSCATION}}\"", String.valueOf(decodeStack));
} catch (IOException e) {
e.printStackTrace();
return null;
}
});
}
}
class TestCodeSocket extends WebSocketAdapter {
@Override
public void onWebSocketConnect(Session sess) {
wsSessionQueue.offer(sess);
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
for (CallbackWrapper run : awaitingRuns.values()) {
run.repeat();
}
}
@Override
public void onWebSocketText(String message) {
JsonNode node;
try {
node = objectMapper.readTree(new StringReader(message));
} catch (IOException e) {
throw new RuntimeException(e);
}
int id = node.get("id").asInt();
var run = awaitingRuns.remove(id);
if (run == null) {
System.err.println("Unexpected run id: " + id);
return;
}
JsonNode resultNode = node.get("result");
JsonNode log = resultNode.get("log");
if (log != null) {
for (JsonNode logEntry : log) {
String str = logEntry.get("message").asText();
switch (logEntry.get("type").asText()) {
case "stdout":
System.out.println(str);
break;
case "stderr":
System.err.println(str);
break;
}
}
}
String status = resultNode.get("status").asText();
if (status.equals("OK")) {
run.complete();
} else {
run.error(new RuntimeException(resultNode.get("errorMessage").asText()));
}
}
}
public static Process customBrowser(String url) {
System.out.println("Open link to run tests: " + url + "?logging=true");
return null;
}
public static Process chromeBrowser(String url) {
return browserTemplate("chrome", url, (profile, params) -> {
addChromeCommand(params);
params.addAll(Arrays.asList(
"--headless",
"--disable-gpu",
"--remote-debugging-port=9222",
"--no-first-run",
"--user-data-dir=" + profile
));
});
}
public static Process firefoxBrowser(String url) {
return browserTemplate("firefox", url, (profile, params) -> {
addFirefoxCommand(params);
params.addAll(Arrays.asList(
"--headless",
"--profile",
profile
));
});
}
private static void addChromeCommand(List<String> params) {
if (isMacos()) {
params.add("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
} else if (isWindows()) {
params.add("cmd.exe");
params.add("start");
params.add("/C");
params.add("chrome");
} else {
params.add("google-chrome-stable");
}
}
private static void addFirefoxCommand(List<String> params) {
if (isMacos()) {
params.add("/Applications/Firefox.app/Contents/MacOS/firefox");
return;
}
if (isWindows()) {
params.add("cmd.exe");
params.add("/C");
params.add("start");
}
params.add("firefox");
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().startsWith("windows");
}
private static boolean isMacos() {
return System.getProperty("os.name").toLowerCase().startsWith("mac");
}
private static Process browserTemplate(String name, String url, BiConsumer<String, List<String>> paramsBuilder) {
File temp;
try {
temp = File.createTempFile("teavm", "teavm");
temp.delete();
temp.mkdirs();
Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteDir(temp)));
System.out.println("Running " + name + " with user data dir: " + temp.getAbsolutePath());
List<String> params = new ArrayList<>();
paramsBuilder.accept(temp.getAbsolutePath(), params);
params.add(url);
ProcessBuilder pb = new ProcessBuilder(params.toArray(new String[0]));
Process process = pb.start();
logStream(process.getInputStream(), name + " stdout");
logStream(process.getErrorStream(), name + " stderr");
new Thread(() -> {
try {
System.out.println(name + " process terminated with code: " + process.waitFor());
} catch (InterruptedException e) {
// ignore
}
});
return process;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void logStream(InputStream stream, String name) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
System.out.println(name + ": " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private static void deleteDir(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
deleteDir(file);
} else {
file.delete();
}
}
dir.delete();
}
}

View File

@ -85,7 +85,7 @@ function launchTest(argument, callback) {
return teavmException;
}
let stack = "";
let je = main.javaException(e);
let je = main.javaException ? main.javaException(e) : void 0;
if (je && je.constructor.$meta) {
stack = je.constructor.$meta.name + ": ";
stack += je.getMessage();

View File

@ -346,7 +346,10 @@ public class IncrementalCBuilder {
vm.installPlugins();
vm.setLastKnownClasses(lastReachedClasses);
vm.entryPoint(mainClass, mainFunctionName != null ? mainFunctionName : "main");
vm.setEntryPoint(mainClass);
if (mainFunctionName != null) {
vm.setEntryPointName(mainFunctionName);
}
log.info("Starting build");
progressListener.last = 0;

View File

@ -457,8 +457,9 @@ public class TeaVMTool {
for (ClassHolderTransformer transformer : resolveTransformers()) {
vm.add(transformer);
}
if (mainClass != null) {
vm.entryPoint(mainClass, entryPointName != null ? entryPointName : "main");
vm.setEntryPoint(mainClass);
if (entryPointName != null) {
vm.setEntryPointName(entryPointName);
}
for (String className : classesToPreserve) {
vm.preserveType(className);

View File

@ -838,7 +838,7 @@ public class CodeServlet extends HttpServlet {
vm.installPlugins();
vm.setLastKnownClasses(lastReachedClasses);
vm.entryPoint(mainClass);
vm.setEntryPoint(mainClass);
log.info("Starting build");
progressListener.last = 0;

View File

@ -21,9 +21,6 @@ plugins {
description = "Test runner for JUnit and TestNG annotations"
configurations {
create("js")
}
dependencies {
compileOnly(libs.junit)
@ -33,26 +30,7 @@ dependencies {
implementation(project(":core"))
implementation(project(":tools:core"))
implementation(libs.jackson.annotations)
implementation(libs.jackson.databind)
implementation(libs.javax.servlet)
implementation(libs.jetty.server)
implementation(libs.jetty.websocket.server)
implementation(libs.jetty.websocket.client)
implementation(libs.jetty.websocket.client)
"js"(project(":tools:deobfuscator-js", "js"))
}
tasks.withType<Jar>().configureEach {
if (name == "relocateJar") {
dependsOn(configurations["js"])
from(project.provider { configurations["js"].map { zipTree(it) } }) {
include("deobfuscator-lib.js")
into("test-server")
rename { "deobfuscator.js" }
}
}
implementation(project(":tools:browser-runner"))
}
teavmPublish {

View File

@ -16,220 +16,52 @@
package org.teavm.junit;
import static org.teavm.junit.PropertyNames.JS_DECODE_STACK;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import java.util.stream.Collectors;
import org.teavm.browserrunner.BrowserRunDescriptor;
import org.teavm.browserrunner.BrowserRunner;
class BrowserRunStrategy implements TestRunStrategy {
private boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true"));
private final File baseDir;
private final String type;
private final Function<String, Process> browserRunner;
private Process browserProcess;
private Server server;
private int port;
private AtomicInteger idGenerator = new AtomicInteger(0);
private BlockingQueue<Session> wsSessionQueue = new LinkedBlockingQueue<>();
private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>();
private ObjectMapper objectMapper = new ObjectMapper();
private File baseDir;
private BrowserRunner runner;
BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
this.baseDir = baseDir;
this.type = type;
this.browserRunner = browserRunner;
runner = new BrowserRunner(baseDir, type, browserRunner,
Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true")));
}
@Override
public void beforeAll() {
runServer();
browserProcess = browserRunner.apply("http://localhost:" + port + "/index.html");
}
private void runServer() {
server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
TestCodeServlet servlet = new TestCodeServlet();
ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.setAsyncSupported(true);
context.addServlet(servletHolder, "/*");
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
port = connector.getLocalPort();
runner.start();
}
@Override
public void afterAll() {
try {
server.stop();
} catch (Exception e) {
e.printStackTrace();
}
if (browserProcess != null) {
browserProcess.destroy();
}
}
static class CallbackWrapper implements TestRunCallback {
private final CountDownLatch latch;
volatile Throwable error;
volatile boolean shouldRepeat;
CallbackWrapper(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void complete() {
latch.countDown();
}
@Override
public void error(Throwable e) {
error = e;
latch.countDown();
}
void repeat() {
latch.countDown();
shouldRepeat = true;
}
runner.stop();
}
@Override
public void runTest(TestRun run) throws IOException {
while (!runTestOnce(run)) {
// repeat
}
var testFile = new File(run.getBaseDirectory(), run.getFileName());
var testPath = baseDir.getAbsoluteFile().toPath().relativize(testFile.toPath()).toString();
var descriptor = new BrowserRunDescriptor(
run.getFileName(),
"tests/" + testPath,
run.isModule(),
additionalJs(run).stream().map(p -> "resources/" + p).collect(Collectors.toList()),
run.getArgument()
);
runner.runTest(descriptor);
}
private boolean runTestOnce(TestRun run) {
Session ws;
try {
do {
ws = wsSessionQueue.poll(1, TimeUnit.SECONDS);
} while (ws == null || !ws.isOpen());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return true;
}
int id = idGenerator.incrementAndGet();
var latch = new CountDownLatch(1);
CallbackWrapper callbackWrapper = new CallbackWrapper(latch);
awaitingRuns.put(id, callbackWrapper);
JsonNodeFactory nf = objectMapper.getNodeFactory();
ObjectNode node = nf.objectNode();
node.set("id", nf.numberNode(id));
ArrayNode array = nf.arrayNode();
node.set("tests", array);
File file = new File(run.getBaseDirectory(), run.getFileName()).getAbsoluteFile();
String relPath = baseDir.getAbsoluteFile().toPath().relativize(file.toPath()).toString();
ObjectNode testNode = nf.objectNode();
testNode.set("type", nf.textNode(type));
testNode.set("name", nf.textNode(run.getFileName()));
var fileNode = nf.objectNode();
fileNode.set("path", nf.textNode("tests/" + relPath));
fileNode.set("type", nf.textNode(run.isModule() ? "module" : "regular"));
testNode.set("file", fileNode);
var additionalJs = additionalJs(run);
if (additionalJs.length > 0) {
var additionalJsJson = nf.arrayNode();
for (var additionalFile : additionalJs) {
var additionFileObj = nf.objectNode();
additionFileObj.set("path", nf.textNode("resources/" + additionalFile));
additionFileObj.set("type", nf.textNode("regular"));
additionalJsJson.add(additionFileObj);
}
testNode.set("additionalFiles", additionalJsJson);
}
if (run.getArgument() != null) {
testNode.set("argument", nf.textNode(run.getArgument()));
}
array.add(testNode);
String message = node.toString();
ws.getRemote().sendStringByFuture(message);
try {
latch.await();
} catch (InterruptedException e) {
// do nothing
}
if (ws.isOpen()) {
wsSessionQueue.offer(ws);
}
if (callbackWrapper.error != null) {
var err = callbackWrapper.error;
if (err instanceof RuntimeException) {
throw (RuntimeException) err;
} else {
throw new RuntimeException(err);
}
}
return !callbackWrapper.shouldRepeat;
}
private String[] additionalJs(TestRun run) {
private Collection<String> additionalJs(TestRun run) {
var result = new LinkedHashSet<String>();
var method = run.getMethod();
@ -247,296 +79,6 @@ class BrowserRunStrategy implements TestRunStrategy {
cls = cls.getSuperclass();
}
return result.toArray(new String[0]);
}
class TestCodeServlet extends HttpServlet {
private WebSocketServletFactory wsFactory;
private Map<String, String> contentCache = new ConcurrentHashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
WebSocketPolicy wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER);
wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy);
wsFactory.setCreator((req, resp) -> new TestCodeSocket());
try {
wsFactory.start();
} catch (Exception e) {
throw new ServletException(e);
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getRequestURI();
if (path != null) {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (req.getMethod().equals("GET")) {
switch (path) {
case "/index.html":
case "/frame.html": {
String content = getFromCache(path, "true".equals(req.getParameter("logging")));
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/html");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
break;
}
case "/client.js":
case "/frame.js":
case "/deobfuscator.js": {
String content = getFromCache(path, false);
if (content != null) {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("application/javascript");
resp.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));
resp.getOutputStream().flush();
return;
}
break;
}
}
if (path.startsWith("/tests/")) {
String relPath = path.substring("/tests/".length());
File file = new File(baseDir, relPath);
if (file.isFile()) {
resp.setStatus(HttpServletResponse.SC_OK);
if (file.getName().endsWith(".js")) {
resp.setContentType("application/javascript");
} else if (file.getName().endsWith(".wasm")) {
resp.setContentType("application/wasm");
}
try (FileInputStream input = new FileInputStream(file)) {
input.transferTo(resp.getOutputStream());
}
resp.getOutputStream().flush();
}
}
if (path.startsWith("/resources/")) {
var relPath = path.substring("/resources/".length());
var classLoader = BrowserRunStrategy.class.getClassLoader();
try (var input = classLoader.getResourceAsStream(relPath)) {
if (input != null) {
resp.setStatus(HttpServletResponse.SC_OK);
input.transferTo(resp.getOutputStream());
} else {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
resp.getOutputStream().flush();
}
}
}
if (path.equals("/ws") && wsFactory.isUpgradeRequest(req, resp)
&& (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted())) {
return;
}
}
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
private String getFromCache(String fileName, boolean logging) {
return contentCache.computeIfAbsent(fileName, fn -> {
ClassLoader loader = BrowserRunStrategy.class.getClassLoader();
try (InputStream input = loader.getResourceAsStream("test-server" + fn);
Reader reader = new InputStreamReader(input)) {
StringBuilder sb = new StringBuilder();
char[] buffer = new char[2048];
while (true) {
int charsRead = reader.read(buffer);
if (charsRead < 0) {
break;
}
sb.append(buffer, 0, charsRead);
}
return sb.toString()
.replace("{{PORT}}", String.valueOf(port))
.replace("\"{{LOGGING}}\"", String.valueOf(logging))
.replace("\"{{DEOBFUSCATION}}\"", String.valueOf(decodeStack));
} catch (IOException e) {
e.printStackTrace();
return null;
}
});
}
}
class TestCodeSocket extends WebSocketAdapter {
@Override
public void onWebSocketConnect(Session sess) {
wsSessionQueue.offer(sess);
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
for (CallbackWrapper run : awaitingRuns.values()) {
run.repeat();
}
}
@Override
public void onWebSocketText(String message) {
JsonNode node;
try {
node = objectMapper.readTree(new StringReader(message));
} catch (IOException e) {
throw new RuntimeException(e);
}
int id = node.get("id").asInt();
TestRunCallback run = awaitingRuns.remove(id);
if (run == null) {
System.err.println("Unexpected run id: " + id);
return;
}
JsonNode resultNode = node.get("result");
JsonNode log = resultNode.get("log");
if (log != null) {
for (JsonNode logEntry : log) {
String str = logEntry.get("message").asText();
switch (logEntry.get("type").asText()) {
case "stdout":
System.out.println(str);
break;
case "stderr":
System.err.println(str);
break;
}
}
}
String status = resultNode.get("status").asText();
if (status.equals("OK")) {
run.complete();
} else {
run.error(new RuntimeException(resultNode.get("errorMessage").asText()));
}
}
}
static Process customBrowser(String url) {
System.out.println("Open link to run tests: " + url + "?logging=true");
return null;
}
static Process chromeBrowser(String url) {
return browserTemplate("chrome", url, (profile, params) -> {
addChromeCommand(params);
params.addAll(Arrays.asList(
"--headless",
"--disable-gpu",
"--remote-debugging-port=9222",
"--no-first-run",
"--user-data-dir=" + profile
));
});
}
static Process firefoxBrowser(String url) {
return browserTemplate("firefox", url, (profile, params) -> {
addFirefoxCommand(params);
params.addAll(Arrays.asList(
"--headless",
"--profile",
profile
));
});
}
private static void addChromeCommand(List<String> params) {
if (isMacos()) {
params.add("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
} else if (isWindows()) {
params.add("cmd.exe");
params.add("start");
params.add("/C");
params.add("chrome");
} else {
params.add("google-chrome-stable");
}
}
private static void addFirefoxCommand(List<String> params) {
if (isMacos()) {
params.add("/Applications/Firefox.app/Contents/MacOS/firefox");
return;
}
if (isWindows()) {
params.add("cmd.exe");
params.add("/C");
params.add("start");
}
params.add("firefox");
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().startsWith("windows");
}
private static boolean isMacos() {
return System.getProperty("os.name").toLowerCase().startsWith("mac");
}
private static Process browserTemplate(String name, String url, BiConsumer<String, List<String>> paramsBuilder) {
File temp;
try {
temp = File.createTempFile("teavm", "teavm");
temp.delete();
temp.mkdirs();
Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteDir(temp)));
System.out.println("Running " + name + " with user data dir: " + temp.getAbsolutePath());
List<String> params = new ArrayList<>();
paramsBuilder.accept(temp.getAbsolutePath(), params);
params.add(url);
ProcessBuilder pb = new ProcessBuilder(params.toArray(new String[0]));
Process process = pb.start();
logStream(process.getInputStream(), name + " stdout");
logStream(process.getErrorStream(), name + " stderr");
new Thread(() -> {
try {
System.out.println(name + " process terminated with code: " + process.waitFor());
} catch (InterruptedException e) {
// ignore
}
});
return process;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void logStream(InputStream stream, String name) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
System.out.println(name + ": " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private static void deleteDir(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
deleteDir(file);
} else {
file.delete();
}
}
dir.delete();
return result;
}
}

View File

@ -35,6 +35,7 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
import org.teavm.backend.javascript.JSModuleType;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.browserrunner.BrowserRunner;
import org.teavm.debugging.information.DebugInformation;
import org.teavm.debugging.information.DebugInformationBuilder;
import org.teavm.model.ClassHolderSource;
@ -49,22 +50,10 @@ class JSPlatformSupport extends TestPlatformSupport<JavaScriptTarget> {
@Override
TestRunStrategy createRunStrategy(File outputDir) {
String runStrategyName = System.getProperty(JS_RUNNER);
if (runStrategyName != null) {
switch (runStrategyName) {
case "browser":
return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::customBrowser);
case "browser-chrome":
return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::chromeBrowser);
case "browser-firefox":
return new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunStrategy::firefoxBrowser);
case "none":
return null;
default:
throw new RuntimeException("Unknown run strategy: " + runStrategyName);
}
}
return null;
var runStrategyName = System.getProperty(JS_RUNNER);
return runStrategyName != null
? new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunner.pickBrowser(runStrategyName))
: null;
}
@Override

View File

@ -93,7 +93,7 @@ abstract class TestPlatformSupport<T extends TeaVMTarget> {
new TestExceptionPlugin().install(vm);
vm.entryPoint(entryPoint);
vm.setEntryPoint(entryPoint);
if (usesFileName()) {
if (!outputFile.getParentFile().exists()) {

View File

@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List;
import org.teavm.backend.wasm.WasmRuntimeType;
import org.teavm.backend.wasm.WasmTarget;
import org.teavm.browserrunner.BrowserRunner;
import org.teavm.model.ClassHolderSource;
import org.teavm.model.MethodReference;
import org.teavm.model.ReferenceCache;
@ -35,20 +36,9 @@ class WebAssemblyPlatformSupport extends BaseWebAssemblyPlatformSupport {
@Override
TestRunStrategy createRunStrategy(File outputDir) {
var runStrategyName = System.getProperty(WASM_RUNNER);
if (runStrategyName != null) {
switch (runStrategyName) {
case "browser":
return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::customBrowser);
case "chrome":
case "browser-chrome":
return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::chromeBrowser);
case "browser-firefox":
return new BrowserRunStrategy(outputDir, "WASM", BrowserRunStrategy::firefoxBrowser);
default:
throw new RuntimeException("Unknown run strategy: " + runStrategyName);
}
}
return null;
return runStrategyName != null
? new BrowserRunStrategy(outputDir, "WASM", BrowserRunner.pickBrowser(runStrategyName))
: null;
}
@Override