From 948244cbf40fe20d90599d4d3b600e6543850fff Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 20 Jul 2023 22:13:50 +0200 Subject: [PATCH] JS: marshall JS object to Java in a wrapper --- .../org/teavm/classlib/java/lang/TClass.java | 3 +- .../java/lang/reflect/TConstructor.java | 6 +- .../AbstractInstructionAnalyzer.java | 1 + .../dependency/DependencyGraphBuilder.java | 1 + .../dependency/FastDependencyAnalyzer.java | 8 +- .../model/analysis/BaseTypeInference.java | 342 ++++++++++++++++++ .../org/teavm/model/emit/ValueEmitter.java | 2 +- .../SystemArrayCopyOptimization.java | 198 +--------- .../org/teavm/backend/javascript/runtime.js | 2 +- .../jso/core/JSFinalizationRegistry.java | 26 ++ .../core/JSFinalizationRegistryConsumer.java | 24 ++ .../main/java/org/teavm/jso/core/JSMap.java | 36 ++ .../java/org/teavm/jso/core/JSObjects.java | 4 + .../java/org/teavm/jso/core/JSSymbol.java | 32 ++ .../java/org/teavm/jso/core/JSWeakMap.java | 34 ++ .../java/org/teavm/jso/core/JSWeakRef.java | 32 ++ .../src/main/java/org/teavm/jso/impl/JS.java | 55 +-- .../org/teavm/jso/impl/JSClassProcessor.java | 227 +++++++++++- .../java/org/teavm/jso/impl/JSMethods.java | 43 ++- .../java/org/teavm/jso/impl/JSOPlugin.java | 12 + .../main/java/org/teavm/jso/impl/JSType.java | 57 +++ .../org/teavm/jso/impl/JSTypeInference.java | 82 +++++ .../org/teavm/jso/impl/JSValueMarshaller.java | 19 +- .../java/org/teavm/jso/impl/JSWrapper.java | 192 ++++++++++ .../teavm/jso/impl/JSWrapperGenerator.java | 48 +++ .../org/teavm/jso/test/JSWrapperTest.java | 237 ++++++++++++ 26 files changed, 1468 insertions(+), 255 deletions(-) create mode 100644 core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistryConsumer.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSMap.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSType.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSTypeInference.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java create mode 100644 tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java index 44be49b5e..0accce26a 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TClass.java @@ -47,6 +47,7 @@ import org.teavm.interop.Unmanaged; import org.teavm.jso.core.JSArray; import org.teavm.platform.Platform; import org.teavm.platform.PlatformClass; +import org.teavm.platform.PlatformObject; import org.teavm.platform.PlatformSequence; import org.teavm.runtime.RuntimeClass; import org.teavm.runtime.RuntimeObject; @@ -402,7 +403,7 @@ public class TClass extends TObject implements TAnnotatedElement, TType { @InjectedBy(ClassGenerator.class) @PluggableDependency(ClassGenerator.class) - public native T newEmptyInstance(); + public native PlatformObject newEmptyInstance(); @SuppressWarnings({ "raw", "unchecked" }) public TConstructor[] getDeclaredConstructors() throws TSecurityException { diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java b/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java index af2f33605..a5917f4a7 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/reflect/TConstructor.java @@ -23,8 +23,6 @@ import org.teavm.classlib.java.lang.TIllegalAccessException; import org.teavm.classlib.java.lang.TIllegalArgumentException; import org.teavm.classlib.java.lang.TInstantiationException; import org.teavm.classlib.java.lang.TObject; -import org.teavm.platform.PlatformObject; -import org.teavm.platform.PlatformSequence; public class TConstructor extends TAccessibleObject implements TMember { private TClass declaringClass; @@ -113,8 +111,8 @@ public class TConstructor extends TAccessibleObject implements TMember { } } - PlatformSequence jsArgs = Converter.arrayFromJava(initargs); - PlatformObject instance = declaringClass.newEmptyInstance(); + var jsArgs = Converter.arrayFromJava(initargs); + var instance = declaringClass.newEmptyInstance(); callable.call(instance, jsArgs); return (T) Converter.toJava(instance); } diff --git a/core/src/main/java/org/teavm/dependency/AbstractInstructionAnalyzer.java b/core/src/main/java/org/teavm/dependency/AbstractInstructionAnalyzer.java index 4d7872e4d..88e2f55d8 100644 --- a/core/src/main/java/org/teavm/dependency/AbstractInstructionAnalyzer.java +++ b/core/src/main/java/org/teavm/dependency/AbstractInstructionAnalyzer.java @@ -233,6 +233,7 @@ abstract class AbstractInstructionAnalyzer extends AbstractInstructionReader { if (className != null) { getAnalyzer().linkClass(className); } + getAnalyzer().linkClass("java.lang.ClassCastException"); } @Override diff --git a/core/src/main/java/org/teavm/dependency/DependencyGraphBuilder.java b/core/src/main/java/org/teavm/dependency/DependencyGraphBuilder.java index 4b438158d..15c8c0579 100644 --- a/core/src/main/java/org/teavm/dependency/DependencyGraphBuilder.java +++ b/core/src/main/java/org/teavm/dependency/DependencyGraphBuilder.java @@ -223,6 +223,7 @@ class DependencyGraphBuilder { @Override public void cast(VariableReader receiver, VariableReader value, ValueType targetType) { super.cast(receiver, value, targetType); + currentExceptionConsumer.consume(dependencyAnalyzer.getType("java.lang.ClassCastException")); DependencyNode valueNode = nodes[value.getIndex()]; DependencyNode receiverNode = nodes[receiver.getIndex()]; ClassReaderSource classSource = dependencyAnalyzer.getClassSource(); diff --git a/core/src/main/java/org/teavm/dependency/FastDependencyAnalyzer.java b/core/src/main/java/org/teavm/dependency/FastDependencyAnalyzer.java index 606232f66..ed2718b8e 100644 --- a/core/src/main/java/org/teavm/dependency/FastDependencyAnalyzer.java +++ b/core/src/main/java/org/teavm/dependency/FastDependencyAnalyzer.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.Map; import org.teavm.common.ServiceRepository; import org.teavm.diagnostics.Diagnostics; -import org.teavm.model.BasicBlockReader; import org.teavm.model.ClassReader; import org.teavm.model.ClassReaderSource; import org.teavm.model.ElementModifier; @@ -32,7 +31,6 @@ import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; import org.teavm.model.ProgramReader; import org.teavm.model.ReferenceCache; -import org.teavm.model.TryCatchBlockReader; import org.teavm.model.ValueType; public class FastDependencyAnalyzer extends DependencyAnalyzer { @@ -59,12 +57,12 @@ public class FastDependencyAnalyzer extends DependencyAnalyzer { ProgramReader program = method.getProgram(); if (program != null) { - FastInstructionAnalyzer instructionAnalyzer = new FastInstructionAnalyzer(this); + var instructionAnalyzer = new FastInstructionAnalyzer(this); instructionAnalyzer.setCaller(method.getReference()); - for (BasicBlockReader block : program.getBasicBlocks()) { + for (var block : program.getBasicBlocks()) { block.readAllInstructions(instructionAnalyzer); - for (TryCatchBlockReader tryCatch : block.readTryCatchBlocks()) { + for (var tryCatch : block.readTryCatchBlocks()) { if (tryCatch.getExceptionType() != null) { linkClass(tryCatch.getExceptionType()); } diff --git a/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java b/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java new file mode 100644 index 000000000..744040ad0 --- /dev/null +++ b/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java @@ -0,0 +1,342 @@ +/* + * 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. + */ +package org.teavm.model.analysis; + +import com.carrotsearch.hppc.IntStack; +import java.util.ArrayDeque; +import java.util.Objects; +import org.teavm.common.Graph; +import org.teavm.common.GraphBuilder; +import org.teavm.model.InvokeDynamicInstruction; +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.AbstractInstructionVisitor; +import org.teavm.model.instructions.ArrayLengthInstruction; +import org.teavm.model.instructions.AssignInstruction; +import org.teavm.model.instructions.BinaryInstruction; +import org.teavm.model.instructions.BoundCheckInstruction; +import org.teavm.model.instructions.CastInstruction; +import org.teavm.model.instructions.CastIntegerInstruction; +import org.teavm.model.instructions.CastNumberInstruction; +import org.teavm.model.instructions.ClassConstantInstruction; +import org.teavm.model.instructions.CloneArrayInstruction; +import org.teavm.model.instructions.ConstructArrayInstruction; +import org.teavm.model.instructions.ConstructInstruction; +import org.teavm.model.instructions.ConstructMultiArrayInstruction; +import org.teavm.model.instructions.DoubleConstantInstruction; +import org.teavm.model.instructions.FloatConstantInstruction; +import org.teavm.model.instructions.GetElementInstruction; +import org.teavm.model.instructions.GetFieldInstruction; +import org.teavm.model.instructions.IntegerConstantInstruction; +import org.teavm.model.instructions.InvokeInstruction; +import org.teavm.model.instructions.IsInstanceInstruction; +import org.teavm.model.instructions.LongConstantInstruction; +import org.teavm.model.instructions.NegateInstruction; +import org.teavm.model.instructions.NullCheckInstruction; +import org.teavm.model.instructions.NullConstantInstruction; +import org.teavm.model.instructions.NumericOperandType; +import org.teavm.model.instructions.StringConstantInstruction; +import org.teavm.model.instructions.UnwrapArrayInstruction; + +public abstract class BaseTypeInference { + private Program program; + private MethodReference reference; + private Object[] types; + private Graph graph; + private Graph arrayGraph; + + public BaseTypeInference(Program program, MethodReference reference) { + this.program = program; + this.reference = reference; + } + + private void prepare() { + types = new Object[program.variableCount()]; + var visitor = new InitialTypeVisitor(program.variableCount()); + var params = Math.min(reference.parameterCount(), program.variableCount() - 1); + for (var i = 0; i < params; ++i) { + visitor.type(program.variableAt(i + 1), reference.parameterType(i)); + } + visitor.type(program.variableAt(0), ValueType.object(reference.getClassName())); + for (var block : program.getBasicBlocks()) { + for (var insn : block) { + insn.acceptVisitor(visitor); + } + for (var phi : block.getPhis()) { + for (var incoming : phi.getIncomings()) { + visitor.graphBuilder.addEdge(incoming.getValue().getIndex(), phi.getReceiver().getIndex()); + } + } + } + graph = visitor.graphBuilder.build(); + arrayGraph = visitor.arrayGraphBuilder.build(); + } + + @SuppressWarnings("unchecked") + private void propagate() { + var stack = new IntStack(); + var typeStack = new ArrayDeque(); + for (var i = 0; i < types.length; ++i) { + if (types[i] != null) { + stack.push(i); + typeStack.push((T) types[i]); + types[i] = null; + } + } + while (!stack.isEmpty()) { + var variable = stack.pop(); + var type = typeStack.pop(); + var formerType = (T) types[variable]; + if (Objects.equals(formerType, type)) { + continue; + } + type = doMerge(type, formerType); + if (Objects.equals(type, formerType)) { + continue; + } + types[variable] = type; + for (var succ : graph.outgoingEdges(variable)) { + if (!Objects.equals(types[succ], type)) { + stack.push(succ); + typeStack.push(type); + } + } + if (arrayGraph.outgoingEdgesCount(variable) > 0) { + var elementType = elementType(type); + for (var succ : arrayGraph.outgoingEdges(variable)) { + if (!Objects.equals(types[succ], elementType)) { + stack.push(succ); + typeStack.push(elementType); + } + } + } + } + } + + public void ensure() { + if (types == null) { + prepare(); + propagate(); + } + } + + @SuppressWarnings("unchecked") + public T typeOf(Variable variable) { + ensure(); + return (T) types[variable.getIndex()]; + } + + + protected abstract T mapType(ValueType type); + + protected abstract T nullType(); + + private T doMerge(T a, T b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } else { + return merge(a, b); + } + } + + protected abstract T merge(T a, T b); + + protected abstract T elementType(T t); + + private class InitialTypeVisitor extends AbstractInstructionVisitor { + private GraphBuilder graphBuilder; + private GraphBuilder arrayGraphBuilder; + + InitialTypeVisitor(int size) { + graphBuilder = new GraphBuilder(size); + arrayGraphBuilder = new GraphBuilder(size); + } + + @Override + public void visit(NullConstantInstruction insn) { + types[insn.getReceiver().getIndex()] = nullType(); + } + + @Override + public void visit(IntegerConstantInstruction insn) { + type(insn.getReceiver(), ValueType.INTEGER); + } + + @Override + public void visit(LongConstantInstruction insn) { + type(insn.getReceiver(), ValueType.LONG); + } + + @Override + public void visit(FloatConstantInstruction insn) { + type(insn.getReceiver(), ValueType.FLOAT); + } + + @Override + public void visit(DoubleConstantInstruction insn) { + type(insn.getReceiver(), ValueType.DOUBLE); + } + + @Override + public void visit(ClassConstantInstruction insn) { + type(insn.getReceiver(), ValueType.object("java/lang/Class")); + } + + @Override + public void visit(StringConstantInstruction insn) { + type(insn.getReceiver(), ValueType.object("java/lang/String")); + } + + @Override + public void visit(ConstructInstruction insn) { + type(insn.getReceiver(), ValueType.object(insn.getType())); + } + + @Override + public void visit(ConstructArrayInstruction insn) { + type(insn.getReceiver(), ValueType.arrayOf(insn.getItemType())); + } + + @Override + public void visit(ConstructMultiArrayInstruction insn) { + var type = insn.getItemType(); + for (var i = 0; i < insn.getDimensions().size(); ++i) { + type = ValueType.arrayOf(type); + } + type(insn.getReceiver(), type); + } + + @Override + public void visit(IsInstanceInstruction insn) { + type(insn.getReceiver(), ValueType.BOOLEAN); + } + + @Override + public void visit(CastInstruction insn) { + type(insn.getReceiver(), insn.getTargetType()); + } + + @Override + public void visit(NegateInstruction insn) { + type(insn.getReceiver(), insn.getOperandType()); + } + + @Override + public void visit(CastNumberInstruction insn) { + type(insn.getReceiver(), insn.getTargetType()); + } + + @Override + public void visit(BinaryInstruction insn) { + type(insn.getReceiver(), insn.getOperandType()); + } + + @Override + public void visit(CastIntegerInstruction insn) { + switch (insn.getTargetType()) { + case BYTE: + type(insn.getReceiver(), ValueType.BYTE); + break; + case CHAR: + type(insn.getReceiver(), ValueType.CHARACTER); + break; + case SHORT: + type(insn.getReceiver(), ValueType.SHORT); + break; + } + } + + @Override + public void visit(ArrayLengthInstruction insn) { + type(insn.getReceiver(), ValueType.INTEGER); + } + + @Override + public void visit(CloneArrayInstruction insn) { + graphBuilder.addEdge(insn.getArray().getIndex(), insn.getReceiver().getIndex()); + } + + @Override + public void visit(BoundCheckInstruction insn) { + type(insn.getReceiver(), ValueType.INTEGER); + } + + @Override + public void visit(InvokeInstruction insn) { + type(insn.getReceiver(), insn.getMethod().getReturnType()); + } + + @Override + public void visit(InvokeDynamicInstruction insn) { + type(insn.getReceiver(), insn.getMethod().getResultType()); + } + + @Override + public void visit(GetFieldInstruction insn) { + type(insn.getReceiver(), insn.getFieldType()); + } + + @Override + public void visit(UnwrapArrayInstruction insn) { + graphBuilder.addEdge(insn.getArray().getIndex(), insn.getReceiver().getIndex()); + } + + @Override + public void visit(GetElementInstruction insn) { + arrayGraphBuilder.addEdge(insn.getArray().getIndex(), insn.getReceiver().getIndex()); + } + + @Override + public void visit(AssignInstruction insn) { + graphBuilder.addEdge(insn.getAssignee().getIndex(), insn.getReceiver().getIndex()); + } + + @Override + public void visit(NullCheckInstruction insn) { + graphBuilder.addEdge(insn.getValue().getIndex(), insn.getReceiver().getIndex()); + } + + void type(Variable target, NumericOperandType type) { + switch (type) { + case INT: + type(target, ValueType.INTEGER); + break; + case LONG: + type(target, ValueType.LONG); + break; + case FLOAT: + type(target, ValueType.FLOAT); + break; + case DOUBLE: + type(target, ValueType.DOUBLE); + break; + } + } + + void type(Variable target, ValueType type) { + if (target != null) { + var t = mapType(type); + if (t != null) { + types[target.getIndex()] = t; + } + } + } + } +} diff --git a/core/src/main/java/org/teavm/model/emit/ValueEmitter.java b/core/src/main/java/org/teavm/model/emit/ValueEmitter.java index a4e16c315..85ec59ac3 100644 --- a/core/src/main/java/org/teavm/model/emit/ValueEmitter.java +++ b/core/src/main/java/org/teavm/model/emit/ValueEmitter.java @@ -111,7 +111,7 @@ public class ValueEmitter { String className = ((ValueType.Object) type).getClassName(); PutFieldInstruction insn = new PutFieldInstruction(); insn.setField(new FieldReference(className, name)); - insn.setFieldType(type); + insn.setFieldType(value.type); insn.setInstance(variable); insn.setValue(value.getVariable()); pe.addInstruction(insn); diff --git a/core/src/main/java/org/teavm/model/optimization/SystemArrayCopyOptimization.java b/core/src/main/java/org/teavm/model/optimization/SystemArrayCopyOptimization.java index 9f83bb7ba..877be6242 100644 --- a/core/src/main/java/org/teavm/model/optimization/SystemArrayCopyOptimization.java +++ b/core/src/main/java/org/teavm/model/optimization/SystemArrayCopyOptimization.java @@ -15,24 +15,12 @@ */ package org.teavm.model.optimization; -import com.carrotsearch.hppc.IntHashSet; -import com.carrotsearch.hppc.IntStack; -import java.util.Arrays; -import org.teavm.model.MethodDescriptor; +import java.util.Objects; 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.AbstractInstructionVisitor; -import org.teavm.model.instructions.AssignInstruction; -import org.teavm.model.instructions.CastInstruction; -import org.teavm.model.instructions.ConstructArrayInstruction; -import org.teavm.model.instructions.ConstructMultiArrayInstruction; -import org.teavm.model.instructions.GetElementInstruction; -import org.teavm.model.instructions.GetFieldInstruction; +import org.teavm.model.analysis.BaseTypeInference; import org.teavm.model.instructions.InvokeInstruction; -import org.teavm.model.instructions.NullCheckInstruction; -import org.teavm.model.instructions.UnwrapArrayInstruction; public class SystemArrayCopyOptimization implements MethodOptimization { private static final MethodReference ARRAY_COPY_METHOD = new MethodReference(System.class, @@ -51,7 +39,7 @@ public class SystemArrayCopyOptimization implements MethodOptimization { var method = invoke.getMethod(); if (method.equals(ARRAY_COPY_METHOD)) { if (typeInference == null) { - typeInference = new TypeInference(program, context.getMethod().getDescriptor()); + typeInference = new TypeInference(program, context.getMethod().getReference()); } var sourceType = typeInference.typeOf(invoke.getArguments().get(0)); var destType = typeInference.typeOf(invoke.getArguments().get(2)); @@ -69,182 +57,32 @@ public class SystemArrayCopyOptimization implements MethodOptimization { return somethingChanged; } - private static class TypeInference { - private ValueType[] types; - private int[] assignments; - private int[] elementAssignments; - private int[][] phis; - private boolean[] present; - private boolean[] calculating; + private static class TypeInference extends BaseTypeInference { + TypeInference(Program program, MethodReference reference) { + super(program, reference); + } - TypeInference(Program program, MethodDescriptor descriptor) { - types = new ValueType[program.variableCount()]; - assignments = new int[program.variableCount()]; - elementAssignments = new int[program.variableCount()]; - phis = new int[program.variableCount()][]; - Arrays.fill(assignments, -1); - Arrays.fill(elementAssignments, -1); - present = new boolean[program.variableCount()]; - calculating = new boolean[program.variableCount()]; - - var visitor = new InitialTypeVisitor(types, assignments, elementAssignments); - var params = Math.min(descriptor.parameterCount(), program.variableCount() - 1); - for (var i = 0; i < params; ++i) { - visitor.type(program.variableAt(i + 1), descriptor.parameterType(i)); + @Override + public ValueType merge(ValueType a, ValueType b) { + if (!Objects.equals(a, b)) { + return null; } - for (var block : program.getBasicBlocks()) { - for (var insn : block) { - insn.acceptVisitor(visitor); - } - for (var phi : block.getPhis()) { - var sourceIndexes = new IntHashSet(); - for (var incoming : phi.getIncomings()) { - sourceIndexes.add(incoming.getValue().getIndex()); - } - var inputs = sourceIndexes.toArray(); - Arrays.sort(inputs); - phis[phi.getReceiver().getIndex()] = inputs; - } - } - - for (int i = 0; i < types.length; ++i) { - if (types[i] != null) { - present[i] = true; - } - } - } - - ValueType typeOf(Variable variable) { - if (!present[variable.getIndex()]) { - calculate(variable.getIndex()); - } - return types[variable.getIndex()]; - } - - private void calculate(int initialVariable) { - var stack = new IntStack(); - stack.push(initialVariable); - while (!stack.isEmpty()) { - var variable = stack.pop(); - if (calculating[variable]) { - calculating[variable] = false; - present[variable] = true; - var inputs = phis[variable]; - ValueType type; - if (inputs != null) { - type = null; - var initialized = false; - for (var input : inputs) { - if (calculating[input]) { - continue; - } - if (initialized) { - if (!type.equals(types[input])) { - type = null; - break; - } - } else { - type = types[input]; - } - } - } else if (assignments[variable] >= 0) { - type = types[assignments[variable]]; - } else if (elementAssignments[variable] >= 0) { - type = types[elementAssignments[variable]]; - type = type instanceof ValueType.Array ? ((ValueType.Array) type).getItemType() : null; - } else { - type = null; - } - types[variable] = type; - } else { - calculating[variable] = true; - stack.push(variable); - var inputs = phis[variable]; - if (inputs != null) { - for (var input : inputs) { - if (!calculating[input] && !present[input]) { - stack.push(input); - } - } - } - var assign = assignments[variable]; - if (assign >= 0 && !present[assign]) { - stack.push(assign); - } - var elemAssign = elementAssignments[variable]; - if (elemAssign >= 0 && !present[elemAssign]) { - stack.push(elemAssign); - } - } - } - } - } - - private static class InitialTypeVisitor extends AbstractInstructionVisitor { - private ValueType[] types; - private int[] assignments; - private int[] elementAssignments; - - InitialTypeVisitor(ValueType[] types, int[] assignments, int[] elementAssignments) { - this.types = types; - this.assignments = assignments; - this.elementAssignments = elementAssignments; + return a; } @Override - public void visit(ConstructArrayInstruction insn) { - types[insn.getReceiver().getIndex()] = insn.getItemType(); - super.visit(insn); + public ValueType elementType(ValueType valueType) { + return valueType instanceof ValueType.Array ? ((ValueType.Array) valueType).getItemType() : null; } @Override - public void visit(ConstructMultiArrayInstruction insn) { - var type = insn.getItemType(); - for (var i = 1; i < insn.getDimensions().size(); ++i) { - type = ValueType.arrayOf(type); - } - types[insn.getReceiver().getIndex()] = insn.getItemType(); + public ValueType nullType() { + return null; } @Override - public void visit(CastInstruction insn) { - type(insn.getReceiver(), insn.getTargetType()); - } - - @Override - public void visit(InvokeInstruction insn) { - type(insn.getReceiver(), insn.getMethod().getReturnType()); - } - - @Override - public void visit(GetFieldInstruction insn) { - type(insn.getReceiver(), insn.getFieldType()); - } - - void type(Variable target, ValueType type) { - if (target != null && type instanceof ValueType.Array) { - types[target.getIndex()] = ((ValueType.Array) type).getItemType(); - } - } - - @Override - public void visit(UnwrapArrayInstruction insn) { - assignments[insn.getReceiver().getIndex()] = insn.getArray().getIndex(); - } - - @Override - public void visit(GetElementInstruction insn) { - elementAssignments[insn.getReceiver().getIndex()] = insn.getArray().getIndex(); - } - - @Override - public void visit(AssignInstruction insn) { - assignments[insn.getReceiver().getIndex()] = insn.getAssignee().getIndex(); - } - - @Override - public void visit(NullCheckInstruction insn) { - assignments[insn.getReceiver().getIndex()] = insn.getValue().getIndex(); + public ValueType mapType(ValueType type) { + return type instanceof ValueType.Array ? ((ValueType.Array) type).getItemType() : null; } } } diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index 92ac93071..dfde046c8 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -27,7 +27,7 @@ function $rt_compare(a, b) { return a > b ? 1 : a < b ? -1 : a === b ? 0 : 1; } function $rt_isInstance(obj, cls) { - return obj !== null && !!obj.constructor.$meta && $rt_isAssignable(obj.constructor, cls); + return obj instanceof $rt_objcls() && !!obj.constructor.$meta && $rt_isAssignable(obj.constructor, cls); } function $rt_isAssignable(from, to) { if (from === to) { diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java new file mode 100644 index 000000000..413fcc443 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java @@ -0,0 +1,26 @@ +/* + * 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. + */ +package org.teavm.jso.core; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +public abstract class JSFinalizationRegistry implements JSObject { + public abstract void register(JSObject obj, JSObject token); + + @JSBody(params = "consumer", script = "return new FinalizationRegistry(consumer);") + public static native JSFinalizationRegistry create(JSFinalizationRegistryConsumer consumer); +} diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistryConsumer.java b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistryConsumer.java new file mode 100644 index 000000000..1201dc8a3 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistryConsumer.java @@ -0,0 +1,24 @@ +/* + * 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. + */ +package org.teavm.jso.core; + +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; + +@JSFunctor +public interface JSFinalizationRegistryConsumer extends JSObject { + void accept(JSObject obj); +} diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java b/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java new file mode 100644 index 000000000..bda5ecb15 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +public abstract class JSMap implements JSObject { + public abstract V get(K key); + + public abstract boolean has(K key); + + public abstract JSMap set(K key, V value); + + public abstract boolean delete(K key); + + public abstract void clear(); + + @JSBody(script = "return new Map();") + @NoSideEffects + public static native JSMap create(); +} diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java b/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java index 020fab64b..b5d3dcc4f 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java @@ -61,4 +61,8 @@ public final class JSObjects { @JSBody(params = { "object", "name" }, script = "return name in object;") @NoSideEffects public static native boolean hasProperty(JSObject object, String name); + + @JSBody(params = "object", script = "return Object.getPrototypeOf(object);") + @NoSideEffects + public static native JSObject getPrototypeOf(JSObject object); } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java new file mode 100644 index 000000000..07de90884 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +public abstract class JSSymbol implements JSObject { + @JSBody(params = "name", script = "return Symbol(name);") + public static native JSSymbol create(String name); + + @JSBody(params = "obj", script = "return obj[this];") + public native T get(JSObject obj); + + @JSBody(params = { "obj", "value" }, script = "obj[this] = value;") + @NoSideEffects + public native void set(JSObject obj, T value); +} diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java new file mode 100644 index 000000000..8f00bd8e7 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +public abstract class JSWeakMap implements JSObject { + public abstract V get(K key); + + public abstract boolean has(K key); + + public abstract JSWeakMap set(K key, V value); + + public abstract boolean remove(K key); + + @JSBody(script = "return new WeakMap();") + @NoSideEffects + public static native JSWeakMap create(); +} diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java new file mode 100644 index 000000000..b90df6ba7 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +public abstract class JSWeakRef implements JSObject { + public abstract T deref(); + + @JSBody(params = "value", script = "return new WeakRef(value);") + @NoSideEffects + public static native JSWeakRef create(T value); + + @JSBody(script = "return typeof WeakRef !== 'undefined';") + @NoSideEffects + public static native boolean isSupported(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java index 25e6f507f..41bd84e37 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java @@ -16,7 +16,6 @@ package org.teavm.jso.impl; import java.lang.reflect.Array; -import java.util.function.Function; import org.teavm.backend.javascript.spi.GeneratedBy; import org.teavm.backend.javascript.spi.InjectedBy; import org.teavm.dependency.PluggableDependency; @@ -148,11 +147,11 @@ final class JS { return result; } - public static Function> arrayWrapper() { + public static WrapFunction> arrayWrapper() { return JS::wrap; } - public static JSArray map(S[] array, Function f) { + public static JSArray map(S[] array, WrapFunction f) { if (array == null) { return null; } @@ -163,7 +162,15 @@ final class JS { return result; } - public static Function> arrayMapper(Function f) { + public interface WrapFunction { + T apply(S obj); + } + + public interface UnwrapFunction { + T apply(S obj); + } + + public static WrapFunction> arrayMapper(WrapFunction f) { return array -> map(array, f); } @@ -178,7 +185,7 @@ final class JS { return result; } - public static Function> booleanArrayWrapper() { + public static WrapFunction> booleanArrayWrapper() { return JS::wrap; } @@ -193,7 +200,7 @@ final class JS { return result; } - public static Function> byteArrayWrapper() { + public static WrapFunction> byteArrayWrapper() { return JS::wrap; } @@ -208,7 +215,7 @@ final class JS { return result; } - public static Function> shortArrayWrapper() { + public static WrapFunction> shortArrayWrapper() { return JS::wrap; } @@ -223,7 +230,7 @@ final class JS { return result; } - public static Function> charArrayWrapper() { + public static WrapFunction> charArrayWrapper() { return JS::wrap; } @@ -238,7 +245,7 @@ final class JS { return result; } - public static Function> intArrayWrapper() { + public static WrapFunction> intArrayWrapper() { return JS::wrap; } @@ -253,7 +260,7 @@ final class JS { return result; } - public static Function> stringArrayWrapper() { + public static WrapFunction> stringArrayWrapper() { return JS::wrap; } @@ -268,7 +275,7 @@ final class JS { return result; } - public static Function> floatArrayWrapper() { + public static WrapFunction> floatArrayWrapper() { return JS::wrap; } @@ -283,7 +290,7 @@ final class JS { return result; } - public static Function> doubleArrayWrapper() { + public static WrapFunction> doubleArrayWrapper() { return JS::wrap; } @@ -299,11 +306,12 @@ final class JS { return result; } - public static Function, T[]> arrayUnwrapper(Class type) { + public static UnwrapFunction, T[]> arrayUnwrapper(Class type) { return array -> unwrapArray(type, array); } - public static T[] unmapArray(Class type, JSArrayReader array, Function f) { + public static T[] unmapArray(Class type, JSArrayReader array, + UnwrapFunction f) { if (array == null) { return null; } @@ -315,7 +323,8 @@ final class JS { return result; } - public static Function, T[]> arrayUnmapper(Class type, Function f) { + public static UnwrapFunction, T[]> arrayUnmapper(Class type, + UnwrapFunction f) { return array -> unmapArray(type, array, f); } @@ -330,7 +339,7 @@ final class JS { return result; } - public static Function, boolean[]> booleanArrayUnwrapper() { + public static UnwrapFunction, boolean[]> booleanArrayUnwrapper() { return JS::unwrapBooleanArray; } @@ -345,7 +354,7 @@ final class JS { return result; } - public static Function, byte[]> byteArrayUnwrapper() { + public static UnwrapFunction, byte[]> byteArrayUnwrapper() { return JS::unwrapByteArray; } @@ -360,7 +369,7 @@ final class JS { return result; } - public static Function, short[]> shortArrayUnwrapper() { + public static UnwrapFunction, short[]> shortArrayUnwrapper() { return JS::unwrapShortArray; } @@ -375,7 +384,7 @@ final class JS { return result; } - public static Function, int[]> intArrayUnwrapper() { + public static UnwrapFunction, int[]> intArrayUnwrapper() { return JS::unwrapIntArray; } @@ -390,7 +399,7 @@ final class JS { return result; } - public static Function, char[]> charArrayUnwrapper() { + public static UnwrapFunction, char[]> charArrayUnwrapper() { return JS::unwrapCharArray; } @@ -405,7 +414,7 @@ final class JS { return result; } - public static Function, float[]> floatArrayUnwrapper() { + public static UnwrapFunction, float[]> floatArrayUnwrapper() { return JS::unwrapFloatArray; } @@ -420,7 +429,7 @@ final class JS { return result; } - public static Function, double[]> doubleArrayUnwrapper() { + public static UnwrapFunction, double[]> doubleArrayUnwrapper() { return JS::unwrapDoubleArray; } @@ -435,7 +444,7 @@ final class JS { return result; } - public static Function, String[]> stringArrayUnwrapper() { + public static UnwrapFunction, String[]> stringArrayUnwrapper() { return JS::unwrapStringArray; } 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 b52ed6e4d..191f26641 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 @@ -58,21 +58,40 @@ 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.ArrayElementType; import org.teavm.model.instructions.AssignInstruction; import org.teavm.model.instructions.CastInstruction; +import org.teavm.model.instructions.ClassConstantInstruction; +import org.teavm.model.instructions.ConstructArrayInstruction; import org.teavm.model.instructions.ExitInstruction; +import org.teavm.model.instructions.GetElementInstruction; +import org.teavm.model.instructions.IntegerConstantInstruction; import org.teavm.model.instructions.InvocationType; import org.teavm.model.instructions.InvokeInstruction; +import org.teavm.model.instructions.IsInstanceInstruction; +import org.teavm.model.instructions.PutElementInstruction; +import org.teavm.model.instructions.PutFieldInstruction; import org.teavm.model.util.InstructionVariableMapper; import org.teavm.model.util.ModelUtils; import org.teavm.model.util.ProgramUtils; class JSClassProcessor { private static final String NO_SIDE_EFFECTS = NoSideEffects.class.getName(); + private static final MethodReference WRAP = new MethodReference(JSWrapper.class, "wrap", Object.class, + Object.class); + private static final MethodReference MAYBE_WRAP = new MethodReference(JSWrapper.class, "maybeWrap", Object.class, + Object.class); + private static final MethodReference UNWRAP = new MethodReference(JSWrapper.class, "unwrap", Object.class, + JSObject.class); + private static final MethodReference MAYBE_UNWRAP = new MethodReference(JSWrapper.class, "maybeUnwrap", + Object.class, JSObject.class); + private static final MethodReference IS_JS = new MethodReference(JSWrapper.class, "isJs", + Object.class, boolean.class); private final ClassReaderSource classSource; private final JSBodyRepository repository; private final JavaInvocationProcessor javaInvocationProcessor; private Program program; + private JSTypeInference types; private final List replacement = new ArrayList<>(); private final JSTypeHelper typeHelper; private final Diagnostics diagnostics; @@ -206,37 +225,157 @@ class JSClassProcessor { void processProgram(MethodHolder methodToProcess) { setCurrentProgram(methodToProcess.getProgram()); + types = new JSTypeInference(typeHelper, program, methodToProcess.getReference()); + types.ensure(); for (int i = 0; i < program.basicBlockCount(); ++i) { - BasicBlock block = program.basicBlockAt(i); - for (Instruction insn : block) { + var block = program.basicBlockAt(i); + for (var insn : block) { if (insn instanceof CastInstruction) { replacement.clear(); - CallLocation callLocation = new CallLocation(methodToProcess.getReference(), insn.getLocation()); + var callLocation = new CallLocation(methodToProcess.getReference(), insn.getLocation()); if (processCast((CastInstruction) insn, callLocation)) { insn.insertNextAll(replacement); insn.delete(); } + } else if (insn instanceof IsInstanceInstruction) { + processIsInstance((IsInstanceInstruction) insn); } else if (insn instanceof InvokeInstruction) { - InvokeInstruction invoke = (InvokeInstruction) insn; + var invoke = (InvokeInstruction) insn; + processInvokeArgs(invoke); - MethodReader method = getMethod(invoke.getMethod().getClassName(), - invoke.getMethod().getDescriptor()); + var method = getMethod(invoke.getMethod().getClassName(), invoke.getMethod().getDescriptor()); if (method == null) { continue; } - CallLocation callLocation = new CallLocation(methodToProcess.getReference(), insn.getLocation()); + var callLocation = new CallLocation(methodToProcess.getReference(), insn.getLocation()); replacement.clear(); if (processInvocation(method, callLocation, invoke, methodToProcess)) { insn.insertNextAll(replacement); insn.delete(); } + } else if (insn instanceof PutFieldInstruction) { + processPutField((PutFieldInstruction) insn); + } else if (insn instanceof GetElementInstruction) { + processGetFromArray((GetElementInstruction) insn); + } else if (insn instanceof PutElementInstruction) { + processPutIntoArray((PutElementInstruction) insn); + } else if (insn instanceof ConstructArrayInstruction) { + processConstructArray((ConstructArrayInstruction) insn); + } else if (insn instanceof ExitInstruction) { + var exit = (ExitInstruction) insn; + exit.setValueToReturn(wrapJsAsJava(insn, exit.getValueToReturn(), + methodToProcess.getResultType())); + } else if (insn instanceof ClassConstantInstruction) { + processClassConstant((ClassConstantInstruction) insn); } } } } + private void processInvokeArgs(InvokeInstruction invoke) { + if (typeHelper.isJavaScriptClass(invoke.getMethod().getClassName())) { + return; + } + Variable[] newArgs = null; + for (var i = 0; i < invoke.getArguments().size(); ++i) { + var type = invoke.getMethod().parameterType(i); + var arg = invoke.getArguments().get(i); + var newArg = wrapJsAsJava(invoke, arg, type); + if (newArg != arg) { + if (newArgs == null) { + newArgs = invoke.getArguments().toArray(new Variable[0]); + } + newArgs[i] = newArg; + } + } + if (newArgs != null) { + invoke.setArguments(newArgs); + } + + if (invoke.getInstance() != null) { + invoke.setInstance(wrapJsAsJava(invoke, invoke.getInstance(), + ValueType.object(invoke.getMethod().getClassName()))); + } + } + + private void processPutField(PutFieldInstruction putField) { + putField.setValue(wrapJsAsJava(putField, putField.getValue(), putField.getFieldType())); + } + + private void processGetFromArray(GetElementInstruction insn) { + if (insn.getType() != ArrayElementType.OBJECT) { + return; + } + + var type = types.typeOf(insn.getReceiver()); + if (type == JSType.JS || type == JSType.MIXED) { + var unwrap = new InvokeInstruction(); + unwrap.setType(InvocationType.SPECIAL); + unwrap.setMethod(type == JSType.MIXED ? MAYBE_UNWRAP : UNWRAP); + unwrap.setArguments(program.createVariable()); + unwrap.setReceiver(insn.getReceiver()); + unwrap.setLocation(insn.getLocation()); + insn.setReceiver(unwrap.getArguments().get(0)); + insn.insertNext(unwrap); + } + } + + private void processPutIntoArray(PutElementInstruction insn) { + if (insn.getType() != ArrayElementType.OBJECT) { + return; + } + + var type = types.typeOf(insn.getValue()); + if (type == JSType.JS || type == JSType.MIXED) { + var wrap = new InvokeInstruction(); + wrap.setType(InvocationType.SPECIAL); + wrap.setMethod(type == JSType.MIXED ? MAYBE_WRAP : WRAP); + wrap.setArguments(insn.getValue()); + wrap.setReceiver(program.createVariable()); + wrap.setLocation(insn.getLocation()); + insn.setValue(wrap.getReceiver()); + insn.insertPrevious(wrap); + } + } + + private void processConstructArray(ConstructArrayInstruction insn) { + insn.setItemType(processType(insn.getItemType())); + } + + private void processClassConstant(ClassConstantInstruction insn) { + insn.setConstant(processType(insn.getConstant())); + } + + private ValueType processType(ValueType type) { + return processType(typeHelper, type); + } + + static ValueType processType(JSTypeHelper typeHelper, ValueType type) { + var originalType = type; + var degree = 0; + while (type instanceof ValueType.Array) { + degree++; + type = ((ValueType.Array) type).getItemType(); + } + if (!(type instanceof ValueType.Object)) { + return originalType; + } + + var className = ((ValueType.Object) type).getClassName(); + if (!typeHelper.isJavaScriptClass(className)) { + return originalType; + } + + type = ValueType.object(JSWrapper.class.getName()); + while (degree-- > 0) { + type = ValueType.arrayOf(type); + } + return type; + } + private boolean processCast(CastInstruction cast, CallLocation location) { if (!(cast.getTargetType() instanceof ValueType.Object)) { + cast.setTargetType(processType(cast.getTargetType())); return false; } @@ -244,6 +383,9 @@ class JSClassProcessor { if (!typeHelper.isJavaScriptClass(targetClassName)) { return false; } + + cast.setValue(unwrapJavaToJs(cast, cast.getValue())); + ClassReader targetClass = classSource.get(targetClassName); if (targetClass.getAnnotations().get(JSFunctor.class.getName()) == null) { AssignInstruction assign = new AssignInstruction(); @@ -255,7 +397,7 @@ class JSClassProcessor { } Variable result = marshaller.unwrapFunctor(location, cast.getValue(), targetClass); - AssignInstruction assign = new AssignInstruction(); + var assign = new AssignInstruction(); assign.setLocation(location.getSourceLocation()); assign.setAssignee(result); assign.setReceiver(cast.getReceiver()); @@ -264,6 +406,75 @@ class JSClassProcessor { return true; } + private void processIsInstance(IsInstanceInstruction isInstance) { + if (!(isInstance.getType() instanceof ValueType.Object)) { + isInstance.setType(processType(isInstance.getType())); + return; + } + + String targetClassName = ((ValueType.Object) isInstance.getType()).getClassName(); + if (!typeHelper.isJavaScriptClass(targetClassName)) { + return; + } + + var type = types.typeOf(isInstance.getValue()); + if (type == JSType.JS) { + var replacement = new IntegerConstantInstruction(); + replacement.setConstant(1); + replacement.setReceiver(isInstance.getReceiver()); + replacement.setLocation(isInstance.getLocation()); + isInstance.replace(replacement); + return; + } + + var replacement = new InvokeInstruction(); + replacement.setType(InvocationType.SPECIAL); + replacement.setMethod(IS_JS); + replacement.setArguments(isInstance.getValue()); + replacement.setReceiver(isInstance.getReceiver()); + replacement.setLocation(isInstance.getLocation()); + isInstance.replace(replacement); + } + + private Variable wrapJsAsJava(Instruction instruction, Variable var, ValueType type) { + if (!(type instanceof ValueType.Object)) { + return var; + } + + var cls = ((ValueType.Object) type).getClassName(); + if (typeHelper.isJavaScriptClass(cls)) { + return var; + } + + var varType = types.typeOf(var); + if (varType != JSType.JS && varType != JSType.MIXED) { + return var; + } + var wrap = new InvokeInstruction(); + wrap.setType(InvocationType.SPECIAL); + wrap.setMethod(varType == JSType.JS ? WRAP : MAYBE_WRAP); + wrap.setArguments(var); + wrap.setReceiver(program.createVariable()); + wrap.setLocation(instruction.getLocation()); + instruction.insertPrevious(wrap); + return wrap.getReceiver(); + } + + private Variable unwrapJavaToJs(Instruction instruction, Variable var) { + var varType = types.typeOf(var); + if (varType != JSType.JAVA && varType != JSType.MIXED) { + return var; + } + var unwrap = new InvokeInstruction(); + unwrap.setType(InvocationType.SPECIAL); + unwrap.setMethod(varType == JSType.JAVA ? UNWRAP : MAYBE_UNWRAP); + unwrap.setArguments(var); + unwrap.setReceiver(program.createVariable()); + unwrap.setLocation(instruction.getLocation()); + instruction.insertPrevious(unwrap); + return unwrap.getReceiver(); + } + private boolean processInvocation(MethodReader method, CallLocation callLocation, InvokeInstruction invoke, MethodHolder methodToProcess) { if (method.getAnnotations().get(JSBody.class.getName()) != null) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java index f652e1dca..542ca2cdd 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java @@ -16,7 +16,6 @@ package org.teavm.jso.impl; import java.util.Arrays; -import java.util.function.Function; import org.teavm.jso.JSObject; import org.teavm.jso.core.JSArray; import org.teavm.jso.core.JSArrayReader; @@ -37,29 +36,29 @@ final class JSMethods { public static final MethodReference ARRAY_DATA = new MethodReference(JS.class, "arrayData", Object.class, JSObject.class); public static final MethodReference ARRAY_MAPPER = new MethodReference(JS.class, "arrayMapper", - Function.class, Function.class); + JS.WrapFunction.class, JS.WrapFunction.class); public static final MethodReference BOOLEAN_ARRAY_WRAPPER = new MethodReference(JS.class, "booleanArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference BYTE_ARRAY_WRAPPER = new MethodReference(JS.class, "byteArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference SHORT_ARRAY_WRAPPER = new MethodReference(JS.class, "shortArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference CHAR_ARRAY_WRAPPER = new MethodReference(JS.class, "charArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference INT_ARRAY_WRAPPER = new MethodReference(JS.class, "intArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference FLOAT_ARRAY_WRAPPER = new MethodReference(JS.class, "floatArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference DOUBLE_ARRAY_WRAPPER = new MethodReference(JS.class, "doubleArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference STRING_ARRAY_WRAPPER = new MethodReference(JS.class, "stringArrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference ARRAY_WRAPPER = new MethodReference(JS.class, "arrayWrapper", - Function.class); + JS.WrapFunction.class); public static final MethodReference ARRAY_UNMAPPER = new MethodReference(JS.class, "arrayUnmapper", - Class.class, Function.class, Function.class); + Class.class, JS.UnwrapFunction.class, JS.UnwrapFunction.class); public static final MethodReference UNMAP_ARRAY = new MethodReference(JS.class, "unmapArray", Class.class, - JSArrayReader.class, Function.class, Object[].class); + JSArrayReader.class, JS.UnwrapFunction.class, Object[].class); public static final MethodReference UNWRAP_BOOLEAN_ARRAY = new MethodReference(JS.class, "unwrapBooleanArray", JSArrayReader.class, boolean[].class); public static final MethodReference UNWRAP_BYTE_ARRAY = new MethodReference(JS.class, "unwrapByteArray", @@ -79,23 +78,23 @@ final class JSMethods { public static final MethodReference UNWRAP_ARRAY = new MethodReference(JS.class, "unwrapArray", Class.class, JSArrayReader.class, JSObject[].class); public static final MethodReference BOOLEAN_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "booleanArrayUnwrapper", Function.class); + "booleanArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference BYTE_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "byteArrayUnwrapper", Function.class); + "byteArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference SHORT_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "shortArrayUnwrapper", Function.class); + "shortArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference CHAR_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "charArrayUnwrapper", Function.class); + "charArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference INT_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "intArrayUnwrapper", Function.class); + "intArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference FLOAT_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "floatArrayUnwrapper", Function.class); + "floatArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference DOUBLE_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "doubleArrayUnwrapper", Function.class); + "doubleArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference STRING_ARRAY_UNWRAPPER = new MethodReference(JS.class, - "stringArrayUnwrapper", Function.class); + "stringArrayUnwrapper", JS.UnwrapFunction.class); public static final MethodReference ARRAY_UNWRAPPER = new MethodReference(JS.class, - "arrayUnwrapper", Class.class, Function.class); + "arrayUnwrapper", Class.class, JS.UnwrapFunction.class); public static final MethodReference DATA_TO_BYTE_ARRAY = new MethodReference(JS.class, "dataToByteArray", JSObject.class, byte[].class); 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 e9dbf0aa4..43dd9c21e 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 @@ -52,6 +52,18 @@ public class JSOPlugin implements TeaVMPlugin { jsHost.add(new MethodReference(JSExceptions.class, "getJSException", Throwable.class, JSObject.class), exceptionsGenerator); + var wrapperGenerator = new JSWrapperGenerator(); + jsHost.add(new MethodReference(JSWrapper.class, "directJavaToJs", Object.class, JSObject.class), + wrapperGenerator); + jsHost.add(new MethodReference(JSWrapper.class, "isJava", Object.class, boolean.class), + wrapperGenerator); + jsHost.add(new MethodReference(JSWrapper.class, "wrapperToJs", JSWrapper.class, JSObject.class), + wrapperGenerator); + jsHost.add(new MethodReference(JSWrapper.class, "jsToWrapper", JSObject.class, JSWrapper.class), + wrapperGenerator); + host.add(new MethodReference(JSWrapper.class, "jsToWrapper", JSObject.class, JSWrapper.class), + wrapperGenerator); + TeaVMPluginUtil.handleNatives(host, JS.class); } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSType.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSType.java new file mode 100644 index 000000000..5d8f916d8 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSType.java @@ -0,0 +1,57 @@ +/* + * 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. + */ +package org.teavm.jso.impl; + +import java.util.Objects; + +public class JSType { + private JSType() { + } + + public static final JSType NULL = new JSType(); + public static final JSType JS = new JSType(); + public static final JSType JAVA = new JSType(); + public static final JSType MIXED = new JSType(); + + public static final class ArrayType extends JSType { + public final JSType elementType; + + private ArrayType(JSType elementType) { + this.elementType = elementType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArrayType arrayType = (ArrayType) o; + return Objects.equals(elementType, arrayType.elementType); + } + + @Override + public int hashCode() { + return Objects.hash(elementType); + } + } + + public static JSType arrayOf(JSType elementType) { + return new ArrayType(elementType); + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeInference.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeInference.java new file mode 100644 index 000000000..651e0d849 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeInference.java @@ -0,0 +1,82 @@ +/* + * 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. + */ +package org.teavm.jso.impl; + +import org.teavm.model.MethodReference; +import org.teavm.model.Program; +import org.teavm.model.ValueType; +import org.teavm.model.analysis.BaseTypeInference; + +public class JSTypeInference extends BaseTypeInference { + private JSTypeHelper typeHelper; + + public JSTypeInference(JSTypeHelper typeHelper, Program program, MethodReference reference) { + super(program, reference); + this.typeHelper = typeHelper; + } + + @Override + protected JSType mapType(ValueType type) { + if (type instanceof ValueType.Object) { + var className = ((ValueType.Object) type).getClassName(); + if (typeHelper.isJavaScriptClass(className)) { + return JSType.JS; + } + } else if (type instanceof ValueType.Array) { + var elementType = mapType(((ValueType.Array) type).getItemType()); + return JSType.arrayOf(elementType); + } + return JSType.JAVA; + } + + @Override + protected JSType nullType() { + return JSType.NULL; + } + + @Override + protected JSType merge(JSType a, JSType b) { + if (a == JSType.NULL) { + return b; + } else if (b == JSType.NULL) { + return a; + } else if (a == b) { + return a; + } else if (a instanceof JSType.ArrayType) { + if (b instanceof JSType.ArrayType) { + var elementType = merge(((JSType.ArrayType) a).elementType, ((JSType.ArrayType) b).elementType); + return JSType.arrayOf(elementType); + } else if (b == JSType.JAVA) { + return JSType.JAVA; + } else { + return JSType.MIXED; + } + } else if (b instanceof JSType.ArrayType) { + if (a == JSType.JAVA) { + return JSType.JAVA; + } else { + return JSType.MIXED; + } + } else { + return JSType.MIXED; + } + } + + @Override + protected JSType elementType(JSType jsType) { + return jsType instanceof JSType.ArrayType ? ((JSType.ArrayType) jsType).elementType : JSType.MIXED; + } +} 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 index d03de63cf..be2136c81 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -17,7 +17,6 @@ package org.teavm.jso.impl; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import org.teavm.diagnostics.Diagnostics; import org.teavm.jso.JSFunctor; import org.teavm.jso.JSObject; @@ -153,7 +152,7 @@ class JSValueMarshaller { insn = new InvokeInstruction(); insn.setMethod(referenceCache.getCached(new MethodReference(JS.class.getName(), "map", - getWrappedType(type), ValueType.parse(Function.class), getWrapperType(type)))); + getWrappedType(type), ValueType.parse(JS.WrapFunction.class), getWrapperType(type)))); insn.setArguments(var, function); insn.setReceiver(result); insn.setType(InvocationType.SPECIAL); @@ -334,8 +333,8 @@ class JSValueMarshaller { if (insn.getMethod().parameterCount() == 2) { Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); + var clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(JSClassProcessor.processType(typeHelper, type)); clsInsn.setLocation(location.getSourceLocation()); clsInsn.setReceiver(cls); replacement.add(clsInsn); @@ -358,8 +357,8 @@ class JSValueMarshaller { if (insn.getMethod().parameterCount() == 1) { Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); + var clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(JSClassProcessor.processType(typeHelper, type)); clsInsn.setLocation(location.getSourceLocation()); clsInsn.setReceiver(cls); replacement.add(clsInsn); @@ -373,8 +372,8 @@ class JSValueMarshaller { type = ValueType.arrayOf(type); Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(type); + var clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(JSClassProcessor.processType(typeHelper, type)); clsInsn.setLocation(location.getSourceLocation()); clsInsn.setReceiver(cls); replacement.add(clsInsn); @@ -389,8 +388,8 @@ class JSValueMarshaller { } Variable cls = program.createVariable(); - ClassConstantInstruction clsInsn = new ClassConstantInstruction(); - clsInsn.setConstant(ValueType.arrayOf(type)); + var clsInsn = new ClassConstantInstruction(); + clsInsn.setConstant(JSClassProcessor.processType(typeHelper, ValueType.arrayOf(type))); clsInsn.setLocation(location.getSourceLocation()); clsInsn.setReceiver(cls); replacement.add(clsInsn); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java new file mode 100644 index 000000000..1622d3506 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java @@ -0,0 +1,192 @@ +/* + * 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. + */ +package org.teavm.jso.impl; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSBoolean; +import org.teavm.jso.core.JSFinalizationRegistry; +import org.teavm.jso.core.JSMap; +import org.teavm.jso.core.JSNumber; +import org.teavm.jso.core.JSObjects; +import org.teavm.jso.core.JSString; +import org.teavm.jso.core.JSWeakMap; +import org.teavm.jso.core.JSWeakRef; + +public final class JSWrapper { + private static final JSWeakMap hashCodes = JSWeakMap.create(); + private static final JSWeakMap> wrappers = JSWeakRef.isSupported() + ? JSWeakMap.create() : null; + private static final JSMap> stringWrappers = JSWeakRef.isSupported() + ? JSMap.create() : null; + private static final JSMap> numberWrappers = JSWeakRef.isSupported() + ? JSMap.create() : null; + private static final JSFinalizationRegistry stringFinalizationRegistry; + private static final JSFinalizationRegistry numberFinalizationRegistry; + private static int hashCodeGen; + + public final JSObject js; + + static { + stringFinalizationRegistry = stringWrappers != null + ? JSFinalizationRegistry.create(token -> stringWrappers.delete((JSString) token)) + : null; + numberFinalizationRegistry = numberWrappers != null + ? JSFinalizationRegistry.create(token -> numberWrappers.delete((JSNumber) token)) + : null; + } + + private JSWrapper(JSObject js) { + this.js = js; + } + + public static Object wrap(Object o) { + if (o == null) { + return null; + } + var js = directJavaToJs(o); + if (wrappers != null) { + var type = JSObjects.typeOf(js); + if (type.equals("object") || type.equals("function")) { + var existingRef = wrappers.get(js); + var existing = !JSObjects.isUndefined(existingRef) ? existingRef.deref() : JSObjects.undefined(); + if (JSObjects.isUndefined(existing)) { + var wrapper = new JSWrapper(js); + wrappers.set(js, JSWeakRef.create(wrapperToJs(wrapper))); + return wrapper; + } else { + return jsToWrapper(existing); + } + } else if (type.equals("string")) { + var jsString = (JSString) js; + var existingRef = stringWrappers.get(jsString); + var existing = !JSObjects.isUndefined(existingRef) ? existingRef.deref() : JSObjects.undefined(); + if (JSObjects.isUndefined(existing)) { + var wrapper = new JSWrapper(js); + var wrapperAsJs = wrapperToJs(wrapper); + stringWrappers.set(jsString, JSWeakRef.create(wrapperAsJs)); + stringFinalizationRegistry.register(wrapperAsJs, jsString); + return wrapper; + } else { + return jsToWrapper(existing); + } + } else if (type.equals("number")) { + var jsNumber = (JSNumber) js; + var existingRef = numberWrappers.get(jsNumber); + var existing = !JSObjects.isUndefined(existingRef) ? existingRef.deref() : JSObjects.undefined(); + if (JSObjects.isUndefined(existing)) { + var wrapper = new JSWrapper(js); + var wrapperAsJs = wrapperToJs(wrapper); + numberWrappers.set(jsNumber, JSWeakRef.create(wrapperAsJs)); + numberFinalizationRegistry.register(wrapperAsJs, jsNumber); + return wrapper; + } else { + return jsToWrapper(existing); + } + } + } + return new JSWrapper(js); + } + + public static Object maybeWrap(Object o) { + return o == null || isJava(o) ? o : wrap(o); + } + + @NoSideEffects + public static native JSObject directJavaToJs(Object obj); + + @NoSideEffects + private static native JSObject wrapperToJs(JSWrapper obj); + + @NoSideEffects + private static native JSWrapper jsToWrapper(JSObject obj); + + @NoSideEffects + public static native boolean isJava(Object obj); + + public static JSObject unwrap(Object o) { + if (o == null) { + return null; + } + return ((JSWrapper) o).js; + } + + public static JSObject maybeUnwrap(Object o) { + if (o == null) { + return null; + } + return isJava(o) ? unwrap(o) : directJavaToJs(o); + } + + public static boolean isJs(Object o) { + if (o == null) { + return false; + } + return !isJava(o) || o instanceof JSWrapper; + } + + @Override + public int hashCode() { + var type = JSObjects.typeOf(js); + if (type.equals("object") || type.equals("symbol") || type.equals("function")) { + var code = hashCodes.get(js); + if (JSObjects.isUndefined(code)) { + code = JSNumber.valueOf(++hashCodeGen); + hashCodes.set(js, code); + } + return code.intValue(); + } else if (type.equals("number")) { + return ((JSNumber) js).intValue(); + } else if (type.equals("bigint")) { + return bigintTruncate(js); + } else if (type.equals("string")) { + var s = (JSString) js; + var hashCode = 0; + for (var i = 0; i < s.getLength(); ++i) { + hashCode = 31 * hashCode + s.charCodeAt(i); + } + return hashCode; + } else if (type.equals("boolean")) { + return js == JSBoolean.valueOf(true) ? 1 : 0; + } else { + return 0; + } + } + + @JSBody(params = "bigint", script = "return BigInt.asIntN(bigint, 32);") + @NoSideEffects + private static native int bigintTruncate(JSObject bigint); + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof JSWrapper)) { + return false; + } + return js == ((JSWrapper) obj).js; + } + + @Override + public String toString() { + return JSObjects.toString(js); + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java new file mode 100644 index 000000000..2795331c6 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapperGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.io.IOException; +import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.InjectorContext; +import org.teavm.dependency.DependencyAgent; +import org.teavm.dependency.DependencyPlugin; +import org.teavm.dependency.MethodDependency; +import org.teavm.model.MethodReference; + +public class JSWrapperGenerator implements Injector, DependencyPlugin { + @Override + public void generate(InjectorContext context, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "directJavaToJs": + case "wrapperToJs": + case "jsToWrapper": + context.writeExpr(context.getArgument(0)); + break; + case "isJava": + context.writeExpr(context.getArgument(0)); + context.getWriter().append(" instanceof ").appendFunction("$rt_objcls").append("()"); + break; + } + } + + @Override + public void methodReached(DependencyAgent agent, MethodDependency method) { + if (method.getMethod().getName().equals("jsToWrapper")) { + method.getResult().propagate(agent.getType(JSWrapper.class.getName())); + } + } +} diff --git a/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java new file mode 100644 index 000000000..b3e87e859 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java @@ -0,0 +1,237 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSNumber; +import org.teavm.jso.core.JSObjects; +import org.teavm.jso.core.JSString; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; + +@RunWith(TeaVMTestRunner.class) +@WholeClassCompilation +@SkipJVM +public class JSWrapperTest { + private List list = new ArrayList<>(); + + private Object field1; + + @Test + public void simple() { + list.add(JSNumber.valueOf(23)); + list.add(JSString.valueOf("q")); + list.add(JSString.valueOf("q")); + list.add("q"); + list.add("w"); + + assertEquals("23", list.get(0).toString()); + assertEquals("q", list.get(1).toString()); + assertEquals(list.get(1), list.get(2)); + assertNotEquals(list.get(0), list.get(2)); + assertNotEquals(list.get(1), list.get(3)); + + assertEquals(23, ((JSNumber) list.get(0)).intValue()); + assertEquals("q", ((JSString) list.get(1)).stringValue()); + + try { + assertEquals("q", ((JSString) list.get(3)).stringValue()); + fail("Expected exception not thrown"); + } catch (ClassCastException e) { + // ok + } + } + + @Test + public void testHashCode() { + list.add(JSNumber.valueOf(23)); + + var o1 = JSObjects.create(); + list.add(o1); + list.add(o1); + + var o2 = JSObjects.create(); + list.add(o2); + + assertEquals(23, list.get(0).hashCode()); + assertEquals(list.get(1).hashCode(), list.get(2).hashCode()); + assertNotEquals(list.get(1).hashCode(), list.get(3).hashCode()); + } + + @Test + public void referentialEquality() { + var o1 = JSObjects.create(); + list.add(o1); + list.add(o1); + + var o2 = JSObjects.create(); + list.add(o2); + + assertSame(list.get(0), list.get(1)); + assertNotSame(list.get(0), list.get(2)); + + assertSame(JSString.valueOf("q"), JSString.valueOf("q")); + assertSame(JSNumber.valueOf(23), JSNumber.valueOf(23)); + } + + @Test + public void equality() { + var o1 = JSObjects.create(); + list.add(o1); + list.add(o1); + + var o2 = JSObjects.create(); + list.add(o2); + + list.add(JSString.valueOf("q")); + list.add(JSString.valueOf("q")); + list.add(JSString.valueOf("w")); + + assertEquals(list.get(0), list.get(1)); + assertNotEquals(list.get(0), list.get(2)); + assertEquals(list.get(3), list.get(4)); + assertNotEquals(list.get(3), list.get(5)); + } + + @Test + public void wrapNull() { + list.add(jsNull()); + assertEquals("null", Objects.toString(list.get(0))); + try { + list.get(0).toString(); + fail("Expected exception not thrown"); + } catch (NullPointerException e) { + // ok + } + } + + @Test + public void unwrapNull() { + list.add(null); + assertTrue(isNull((JSObject) list.get(0))); + } + + @Test + public void instanceOf() { + list.add(23); + list.add(JSNumber.valueOf(23)); + list.add(null); + list.add(jsNull()); + + assertTrue(list.get(0) instanceof Object); + assertTrue(list.get(0) instanceof Integer); + assertFalse(list.get(0) instanceof JSNumber); + + assertTrue(list.get(1) instanceof Object); + assertFalse(list.get(1) instanceof Integer); + assertTrue(list.get(1) instanceof JSNumber); + + assertFalse(list.get(2) instanceof Object); + assertFalse(list.get(2) instanceof Integer); + assertFalse(list.get(2) instanceof JSNumber); + + assertTrue(JSObjects.create() instanceof JSObject); + } + + @Test + public void mergeTypes() { + for (var i = 0; i < 2; ++i) { + var o = i == 0 ? JSNumber.valueOf(23) : 23; + list.add(o); + } + + assertTrue(list.get(0) instanceof JSNumber); + assertFalse(list.get(0) instanceof Integer); + + assertFalse(list.get(1) instanceof JSNumber); + assertTrue(list.get(1) instanceof Integer); + } + + @Test + public void testClass() { + list.add(23); + list.add(JSNumber.valueOf(23)); + + assertEquals(Integer.class, list.get(0).getClass()); + assertEquals(JSNumber.class, list.get(1).getClass()); + + assertEquals("java.lang.Integer", list.get(0).getClass().getName()); + assertEquals("org.teavm.jso.impl.JSWrapper", list.get(1).getClass().getName()); + } + + @Test + public void array() { + var array = new JSString[3]; + array[0] = JSString.valueOf("q"); + array[1] = JSString.valueOf("q"); + array[2] = JSString.valueOf("w"); + + assertEquals(JSString.valueOf("q"), array[0]); + assertEquals(JSString.valueOf("w"), array[2]); + assertEquals("q", array[0].stringValue()); + assertEquals("w", array[2].stringValue()); + assertEquals(array[0], array[1]); + assertEquals(JSString[].class, array.getClass()); + assertEquals(JSString.class, array.getClass().getComponentType()); + } + + @Test + public void objectArray() { + var array = new Object[4]; + array[0] = JSString.valueOf("q"); + array[1] = JSString.valueOf("q"); + array[2] = JSString.valueOf("w"); + array[3] = "q"; + + assertEquals(JSString.valueOf("q"), array[0]); + assertEquals(JSString.valueOf("w"), array[2]); + assertEquals("q", array[3]); + + assertEquals("q", ((JSString) array[0]).stringValue()); + assertEquals(array[0], array[1]); + assertNotEquals(array[0], array[2]); + assertNotEquals(array[0], array[3]); + } + + @Test + public void field() { + field1 = 23; + assertEquals("java.lang.Integer", field1.getClass().getName()); + + field1 = JSNumber.valueOf(23); + assertEquals("org.teavm.jso.impl.JSWrapper", field1.getClass().getName()); + } + + @JSBody(script = "return null;") + private static native JSObject jsNull(); + + @JSBody(params = "o", script = "return o === null;") + private static native boolean isNull(JSObject o); +}