mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 08:14:09 -08:00
parent
cf850157f0
commit
8db406c603
|
@ -27,7 +27,6 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -139,7 +138,6 @@ import org.teavm.runtime.RuntimeArray;
|
||||||
import org.teavm.runtime.RuntimeClass;
|
import org.teavm.runtime.RuntimeClass;
|
||||||
import org.teavm.runtime.RuntimeObject;
|
import org.teavm.runtime.RuntimeObject;
|
||||||
import org.teavm.vm.BuildTarget;
|
import org.teavm.vm.BuildTarget;
|
||||||
import org.teavm.vm.TeaVMEntryPoint;
|
|
||||||
import org.teavm.vm.TeaVMTarget;
|
import org.teavm.vm.TeaVMTarget;
|
||||||
import org.teavm.vm.TeaVMTargetController;
|
import org.teavm.vm.TeaVMTargetController;
|
||||||
import org.teavm.vm.spi.TeaVMHostExtension;
|
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,
|
private void generateMain(GenerationContext context, CodeWriter writer, IncludeManager includes,
|
||||||
ListableClassHolderSource classes, List<? extends ValueType> types) {
|
ListableClassHolderSource classes, List<? extends ValueType> types) {
|
||||||
Iterator<? extends TeaVMEntryPoint> entryPointIter = controller.getEntryPoints().values().iterator();
|
var mainFunctionName = controller.getEntryPointName();
|
||||||
String mainFunctionName = entryPointIter.hasNext() ? entryPointIter.next().getPublicName() : null;
|
|
||||||
if (mainFunctionName == null) {
|
if (mainFunctionName == null) {
|
||||||
mainFunctionName = "main";
|
mainFunctionName = "main";
|
||||||
}
|
}
|
||||||
|
@ -936,16 +933,14 @@ public class CTarget implements TeaVMTarget, TeaVMCHost {
|
||||||
|
|
||||||
private void generateCallToMainMethod(IntrinsicContext context, InvocationExpr invocation) {
|
private void generateCallToMainMethod(IntrinsicContext context, InvocationExpr invocation) {
|
||||||
NameProvider names = context.names();
|
NameProvider names = context.names();
|
||||||
Iterator<? extends TeaVMEntryPoint> entryPointIter = controller.getEntryPoints().values().iterator();
|
var method = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class),
|
||||||
if (entryPointIter.hasNext()) {
|
ValueType.parse(void.class));
|
||||||
TeaVMEntryPoint entryPoint = entryPointIter.next();
|
context.importMethod(method, true);
|
||||||
context.importMethod(entryPoint.getMethod(), true);
|
String mainMethod = names.forMethod(method);
|
||||||
String mainMethod = names.forMethod(entryPoint.getMethod());
|
|
||||||
context.writer().print(mainMethod + "(");
|
context.writer().print(mainMethod + "(");
|
||||||
context.emit(invocation.getArguments().get(0));
|
context.emit(invocation.getArguments().get(0));
|
||||||
context.writer().print(")");
|
context.writer().print(")");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] getPlatformTags() {
|
public String[] getPlatformTags() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,6 +129,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
private final Map<String, String> importedModules = new LinkedHashMap<>();
|
private final Map<String, String> importedModules = new LinkedHashMap<>();
|
||||||
private JavaScriptTemplateFactory templateFactory;
|
private JavaScriptTemplateFactory templateFactory;
|
||||||
private JSModuleType moduleType = JSModuleType.UMD;
|
private JSModuleType moduleType = JSModuleType.UMD;
|
||||||
|
private List<ExportedDeclaration> exports = new ArrayList<>();
|
||||||
private int maxTopLevelNames = 80_000;
|
private int maxTopLevelNames = 80_000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -398,7 +399,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
|
|
||||||
var rememberingWriter = new RememberingSourceWriter(debugEmitter != null);
|
var rememberingWriter = new RememberingSourceWriter(debugEmitter != null);
|
||||||
var renderer = new Renderer(rememberingWriter, asyncMethods, renderingContext, controller.getDiagnostics(),
|
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.setProperties(controller.getProperties());
|
||||||
renderer.setProgressConsumer(controller::reportProgress);
|
renderer.setProgressConsumer(controller::reportProgress);
|
||||||
|
|
||||||
|
@ -414,15 +416,19 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
renderer.renderStringPool();
|
renderer.renderStringPool();
|
||||||
renderer.renderStringConstants();
|
renderer.renderStringConstants();
|
||||||
renderer.renderCompatibilityStubs();
|
renderer.renderCompatibilityStubs();
|
||||||
for (var entry : controller.getEntryPoints().entrySet()) {
|
|
||||||
var alias = "$rt_export_" + entry.getKey();
|
var alias = "$rt_export_main";
|
||||||
var ref = entry.getValue().getMethod();
|
var ref = new MethodReference(controller.getEntryPoint(), "main", ValueType.parse(String[].class),
|
||||||
|
ValueType.parse(void.class));
|
||||||
|
if (classes.resolve(ref) != null) {
|
||||||
rememberingWriter.startVariableDeclaration().appendFunction(alias)
|
rememberingWriter.startVariableDeclaration().appendFunction(alias)
|
||||||
.appendFunction("$rt_mainStarter").append("(").appendMethod(ref);
|
.appendFunction("$rt_mainStarter").append("(").appendMethod(ref);
|
||||||
rememberingWriter.append(")").endDeclaration();
|
rememberingWriter.append(")").endDeclaration();
|
||||||
rememberingWriter.appendFunction(alias).append(".")
|
rememberingWriter.appendFunction(alias).append(".")
|
||||||
.append("javaException").ws().append("=").ws().appendFunction("$rt_javaException")
|
.append("javaException").ws().append("=").ws().appendFunction("$rt_javaException")
|
||||||
.append(";").newLine();
|
.append(";").newLine();
|
||||||
|
exports.add(new ExportedDeclaration(w -> w.appendFunction(alias),
|
||||||
|
n -> n.functionName(alias), controller.getEntryPointName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var listener : rendererListeners) {
|
for (var listener : rendererListeners) {
|
||||||
|
@ -448,8 +454,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
for (var module : importedModules.values()) {
|
for (var module : importedModules.values()) {
|
||||||
naming.functionName(module);
|
naming.functionName(module);
|
||||||
}
|
}
|
||||||
for (var exportedName : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
naming.functionName("$rt_export_" + exportedName);
|
export.nameFreq.accept(naming);
|
||||||
}
|
}
|
||||||
var frequencyEstimator = new NameFrequencyEstimator();
|
var frequencyEstimator = new NameFrequencyEstimator();
|
||||||
runtime.replay(frequencyEstimator, RememberedSource.FILTER_REF);
|
runtime.replay(frequencyEstimator, RememberedSource.FILTER_REF);
|
||||||
|
@ -553,8 +559,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printIIFStart(SourceWriter writer) {
|
private void printIIFStart(SourceWriter writer) {
|
||||||
for (var exportedName : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
writer.append("var ").appendGlobal(exportedName).append(";").softNewLine();
|
writer.append("var ").appendGlobal(export.alias).append(";").softNewLine();
|
||||||
}
|
}
|
||||||
writer.append("(function()").appendBlockStart();
|
writer.append("(function()").appendBlockStart();
|
||||||
|
|
||||||
|
@ -603,40 +609,42 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printUmdEnd(SourceWriter writer) {
|
private void printUmdEnd(SourceWriter writer) {
|
||||||
for (var export : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
writer.appendFunction("$rt_exports").append(".").append(export).ws().append("=").ws()
|
writer.appendFunction("$rt_exports").append(".").append(export.alias).ws()
|
||||||
.appendFunction("$rt_export_" + export).append(";").softNewLine();
|
.append("=").ws();
|
||||||
|
export.name.accept(writer);
|
||||||
|
writer.append(";").softNewLine();
|
||||||
}
|
}
|
||||||
writer.outdent().append("}));").newLine();
|
writer.outdent().append("}));").newLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printCommonJsEnd(SourceWriter writer) {
|
private void printCommonJsEnd(SourceWriter writer) {
|
||||||
for (var export : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
writer.appendFunction("exports.").append(export).ws().append("=").ws()
|
writer.append("exports.").append(export.alias).ws().append("=").ws();
|
||||||
.appendFunction("$rt_export_" + export).append(";").softNewLine();
|
export.name.accept(writer);
|
||||||
|
writer.append(";").softNewLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printIFFEnd(SourceWriter writer) {
|
private void printIFFEnd(SourceWriter writer) {
|
||||||
for (var exportedName : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
writer.appendGlobal(exportedName).ws().append("=").ws().appendFunction("$rt_export_" + exportedName)
|
writer.appendGlobal(export.alias).ws().append("=").ws();
|
||||||
.append(";").softNewLine();
|
export.name.accept(writer);
|
||||||
|
writer.append(";").softNewLine();
|
||||||
}
|
}
|
||||||
writer.outdent().append("})();");
|
writer.outdent().append("})();");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printES2015End(SourceWriter writer) {
|
private void printES2015End(SourceWriter writer) {
|
||||||
if (controller.getEntryPoints().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
writer.append("export").ws().append("{").ws();
|
writer.append("export").ws().append("{").ws();
|
||||||
var first = true;
|
var first = true;
|
||||||
for (var exportedName : controller.getEntryPoints().keySet()) {
|
for (var export : exports) {
|
||||||
if (!first) {
|
if (!first) {
|
||||||
writer.append(",").ws();
|
writer.append(",").ws();
|
||||||
}
|
}
|
||||||
first = false;
|
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();
|
writer.ws().append("};").softNewLine();
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.teavm.ast.RegularMethodNode;
|
||||||
import org.teavm.ast.analysis.LocationGraphBuilder;
|
import org.teavm.ast.analysis.LocationGraphBuilder;
|
||||||
import org.teavm.ast.decompilation.DecompilationException;
|
import org.teavm.ast.decompilation.DecompilationException;
|
||||||
import org.teavm.ast.decompilation.Decompiler;
|
import org.teavm.ast.decompilation.Decompiler;
|
||||||
|
import org.teavm.backend.javascript.ExportedDeclaration;
|
||||||
import org.teavm.backend.javascript.codegen.SourceWriter;
|
import org.teavm.backend.javascript.codegen.SourceWriter;
|
||||||
import org.teavm.backend.javascript.spi.GeneratedBy;
|
import org.teavm.backend.javascript.spi.GeneratedBy;
|
||||||
import org.teavm.backend.javascript.spi.Generator;
|
import org.teavm.backend.javascript.spi.Generator;
|
||||||
|
@ -89,11 +90,15 @@ public class Renderer implements RenderingManager {
|
||||||
private JavaScriptTemplateFactory templateFactory;
|
private JavaScriptTemplateFactory templateFactory;
|
||||||
private boolean threadLibraryUsed;
|
private boolean threadLibraryUsed;
|
||||||
private AstDependencyExtractor dependencyExtractor = new AstDependencyExtractor();
|
private AstDependencyExtractor dependencyExtractor = new AstDependencyExtractor();
|
||||||
|
private List<ExportedDeclaration> exports;
|
||||||
|
private String entryPoint;
|
||||||
|
|
||||||
public static final MethodDescriptor CLINIT_METHOD = new MethodDescriptor("<clinit>", ValueType.VOID);
|
public static final MethodDescriptor CLINIT_METHOD = new MethodDescriptor("<clinit>", ValueType.VOID);
|
||||||
|
|
||||||
public Renderer(SourceWriter writer, Set<MethodReference> asyncMethods, RenderingContext context,
|
public Renderer(SourceWriter writer, Set<MethodReference> asyncMethods, RenderingContext context,
|
||||||
Diagnostics diagnostics, Map<MethodReference, Generator> generators,
|
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.writer = writer;
|
||||||
this.classSource = context.getClassSource();
|
this.classSource = context.getClassSource();
|
||||||
this.classLoader = context.getClassLoader();
|
this.classLoader = context.getClassLoader();
|
||||||
|
@ -106,6 +111,8 @@ public class Renderer implements RenderingManager {
|
||||||
this.astCache = astCache;
|
this.astCache = astCache;
|
||||||
this.cacheStatus = cacheStatus;
|
this.cacheStatus = cacheStatus;
|
||||||
this.templateFactory = templateFactory;
|
this.templateFactory = templateFactory;
|
||||||
|
this.exports = exports;
|
||||||
|
this.entryPoint = entryPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -113,6 +120,27 @@ public class Renderer implements RenderingManager {
|
||||||
return writer;
|
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() {
|
public boolean isThreadLibraryUsed() {
|
||||||
return threadLibraryUsed;
|
return threadLibraryUsed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,22 @@ import java.util.Properties;
|
||||||
import org.teavm.backend.javascript.codegen.SourceWriter;
|
import org.teavm.backend.javascript.codegen.SourceWriter;
|
||||||
import org.teavm.common.ServiceRepository;
|
import org.teavm.common.ServiceRepository;
|
||||||
import org.teavm.model.ListableClassReaderSource;
|
import org.teavm.model.ListableClassReaderSource;
|
||||||
|
import org.teavm.model.MethodReference;
|
||||||
|
|
||||||
public interface RenderingManager extends ServiceRepository {
|
public interface RenderingManager extends ServiceRepository {
|
||||||
SourceWriter getWriter();
|
SourceWriter getWriter();
|
||||||
|
|
||||||
|
void exportMethod(MethodReference method, String alias);
|
||||||
|
|
||||||
|
void exportClass(String className, String alias);
|
||||||
|
|
||||||
|
void exportFunction(String functionName, String alias);
|
||||||
|
|
||||||
ListableClassReaderSource getClassSource();
|
ListableClassReaderSource getClassSource();
|
||||||
|
|
||||||
ClassLoader getClassLoader();
|
ClassLoader getClassLoader();
|
||||||
|
|
||||||
Properties getProperties();
|
Properties getProperties();
|
||||||
|
|
||||||
|
String getEntryPoint();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.WasmReturn;
|
||||||
import org.teavm.backend.wasm.model.expression.WasmSetLocal;
|
import org.teavm.backend.wasm.model.expression.WasmSetLocal;
|
||||||
import org.teavm.backend.wasm.model.expression.WasmStoreInt32;
|
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.optimization.UnusedFunctionElimination;
|
||||||
import org.teavm.backend.wasm.render.ReportingWasmBinaryStatsCollector;
|
import org.teavm.backend.wasm.render.ReportingWasmBinaryStatsCollector;
|
||||||
import org.teavm.backend.wasm.render.WasmBinaryRenderer;
|
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.RuntimeObject;
|
||||||
import org.teavm.runtime.ShadowStack;
|
import org.teavm.runtime.ShadowStack;
|
||||||
import org.teavm.vm.BuildTarget;
|
import org.teavm.vm.BuildTarget;
|
||||||
import org.teavm.vm.TeaVMEntryPoint;
|
|
||||||
import org.teavm.vm.TeaVMTarget;
|
import org.teavm.vm.TeaVMTarget;
|
||||||
import org.teavm.vm.TeaVMTargetController;
|
import org.teavm.vm.TeaVMTargetController;
|
||||||
import org.teavm.vm.spi.TeaVMHostExtension;
|
import org.teavm.vm.spi.TeaVMHostExtension;
|
||||||
|
@ -1197,13 +1195,12 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
|
||||||
public WasmExpression apply(InvocationExpr invocation, WasmIntrinsicManager manager) {
|
public WasmExpression apply(InvocationExpr invocation, WasmIntrinsicManager manager) {
|
||||||
switch (invocation.getMethod().getName()) {
|
switch (invocation.getMethod().getName()) {
|
||||||
case "runMain": {
|
case "runMain": {
|
||||||
var entryPointIter = controller.getEntryPoints().values().iterator();
|
var entryPoint = new MethodReference(controller.getEntryPoint(),
|
||||||
if (entryPointIter.hasNext()) {
|
"main", ValueType.parse(String[].class), ValueType.parse(void.class));
|
||||||
TeaVMEntryPoint entryPoint = entryPointIter.next();
|
String name = manager.getNames().forMethod(entryPoint);
|
||||||
String name = manager.getNames().forMethod(entryPoint.getMethod());
|
|
||||||
WasmCall call = new WasmCall(name);
|
WasmCall call = new WasmCall(name);
|
||||||
var arg = manager.generate(invocation.getArguments().get(0));
|
var arg = manager.generate(invocation.getArguments().get(0));
|
||||||
if (manager.isManagedMethodCall(entryPoint.getMethod())) {
|
if (manager.isManagedMethodCall(entryPoint)) {
|
||||||
var block = new WasmBlock(false);
|
var block = new WasmBlock(false);
|
||||||
block.setType(WasmType.INT32);
|
block.setType(WasmType.INT32);
|
||||||
var callSiteId = manager.generateCallSiteId(invocation.getLocation());
|
var callSiteId = manager.generateCallSiteId(invocation.getLocation());
|
||||||
|
@ -1215,11 +1212,6 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
|
||||||
call.getArguments().add(arg);
|
call.getArguments().add(arg);
|
||||||
call.setLocation(invocation.getLocation());
|
call.setLocation(invocation.getLocation());
|
||||||
return call;
|
return call;
|
||||||
} else {
|
|
||||||
var unreachable = new WasmUnreachable();
|
|
||||||
unreachable.setLocation(invocation.getLocation());
|
|
||||||
return unreachable;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "setCurrentThread": {
|
case "setCurrentThread": {
|
||||||
String name = manager.getNames().forMethod(new MethodReference(Thread.class,
|
String name = manager.getNames().forMethod(new MethodReference(Thread.class,
|
||||||
|
|
|
@ -24,11 +24,20 @@ import org.teavm.model.*;
|
||||||
|
|
||||||
public class DependencyAgent implements DependencyInfo, ServiceRepository {
|
public class DependencyAgent implements DependencyInfo, ServiceRepository {
|
||||||
private DependencyAnalyzer analyzer;
|
private DependencyAnalyzer analyzer;
|
||||||
|
private String entryPoint;
|
||||||
|
|
||||||
DependencyAgent(DependencyAnalyzer analyzer) {
|
DependencyAgent(DependencyAnalyzer analyzer) {
|
||||||
this.analyzer = analyzer;
|
this.analyzer = analyzer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getEntryPoint() {
|
||||||
|
return entryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEntryPoint(String entryPoint) {
|
||||||
|
this.entryPoint = entryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
public DependencyNode createNode() {
|
public DependencyNode createNode() {
|
||||||
return analyzer.createNode();
|
return analyzer.createNode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,11 @@ public abstract class DependencyAnalyzer implements DependencyInfo {
|
||||||
classType = getType("java.lang.Class");
|
classType = getType("java.lang.Class");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEntryPoint(String entryPoint) {
|
||||||
|
classSource.setEntryPoint(entryPoint);
|
||||||
|
agent.setEntryPoint(entryPoint);
|
||||||
|
}
|
||||||
|
|
||||||
public void setObfuscated(boolean obfuscated) {
|
public void setObfuscated(boolean obfuscated) {
|
||||||
classSource.obfuscated = obfuscated;
|
classSource.obfuscated = obfuscated;
|
||||||
}
|
}
|
||||||
|
@ -290,20 +295,6 @@ public abstract class DependencyAnalyzer implements DependencyInfo {
|
||||||
classSource.addTransformer(transformer);
|
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;
|
private int propagationDepth;
|
||||||
|
|
||||||
void schedulePropagation(DependencyConsumer consumer, DependencyType type) {
|
void schedulePropagation(DependencyConsumer consumer, DependencyType type) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ class DependencyClassSource implements ClassHolderSource {
|
||||||
Map<String, Optional<ClassHolder>> cache = new LinkedHashMap<>(1000, 0.5f);
|
Map<String, Optional<ClassHolder>> cache = new LinkedHashMap<>(1000, 0.5f);
|
||||||
private ReferenceResolver referenceResolver;
|
private ReferenceResolver referenceResolver;
|
||||||
private ClassInitInsertion classInitInsertion;
|
private ClassInitInsertion classInitInsertion;
|
||||||
|
private String entryPoint;
|
||||||
|
|
||||||
DependencyClassSource(ClassReaderSource innerSource, Diagnostics diagnostics,
|
DependencyClassSource(ClassReaderSource innerSource, Diagnostics diagnostics,
|
||||||
IncrementalDependencyRegistration dependencyRegistration, String[] platformTags) {
|
IncrementalDependencyRegistration dependencyRegistration, String[] platformTags) {
|
||||||
|
@ -117,10 +118,6 @@ class DependencyClassSource implements ClassHolderSource {
|
||||||
return generatedClasses.keySet();
|
return generatedClasses.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<ClassHolder> getGeneratedClasses() {
|
|
||||||
return generatedClasses.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isGeneratedClass(String className) {
|
public boolean isGeneratedClass(String className) {
|
||||||
return generatedClasses.containsKey(className);
|
return generatedClasses.containsKey(className);
|
||||||
}
|
}
|
||||||
|
@ -129,6 +126,10 @@ class DependencyClassSource implements ClassHolderSource {
|
||||||
transformers.add(transformer);
|
transformers.add(transformer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setEntryPoint(String entryPoint) {
|
||||||
|
this.entryPoint = entryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
transformers.clear();
|
transformers.clear();
|
||||||
}
|
}
|
||||||
|
@ -163,5 +164,10 @@ class DependencyClassSource implements ClassHolderSource {
|
||||||
public void submit(ClassHolder cls) {
|
public void submit(ClassHolder cls) {
|
||||||
DependencyClassSource.this.submit(cls);
|
DependencyClassSource.this.submit(cls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntryPoint() {
|
||||||
|
return entryPoint;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,5 +29,7 @@ public interface ClassHolderTransformerContext {
|
||||||
|
|
||||||
boolean isStrict();
|
boolean isStrict();
|
||||||
|
|
||||||
|
String getEntryPoint();
|
||||||
|
|
||||||
void submit(ClassHolder cls);
|
void submit(ClassHolder cls);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,14 +50,17 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo {
|
||||||
private Map<MethodReference, MethodInfo> methodInfoMap = new HashMap<>();
|
private Map<MethodReference, MethodInfo> methodInfoMap = new HashMap<>();
|
||||||
private ListableClassReaderSource classes;
|
private ListableClassReaderSource classes;
|
||||||
private ClassHierarchy hierarchy;
|
private ClassHierarchy hierarchy;
|
||||||
|
private String entryPoint;
|
||||||
private List<String> order = new ArrayList<>();
|
private List<String> order = new ArrayList<>();
|
||||||
private List<? extends String> readonlyOrder = Collections.unmodifiableList(order);
|
private List<? extends String> readonlyOrder = Collections.unmodifiableList(order);
|
||||||
private String currentAnalyzedClass;
|
private String currentAnalyzedClass;
|
||||||
private DependencyInfo dependencyInfo;
|
private DependencyInfo dependencyInfo;
|
||||||
|
|
||||||
public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy) {
|
public ClassInitializerAnalysis(ListableClassReaderSource classes, ClassHierarchy hierarchy,
|
||||||
|
String entryPoint) {
|
||||||
this.classes = classes;
|
this.classes = classes;
|
||||||
this.hierarchy = hierarchy;
|
this.hierarchy = hierarchy;
|
||||||
|
this.entryPoint = entryPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void analyze(DependencyInfo dependencyInfo) {
|
public void analyze(DependencyInfo dependencyInfo) {
|
||||||
|
@ -99,6 +102,11 @@ public class ClassInitializerAnalysis implements ClassInitializerInfo {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (className.equals(entryPoint)) {
|
||||||
|
classStatuses.put(className, DYNAMIC);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var cls = classes.get(className);
|
var cls = classes.get(className);
|
||||||
|
|
||||||
if (cls == null || cls.getAnnotations().get(StaticInit.class.getName()) != null) {
|
if (cls == null || cls.getAnnotations().get(StaticInit.class.getName()) != null) {
|
||||||
|
|
|
@ -23,7 +23,6 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -73,6 +72,7 @@ import org.teavm.model.ProgramCache;
|
||||||
import org.teavm.model.ValueType;
|
import org.teavm.model.ValueType;
|
||||||
import org.teavm.model.analysis.ClassInitializerAnalysis;
|
import org.teavm.model.analysis.ClassInitializerAnalysis;
|
||||||
import org.teavm.model.analysis.ClassInitializerInfo;
|
import org.teavm.model.analysis.ClassInitializerInfo;
|
||||||
|
import org.teavm.model.instructions.ExitInstruction;
|
||||||
import org.teavm.model.instructions.InitClassInstruction;
|
import org.teavm.model.instructions.InitClassInstruction;
|
||||||
import org.teavm.model.instructions.InvokeInstruction;
|
import org.teavm.model.instructions.InvokeInstruction;
|
||||||
import org.teavm.model.optimization.ArrayUnwrapMotion;
|
import org.teavm.model.optimization.ArrayUnwrapMotion;
|
||||||
|
@ -134,12 +134,13 @@ import org.teavm.vm.spi.TeaVMPlugin;
|
||||||
public class TeaVM implements TeaVMHost, ServiceRepository {
|
public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
private static final MethodDescriptor MAIN_METHOD_DESC = new MethodDescriptor("main",
|
private static final MethodDescriptor MAIN_METHOD_DESC = new MethodDescriptor("main",
|
||||||
ValueType.arrayOf(ValueType.object("java.lang.String")), ValueType.VOID);
|
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 DependencyAnalyzer dependencyAnalyzer;
|
||||||
private final AccumulationDiagnostics diagnostics = new AccumulationDiagnostics();
|
private final AccumulationDiagnostics diagnostics = new AccumulationDiagnostics();
|
||||||
private final ClassLoader classLoader;
|
private final ClassLoader classLoader;
|
||||||
private final Map<String, TeaVMEntryPoint> entryPoints = new LinkedHashMap<>();
|
private String entryPoint;
|
||||||
private final Map<String, TeaVMEntryPoint> readonlyEntryPoints = Collections.unmodifiableMap(entryPoints);
|
private String entryPointName = "main";
|
||||||
private final Set<String> preservedClasses = new HashSet<>();
|
private final Set<String> preservedClasses = new HashSet<>();
|
||||||
private final Set<String> readonlyPreservedClasses = Collections.unmodifiableSet(preservedClasses);
|
private final Set<String> readonlyPreservedClasses = Collections.unmodifiableSet(preservedClasses);
|
||||||
private final Map<Class<?>, Object> services = new HashMap<>();
|
private final Map<Class<?>, Object> services = new HashMap<>();
|
||||||
|
@ -284,39 +285,50 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
return target.getPlatformTags();
|
return target.getPlatformTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void entryPoint(String className, String name) {
|
public void setEntryPoint(String entryPoint) {
|
||||||
if (entryPoints.containsKey(name)) {
|
this.entryPoint = entryPoint;
|
||||||
throw new IllegalArgumentException("Entry point with public name `" + name + "' already defined "
|
|
||||||
+ "for class " + className);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cls = dependencyAnalyzer.getClassSource().get(className);
|
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) {
|
if (cls == null) {
|
||||||
diagnostics.error(null, "There's no main class: '{{c0}}'", className);
|
diagnostics.error(null, "There's no main class: '{{c0}}'", entryPoint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cls.getMethod(MAIN_METHOD_DESC) == null) {
|
var mainMethod = cls.getMethod(MAIN_METHOD_DESC) != null
|
||||||
diagnostics.error(null, "Specified main class '{{c0}}' does not have method '" + MAIN_METHOD_DESC + "'",
|
? dependencyAnalyzer.linkMethod(new MethodReference(entryPoint,
|
||||||
cls.getName());
|
"main", ValueType.parse(String[].class), ValueType.VOID))
|
||||||
return;
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
var mainMethod = dependencyAnalyzer.linkMethod(new MethodReference(className,
|
|
||||||
"main", ValueType.parse(String[].class), ValueType.VOID));
|
|
||||||
|
|
||||||
var entryPoint = new TeaVMEntryPoint(name, mainMethod);
|
|
||||||
dependencyAnalyzer.defer(() -> {
|
dependencyAnalyzer.defer(() -> {
|
||||||
dependencyAnalyzer.linkClass(className).initClass(null);
|
dependencyAnalyzer.linkClass(entryPoint).initClass(null);
|
||||||
|
if (mainMethod != null) {
|
||||||
mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;"));
|
mainMethod.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/String;"));
|
||||||
mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String"));
|
mainMethod.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType("java.lang.String"));
|
||||||
mainMethod.use();
|
mainMethod.use();
|
||||||
});
|
|
||||||
entryPoints.put(name, entryPoint);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
public void entryPoint(String className) {
|
|
||||||
entryPoint(className, "main");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void preserveType(String className) {
|
public void preserveType(String className) {
|
||||||
|
@ -368,6 +380,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processEntryPoint();
|
||||||
dependencyAnalyzer.setAsyncSupported(target.isAsyncSupported());
|
dependencyAnalyzer.setAsyncSupported(target.isAsyncSupported());
|
||||||
dependencyAnalyzer.setInterruptor(() -> {
|
dependencyAnalyzer.setInterruptor(() -> {
|
||||||
int progress = dependencyAnalyzer.getReachableClasses().size();
|
int progress = dependencyAnalyzer.getReachableClasses().size();
|
||||||
|
@ -453,7 +466,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
var classInitializerAnalysis = new ClassInitializerAnalysis(classSet,
|
var classInitializerAnalysis = new ClassInitializerAnalysis(classSet,
|
||||||
dependencyAnalyzer.getClassHierarchy());
|
dependencyAnalyzer.getClassHierarchy(), entryPoint);
|
||||||
classInitializerAnalysis.analyze(dependencyAnalyzer);
|
classInitializerAnalysis.analyze(dependencyAnalyzer);
|
||||||
classInitializerInfo = classInitializerAnalysis;
|
classInitializerInfo = classInitializerAnalysis;
|
||||||
insertClassInit(classSet);
|
insertClassInit(classSet);
|
||||||
|
@ -535,13 +548,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var initializers = target.getInitializerMethods();
|
addInitializersToEntryPoint(classes, new MethodReference(entryPoint, CLINIT_DESC));
|
||||||
if (initializers == null) {
|
|
||||||
initializers = entryPoints.values().stream().map(ep -> ep.getMethod()).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
for (var initMethod : initializers) {
|
|
||||||
addInitializersToEntryPoint(classes, initMethod);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addInitializersToEntryPoint(ClassHolderSource classes, MethodReference methodRef) {
|
private void addInitializersToEntryPoint(ClassHolderSource classes, MethodReference methodRef) {
|
||||||
|
@ -560,7 +567,7 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
Instruction first = block.getFirstInstruction();
|
Instruction first = block.getFirstInstruction();
|
||||||
for (String className : classInitializerInfo.getInitializationOrder()) {
|
for (String className : classInitializerInfo.getInitializationOrder()) {
|
||||||
var invoke = new InvokeInstruction();
|
var invoke = new InvokeInstruction();
|
||||||
invoke.setMethod(new MethodReference(className, "<clinit>", ValueType.VOID));
|
invoke.setMethod(new MethodReference(className, CLINIT_DESC));
|
||||||
first.insertPrevious(invoke);
|
first.insertPrevious(invoke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -914,8 +921,13 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, TeaVMEntryPoint> getEntryPoints() {
|
public String getEntryPoint() {
|
||||||
return readonlyEntryPoints;
|
return entryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntryPointName() {
|
||||||
|
return entryPointName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1032,9 +1044,9 @@ public class TeaVM implements TeaVMHost, ServiceRepository {
|
||||||
|
|
||||||
ListableClassReaderSourceAdapter(ClassReaderSource classSource, Set<String> classes) {
|
ListableClassReaderSourceAdapter(ClassReaderSource classSource, Set<String> classes) {
|
||||||
this.classSource = classSource;
|
this.classSource = classSource;
|
||||||
this.classes = Collections.unmodifiableSet(classes.stream()
|
this.classes = classes.stream()
|
||||||
.filter(className -> classSource.get(className) != null)
|
.filter(className -> classSource.get(className) != null)
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.teavm.vm;
|
package org.teavm.vm;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
@ -48,7 +47,9 @@ public interface TeaVMTargetController {
|
||||||
|
|
||||||
boolean isFriendlyToDebugger();
|
boolean isFriendlyToDebugger();
|
||||||
|
|
||||||
Map<? extends String, ? extends TeaVMEntryPoint> getEntryPoints();
|
String getEntryPoint();
|
||||||
|
|
||||||
|
String getEntryPointName();
|
||||||
|
|
||||||
Set<? extends String> getPreservedClasses();
|
Set<? extends String> getPreservedClasses();
|
||||||
|
|
||||||
|
|
27
jso/core/src/main/java/org/teavm/jso/JSClass.java
Normal file
27
jso/core/src/main/java/org/teavm/jso/JSClass.java
Normal 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 "";
|
||||||
|
}
|
26
jso/core/src/main/java/org/teavm/jso/JSExport.java
Normal file
26
jso/core/src/main/java/org/teavm/jso/JSExport.java
Normal 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 {
|
||||||
|
}
|
27
jso/core/src/main/java/org/teavm/jso/JSExportClasses.java
Normal file
27
jso/core/src/main/java/org/teavm/jso/JSExportClasses.java
Normal 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();
|
||||||
|
}
|
|
@ -16,12 +16,16 @@
|
||||||
package org.teavm.jso.impl;
|
package org.teavm.jso.impl;
|
||||||
|
|
||||||
import java.util.HashMap;
|
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.codegen.SourceWriter;
|
||||||
import org.teavm.backend.javascript.rendering.RenderingManager;
|
import org.teavm.backend.javascript.rendering.RenderingManager;
|
||||||
import org.teavm.backend.javascript.spi.VirtualMethodContributor;
|
import org.teavm.backend.javascript.spi.VirtualMethodContributor;
|
||||||
import org.teavm.backend.javascript.spi.VirtualMethodContributorContext;
|
import org.teavm.backend.javascript.spi.VirtualMethodContributorContext;
|
||||||
|
import org.teavm.jso.JSClass;
|
||||||
import org.teavm.model.AnnotationReader;
|
import org.teavm.model.AnnotationReader;
|
||||||
import org.teavm.model.ClassReader;
|
import org.teavm.model.ClassReader;
|
||||||
|
import org.teavm.model.ElementModifier;
|
||||||
import org.teavm.model.FieldReader;
|
import org.teavm.model.FieldReader;
|
||||||
import org.teavm.model.FieldReference;
|
import org.teavm.model.FieldReference;
|
||||||
import org.teavm.model.ListableClassReaderSource;
|
import org.teavm.model.ListableClassReaderSource;
|
||||||
|
@ -36,29 +40,145 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
|
||||||
private SourceWriter writer;
|
private SourceWriter writer;
|
||||||
private ListableClassReaderSource classSource;
|
private ListableClassReaderSource classSource;
|
||||||
private JSTypeHelper typeHelper;
|
private JSTypeHelper typeHelper;
|
||||||
|
private RenderingManager context;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void begin(RenderingManager context, BuildTarget buildTarget) {
|
public void begin(RenderingManager context, BuildTarget buildTarget) {
|
||||||
writer = context.getWriter();
|
writer = context.getWriter();
|
||||||
classSource = context.getClassSource();
|
classSource = context.getClassSource();
|
||||||
typeHelper = new JSTypeHelper(context.getClassSource());
|
typeHelper = new JSTypeHelper(context.getClassSource());
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void complete() {
|
public void complete() {
|
||||||
|
exportClasses();
|
||||||
|
exportModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportClasses() {
|
||||||
if (!hasClassesToExpose()) {
|
if (!hasClassesToExpose()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.startVariableDeclaration().appendFunction("$rt_jso_marker")
|
writer.startVariableDeclaration().appendFunction("$rt_jso_marker")
|
||||||
.appendGlobal("Symbol").append("('jsoClass')").endDeclaration();
|
.appendGlobal("Symbol").append("('jsoClass')").endDeclaration();
|
||||||
writer.append("(function()").ws().append("{").softNewLine().indent();
|
writer.append("(()").ws().append("=>").ws().append("{").softNewLine().indent();
|
||||||
writer.append("var c;").softNewLine();
|
writer.append("let c;").softNewLine();
|
||||||
for (String className : classSource.getClassNames()) {
|
for (var className : classSource.getClassNames()) {
|
||||||
ClassReader classReader = classSource.get(className);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 methods = new HashMap<String, MethodDescriptor>();
|
||||||
var properties = new HashMap<String, PropertyInfo>();
|
var properties = new HashMap<String, PropertyInfo>();
|
||||||
for (var method : classReader.getMethods()) {
|
for (var method : classReader.getMethods()) {
|
||||||
|
if (!filter.test(method)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var methodAlias = getPublicAlias(method);
|
var methodAlias = getPublicAlias(method);
|
||||||
if (methodAlias != null) {
|
if (methodAlias != null) {
|
||||||
switch (methodAlias.kind) {
|
switch (methodAlias.kind) {
|
||||||
|
@ -78,62 +198,51 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new Members(methods, properties);
|
||||||
|
}
|
||||||
|
|
||||||
var isJsClassImpl = typeHelper.isJavaScriptImplementation(className);
|
private void exportModule() {
|
||||||
if (methods.isEmpty() && properties.isEmpty() && !isJsClassImpl) {
|
var cls = classSource.get(context.getEntryPoint());
|
||||||
|
for (var method : cls.getMethods()) {
|
||||||
|
if (!method.hasModifier(ElementModifier.STATIC)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
var methodAlias = getPublicAlias(method);
|
||||||
writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;")
|
if (methodAlias != null && methodAlias.kind == AliasKind.METHOD) {
|
||||||
.softNewLine();
|
context.exportMethod(method.getReference(), methodAlias.name);
|
||||||
if (isJsClassImpl) {
|
}
|
||||||
writer.append("c[").appendFunction("$rt_jso_marker").append("]").ws().append("=").ws().append("true;")
|
}
|
||||||
.softNewLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var aliasEntry : methods.entrySet()) {
|
private void exportClassFromModule(ClassReader cls) {
|
||||||
if (classReader.getMethod(aliasEntry.getValue()) == null) {
|
var name = cls.getSimpleName();
|
||||||
continue;
|
if (name == null) {
|
||||||
|
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
|
||||||
}
|
}
|
||||||
if (isKeyword(aliasEntry.getKey())) {
|
var jsExport = cls.getAnnotations().get(JSClass.class.getName());
|
||||||
writer.append("c[\"").append(aliasEntry.getKey()).append("\"]");
|
if (jsExport != null) {
|
||||||
} else {
|
var nameValue = jsExport.getValue("name");
|
||||||
writer.append("c.").append(aliasEntry.getKey());
|
if (nameValue != null) {
|
||||||
}
|
var nameValueString = nameValue.getString();
|
||||||
writer.ws().append("=").ws().append("c.").appendVirtualMethod(aliasEntry.getValue())
|
if (!nameValueString.isEmpty()) {
|
||||||
.append(";").softNewLine();
|
name = nameValueString;
|
||||||
}
|
|
||||||
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();
|
}
|
||||||
|
context.exportClass(cls.getName(), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasClassesToExpose() {
|
private boolean hasClassesToExpose() {
|
||||||
for (String className : classSource.getClassNames()) {
|
for (String className : classSource.getClassNames()) {
|
||||||
ClassReader cls = classSource.get(className);
|
ClassReader cls = classSource.get(className);
|
||||||
if (cls.getMethods().stream().anyMatch(method -> getPublicAlias(method) != null)
|
if (typeHelper.isJavaScriptImplementation(className)) {
|
||||||
|| typeHelper.isJavaScriptImplementation(className)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
for (var method : cls.getMethods()) {
|
||||||
|
if (!method.hasModifier(ElementModifier.STATIC) && getPublicAlias(method) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -242,12 +351,22 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
|
||||||
return methodReader != null && getPublicAlias(methodReader) != null;
|
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 getter;
|
||||||
MethodDescriptor setter;
|
MethodDescriptor setter;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class Alias {
|
private static class Alias {
|
||||||
final String name;
|
final String name;
|
||||||
final AliasKind kind;
|
final AliasKind kind;
|
||||||
|
|
||||||
|
@ -257,7 +376,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AliasKind {
|
private enum AliasKind {
|
||||||
METHOD,
|
METHOD,
|
||||||
GETTER,
|
GETTER,
|
||||||
SETTER
|
SETTER
|
||||||
|
|
|
@ -18,11 +18,14 @@ package org.teavm.jso.impl;
|
||||||
import org.teavm.dependency.AbstractDependencyListener;
|
import org.teavm.dependency.AbstractDependencyListener;
|
||||||
import org.teavm.dependency.DependencyAgent;
|
import org.teavm.dependency.DependencyAgent;
|
||||||
import org.teavm.dependency.MethodDependency;
|
import org.teavm.dependency.MethodDependency;
|
||||||
|
import org.teavm.jso.JSExportClasses;
|
||||||
import org.teavm.model.AnnotationReader;
|
import org.teavm.model.AnnotationReader;
|
||||||
import org.teavm.model.CallLocation;
|
import org.teavm.model.CallLocation;
|
||||||
import org.teavm.model.ClassReader;
|
import org.teavm.model.ClassReader;
|
||||||
|
import org.teavm.model.ElementModifier;
|
||||||
import org.teavm.model.MethodReader;
|
import org.teavm.model.MethodReader;
|
||||||
import org.teavm.model.MethodReference;
|
import org.teavm.model.MethodReference;
|
||||||
|
import org.teavm.model.ValueType;
|
||||||
|
|
||||||
class JSDependencyListener extends AbstractDependencyListener {
|
class JSDependencyListener extends AbstractDependencyListener {
|
||||||
private JSBodyRepository repository;
|
private JSBodyRepository repository;
|
||||||
|
@ -55,9 +58,23 @@ class JSDependencyListener extends AbstractDependencyListener {
|
||||||
}
|
}
|
||||||
if (exposeAnnot != null) {
|
if (exposeAnnot != null) {
|
||||||
MethodDependency methodDep = agent.linkMethod(method.getReference());
|
MethodDependency methodDep = agent.linkMethod(method.getReference());
|
||||||
|
if (methodDep.getMethod() != null) {
|
||||||
|
if (!methodDep.getMethod().hasModifier(ElementModifier.STATIC)) {
|
||||||
methodDep.getVariable(0).propagate(agent.getType(className));
|
methodDep.getVariable(0).propagate(agent.getType(className));
|
||||||
|
}
|
||||||
methodDep.use();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,12 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.teavm.diagnostics.Diagnostics;
|
import org.teavm.diagnostics.Diagnostics;
|
||||||
|
import org.teavm.jso.JSExport;
|
||||||
import org.teavm.jso.JSMethod;
|
import org.teavm.jso.JSMethod;
|
||||||
import org.teavm.jso.JSObject;
|
import org.teavm.jso.JSObject;
|
||||||
import org.teavm.jso.JSProperty;
|
import org.teavm.jso.JSProperty;
|
||||||
import org.teavm.model.AccessLevel;
|
import org.teavm.model.AccessLevel;
|
||||||
import org.teavm.model.AnnotationHolder;
|
import org.teavm.model.AnnotationHolder;
|
||||||
import org.teavm.model.AnnotationReader;
|
|
||||||
import org.teavm.model.AnnotationValue;
|
import org.teavm.model.AnnotationValue;
|
||||||
import org.teavm.model.BasicBlock;
|
import org.teavm.model.BasicBlock;
|
||||||
import org.teavm.model.CallLocation;
|
import org.teavm.model.CallLocation;
|
||||||
|
@ -99,6 +99,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
}
|
}
|
||||||
|
|
||||||
exposeMethods(cls, exposedClass, context.getDiagnostics(), functorMethod);
|
exposeMethods(cls, exposedClass, context.getDiagnostics(), functorMethod);
|
||||||
|
exportStaticMethods(cls, context.getDiagnostics());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics,
|
private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics,
|
||||||
|
@ -156,6 +157,76 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
classHolder.addMethod(exportedMethod);
|
classHolder.addMethod(exportedMethod);
|
||||||
|
|
||||||
var export = classToExpose.methods.get(method);
|
var export = classToExpose.methods.get(method);
|
||||||
|
exportedMethod.getAnnotations().add(createExportAnnotation(export));
|
||||||
|
|
||||||
|
if (methodRef.equals(functorMethod)) {
|
||||||
|
addFunctorField(classHolder, exportedMethod.getReference());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
String annotationName;
|
||||||
switch (export.kind) {
|
switch (export.kind) {
|
||||||
case GETTER:
|
case GETTER:
|
||||||
|
@ -168,14 +239,9 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
annotationName = JSMethodToExpose.class.getName();
|
annotationName = JSMethodToExpose.class.getName();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
AnnotationHolder annot = new AnnotationHolder(annotationName);
|
var annot = new AnnotationHolder(annotationName);
|
||||||
annot.getValues().put("name", new AnnotationValue(export.alias));
|
annot.getValues().put("name", new AnnotationValue(export.alias));
|
||||||
exportedMethod.getAnnotations().add(annot);
|
return annot;
|
||||||
|
|
||||||
if (methodRef.equals(functorMethod)) {
|
|
||||||
addFunctorField(classHolder, exportedMethod.getReference());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExposedClass getExposedClass(String name) {
|
private ExposedClass getExposedClass(String name) {
|
||||||
|
@ -206,7 +272,9 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
exposedCls.inheritedMethods.addAll(parent.methods.keySet());
|
exposedCls.inheritedMethods.addAll(parent.methods.keySet());
|
||||||
exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces);
|
exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces);
|
||||||
}
|
}
|
||||||
addInterfaces(exposedCls, cls);
|
if (!addInterfaces(exposedCls, cls)) {
|
||||||
|
addExportedMethods(exposedCls, cls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) {
|
private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) {
|
||||||
|
@ -226,13 +294,46 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
|| (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) {
|
|| (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
addExportedMethod(exposedCls, method);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addExportedMethods(exposedCls, iface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean addInterface(ExposedClass exposedCls, ClassReader cls) {
|
||||||
|
if (cls.getName().equals(JSObject.class.getName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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())) {
|
if (!exposedCls.inheritedMethods.contains(method.getDescriptor())) {
|
||||||
|
exposedCls.methods.put(method.getDescriptor(), createMethodExport(method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodExport createMethodExport(MethodReader method) {
|
||||||
String name = null;
|
String name = null;
|
||||||
MethodKind kind = MethodKind.METHOD;
|
MethodKind kind = MethodKind.METHOD;
|
||||||
AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
|
var methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
|
||||||
if (methodAnnot != null) {
|
if (methodAnnot != null) {
|
||||||
name = method.getName();
|
name = method.getName();
|
||||||
AnnotationValue nameVal = methodAnnot.getValue("value");
|
var nameVal = methodAnnot.getValue("value");
|
||||||
if (nameVal != null) {
|
if (nameVal != null) {
|
||||||
String nameStr = nameVal.getString();
|
String nameStr = nameVal.getString();
|
||||||
if (!nameStr.isEmpty()) {
|
if (!nameStr.isEmpty()) {
|
||||||
|
@ -242,7 +343,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
} else {
|
} else {
|
||||||
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
|
var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
|
||||||
if (propertyAnnot != null) {
|
if (propertyAnnot != null) {
|
||||||
AnnotationValue nameVal = propertyAnnot.getValue("value");
|
var nameVal = propertyAnnot.getValue("value");
|
||||||
if (nameVal != null) {
|
if (nameVal != null) {
|
||||||
String nameStr = nameVal.getString();
|
String nameStr = nameVal.getString();
|
||||||
if (!nameStr.isEmpty()) {
|
if (!nameStr.isEmpty()) {
|
||||||
|
@ -275,19 +376,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
|
||||||
if (name == null) {
|
if (name == null) {
|
||||||
name = method.getName();
|
name = method.getName();
|
||||||
}
|
}
|
||||||
exposedCls.methods.put(method.getDescriptor(), new MethodExport(name, kind));
|
return new MethodExport(name, kind);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean addInterface(ExposedClass exposedCls, ClassReader cls) {
|
|
||||||
if (cls.getName().equals(JSObject.class.getName())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return addInterfaces(exposedCls, cls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFunctorField(ClassHolder cls, MethodReference method) {
|
private void addFunctorField(ClassHolder cls, MethodReference method) {
|
||||||
|
|
|
@ -127,6 +127,17 @@ class JSValueMarshaller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!className.equals("java.lang.String")) {
|
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;
|
return var;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -317,6 +328,15 @@ class JSValueMarshaller {
|
||||||
return unwrap(var, "unwrapString", JSMethods.JS_OBJECT, stringType, location.getSourceLocation());
|
return unwrap(var, "unwrapString", JSMethods.JS_OBJECT, stringType, location.getSourceLocation());
|
||||||
} else if (typeHelper.isJavaScriptClass(className)) {
|
} else if (typeHelper.isJavaScriptClass(className)) {
|
||||||
return var;
|
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) {
|
} else if (type instanceof ValueType.Array) {
|
||||||
return unwrapArray(location, var, (ValueType.Array) type);
|
return unwrapArray(location, var, (ValueType.Array) type);
|
||||||
|
|
36
samples/module-test/build.gradle.kts
Normal file
36
samples/module-test/build.gradle.kts
Normal 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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ include("kotlin")
|
||||||
include("scala")
|
include("scala")
|
||||||
include("web-apis")
|
include("web-apis")
|
||||||
include("software3d")
|
include("software3d")
|
||||||
|
include("module-test")
|
||||||
|
|
||||||
gradle.allprojects {
|
gradle.allprojects {
|
||||||
apply<WarPlugin>()
|
apply<WarPlugin>()
|
||||||
|
|
|
@ -36,6 +36,7 @@ include("jso:core", "jso:apis", "jso:impl")
|
||||||
include("platform")
|
include("platform")
|
||||||
include("classlib")
|
include("classlib")
|
||||||
include("tools:core")
|
include("tools:core")
|
||||||
|
include("tools:browser-runner")
|
||||||
include("tools:deobfuscator-js")
|
include("tools:deobfuscator-js")
|
||||||
include("tools:junit")
|
include("tools:junit")
|
||||||
include("tools:devserver")
|
include("tools:devserver")
|
||||||
|
|
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
testImplementation(project(":metaprogramming:impl"))
|
testImplementation(project(":metaprogramming:impl"))
|
||||||
testImplementation(project(":tools:core"))
|
testImplementation(project(":tools:core"))
|
||||||
testImplementation(project(":tools:junit"))
|
testImplementation(project(":tools:junit"))
|
||||||
|
testImplementation(project(":tools:browser-runner"))
|
||||||
testImplementation(libs.hppc)
|
testImplementation(libs.hppc)
|
||||||
testImplementation(libs.rhino)
|
testImplementation(libs.rhino)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
|
|
@ -79,7 +79,7 @@ public class ClassValueTest {
|
||||||
TeaVM vm = new TeaVMBuilder(target).build();
|
TeaVM vm = new TeaVMBuilder(target).build();
|
||||||
vm.add(new DependencyTestPatcher(getClass().getName(), methodName));
|
vm.add(new DependencyTestPatcher(getClass().getName(), methodName));
|
||||||
vm.installPlugins();
|
vm.installPlugins();
|
||||||
vm.entryPoint(getClass().getName());
|
vm.setEntryPoint(getClass().getName());
|
||||||
vm.build(fileName -> new ByteArrayOutputStream(), "tmp");
|
vm.build(fileName -> new ByteArrayOutputStream(), "tmp");
|
||||||
if (!vm.getProblemProvider().getSevereProblems().isEmpty()) {
|
if (!vm.getProblemProvider().getSevereProblems().isEmpty()) {
|
||||||
fail("Code compiled with errors:\n" + describeProblems(vm));
|
fail("Code compiled with errors:\n" + describeProblems(vm));
|
||||||
|
|
|
@ -136,7 +136,7 @@ public class DependencyTest {
|
||||||
|
|
||||||
MethodReference testMethod = new MethodReference(DependencyTestData.class,
|
MethodReference testMethod = new MethodReference(DependencyTestData.class,
|
||||||
testName.getMethodName(), void.class);
|
testName.getMethodName(), void.class);
|
||||||
vm.entryPoint(DependencyTestData.class.getName());
|
vm.setEntryPoint(DependencyTestData.class.getName());
|
||||||
vm.build(fileName -> new ByteArrayOutputStream(), "out");
|
vm.build(fileName -> new ByteArrayOutputStream(), "out");
|
||||||
|
|
||||||
List<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
List<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
||||||
|
|
|
@ -204,7 +204,7 @@ public class IncrementalTest {
|
||||||
target.setObfuscated(false);
|
target.setObfuscated(false);
|
||||||
target.setStrict(true);
|
target.setStrict(true);
|
||||||
vm.add(new EntryPointTransformer(entryPoint));
|
vm.add(new EntryPointTransformer(entryPoint));
|
||||||
vm.entryPoint(EntryPoint.class.getName());
|
vm.setEntryPoint(EntryPoint.class.getName());
|
||||||
vm.installPlugins();
|
vm.installPlugins();
|
||||||
vm.build(buildTarget, name);
|
vm.build(buildTarget, name);
|
||||||
List<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
List<Problem> problems = vm.getProblemProvider().getSevereProblems();
|
||||||
|
|
120
tests/src/test/java/org/teavm/jso/export/ExportTest.java
Normal file
120
tests/src/test/java/org/teavm/jso/export/ExportTest.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2013 Alexey Andreev.
|
* Copyright 2024 Alexey Andreev.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.teavm.vm;
|
package org.teavm.jso.export;
|
||||||
|
|
||||||
import org.teavm.dependency.MethodDependency;
|
import org.teavm.jso.JSExport;
|
||||||
import org.teavm.model.MethodReference;
|
import org.teavm.jso.JSObject;
|
||||||
|
import org.teavm.jso.JSProperty;
|
||||||
|
|
||||||
public class TeaVMEntryPoint {
|
public final class ModuleWithConsumedObject {
|
||||||
String publicName;
|
private ModuleWithConsumedObject() {
|
||||||
MethodDependency methodDep;
|
|
||||||
|
|
||||||
TeaVMEntryPoint(String publicName, MethodDependency methodDep) {
|
|
||||||
this.publicName = publicName;
|
|
||||||
this.methodDep = methodDep;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPublicName() {
|
@JSExport
|
||||||
return publicName;
|
public static String takeObject(I o) {
|
||||||
|
return "object taken: foo = " + o.foo() + ", bar = " + o.getBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MethodReference getMethod() {
|
public interface I extends JSObject {
|
||||||
return methodDep.getReference();
|
int foo();
|
||||||
|
|
||||||
|
@JSProperty
|
||||||
|
String getBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
28
tests/src/test/java/org/teavm/jso/export/SimpleModule.java
Normal file
28
tests/src/test/java/org/teavm/jso/export/SimpleModule.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,7 +97,7 @@ public class JSOTest {
|
||||||
TeaVM vm = new TeaVMBuilder(new JavaScriptTarget()).build();
|
TeaVM vm = new TeaVMBuilder(new JavaScriptTarget()).build();
|
||||||
vm.add(new DependencyTestPatcher(JSOTest.class.getName(), methodName));
|
vm.add(new DependencyTestPatcher(JSOTest.class.getName(), methodName));
|
||||||
vm.installPlugins();
|
vm.installPlugins();
|
||||||
vm.entryPoint(JSOTest.class.getName());
|
vm.setEntryPoint(JSOTest.class.getName());
|
||||||
vm.build(name -> new ByteArrayOutputStream(), "tmp");
|
vm.build(name -> new ByteArrayOutputStream(), "tmp");
|
||||||
return vm.getProblemProvider().getSevereProblems();
|
return vm.getProblemProvider().getSevereProblems();
|
||||||
}
|
}
|
||||||
|
|
39
tests/src/test/resources/org/teavm/jso/export/assert.js
Normal file
39
tests/src/test/resources/org/teavm/jso/export/assert.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}));
|
||||||
|
}
|
25
tests/src/test/resources/org/teavm/jso/export/initializer.js
Normal file
25
tests/src/test/resources/org/teavm/jso/export/initializer.js
Normal 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());
|
||||||
|
}
|
52
tests/src/test/resources/org/teavm/jso/export/primitives.js
Normal file
52
tests/src/test/resources/org/teavm/jso/export/primitives.js
Normal 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();
|
||||||
|
}
|
20
tests/src/test/resources/org/teavm/jso/export/simple.js
Normal file
20
tests/src/test/resources/org/teavm/jso/export/simple.js
Normal 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());
|
||||||
|
}
|
53
tools/browser-runner/build.gradle.kts
Normal file
53
tools/browser-runner/build.gradle.kts
Normal 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"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,7 +85,7 @@ function launchTest(argument, callback) {
|
||||||
return teavmException;
|
return teavmException;
|
||||||
}
|
}
|
||||||
let stack = "";
|
let stack = "";
|
||||||
let je = main.javaException(e);
|
let je = main.javaException ? main.javaException(e) : void 0;
|
||||||
if (je && je.constructor.$meta) {
|
if (je && je.constructor.$meta) {
|
||||||
stack = je.constructor.$meta.name + ": ";
|
stack = je.constructor.$meta.name + ": ";
|
||||||
stack += je.getMessage();
|
stack += je.getMessage();
|
|
@ -346,7 +346,10 @@ public class IncrementalCBuilder {
|
||||||
vm.installPlugins();
|
vm.installPlugins();
|
||||||
|
|
||||||
vm.setLastKnownClasses(lastReachedClasses);
|
vm.setLastKnownClasses(lastReachedClasses);
|
||||||
vm.entryPoint(mainClass, mainFunctionName != null ? mainFunctionName : "main");
|
vm.setEntryPoint(mainClass);
|
||||||
|
if (mainFunctionName != null) {
|
||||||
|
vm.setEntryPointName(mainFunctionName);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Starting build");
|
log.info("Starting build");
|
||||||
progressListener.last = 0;
|
progressListener.last = 0;
|
||||||
|
|
|
@ -457,8 +457,9 @@ public class TeaVMTool {
|
||||||
for (ClassHolderTransformer transformer : resolveTransformers()) {
|
for (ClassHolderTransformer transformer : resolveTransformers()) {
|
||||||
vm.add(transformer);
|
vm.add(transformer);
|
||||||
}
|
}
|
||||||
if (mainClass != null) {
|
vm.setEntryPoint(mainClass);
|
||||||
vm.entryPoint(mainClass, entryPointName != null ? entryPointName : "main");
|
if (entryPointName != null) {
|
||||||
|
vm.setEntryPointName(entryPointName);
|
||||||
}
|
}
|
||||||
for (String className : classesToPreserve) {
|
for (String className : classesToPreserve) {
|
||||||
vm.preserveType(className);
|
vm.preserveType(className);
|
||||||
|
|
|
@ -838,7 +838,7 @@ public class CodeServlet extends HttpServlet {
|
||||||
vm.installPlugins();
|
vm.installPlugins();
|
||||||
|
|
||||||
vm.setLastKnownClasses(lastReachedClasses);
|
vm.setLastKnownClasses(lastReachedClasses);
|
||||||
vm.entryPoint(mainClass);
|
vm.setEntryPoint(mainClass);
|
||||||
|
|
||||||
log.info("Starting build");
|
log.info("Starting build");
|
||||||
progressListener.last = 0;
|
progressListener.last = 0;
|
||||||
|
|
|
@ -21,9 +21,6 @@ plugins {
|
||||||
|
|
||||||
description = "Test runner for JUnit and TestNG annotations"
|
description = "Test runner for JUnit and TestNG annotations"
|
||||||
|
|
||||||
configurations {
|
|
||||||
create("js")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.junit)
|
compileOnly(libs.junit)
|
||||||
|
@ -33,26 +30,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":tools:core"))
|
implementation(project(":tools:core"))
|
||||||
implementation(libs.jackson.annotations)
|
implementation(project(":tools:browser-runner"))
|
||||||
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 {
|
teavmPublish {
|
||||||
|
|
|
@ -16,220 +16,52 @@
|
||||||
package org.teavm.junit;
|
package org.teavm.junit;
|
||||||
|
|
||||||
import static org.teavm.junit.PropertyNames.JS_DECODE_STACK;
|
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.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.util.Collection;
|
||||||
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.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
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 java.util.function.Function;
|
||||||
import javax.servlet.ServletConfig;
|
import java.util.stream.Collectors;
|
||||||
import javax.servlet.ServletException;
|
import org.teavm.browserrunner.BrowserRunDescriptor;
|
||||||
import javax.servlet.http.HttpServlet;
|
import org.teavm.browserrunner.BrowserRunner;
|
||||||
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;
|
|
||||||
|
|
||||||
class BrowserRunStrategy implements TestRunStrategy {
|
class BrowserRunStrategy implements TestRunStrategy {
|
||||||
private boolean decodeStack = Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true"));
|
private File baseDir;
|
||||||
private final File baseDir;
|
private BrowserRunner runner;
|
||||||
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();
|
|
||||||
|
|
||||||
BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
|
BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
|
||||||
this.baseDir = baseDir;
|
this.baseDir = baseDir;
|
||||||
this.type = type;
|
runner = new BrowserRunner(baseDir, type, browserRunner,
|
||||||
this.browserRunner = browserRunner;
|
Boolean.parseBoolean(System.getProperty(JS_DECODE_STACK, "true")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeAll() {
|
public void beforeAll() {
|
||||||
runServer();
|
runner.start();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterAll() {
|
public void afterAll() {
|
||||||
try {
|
runner.stop();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void runTest(TestRun run) throws IOException {
|
public void runTest(TestRun run) throws IOException {
|
||||||
while (!runTestOnce(run)) {
|
var testFile = new File(run.getBaseDirectory(), run.getFileName());
|
||||||
// repeat
|
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) {
|
private Collection<String> additionalJs(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) {
|
|
||||||
var result = new LinkedHashSet<String>();
|
var result = new LinkedHashSet<String>();
|
||||||
|
|
||||||
var method = run.getMethod();
|
var method = run.getMethod();
|
||||||
|
@ -247,296 +79,6 @@ class BrowserRunStrategy implements TestRunStrategy {
|
||||||
cls = cls.getSuperclass();
|
cls = cls.getSuperclass();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.toArray(new String[0]);
|
return result;
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import org.teavm.backend.javascript.JSModuleType;
|
import org.teavm.backend.javascript.JSModuleType;
|
||||||
import org.teavm.backend.javascript.JavaScriptTarget;
|
import org.teavm.backend.javascript.JavaScriptTarget;
|
||||||
|
import org.teavm.browserrunner.BrowserRunner;
|
||||||
import org.teavm.debugging.information.DebugInformation;
|
import org.teavm.debugging.information.DebugInformation;
|
||||||
import org.teavm.debugging.information.DebugInformationBuilder;
|
import org.teavm.debugging.information.DebugInformationBuilder;
|
||||||
import org.teavm.model.ClassHolderSource;
|
import org.teavm.model.ClassHolderSource;
|
||||||
|
@ -49,22 +50,10 @@ class JSPlatformSupport extends TestPlatformSupport<JavaScriptTarget> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
TestRunStrategy createRunStrategy(File outputDir) {
|
TestRunStrategy createRunStrategy(File outputDir) {
|
||||||
String runStrategyName = System.getProperty(JS_RUNNER);
|
var runStrategyName = System.getProperty(JS_RUNNER);
|
||||||
if (runStrategyName != null) {
|
return runStrategyName != null
|
||||||
switch (runStrategyName) {
|
? new BrowserRunStrategy(outputDir, "JAVASCRIPT", BrowserRunner.pickBrowser(runStrategyName))
|
||||||
case "browser":
|
: null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -93,7 +93,7 @@ abstract class TestPlatformSupport<T extends TeaVMTarget> {
|
||||||
|
|
||||||
new TestExceptionPlugin().install(vm);
|
new TestExceptionPlugin().install(vm);
|
||||||
|
|
||||||
vm.entryPoint(entryPoint);
|
vm.setEntryPoint(entryPoint);
|
||||||
|
|
||||||
if (usesFileName()) {
|
if (usesFileName()) {
|
||||||
if (!outputFile.getParentFile().exists()) {
|
if (!outputFile.getParentFile().exists()) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.teavm.backend.wasm.WasmRuntimeType;
|
import org.teavm.backend.wasm.WasmRuntimeType;
|
||||||
import org.teavm.backend.wasm.WasmTarget;
|
import org.teavm.backend.wasm.WasmTarget;
|
||||||
|
import org.teavm.browserrunner.BrowserRunner;
|
||||||
import org.teavm.model.ClassHolderSource;
|
import org.teavm.model.ClassHolderSource;
|
||||||
import org.teavm.model.MethodReference;
|
import org.teavm.model.MethodReference;
|
||||||
import org.teavm.model.ReferenceCache;
|
import org.teavm.model.ReferenceCache;
|
||||||
|
@ -35,20 +36,9 @@ class WebAssemblyPlatformSupport extends BaseWebAssemblyPlatformSupport {
|
||||||
@Override
|
@Override
|
||||||
TestRunStrategy createRunStrategy(File outputDir) {
|
TestRunStrategy createRunStrategy(File outputDir) {
|
||||||
var runStrategyName = System.getProperty(WASM_RUNNER);
|
var runStrategyName = System.getProperty(WASM_RUNNER);
|
||||||
if (runStrategyName != null) {
|
return runStrategyName != null
|
||||||
switch (runStrategyName) {
|
? new BrowserRunStrategy(outputDir, "WASM", BrowserRunner.pickBrowser(runStrategyName))
|
||||||
case "browser":
|
: null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in New Issue
Block a user