From 22864c88f268ca0c72c1dbe7100ef1b381edaa9b Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 7 Aug 2023 16:51:02 +0200 Subject: [PATCH] js: support WeakReference --- .../java/lang/ref/TWeakReference.java | 2 +- .../backend/javascript/JavaScriptTarget.java | 25 ++++- .../ref/ReferenceQueueGenerator.java | 58 ++++++++++ .../ref/ReferenceQueueTransformer.java | 49 ++++++++ .../ref/WeakReferenceDependencyListener.java | 58 ++++++++++ .../ref/WeakReferenceGenerator.java | 92 +++++++++++++++ .../ref/WeakReferenceTransformer.java | 46 ++++++++ .../runtime/ref/RuntimeWeakRef.java | 20 ++++ .../java/lang/ref/WeakReferenceTest.java | 106 ++++++++++++++++++ 9 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueGenerator.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueTransformer.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceGenerator.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceTransformer.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/runtime/ref/RuntimeWeakRef.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/ref/TWeakReference.java b/classlib/src/main/java/org/teavm/classlib/java/lang/ref/TWeakReference.java index eada940f0..17fef5945 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/ref/TWeakReference.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/ref/TWeakReference.java @@ -19,7 +19,7 @@ public class TWeakReference extends TReference { private T value; public TWeakReference(T value) { - this.value = value; + this(value, null); } public TWeakReference(T value, @SuppressWarnings("unused") TReferenceQueue queue) { diff --git a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java index 99aa1180c..f87538235 100644 --- a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java +++ b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java @@ -21,6 +21,9 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -50,6 +53,11 @@ import org.teavm.backend.javascript.codegen.SourceWriter; import org.teavm.backend.javascript.codegen.SourceWriterBuilder; import org.teavm.backend.javascript.decompile.PreparedClass; import org.teavm.backend.javascript.decompile.PreparedMethod; +import org.teavm.backend.javascript.intrinsics.ref.ReferenceQueueGenerator; +import org.teavm.backend.javascript.intrinsics.ref.ReferenceQueueTransformer; +import org.teavm.backend.javascript.intrinsics.ref.WeakReferenceDependencyListener; +import org.teavm.backend.javascript.intrinsics.ref.WeakReferenceGenerator; +import org.teavm.backend.javascript.intrinsics.ref.WeakReferenceTransformer; import org.teavm.backend.javascript.rendering.Renderer; import org.teavm.backend.javascript.rendering.RenderingContext; import org.teavm.backend.javascript.rendering.RenderingUtil; @@ -143,7 +151,10 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { @Override public List getTransformers() { - return Collections.emptyList(); + return List.of( + new WeakReferenceTransformer(), + new ReferenceQueueTransformer() + ); } @Override @@ -154,6 +165,16 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { @Override public void setController(TeaVMTargetController controller) { this.controller = controller; + + var weakRefGenerator = new WeakReferenceGenerator(); + methodGenerators.put(new MethodReference(WeakReference.class, "", Object.class, + ReferenceQueue.class, void.class), weakRefGenerator); + methodGenerators.put(new MethodReference(WeakReference.class, "get", Object.class), weakRefGenerator); + methodGenerators.put(new MethodReference(WeakReference.class, "clear", void.class), weakRefGenerator); + + var refQueueGenerator = new ReferenceQueueGenerator(); + methodGenerators.put(new MethodReference(ReferenceQueue.class, "", void.class), refQueueGenerator); + methodGenerators.put(new MethodReference(ReferenceQueue.class, "poll", Reference.class), refQueueGenerator); } @Override @@ -321,6 +342,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { } } }); + + dependencyAnalyzer.addDependencyListener(new WeakReferenceDependencyListener()); } public static void includeStackTraceMethods(DependencyAnalyzer dependencyAnalyzer) { diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueGenerator.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueGenerator.java new file mode 100644 index 000000000..cd761a3de --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueGenerator.java @@ -0,0 +1,58 @@ +/* + * 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.backend.javascript.intrinsics.ref; + +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import org.teavm.backend.javascript.codegen.SourceWriter; +import org.teavm.backend.javascript.spi.Generator; +import org.teavm.backend.javascript.spi.GeneratorContext; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; + +public class ReferenceQueueGenerator implements Generator { + private static final FieldReference INNER_FIELD = new FieldReference(ReferenceQueue.class.getName(), "inner"); + private static final FieldReference REGISTRY_FIELD = new FieldReference(ReferenceQueue.class.getName(), + "registry"); + + @Override + public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "": + generateInitMethod(context, writer); + break; + case "poll": + generatePollMethod(context, writer); + break; + } + } + + private void generateInitMethod(GeneratorContext context, SourceWriter writer) throws IOException { + writer.append(context.getParameterName(0)).append(".").appendField(INNER_FIELD).ws().append("=") + .ws().append("[];").softNewLine(); + writer.append(context.getParameterName(0)).append(".").appendField(REGISTRY_FIELD).ws().append("=") + .ws().append("new $rt_globals.FinalizationRegistry(x").ws().append("=>").ws() + .append(context.getParameterName(0)).appendField(INNER_FIELD) + .append(".push(x));").softNewLine(); + } + + private void generatePollMethod(GeneratorContext context, SourceWriter writer) throws IOException { + writer.append("var value").ws().append("=").ws().append(context.getParameterName(0)) + .append(".").appendField(INNER_FIELD).append(".shift();").softNewLine(); + writer.append("return typeof value").ws().append("!==").ws().append("'undefined'").ws() + .append("?").ws().append("value").ws().append(":").ws().append("null;").softNewLine(); + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueTransformer.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueTransformer.java new file mode 100644 index 000000000..ded9373b3 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/ReferenceQueueTransformer.java @@ -0,0 +1,49 @@ +/* + * 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.backend.javascript.intrinsics.ref; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import org.teavm.model.ClassHolder; +import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.ElementModifier; +import org.teavm.model.FieldHolder; +import org.teavm.model.MethodDescriptor; +import org.teavm.model.ValueType; + +public class ReferenceQueueTransformer implements ClassHolderTransformer { + @Override + public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { + if (cls.getName().equals(ReferenceQueue.class.getName())) { + var field = new FieldHolder("inner"); + field.setType(ValueType.parse(Object.class)); + cls.addField(field); + + field = new FieldHolder("registry"); + field.setType(ValueType.parse(Object.class)); + cls.addField(field); + + var pollMethod = cls.getMethod(new MethodDescriptor("poll", Reference.class)); + pollMethod.setProgram(null); + pollMethod.getModifiers().add(ElementModifier.NATIVE); + + var constructor = cls.getMethod(new MethodDescriptor("", void.class)); + constructor.setProgram(null); + constructor.getModifiers().add(ElementModifier.NATIVE); + } + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java new file mode 100644 index 000000000..ffeb72d01 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceDependencyListener.java @@ -0,0 +1,58 @@ +/* + * 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.backend.javascript.intrinsics.ref; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import org.teavm.dependency.AbstractDependencyListener; +import org.teavm.dependency.DependencyAgent; +import org.teavm.dependency.MethodDependency; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; + +public class WeakReferenceDependencyListener extends AbstractDependencyListener { + @Override + public void methodReached(DependencyAgent agent, MethodDependency method) { + if (method.getMethod().getOwnerName().equals(WeakReference.class.getName())) { + referenceMethodReached(agent, method); + } else if (method.getMethod().getOwnerName().equals(ReferenceQueue.class.getName())) { + agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "inner")); + agent.linkField(new FieldReference(ReferenceQueue.class.getName(), "registry")); + } + } + + private void referenceMethodReached(DependencyAgent agent, MethodDependency method) { + switch (method.getMethod().getName()) { + case "": { + if (method.getParameterCount() == 2) { + var field = agent.linkField(new FieldReference(method.getMethod().getOwnerName(), "value")); + method.getVariable(1).connect(field.getValue()); + var pollResult = agent + .linkMethod(new MethodReference(ReferenceQueue.class, "poll", Reference.class)) + .getResult(); + method.getVariable(0).connect(pollResult); + } + break; + } + case "get": { + var field = agent.linkField(new FieldReference(method.getMethod().getOwnerName(), "value")); + field.getValue().connect(method.getResult()); + break; + } + } + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceGenerator.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceGenerator.java new file mode 100644 index 000000000..8560cd8c1 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceGenerator.java @@ -0,0 +1,92 @@ +/* + * 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.backend.javascript.intrinsics.ref; + +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import org.teavm.backend.javascript.codegen.SourceWriter; +import org.teavm.backend.javascript.spi.Generator; +import org.teavm.backend.javascript.spi.GeneratorContext; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; + +public class WeakReferenceGenerator implements Generator { + @Override + public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "": + generateConstructor(context, writer); + break; + case "get": + generateGet(context, writer); + break; + case "clear": + generateClear(context, writer); + break; + } + } + + private void generateConstructor(GeneratorContext context, SourceWriter writer) throws IOException { + writer.append("var supported").ws().append("=").ws(); + isSupported(writer).append(";").softNewLine(); + writer.append("var value").ws().append("=").ws().append("supported").ws() + .append("?").ws().append("new $rt_globals.WeakRef(") + .append(context.getParameterName(1)).append(")").ws(); + writer.append(":").ws().append(context.getParameterName(0)).append(";").softNewLine(); + + writer.append(context.getParameterName(0)).append(".") + .appendField(new FieldReference(WeakReference.class.getName(), "value")) + .ws().append("=").ws().append("value;").softNewLine(); + + writer.appendIf().append(context.getParameterName(2)).ws().append("!==").ws().append("null") + .ws().append("&&").ws().append("supported)") + .appendBlockStart(); + writer.append(context.getParameterName(2)).append(".") + .appendField(new FieldReference(ReferenceQueue.class.getName(), "registry")) + .append(".").append("register(").append(context.getParameterName(1)) + .append(",").ws().append("value);").softNewLine(); + writer.appendBlockEnd(); + } + + private void generateGet(GeneratorContext context, SourceWriter writer) throws IOException { + writer.append("var value").ws().append("=").ws().append(context.getParameterName(0)).append(".") + .appendField(new FieldReference(WeakReference.class.getName(), "value")) + .append(";").softNewLine(); + + writer.appendIf(); + isSupported(writer).append(")").appendBlockStart(); + writer.appendIf().append("value").ws().append("===").ws().append("null)") + .ws().append("return null;").softNewLine(); + writer.append("var result").ws().append("=").ws().append("value.deref();").softNewLine(); + writer.append("return typeof result").ws().append("!==").ws().append("'undefined'") + .ws().append("?").ws().append("result").ws().append(":").ws().append("null;").softNewLine(); + writer.appendElse(); + writer.append("return value;").softNewLine(); + writer.appendBlockEnd(); + } + + private void generateClear(GeneratorContext context, SourceWriter writer) throws IOException { + writer.append(context.getParameterName(0)).append(".") + .appendField(new FieldReference(WeakReference.class.getName(), "value")).ws(); + writer.append("=").ws().append("null;").softNewLine(); + } + + private SourceWriter isSupported(SourceWriter writer) throws IOException { + return writer.append("typeof ").append("$rt_globals.WeakRef").ws().append("!==").ws() + .append("'undefined'"); + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceTransformer.java b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceTransformer.java new file mode 100644 index 000000000..4d22cb0ca --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/intrinsics/ref/WeakReferenceTransformer.java @@ -0,0 +1,46 @@ +/* + * 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.backend.javascript.intrinsics.ref; + +import java.lang.ref.WeakReference; +import org.teavm.model.ClassHolder; +import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.ElementModifier; + +public class WeakReferenceTransformer implements ClassHolderTransformer { + @Override + public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { + if (!cls.getName().equals(WeakReference.class.getName())) { + return; + } + for (var method : cls.getMethods()) { + switch (method.getName()) { + case "": + if (method.parameterCount() == 2) { + method.setProgram(null); + method.getModifiers().add(ElementModifier.NATIVE); + } + break; + case "get": + case "clear": + method.setProgram(null); + method.getModifiers().add(ElementModifier.NATIVE); + break; + } + } + } +} diff --git a/core/src/main/java/org/teavm/backend/javascript/runtime/ref/RuntimeWeakRef.java b/core/src/main/java/org/teavm/backend/javascript/runtime/ref/RuntimeWeakRef.java new file mode 100644 index 000000000..86bf718cc --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/runtime/ref/RuntimeWeakRef.java @@ -0,0 +1,20 @@ +/* + * 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.backend.javascript.runtime.ref; + +public final class RuntimeWeakRef { + +} diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java new file mode 100644 index 000000000..13e8bb1e1 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/lang/ref/WeakReferenceTest.java @@ -0,0 +1,106 @@ +/* + * 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.classlib.java.lang.ref; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; + +@RunWith(TeaVMTestRunner.class) +@WholeClassCompilation +@SkipJVM +public class WeakReferenceTest { + private Node lastNode; + + @Test + @Ignore + public void deref() { + var ref = createAndTestRef(null); + + for (var i = 0; i < 100; ++i) { + lastNode = createNodes(18); + if (ref.get() == null) { + break; + } + } + assertNull(ref.get()); + } + + @Test + @Ignore + public void refQueue() { + var queue = new ReferenceQueue<>(); + var ref = createAndTestRef(queue); + var hasValue = false; + for (var i = 0; i < 100; ++i) { + lastNode = createNodes(18); + var polledRef = queue.poll(); + if (polledRef != null) { + hasValue = true; + assertNull(ref.get()); + break; + } else { + assertNotNull(ref.get()); + } + } + assertTrue(hasValue); + } + + private WeakReference createAndTestRef(ReferenceQueue queue) { + var obj = new byte[4 * 1024 * 1024]; + var ref = new WeakReference(obj, queue); + assertSame(obj, ref.get()); + return ref; + } + + @Test + public void clear() { + var obj = new Object(); + var ref = new WeakReference<>(obj); + assertSame(obj, ref.get()); + + ref.clear(); + assertNull(ref.get()); + } + + private Node createNodes(int depth) { + if (depth == 0) { + return null; + } else { + return new Node(createNodes(depth - 1), createNodes(depth - 1)); + } + } + + private class Node { + Node left; + Node right; + byte[] data = new byte[64]; + + Node(Node left, Node right) { + this.left = left; + this.right = right; + } + } +}