From 45ba247265be6edf37d92d246268fb097aceac63 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 6 Jun 2017 21:34:24 +0300 Subject: [PATCH] When Java class gets exported to JS, generate bridges for its methods so that these bridges perform type conversion between Java and JS --- .../BootstrapMethodSubstitutor.java | 4 - .../dependency/DependencyClassSource.java | 6 + .../org/teavm/dependency/DynamicCallSite.java | 4 - .../main/java/org/teavm/model/Program.java | 4 +- .../java/org/teavm/jso/impl/FunctorImpl.java | 5 - .../org/teavm/jso/impl/JSAliasRenderer.java | 83 ++-- .../org/teavm/jso/impl/JSClassProcessor.java | 447 ++---------------- .../teavm/jso/impl/JSDependencyListener.java | 135 +----- .../org/teavm/jso/impl/JSMethodToExpose.java | 27 ++ .../java/org/teavm/jso/impl/JSOPlugin.java | 2 +- .../jso/impl/JSObjectClassTransformer.java | 223 ++++++++- .../org/teavm/jso/impl/JSValueMarshaller.java | 405 ++++++++++++++++ .../java/org/teavm/jso/test/ExportClass.java | 56 +++ 13 files changed, 806 insertions(+), 595 deletions(-) create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSMethodToExpose.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java create mode 100644 tests/src/test/java/org/teavm/jso/test/ExportClass.java diff --git a/core/src/main/java/org/teavm/dependency/BootstrapMethodSubstitutor.java b/core/src/main/java/org/teavm/dependency/BootstrapMethodSubstitutor.java index d84b20445..3b78b8972 100644 --- a/core/src/main/java/org/teavm/dependency/BootstrapMethodSubstitutor.java +++ b/core/src/main/java/org/teavm/dependency/BootstrapMethodSubstitutor.java @@ -18,10 +18,6 @@ package org.teavm.dependency; import org.teavm.model.emit.ProgramEmitter; import org.teavm.model.emit.ValueEmitter; -/** - * - * @author Alexey Andreev - */ @FunctionalInterface public interface BootstrapMethodSubstitutor { ValueEmitter substitute(DynamicCallSite callSite, ProgramEmitter pe); diff --git a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java index 8278f15a5..818ee956a 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyClassSource.java +++ b/core/src/main/java/org/teavm/dependency/DependencyClassSource.java @@ -51,6 +51,12 @@ class DependencyClassSource implements ClassHolderSource { if (innerSource.get(cls.getName()) != null || generatedClasses.containsKey(cls.getName())) { throw new IllegalArgumentException("Class " + cls.getName() + " is already defined"); } + if (!transformers.isEmpty()) { + for (ClassHolderTransformer transformer : transformers) { + transformer.transformClass(cls, innerSource, diagnostics); + } + cls = ModelUtils.copyClass(cls); + } generatedClasses.put(cls.getName(), cls); for (MethodHolder method : cls.getMethods()) { if (method.getProgram() != null && method.getProgram().basicBlockCount() > 0) { diff --git a/core/src/main/java/org/teavm/dependency/DynamicCallSite.java b/core/src/main/java/org/teavm/dependency/DynamicCallSite.java index 5e1268ec4..7d6731b2b 100644 --- a/core/src/main/java/org/teavm/dependency/DynamicCallSite.java +++ b/core/src/main/java/org/teavm/dependency/DynamicCallSite.java @@ -23,10 +23,6 @@ import org.teavm.model.MethodHandle; import org.teavm.model.RuntimeConstant; import org.teavm.model.emit.ValueEmitter; -/** - * - * @author Alexey Andreev - */ public class DynamicCallSite { private MethodDescriptor calledMethod; private ValueEmitter instance; diff --git a/core/src/main/java/org/teavm/model/Program.java b/core/src/main/java/org/teavm/model/Program.java index ba6b0b57d..c35e01b9a 100644 --- a/core/src/main/java/org/teavm/model/Program.java +++ b/core/src/main/java/org/teavm/model/Program.java @@ -145,8 +145,8 @@ public class Program implements ProgramReader { @Override public Variable variableAt(int index) { - if (index < 0) { - throw new IllegalArgumentException("Index " + index + " is negative"); + if (index < 0 || index >= variables.size()) { + throw new IllegalArgumentException("Index " + index + " is out of range"); } return variables.get(index); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/FunctorImpl.java b/jso/impl/src/main/java/org/teavm/jso/impl/FunctorImpl.java index 6758a71d8..dd9dee396 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/FunctorImpl.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/FunctorImpl.java @@ -20,12 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * - * @author Alexey Andreev - */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) @interface FunctorImpl { - String value(); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java index f416869c6..4342c4697 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java @@ -16,29 +16,24 @@ package org.teavm.jso.impl; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.rendering.RenderingManager; -import org.teavm.jso.impl.JSDependencyListener.ExposedClass; +import org.teavm.model.AnnotationReader; import org.teavm.model.ClassReader; -import org.teavm.model.ClassReaderSource; +import org.teavm.model.FieldReader; +import org.teavm.model.FieldReference; +import org.teavm.model.ListableClassReaderSource; import org.teavm.model.MethodDescriptor; +import org.teavm.model.MethodReader; import org.teavm.vm.BuildTarget; import org.teavm.vm.spi.RendererListener; -/** - * - * @author Alexey Andreev - */ class JSAliasRenderer implements RendererListener { private static String variableChars = "abcdefghijklmnopqrstuvwxyz"; - private JSDependencyListener dependencyListener; private SourceWriter writer; - private ClassReaderSource classSource; - - public JSAliasRenderer(JSDependencyListener dependencyListener) { - this.dependencyListener = dependencyListener; - } + private ListableClassReaderSource classSource; @Override public void begin(RenderingManager context, BuildTarget buildTarget) throws IOException { @@ -48,25 +43,32 @@ class JSAliasRenderer implements RendererListener { @Override public void complete() throws IOException { - if (!dependencyListener.isAnyAliasExists()) { + if (!hasClassesToExpose()) { return; } writer.append("(function()").ws().append("{").softNewLine().indent(); writer.append("var c;").softNewLine(); - for (Map.Entry entry : dependencyListener.getExposedClasses().entrySet()) { - ExposedClass cls = entry.getValue(); - ClassReader classReader = classSource.get(entry.getKey()); - if (classReader == null || cls.methods.isEmpty()) { + for (String className : classSource.getClassNames()) { + ClassReader classReader = classSource.get(className); + Map methods = new HashMap<>(); + for (MethodReader method : classReader.getMethods()) { + String methodAlias = getPublicAlias(method); + if (methodAlias != null) { + methods.put(method.getDescriptor(), methodAlias); + } + } + if (methods.isEmpty()) { continue; } + boolean first = true; - for (Map.Entry aliasEntry : cls.methods.entrySet()) { + for (Map.Entry aliasEntry : methods.entrySet()) { if (classReader.getMethod(aliasEntry.getKey()) == null) { continue; } if (first) { - writer.append("c").ws().append("=").ws().appendClass(entry.getKey()).append(".prototype;") + writer.append("c").ws().append("=").ws().appendClass(className).append(".prototype;") .softNewLine(); first = false; } @@ -78,13 +80,33 @@ class JSAliasRenderer implements RendererListener { writer.ws().append("=").ws().append("c.").appendMethod(aliasEntry.getKey()).append(";").softNewLine(); } - if (cls.functorField != null) { - writeFunctor(cls); + FieldReader functorField = getFunctorField(classReader); + if (functorField != null) { + writeFunctor(classReader, functorField.getReference()); } } writer.outdent().append("})();").newLine(); } + private boolean hasClassesToExpose() { + for (String className : classSource.getClassNames()) { + ClassReader cls = classSource.get(className); + if (cls.getMethods().stream().anyMatch(method -> getPublicAlias(method) != null)) { + return true; + } + } + return false; + } + + private String getPublicAlias(MethodReader method) { + AnnotationReader annot = method.getAnnotations().get(JSMethodToExpose.class.getName()); + return annot != null ? annot.getValue("name").getString() : null; + } + + private FieldReader getFunctorField(ClassReader cls) { + return cls.getField("$$jso_functor$$"); + } + private boolean isKeyword(String id) { switch (id) { case "with": @@ -104,28 +126,31 @@ class JSAliasRenderer implements RendererListener { } } - private void writeFunctor(ExposedClass cls) throws IOException { - String alias = cls.methods.get(cls.functorMethod); + private void writeFunctor(ClassReader cls, FieldReference functorField) throws IOException { + AnnotationReader implAnnot = cls.getAnnotations().get(FunctorImpl.class.getName()); + MethodDescriptor functorMethod = MethodDescriptor.parse(implAnnot.getValue("value").getString()); + String alias = cls.getMethod(functorMethod).getAnnotations() + .get(JSMethodToExpose.class.getName()).getValue("name").getString(); if (alias == null) { return; } writer.append("c.jso$functor$").append(alias).ws().append("=").ws().append("function()").ws().append("{") .indent().softNewLine(); - writer.append("if").ws().append("(!this.").appendField(cls.functorField).append(")").ws().append("{") + writer.append("if").ws().append("(!this.").appendField(functorField).append(")").ws().append("{") .indent().softNewLine(); writer.append("var self").ws().append('=').ws().append("this;").softNewLine(); - writer.append("this.").appendField(cls.functorField).ws().append('=').ws().append("function("); - appendArguments(cls.functorMethod.parameterCount()); + writer.append("this.").appendField(functorField).ws().append('=').ws().append("function("); + appendArguments(functorMethod.parameterCount()); writer.append(")").ws().append('{').indent().softNewLine(); - writer.append("return self.").appendMethod(cls.functorMethod).append('('); - appendArguments(cls.functorMethod.parameterCount()); + writer.append("return self.").appendMethod(functorMethod).append('('); + appendArguments(functorMethod.parameterCount()); writer.append(");").softNewLine(); writer.outdent().append("};").softNewLine(); writer.outdent().append("}").softNewLine(); - writer.append("return this.").appendField(cls.functorField).append(';').softNewLine(); + writer.append("return this.").appendField(functorField).append(';').softNewLine(); writer.outdent().append("};").softNewLine(); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java index d232d357a..32b2e19ef 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; import org.mozilla.javascript.CompilerEnvirons; import org.mozilla.javascript.Context; import org.mozilla.javascript.ast.AstNode; @@ -43,8 +42,6 @@ import org.teavm.jso.JSIndexer; import org.teavm.jso.JSMethod; import org.teavm.jso.JSObject; import org.teavm.jso.JSProperty; -import org.teavm.jso.core.JSArray; -import org.teavm.jso.core.JSArrayReader; import org.teavm.model.AccessLevel; import org.teavm.model.AnnotationContainerReader; import org.teavm.model.AnnotationHolder; @@ -56,7 +53,6 @@ import org.teavm.model.ClassHolder; import org.teavm.model.ClassReader; import org.teavm.model.ClassReaderSource; import org.teavm.model.ElementModifier; -import org.teavm.model.FieldHolder; import org.teavm.model.Instruction; import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodHolder; @@ -67,8 +63,6 @@ import org.teavm.model.TextLocation; import org.teavm.model.ValueType; import org.teavm.model.Variable; import org.teavm.model.instructions.AssignInstruction; -import org.teavm.model.instructions.CastInstruction; -import org.teavm.model.instructions.ClassConstantInstruction; import org.teavm.model.instructions.ExitInstruction; import org.teavm.model.instructions.InvocationType; import org.teavm.model.instructions.InvokeInstruction; @@ -86,13 +80,15 @@ class JSClassProcessor { private final JSTypeHelper typeHelper; private final Diagnostics diagnostics; private int methodIndexGenerator; - private final Map overridenMethodCache = new HashMap<>(); + private final Map overriddenMethodCache = new HashMap<>(); + private JSValueMarshaller marshaller; - public JSClassProcessor(ClassReaderSource classSource, JSBodyRepository repository, Diagnostics diagnostics) { + JSClassProcessor(ClassReaderSource classSource, JSTypeHelper typeHelper, JSBodyRepository repository, + Diagnostics diagnostics) { this.classSource = classSource; + this.typeHelper = typeHelper; this.repository = repository; this.diagnostics = diagnostics; - typeHelper = new JSTypeHelper(classSource); javaInvocationProcessor = new JavaInvocationProcessor(typeHelper, repository, classSource, diagnostics); } @@ -100,15 +96,7 @@ class JSClassProcessor { return classSource; } - public boolean isNative(String className) { - return typeHelper.isJavaScriptClass(className); - } - - public boolean isNativeImplementation(String className) { - return typeHelper.isJavaScriptImplementation(className); - } - - public MethodReference isFunctor(String className) { + MethodReference isFunctor(String className) { if (!typeHelper.isJavaScriptImplementation(className)) { return null; } @@ -135,7 +123,7 @@ class JSClassProcessor { }); } - public void processClass(ClassHolder cls) { + void processClass(ClassHolder cls) { Set preservedMethods = new HashSet<>(); for (String iface : cls.getInterfaces()) { if (typeHelper.isJavaScriptClass(iface)) { @@ -154,7 +142,7 @@ class JSClassProcessor { } } - public void processMemberMethods(ClassHolder cls) { + void processMemberMethods(ClassHolder cls) { for (MethodHolder method : cls.getMethods().toArray(new MethodHolder[0])) { if (method.hasModifier(ElementModifier.STATIC)) { continue; @@ -185,10 +173,10 @@ class JSClassProcessor { private MethodReader getOverriddenMethod(MethodReader finalMethod) { MethodReference ref = finalMethod.getReference(); - if (!overridenMethodCache.containsKey(ref)) { - overridenMethodCache.put(ref, findOverriddenMethod(finalMethod.getOwnerName(), finalMethod)); + if (!overriddenMethodCache.containsKey(ref)) { + overriddenMethodCache.put(ref, findOverriddenMethod(finalMethod.getOwnerName(), finalMethod)); } - return overridenMethodCache.get(ref); + return overriddenMethodCache.get(ref); } private MethodReader findOverriddenMethod(String className, MethodReader finalMethod) { @@ -203,22 +191,7 @@ class JSClassProcessor { .orElse(null); } - public void addFunctorField(ClassHolder cls, MethodReference method) { - if (cls.getAnnotations().get(FunctorImpl.class.getName()) != null) { - return; - } - - FieldHolder field = new FieldHolder("$$jso_functor$$"); - field.setLevel(AccessLevel.PUBLIC); - field.setType(ValueType.parse(JSObject.class)); - cls.addField(field); - - AnnotationHolder annot = new AnnotationHolder(FunctorImpl.class.getName()); - annot.getValues().put("value", new AnnotationValue(method.getDescriptor().toString())); - cls.getAnnotations().add(annot); - } - - public void makeSync(ClassHolder cls) { + void makeSync(ClassHolder cls) { Set methods = new HashSet<>(); findInheritedMethods(cls, methods, new HashSet<>()); for (MethodHolder method : cls.getMethods()) { @@ -233,14 +206,14 @@ class JSClassProcessor { if (!visited.add(cls.getName())) { return; } - if (isNative(cls.getName())) { + if (typeHelper.isJavaScriptClass(cls.getName())) { for (MethodReader method : cls.getMethods()) { if (!method.hasModifier(ElementModifier.STATIC) && !method.hasModifier(ElementModifier.FINAL) && method.getLevel() != AccessLevel.PRIVATE) { methods.add(method.getDescriptor()); } } - } else if (isNativeImplementation(cls.getName())) { + } else if (typeHelper.isJavaScriptImplementation(cls.getName())) { if (cls.getParent() != null && !cls.getParent().equals(cls.getName())) { ClassReader parentCls = classSource.get(cls.getParent()); if (parentCls != null) { @@ -266,8 +239,13 @@ class JSClassProcessor { return staticSignature; } + private void setCurrentProgram(Program program) { + this.program = program; + marshaller = new JSValueMarshaller(diagnostics, typeHelper, program, replacement); + } + void processProgram(MethodHolder methodToProcess) { - program = methodToProcess.getProgram(); + setCurrentProgram(methodToProcess.getProgram()); for (int i = 0; i < program.basicBlockCount(); ++i) { BasicBlock block = program.basicBlockAt(i); for (Instruction insn : block) { @@ -366,7 +344,7 @@ class JSClassProcessor { } replacement.add(newInvoke); if (result != null) { - result = unwrap(callLocation, result, method.getResultType()); + result = marshaller.unwrap(callLocation, result, method.getResultType()); copyVar(result, invoke.getReceiver(), invoke.getLocation()); } @@ -387,7 +365,7 @@ class JSClassProcessor { Variable result = invoke.getReceiver() != null ? program.createVariable() : null; addPropertyGet(propertyName, invoke.getInstance(), result, invoke.getLocation()); if (result != null) { - result = unwrap(callLocation, result, method.getResultType()); + result = marshaller.unwrap(callLocation, result, method.getResultType()); copyVar(result, invoke.getReceiver(), invoke.getLocation()); } return true; @@ -416,18 +394,18 @@ class JSClassProcessor { private boolean processIndexer(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) { if (isProperGetIndexer(method.getDescriptor())) { Variable result = invoke.getReceiver() != null ? program.createVariable() : null; - addIndexerGet(invoke.getInstance(), wrap(invoke.getArguments().get(0), + addIndexerGet(invoke.getInstance(), marshaller.wrap(invoke.getArguments().get(0), method.parameterType(0), invoke.getLocation(), false), result, invoke.getLocation()); if (result != null) { - result = unwrap(callLocation, result, method.getResultType()); + result = marshaller.unwrap(callLocation, result, method.getResultType()); copyVar(result, invoke.getReceiver(), invoke.getLocation()); } return true; } if (isProperSetIndexer(method.getDescriptor())) { - Variable index = wrap(invoke.getArguments().get(0), method.parameterType(0), + Variable index = marshaller.wrap(invoke.getArguments().get(0), method.parameterType(0), invoke.getLocation(), false); - Variable value = wrap(invoke.getArguments().get(1), method.parameterType(1), + Variable value = marshaller.wrap(invoke.getArguments().get(1), method.parameterType(1), invoke.getLocation(), false); addIndexerSet(invoke.getInstance(), index, value, invoke.getLocation()); return true; @@ -501,7 +479,7 @@ class JSClassProcessor { } replacement.add(newInvoke); if (result != null) { - result = unwrap(callLocation, result, method.getResultType()); + result = marshaller.unwrap(callLocation, result, method.getResultType()); copyVar(result, invoke.getReceiver(), invoke.getLocation()); } @@ -597,7 +575,7 @@ class JSClassProcessor { } } - public void createJSMethods(ClassHolder cls) { + void createJSMethods(ClassHolder cls) { for (MethodHolder method : cls.getMethods().toArray(new MethodHolder[0])) { MethodReference methodRef = method.getReference(); if (method.getAnnotations().get(JSBody.class.getName()) == null) { @@ -637,7 +615,7 @@ class JSClassProcessor { callerMethod.getModifiers().add(ElementModifier.STATIC); CallLocation location = new CallLocation(callback); - program = new Program(); + setCurrentProgram(new Program()); for (int i = 0; i <= callback.parameterCount(); ++i) { program.createVariable(); } @@ -649,11 +627,12 @@ class JSClassProcessor { insn.setMethod(calleeRef); replacement.clear(); if (!callee.hasModifier(ElementModifier.STATIC)) { - insn.setInstance(unwrap(location, program.variableAt(paramIndex++), + insn.setInstance(marshaller.unwrap(location, program.variableAt(paramIndex++), ValueType.object(calleeRef.getClassName()))); } for (int i = 0; i < callee.parameterCount(); ++i) { - insn.getArguments().add(unwrap(location, program.variableAt(paramIndex++), callee.parameterType(i))); + insn.getArguments().add(marshaller.unwrap(location, program.variableAt(paramIndex++), + callee.parameterType(i))); } if (callee.getResultType() != ValueType.VOID) { insn.setReceiver(program.createVariable()); @@ -664,7 +643,7 @@ class JSClassProcessor { ExitInstruction exit = new ExitInstruction(); if (insn.getReceiver() != null) { replacement.clear(); - exit.setValueToReturn(wrap(insn.getReceiver(), callee.getResultType(), null, false)); + exit.setValueToReturn(marshaller.wrap(insn.getReceiver(), callee.getResultType(), null, false)); block.addAll(replacement); } block.add(exit); @@ -732,7 +711,7 @@ class JSClassProcessor { } private Variable addStringWrap(Variable var, TextLocation location) { - return wrap(var, ValueType.object("java.lang.String"), location, false); + return marshaller.wrap(var, ValueType.object("java.lang.String"), location, false); } private Variable addString(String str, TextLocation location) { @@ -745,240 +724,6 @@ class JSClassProcessor { return var; } - private Variable unwrap(CallLocation location, Variable var, ValueType type) { - if (type instanceof ValueType.Primitive) { - switch (((ValueType.Primitive) type).getKind()) { - case BOOLEAN: - return unwrap(var, "unwrapBoolean", ValueType.parse(JSObject.class), ValueType.BOOLEAN, - location.getSourceLocation()); - case BYTE: - return unwrap(var, "unwrapByte", ValueType.parse(JSObject.class), ValueType.BYTE, - location.getSourceLocation()); - case SHORT: - return unwrap(var, "unwrapShort", ValueType.parse(JSObject.class), ValueType.SHORT, - location.getSourceLocation()); - case INTEGER: - return unwrap(var, "unwrapInt", ValueType.parse(JSObject.class), ValueType.INTEGER, - location.getSourceLocation()); - case CHARACTER: - return unwrap(var, "unwrapCharacter", ValueType.parse(JSObject.class), ValueType.CHARACTER, - location.getSourceLocation()); - case DOUBLE: - return unwrap(var, "unwrapDouble", ValueType.parse(JSObject.class), ValueType.DOUBLE, - location.getSourceLocation()); - case FLOAT: - return unwrap(var, "unwrapFloat", ValueType.parse(JSObject.class), ValueType.FLOAT, - location.getSourceLocation()); - case LONG: - break; - } - } else if (type instanceof ValueType.Object) { - String className = ((ValueType.Object) type).getClassName(); - if (className.equals(JSObject.class.getName())) { - return var; - } else if (className.equals("java.lang.String")) { - return unwrap(var, "unwrapString", ValueType.parse(JSObject.class), ValueType.parse(String.class), - location.getSourceLocation()); - } else if (isNative(className)) { - Variable result = program.createVariable(); - CastInstruction castInsn = new CastInstruction(); - castInsn.setReceiver(result); - castInsn.setValue(var); - castInsn.setTargetType(type); - castInsn.setLocation(location.getSourceLocation()); - replacement.add(castInsn); - return result; - } - } else if (type instanceof ValueType.Array) { - return unwrapArray(location, var, (ValueType.Array) type); - } - diagnostics.error(location, "Unsupported type: {{t0}}", type); - return var; - } - - private Variable unwrapArray(CallLocation location, Variable var, ValueType.Array type) { - ValueType itemType = type; - int degree = 0; - while (itemType instanceof ValueType.Array) { - ++degree; - itemType = ((ValueType.Array) itemType).getItemType(); - } - - CastInstruction castInsn = new CastInstruction(); - castInsn.setValue(var); - castInsn.setTargetType(ValueType.parse(JSArrayReader.class)); - var = program.createVariable(); - castInsn.setReceiver(var); - castInsn.setLocation(location.getSourceLocation()); - replacement.add(castInsn); - - var = degree == 1 - ? unwrapSingleDimensionArray(location, var, itemType) - : unwrapMultiDimensionArray(location, var, itemType, degree); - - return var; - } - - private Variable unwrapSingleDimensionArray(CallLocation location, Variable var, ValueType type) { - Variable result = program.createVariable(); - - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(singleDimensionArrayUnwrapper(type)); - insn.setType(InvocationType.SPECIAL); - - if (insn.getMethod().parameterCount() == 2) { - Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); - clsInsn.setLocation(location.getSourceLocation()); - clsInsn.setReceiver(cls); - replacement.add(clsInsn); - insn.getArguments().add(cls); - } - - insn.getArguments().add(var); - insn.setReceiver(result); - replacement.add(insn); - return result; - } - - private Variable unwrapMultiDimensionArray(CallLocation location, Variable var, ValueType type, int degree) { - Variable function = program.createVariable(); - - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(multipleDimensionArrayUnwrapper(type)); - insn.setType(InvocationType.SPECIAL); - - if (insn.getMethod().parameterCount() == 1) { - Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); - clsInsn.setLocation(location.getSourceLocation()); - clsInsn.setReceiver(cls); - replacement.add(clsInsn); - insn.getArguments().add(cls); - } - - insn.setReceiver(function); - replacement.add(insn); - - while (--degree > 1) { - type = ValueType.arrayOf(type); - Variable cls = program.createVariable(); - - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); - clsInsn.setLocation(location.getSourceLocation()); - clsInsn.setReceiver(cls); - replacement.add(clsInsn); - - insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class, "arrayUnmapper", Class.class, Function.class, - Function.class)); - insn.setType(InvocationType.SPECIAL); - insn.getArguments().add(cls); - insn.getArguments().add(function); - function = program.createVariable(); - insn.setReceiver(function); - replacement.add(insn); - } - - Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(ValueType.arrayOf(type)); - clsInsn.setLocation(location.getSourceLocation()); - clsInsn.setReceiver(cls); - replacement.add(clsInsn); - - insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class, "unmapArray", Class.class, JSArrayReader.class, Function.class, - Object[].class)); - insn.getArguments().add(cls); - insn.getArguments().add(var); - insn.getArguments().add(function); - insn.setReceiver(var); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location.getSourceLocation()); - replacement.add(insn); - - return var; - } - - private MethodReference singleDimensionArrayUnwrapper(ValueType itemType) { - if (itemType instanceof ValueType.Primitive) { - switch (((ValueType.Primitive) itemType).getKind()) { - case BOOLEAN: - return new MethodReference(JS.class, "unwrapBooleanArray", JSArrayReader.class, boolean[].class); - case BYTE: - return new MethodReference(JS.class, "unwrapByteArray", JSArrayReader.class, byte[].class); - case SHORT: - return new MethodReference(JS.class, "unwrapShortArray", JSArrayReader.class, short[].class); - case CHARACTER: - return new MethodReference(JS.class, "unwrapCharArray", JSArrayReader.class, char[].class); - case INTEGER: - return new MethodReference(JS.class, "unwrapIntArray", JSArrayReader.class, int[].class); - case FLOAT: - return new MethodReference(JS.class, "unwrapFloatArray", JSArrayReader.class, float[].class); - case DOUBLE: - return new MethodReference(JS.class, "unwrapDoubleArray", JSArrayReader.class, double[].class); - default: - break; - } - } else if (itemType.isObject(String.class)) { - return new MethodReference(JS.class, "unwrapStringArray", JSArrayReader.class, String[].class); - } - return new MethodReference(JS.class, "unwrapArray", Class.class, JSArrayReader.class, JSObject[].class); - } - - private MethodReference multipleDimensionArrayUnwrapper(ValueType itemType) { - if (itemType instanceof ValueType.Primitive) { - switch (((ValueType.Primitive) itemType).getKind()) { - case BOOLEAN: - return new MethodReference(JS.class, "booleanArrayUnwrapper", Function.class); - case BYTE: - return new MethodReference(JS.class, "byteArrayUnwrapper", Function.class); - case SHORT: - return new MethodReference(JS.class, "shortArrayUnwrapper", Function.class); - case CHARACTER: - return new MethodReference(JS.class, "charArrayUnwrapper", Function.class); - case INTEGER: - return new MethodReference(JS.class, "intArrayUnwrapper", Function.class); - case FLOAT: - return new MethodReference(JS.class, "floatArrayUnwrapper", Function.class); - case DOUBLE: - return new MethodReference(JS.class, "doubleArrayUnwrapper", Function.class); - default: - break; - } - } else if (itemType.isObject(String.class)) { - return new MethodReference(JS.class, "stringArrayUnwrapper", Function.class); - } - return new MethodReference(JS.class, "arrayUnwrapper", Class.class, Function.class); - } - - private Variable unwrap(Variable var, String methodName, ValueType argType, ValueType resultType, - TextLocation location) { - if (!argType.isObject(JSObject.class.getName())) { - Variable castValue = program.createVariable(); - CastInstruction castInsn = new CastInstruction(); - castInsn.setValue(var); - castInsn.setReceiver(castValue); - castInsn.setLocation(location); - castInsn.setTargetType(argType); - replacement.add(castInsn); - var = castValue; - } - Variable result = program.createVariable(); - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class.getName(), methodName, argType, resultType)); - insn.getArguments().add(var); - insn.setReceiver(result); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location); - replacement.add(insn); - return result; - } - private Variable wrapArgument(CallLocation location, Variable var, ValueType type, boolean byRef) { if (type instanceof ValueType.Object) { String className = ((ValueType.Object) type).getClassName(); @@ -987,7 +732,7 @@ class JSClassProcessor { return wrapFunctor(location, var, cls); } } - return wrap(var, type, location.getSourceLocation(), byRef); + return marshaller.wrap(var, type, location.getSourceLocation(), byRef); } private boolean isProperFunctor(ClassReader type) { @@ -1020,128 +765,6 @@ class JSClassProcessor { return functor; } - private Variable wrap(Variable var, ValueType type, TextLocation location, boolean byRef) { - if (byRef) { - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class, "arrayData", Object.class, JSObject.class)); - insn.setReceiver(program.createVariable()); - insn.setType(InvocationType.SPECIAL); - insn.getArguments().add(var); - replacement.add(insn); - return insn.getReceiver(); - } - - if (type instanceof ValueType.Object) { - String className = ((ValueType.Object) type).getClassName(); - if (!className.equals("java.lang.String")) { - return var; - } - } - Variable result = program.createVariable(); - - ValueType itemType = type; - int degree = 0; - while (itemType instanceof ValueType.Array) { - itemType = ((ValueType.Array) itemType).getItemType(); - ++degree; - } - - if (degree <= 1) { - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class.getName(), "wrap", getWrappedType(type), - getWrapperType(type))); - insn.getArguments().add(var); - insn.setReceiver(result); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location); - replacement.add(insn); - } else { - Variable function = program.createVariable(); - - InvokeInstruction insn = new InvokeInstruction(); - insn.setMethod(getWrapperFunction(itemType)); - insn.setReceiver(function); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location); - replacement.add(insn); - - while (--degree > 1) { - insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class, "arrayMapper", Function.class, Function.class)); - insn.getArguments().add(function); - function = program.createVariable(); - insn.setReceiver(function); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location); - replacement.add(insn); - } - - insn = new InvokeInstruction(); - insn.setMethod(new MethodReference(JS.class.getName(), "map", getWrappedType(type), - ValueType.parse(Function.class), getWrapperType(type))); - insn.getArguments().add(var); - insn.getArguments().add(function); - insn.setReceiver(result); - insn.setType(InvocationType.SPECIAL); - insn.setLocation(location); - replacement.add(insn); - } - return result; - } - - private MethodReference getWrapperFunction(ValueType type) { - if (type instanceof ValueType.Primitive) { - switch (((ValueType.Primitive) type).getKind()) { - case BOOLEAN: - return new MethodReference(JS.class, "booleanArrayWrapper", Function.class); - case BYTE: - return new MethodReference(JS.class, "byteArrayWrapper", Function.class); - case SHORT: - return new MethodReference(JS.class, "shortArrayWrapper", Function.class); - case CHARACTER: - return new MethodReference(JS.class, "charArrayWrapper", Function.class); - case INTEGER: - return new MethodReference(JS.class, "intArrayWrapper", Function.class); - case FLOAT: - return new MethodReference(JS.class, "floatArrayWrapper", Function.class); - case DOUBLE: - return new MethodReference(JS.class, "doubleArrayWrapper", Function.class); - default: - break; - } - } else if (type.isObject(String.class)) { - return new MethodReference(JS.class, "stringArrayWrapper", Function.class); - } - return new MethodReference(JS.class, "arrayWrapper", Function.class); - } - - private ValueType getWrappedType(ValueType type) { - if (type instanceof ValueType.Array) { - ValueType itemType = ((ValueType.Array) type).getItemType(); - if (itemType instanceof ValueType.Array) { - return ValueType.parse(Object[].class); - } else { - return ValueType.arrayOf(getWrappedType(itemType)); - } - } else if (type instanceof ValueType.Object) { - if (type.isObject(String.class)) { - return type; - } else { - return ValueType.parse(JSObject.class); - } - } else { - return type; - } - } - - private ValueType getWrapperType(ValueType type) { - if (type instanceof ValueType.Array) { - return ValueType.parse(JSArray.class); - } else { - return ValueType.parse(JSObject.class); - } - } - private MethodReader getMethod(MethodReference ref) { ClassReader cls = classSource.get(ref.getClassName()); if (cls == null) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java index 675094a03..6f292f60a 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSDependencyListener.java @@ -15,48 +15,23 @@ */ package org.teavm.jso.impl; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.teavm.dependency.AbstractDependencyListener; import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.MethodDependency; -import org.teavm.jso.JSMethod; -import org.teavm.jso.JSObject; import org.teavm.model.AnnotationReader; -import org.teavm.model.AnnotationValue; import org.teavm.model.CallLocation; import org.teavm.model.ClassReader; -import org.teavm.model.ClassReaderSource; -import org.teavm.model.ElementModifier; -import org.teavm.model.FieldReader; -import org.teavm.model.FieldReference; -import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; -/** - * - * @author Alexey Andreev - */ class JSDependencyListener extends AbstractDependencyListener { - private Map exposedClasses = new HashMap<>(); - private ClassReaderSource classSource; - private DependencyAgent agent; private JSBodyRepository repository; - private boolean anyAliasExists; - public JSDependencyListener(JSBodyRepository repository) { + JSDependencyListener(JSBodyRepository repository) { this.repository = repository; } - @Override - public void started(DependencyAgent agent) { - this.agent = agent; - classSource = agent.getClassSource(); - } - @Override public void methodReached(DependencyAgent agent, MethodDependency method, CallLocation location) { MethodReference ref = method.getReference(); @@ -70,108 +45,14 @@ class JSDependencyListener extends AbstractDependencyListener { @Override public void classReached(DependencyAgent agent, String className, CallLocation location) { - getExposedClass(className); - } - - boolean isAnyAliasExists() { - return anyAliasExists; - } - - Map getExposedClasses() { - return exposedClasses; - } - - static class ExposedClass { - Map inheritedMethods = new HashMap<>(); - Map methods = new HashMap<>(); - Set implementedInterfaces = new HashSet<>(); - FieldReference functorField; - MethodDescriptor functorMethod; - } - - private ExposedClass getExposedClass(String name) { - return exposedClasses.computeIfAbsent(name, this::createExposedClass); - } - - private ExposedClass createExposedClass(String name) { - ClassReader cls = classSource.get(name); - ExposedClass exposedCls = new ExposedClass(); - if (cls == null || cls.hasModifier(ElementModifier.INTERFACE)) { - return exposedCls; - } - if (cls.getParent() != null && !cls.getParent().equals(cls.getName())) { - ExposedClass parent = getExposedClass(cls.getParent()); - exposedCls.inheritedMethods.putAll(parent.inheritedMethods); - exposedCls.inheritedMethods.putAll(parent.methods); - exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces); - } - addInterfaces(exposedCls, cls); - if (!cls.hasModifier(ElementModifier.ABSTRACT)) { - for (MethodReader method : cls.getMethods()) { - if (method.getName().equals("")) { - continue; - } - if (exposedCls.inheritedMethods.containsKey(method.getDescriptor()) - || exposedCls.methods.containsKey(method.getDescriptor())) { - MethodDependency methodDep = agent.linkMethod(method.getReference(), null); - methodDep.getVariable(0).propagate(agent.getType(name)); - methodDep.use(); - } + ClassReader cls = agent.getClassSource().get(className); + for (MethodReader method : cls.getMethods()) { + AnnotationReader exposeAnnot = method.getAnnotations().get(JSMethodToExpose.class.getName()); + if (exposeAnnot != null) { + MethodDependency methodDep = agent.linkMethod(method.getReference(), null); + methodDep.getVariable(0).propagate(agent.getType(className)); + methodDep.use(); } } - if (exposedCls.functorField == null) { - FieldReader functorField = cls.getField("$$jso_functor$$"); - if (functorField != null) { - exposedCls.functorField = functorField.getReference(); - AnnotationReader annot = cls.getAnnotations().get(FunctorImpl.class.getName()); - exposedCls.functorMethod = MethodDescriptor.parse(annot.getValue("value").getString()); - } - } - return exposedCls; - } - - private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) { - boolean added = false; - for (String ifaceName : cls.getInterfaces()) { - if (exposedCls.implementedInterfaces.contains(ifaceName)) { - continue; - } - ClassReader iface = classSource.get(ifaceName); - if (iface == null) { - continue; - } - if (addInterface(exposedCls, iface)) { - added = true; - for (MethodReader method : iface.getMethods()) { - if (method.hasModifier(ElementModifier.STATIC) - || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) { - continue; - } - if (!exposedCls.inheritedMethods.containsKey(method.getDescriptor())) { - String name = method.getName(); - AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); - if (methodAnnot != null) { - AnnotationValue nameVal = methodAnnot.getValue("value"); - if (nameVal != null) { - String nameStr = nameVal.getString(); - if (!nameStr.isEmpty()) { - name = nameStr; - } - } - } - exposedCls.methods.put(method.getDescriptor(), name); - anyAliasExists = true; - } - } - } - } - return added; - } - - private boolean addInterface(ExposedClass exposedCls, ClassReader cls) { - if (cls.getName().equals(JSObject.class.getName())) { - return true; - } - return addInterfaces(exposedCls, cls); } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethodToExpose.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethodToExpose.java new file mode 100644 index 000000000..bba79b73c --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethodToExpose.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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.impl; + +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.METHOD) +@interface JSMethodToExpose { + String name(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java index 39beea13a..ec848d1d3 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java @@ -30,7 +30,7 @@ public class JSOPlugin implements TeaVMPlugin { host.registerService(JSBodyRepository.class, repository); host.add(new JSObjectClassTransformer(repository)); JSDependencyListener dependencyListener = new JSDependencyListener(repository); - JSAliasRenderer aliasRenderer = new JSAliasRenderer(dependencyListener); + JSAliasRenderer aliasRenderer = new JSAliasRenderer(); host.add(dependencyListener); host.getExtension(TeaVMJavaScriptHost.class).add(aliasRenderer); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java index 00214c46a..5ab1de62a 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java @@ -15,44 +15,245 @@ */ package org.teavm.jso.impl; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.teavm.diagnostics.Diagnostics; +import org.teavm.jso.JSMethod; +import org.teavm.jso.JSObject; +import org.teavm.model.AccessLevel; +import org.teavm.model.AnnotationHolder; +import org.teavm.model.AnnotationReader; +import org.teavm.model.AnnotationValue; +import org.teavm.model.BasicBlock; +import org.teavm.model.CallLocation; import org.teavm.model.ClassHolder; import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassReader; import org.teavm.model.ClassReaderSource; +import org.teavm.model.ElementModifier; +import org.teavm.model.FieldHolder; +import org.teavm.model.Instruction; +import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodHolder; +import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; +import org.teavm.model.Program; +import org.teavm.model.ValueType; +import org.teavm.model.Variable; +import org.teavm.model.instructions.ExitInstruction; +import org.teavm.model.instructions.InvocationType; +import org.teavm.model.instructions.InvokeInstruction; -public class JSObjectClassTransformer implements ClassHolderTransformer { +class JSObjectClassTransformer implements ClassHolderTransformer { private JSClassProcessor processor; private JSBodyRepository repository; + private JSTypeHelper typeHelper; + private ClassReaderSource innerSource; + private Map exposedClasses = new HashMap<>(); - public JSObjectClassTransformer(JSBodyRepository repository) { + JSObjectClassTransformer(JSBodyRepository repository) { this.repository = repository; } @Override public void transformClass(ClassHolder cls, ClassReaderSource innerSource, Diagnostics diagnostics) { + this.innerSource = innerSource; if (processor == null || processor.getClassSource() != innerSource) { - processor = new JSClassProcessor(innerSource, repository, diagnostics); + typeHelper = new JSTypeHelper(innerSource); + processor = new JSClassProcessor(innerSource, typeHelper, repository, diagnostics); } processor.processClass(cls); - if (processor.isNative(cls.getName())) { + if (typeHelper.isJavaScriptClass(cls.getName())) { processor.processMemberMethods(cls); } - if (processor.isNativeImplementation(cls.getName())) { + if (typeHelper.isJavaScriptImplementation(cls.getName())) { processor.makeSync(cls); } - MethodReference functorMethod = processor.isFunctor(cls.getName()); - if (functorMethod != null) { - if (processor.isFunctor(cls.getParent()) == null) { - processor.addFunctorField(cls, functorMethod); - } - } + for (MethodHolder method : cls.getMethods().toArray(new MethodHolder[0])) { if (method.getProgram() != null) { processor.processProgram(method); } } processor.createJSMethods(cls); + + MethodReference functorMethod = processor.isFunctor(cls.getName()); + if (functorMethod != null) { + if (processor.isFunctor(cls.getParent()) != null) { + functorMethod = null; + } + } + + ClassReader originalClass = innerSource.get(cls.getName()); + ExposedClass exposedClass; + if (originalClass != null) { + exposedClass = getExposedClass(cls.getName()); + } else { + exposedClass = new ExposedClass(); + createExposedClass(cls, exposedClass); + } + + exposeMethods(cls, exposedClass, diagnostics, functorMethod); + } + + private void exposeMethods(ClassHolder classHolder, ExposedClass classToExpose, Diagnostics diagnostics, + MethodReference functorMethod) { + int index = 0; + for (MethodDescriptor method : classToExpose.methods.keySet()) { + MethodReference methodRef = new MethodReference(classHolder.getName(), method); + CallLocation callLocation = new CallLocation(methodRef); + + ValueType[] exportedMethodSignature = Arrays.stream(method.getSignature()) + .map(type -> ValueType.object(JSObject.class.getName())) + .toArray(ValueType[]::new); + MethodDescriptor exportedMethodDesc = new MethodDescriptor(method.getName() + "$exported$" + index++, + exportedMethodSignature); + MethodHolder exportedMethod = new MethodHolder(exportedMethodDesc); + Program program = new Program(); + exportedMethod.setProgram(program); + program.createVariable(); + + BasicBlock basicBlock = program.createBasicBlock(); + List marshallInstructions = new ArrayList<>(); + JSValueMarshaller marshaller = new JSValueMarshaller(diagnostics, typeHelper, program, + marshallInstructions); + + List variablesToPass = new ArrayList<>(); + for (int i = 0; i < method.parameterCount(); ++i) { + variablesToPass.add(program.createVariable()); + } + + for (int i = 0; i < method.parameterCount(); ++i) { + Variable var = marshaller.unwrap(callLocation, variablesToPass.get(i), method.parameterType(i)); + variablesToPass.set(i, var); + } + + basicBlock.addAll(marshallInstructions); + marshallInstructions.clear(); + + InvokeInstruction invocation = new InvokeInstruction(); + invocation.setType(InvocationType.VIRTUAL); + invocation.setInstance(program.variableAt(0)); + invocation.setMethod(methodRef); + invocation.getArguments().addAll(variablesToPass); + basicBlock.add(invocation); + + ExitInstruction exit = new ExitInstruction(); + if (method.getResultType() != ValueType.VOID) { + invocation.setReceiver(program.createVariable()); + exit.setValueToReturn(marshaller.wrap(invocation.getReceiver(), method.getResultType(), + null, false)); + basicBlock.addAll(marshallInstructions); + marshallInstructions.clear(); + } + basicBlock.add(exit); + + classHolder.addMethod(exportedMethod); + + String publicAlias = classToExpose.methods.get(method); + AnnotationHolder annot = new AnnotationHolder(JSMethodToExpose.class.getName()); + annot.getValues().put("name", new AnnotationValue(publicAlias)); + exportedMethod.getAnnotations().add(annot); + + if (methodRef.equals(functorMethod)) { + addFunctorField(classHolder, exportedMethod.getReference()); + } + } + } + + private ExposedClass getExposedClass(String name) { + return exposedClasses.computeIfAbsent(name, this::createExposedClass); + } + + private ExposedClass createExposedClass(String name) { + ClassReader cls = innerSource.get(name); + ExposedClass exposedCls = new ExposedClass(); + if (cls != null) { + createExposedClass(cls, exposedCls); + } + return exposedCls; + } + + private void createExposedClass(ClassReader cls, ExposedClass exposedCls) { + if (cls.hasModifier(ElementModifier.INTERFACE)) { + return; + } + if (cls.getParent() != null && !cls.getParent().equals(cls.getName())) { + ExposedClass parent = getExposedClass(cls.getParent()); + exposedCls.inheritedMethods.putAll(parent.inheritedMethods); + exposedCls.inheritedMethods.putAll(parent.methods); + exposedCls.implementedInterfaces.addAll(parent.implementedInterfaces); + } + addInterfaces(exposedCls, cls); + } + + private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) { + boolean added = false; + for (String ifaceName : cls.getInterfaces()) { + if (exposedCls.implementedInterfaces.contains(ifaceName)) { + continue; + } + ClassReader iface = innerSource.get(ifaceName); + if (iface == null) { + continue; + } + if (addInterface(exposedCls, iface)) { + added = true; + for (MethodReader method : iface.getMethods()) { + if (method.hasModifier(ElementModifier.STATIC) + || (method.getProgram() != null && method.getProgram().basicBlockCount() > 0)) { + continue; + } + if (!exposedCls.inheritedMethods.containsKey(method.getDescriptor())) { + String name = method.getName(); + AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName()); + if (methodAnnot != null) { + AnnotationValue nameVal = methodAnnot.getValue("value"); + if (nameVal != null) { + String nameStr = nameVal.getString(); + if (!nameStr.isEmpty()) { + name = nameStr; + } + } + } + exposedCls.methods.put(method.getDescriptor(), name); + } + } + } + } + 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) { + if (cls.getAnnotations().get(FunctorImpl.class.getName()) != null) { + return; + } + + FieldHolder field = new FieldHolder("$$jso_functor$$"); + field.setLevel(AccessLevel.PUBLIC); + field.setType(ValueType.parse(JSObject.class)); + cls.addField(field); + + AnnotationHolder annot = new AnnotationHolder(FunctorImpl.class.getName()); + annot.getValues().put("value", new AnnotationValue(method.getDescriptor().toString())); + cls.getAnnotations().add(annot); + } + + static class ExposedClass { + Map inheritedMethods = new HashMap<>(); + Map methods = new HashMap<>(); + Set implementedInterfaces = new HashSet<>(); } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java new file mode 100644 index 000000000..fb115ae6b --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -0,0 +1,405 @@ +/* + * Copyright 2017 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.impl; + +import java.util.List; +import java.util.function.Function; +import org.teavm.diagnostics.Diagnostics; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSArray; +import org.teavm.jso.core.JSArrayReader; +import org.teavm.model.CallLocation; +import org.teavm.model.Instruction; +import org.teavm.model.MethodReference; +import org.teavm.model.Program; +import org.teavm.model.TextLocation; +import org.teavm.model.ValueType; +import org.teavm.model.Variable; +import org.teavm.model.instructions.CastInstruction; +import org.teavm.model.instructions.ClassConstantInstruction; +import org.teavm.model.instructions.InvocationType; +import org.teavm.model.instructions.InvokeInstruction; + +class JSValueMarshaller { + private Diagnostics diagnostics; + private JSTypeHelper typeHelper; + private Program program; + private List replacement; + + JSValueMarshaller(Diagnostics diagnostics, JSTypeHelper typeHelper, Program program, + List replacement) { + this.diagnostics = diagnostics; + this.typeHelper = typeHelper; + this.program = program; + this.replacement = replacement; + } + + public Variable wrap(Variable var, ValueType type, TextLocation location, boolean byRef) { + if (byRef) { + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class, "arrayData", Object.class, JSObject.class)); + insn.setReceiver(program.createVariable()); + insn.setType(InvocationType.SPECIAL); + insn.getArguments().add(var); + replacement.add(insn); + return insn.getReceiver(); + } + + if (type instanceof ValueType.Object) { + String className = ((ValueType.Object) type).getClassName(); + if (!className.equals("java.lang.String")) { + return var; + } + } + Variable result = program.createVariable(); + + ValueType itemType = type; + int degree = 0; + while (itemType instanceof ValueType.Array) { + itemType = ((ValueType.Array) itemType).getItemType(); + ++degree; + } + + if (degree <= 1) { + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class.getName(), "wrap", getWrappedType(type), + getWrapperType(type))); + insn.getArguments().add(var); + insn.setReceiver(result); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location); + replacement.add(insn); + } else { + Variable function = program.createVariable(); + + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(getWrapperFunction(itemType)); + insn.setReceiver(function); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location); + replacement.add(insn); + + while (--degree > 1) { + insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class, "arrayMapper", Function.class, Function.class)); + insn.getArguments().add(function); + function = program.createVariable(); + insn.setReceiver(function); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location); + replacement.add(insn); + } + + insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class.getName(), "map", getWrappedType(type), + ValueType.parse(Function.class), getWrapperType(type))); + insn.getArguments().add(var); + insn.getArguments().add(function); + insn.setReceiver(result); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location); + replacement.add(insn); + } + return result; + } + + private ValueType getWrappedType(ValueType type) { + if (type instanceof ValueType.Array) { + ValueType itemType = ((ValueType.Array) type).getItemType(); + if (itemType instanceof ValueType.Array) { + return ValueType.parse(Object[].class); + } else { + return ValueType.arrayOf(getWrappedType(itemType)); + } + } else if (type instanceof ValueType.Object) { + if (type.isObject(String.class)) { + return type; + } else { + return ValueType.parse(JSObject.class); + } + } else { + return type; + } + } + + private ValueType getWrapperType(ValueType type) { + if (type instanceof ValueType.Array) { + return ValueType.parse(JSArray.class); + } else { + return ValueType.parse(JSObject.class); + } + } + + private MethodReference getWrapperFunction(ValueType type) { + if (type instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) type).getKind()) { + case BOOLEAN: + return new MethodReference(JS.class, "booleanArrayWrapper", Function.class); + case BYTE: + return new MethodReference(JS.class, "byteArrayWrapper", Function.class); + case SHORT: + return new MethodReference(JS.class, "shortArrayWrapper", Function.class); + case CHARACTER: + return new MethodReference(JS.class, "charArrayWrapper", Function.class); + case INTEGER: + return new MethodReference(JS.class, "intArrayWrapper", Function.class); + case FLOAT: + return new MethodReference(JS.class, "floatArrayWrapper", Function.class); + case DOUBLE: + return new MethodReference(JS.class, "doubleArrayWrapper", Function.class); + default: + break; + } + } else if (type.isObject(String.class)) { + return new MethodReference(JS.class, "stringArrayWrapper", Function.class); + } + return new MethodReference(JS.class, "arrayWrapper", Function.class); + } + + Variable unwrap(CallLocation location, Variable var, ValueType type) { + if (type instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) type).getKind()) { + case BOOLEAN: + return unwrap(var, "unwrapBoolean", ValueType.parse(JSObject.class), ValueType.BOOLEAN, + location.getSourceLocation()); + case BYTE: + return unwrap(var, "unwrapByte", ValueType.parse(JSObject.class), ValueType.BYTE, + location.getSourceLocation()); + case SHORT: + return unwrap(var, "unwrapShort", ValueType.parse(JSObject.class), ValueType.SHORT, + location.getSourceLocation()); + case INTEGER: + return unwrap(var, "unwrapInt", ValueType.parse(JSObject.class), ValueType.INTEGER, + location.getSourceLocation()); + case CHARACTER: + return unwrap(var, "unwrapCharacter", ValueType.parse(JSObject.class), ValueType.CHARACTER, + location.getSourceLocation()); + case DOUBLE: + return unwrap(var, "unwrapDouble", ValueType.parse(JSObject.class), ValueType.DOUBLE, + location.getSourceLocation()); + case FLOAT: + return unwrap(var, "unwrapFloat", ValueType.parse(JSObject.class), ValueType.FLOAT, + location.getSourceLocation()); + case LONG: + break; + } + } else if (type instanceof ValueType.Object) { + String className = ((ValueType.Object) type).getClassName(); + if (className.equals(JSObject.class.getName())) { + return var; + } else if (className.equals("java.lang.String")) { + return unwrap(var, "unwrapString", ValueType.parse(JSObject.class), ValueType.parse(String.class), + location.getSourceLocation()); + } else if (typeHelper.isJavaScriptClass(className)) { + Variable result = program.createVariable(); + CastInstruction castInsn = new CastInstruction(); + castInsn.setReceiver(result); + castInsn.setValue(var); + castInsn.setTargetType(type); + castInsn.setLocation(location.getSourceLocation()); + replacement.add(castInsn); + return result; + } + } else if (type instanceof ValueType.Array) { + return unwrapArray(location, var, (ValueType.Array) type); + } + diagnostics.error(location, "Unsupported type: {{t0}}", type); + return var; + } + + private Variable unwrapArray(CallLocation location, Variable var, ValueType.Array type) { + ValueType itemType = type; + int degree = 0; + while (itemType instanceof ValueType.Array) { + ++degree; + itemType = ((ValueType.Array) itemType).getItemType(); + } + + CastInstruction castInsn = new CastInstruction(); + castInsn.setValue(var); + castInsn.setTargetType(ValueType.parse(JSArrayReader.class)); + var = program.createVariable(); + castInsn.setReceiver(var); + castInsn.setLocation(location.getSourceLocation()); + replacement.add(castInsn); + + var = degree == 1 + ? unwrapSingleDimensionArray(location, var, itemType) + : unwrapMultiDimensionArray(location, var, itemType, degree); + + return var; + } + + private Variable unwrapSingleDimensionArray(CallLocation location, Variable var, ValueType type) { + Variable result = program.createVariable(); + + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(singleDimensionArrayUnwrapper(type)); + insn.setType(InvocationType.SPECIAL); + + if (insn.getMethod().parameterCount() == 2) { + Variable cls = program.createVariable(); + ClassConstantInstruction clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(type); + clsInsn.setLocation(location.getSourceLocation()); + clsInsn.setReceiver(cls); + replacement.add(clsInsn); + insn.getArguments().add(cls); + } + + insn.getArguments().add(var); + insn.setReceiver(result); + replacement.add(insn); + return result; + } + + private Variable unwrapMultiDimensionArray(CallLocation location, Variable var, ValueType type, int degree) { + Variable function = program.createVariable(); + + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(multipleDimensionArrayUnwrapper(type)); + insn.setType(InvocationType.SPECIAL); + + if (insn.getMethod().parameterCount() == 1) { + Variable cls = program.createVariable(); + ClassConstantInstruction clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(type); + clsInsn.setLocation(location.getSourceLocation()); + clsInsn.setReceiver(cls); + replacement.add(clsInsn); + insn.getArguments().add(cls); + } + + insn.setReceiver(function); + replacement.add(insn); + + while (--degree > 1) { + type = ValueType.arrayOf(type); + Variable cls = program.createVariable(); + + ClassConstantInstruction clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(type); + clsInsn.setLocation(location.getSourceLocation()); + clsInsn.setReceiver(cls); + replacement.add(clsInsn); + + insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class, "arrayUnmapper", Class.class, Function.class, + Function.class)); + insn.setType(InvocationType.SPECIAL); + insn.getArguments().add(cls); + insn.getArguments().add(function); + function = program.createVariable(); + insn.setReceiver(function); + replacement.add(insn); + } + + Variable cls = program.createVariable(); + ClassConstantInstruction clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(ValueType.arrayOf(type)); + clsInsn.setLocation(location.getSourceLocation()); + clsInsn.setReceiver(cls); + replacement.add(clsInsn); + + insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class, "unmapArray", Class.class, JSArrayReader.class, Function.class, + Object[].class)); + insn.getArguments().add(cls); + insn.getArguments().add(var); + insn.getArguments().add(function); + insn.setReceiver(var); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location.getSourceLocation()); + replacement.add(insn); + + return var; + } + + private MethodReference singleDimensionArrayUnwrapper(ValueType itemType) { + if (itemType instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) itemType).getKind()) { + case BOOLEAN: + return new MethodReference(JS.class, "unwrapBooleanArray", JSArrayReader.class, boolean[].class); + case BYTE: + return new MethodReference(JS.class, "unwrapByteArray", JSArrayReader.class, byte[].class); + case SHORT: + return new MethodReference(JS.class, "unwrapShortArray", JSArrayReader.class, short[].class); + case CHARACTER: + return new MethodReference(JS.class, "unwrapCharArray", JSArrayReader.class, char[].class); + case INTEGER: + return new MethodReference(JS.class, "unwrapIntArray", JSArrayReader.class, int[].class); + case FLOAT: + return new MethodReference(JS.class, "unwrapFloatArray", JSArrayReader.class, float[].class); + case DOUBLE: + return new MethodReference(JS.class, "unwrapDoubleArray", JSArrayReader.class, double[].class); + default: + break; + } + } else if (itemType.isObject(String.class)) { + return new MethodReference(JS.class, "unwrapStringArray", JSArrayReader.class, String[].class); + } + return new MethodReference(JS.class, "unwrapArray", Class.class, JSArrayReader.class, JSObject[].class); + } + + private MethodReference multipleDimensionArrayUnwrapper(ValueType itemType) { + if (itemType instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) itemType).getKind()) { + case BOOLEAN: + return new MethodReference(JS.class, "booleanArrayUnwrapper", Function.class); + case BYTE: + return new MethodReference(JS.class, "byteArrayUnwrapper", Function.class); + case SHORT: + return new MethodReference(JS.class, "shortArrayUnwrapper", Function.class); + case CHARACTER: + return new MethodReference(JS.class, "charArrayUnwrapper", Function.class); + case INTEGER: + return new MethodReference(JS.class, "intArrayUnwrapper", Function.class); + case FLOAT: + return new MethodReference(JS.class, "floatArrayUnwrapper", Function.class); + case DOUBLE: + return new MethodReference(JS.class, "doubleArrayUnwrapper", Function.class); + default: + break; + } + } else if (itemType.isObject(String.class)) { + return new MethodReference(JS.class, "stringArrayUnwrapper", Function.class); + } + return new MethodReference(JS.class, "arrayUnwrapper", Class.class, Function.class); + } + + private Variable unwrap(Variable var, String methodName, ValueType argType, ValueType resultType, + TextLocation location) { + if (!argType.isObject(JSObject.class.getName())) { + Variable castValue = program.createVariable(); + CastInstruction castInsn = new CastInstruction(); + castInsn.setValue(var); + castInsn.setReceiver(castValue); + castInsn.setLocation(location); + castInsn.setTargetType(argType); + replacement.add(castInsn); + var = castValue; + } + Variable result = program.createVariable(); + InvokeInstruction insn = new InvokeInstruction(); + insn.setMethod(new MethodReference(JS.class.getName(), methodName, argType, resultType)); + insn.getArguments().add(var); + insn.setReceiver(result); + insn.setType(InvocationType.SPECIAL); + insn.setLocation(location); + replacement.add(insn); + return result; + } +} diff --git a/tests/src/test/java/org/teavm/jso/test/ExportClass.java b/tests/src/test/java/org/teavm/jso/test/ExportClass.java new file mode 100644 index 000000000..697fb328b --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/ExportClass.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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.test; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +@SkipJVM +public class ExportClass { + @Test + public void simpleClassExported() { + assertEquals("(OK)", callIFromJs(new SimpleClass())); + assertEquals("[OK]", callIFromJs(new DerivedSimpleClass())); + } + + @JSBody(params = "a", script = "return a.foo('OK');") + private static native String callIFromJs(I a); + + interface I extends JSObject { + String foo(String a); + } + + static class SimpleClass implements I { + @Override + public String foo(String a) { + return "(" + a + ")"; + } + } + + static class DerivedSimpleClass implements I { + @Override + public String foo(String a) { + return "[" + a + "]"; + } + } + +}