From 4c4677ecfe9354cbc6764910fc22b70576264718 Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 15 May 2025 20:24:51 +0200 Subject: [PATCH 1/4] Implement JSObject field accesses using @JS Every access generates a method call to a dynamically generated method that is (implicitly) annotated with @JS(...) to perform the access on the JS side. This replaces the more clunky ForeignCall-based implementation that emitted all the necessary conversion/coercion code in-line at every access. --- .../webimage/JSObjectAccessFeature.java | 117 ------------------ .../webimage/codegen/JSBodyFeature.java | 66 ++++++---- .../codegen/WebImageJSNodeLowerer.java | 85 +------------ .../webimage/js/JSObjectAccessMethod.java | 114 +++++++++++++++++ .../js/JSObjectAccessMethodHolder.java | 37 ++++++ .../js/JSObjectAccessMethodSupport.java | 109 ++++++++++++++++ .../svm/hosted/webimage/js/JSStubMethod.java | 33 +++-- .../webimage/js/JSSubstitutionProcessor.java | 14 ++- .../svm/webimage/api/JSObjectAccess.java | 100 --------------- 9 files changed, 334 insertions(+), 341 deletions(-) delete mode 100644 web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/JSObjectAccessFeature.java create mode 100644 web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethod.java create mode 100644 web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodHolder.java create mode 100644 web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java delete mode 100644 web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/api/JSObjectAccess.java diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/JSObjectAccessFeature.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/JSObjectAccessFeature.java deleted file mode 100644 index 4359585987e9..000000000000 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/JSObjectAccessFeature.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.oracle.svm.hosted.webimage; - -import static jdk.graal.compiler.core.common.spi.ForeignCallDescriptor.CallSideEffect.NO_SIDE_EFFECT; - -import java.util.EnumMap; -import java.util.Map; - -import org.graalvm.nativeimage.Platforms; - -import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; -import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.graal.meta.SubstrateForeignCallsProvider; -import com.oracle.svm.core.snippets.SnippetRuntime; -import com.oracle.svm.core.snippets.SnippetRuntime.SubstrateForeignCallDescriptor; -import com.oracle.svm.webimage.api.JSObjectAccess; -import com.oracle.svm.webimage.platform.WebImageJSPlatform; - -import jdk.vm.ci.meta.JavaKind; - -@AutomaticallyRegisteredFeature -@Platforms(WebImageJSPlatform.class) -public class JSObjectAccessFeature implements InternalFeature { - - public static final SubstrateForeignCallDescriptor GET_BOOLEAN = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getBoolean", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_BYTE = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getByte", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_SHORT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getShort", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_CHAR = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getChar", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_INT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getInt", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_FLOAT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getFloat", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_LONG = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getLong", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_DOUBLE = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getDouble", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor GET_OBJECT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "getObject", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_BOOLEAN = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putBoolean", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_BYTE = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putByte", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_SHORT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putShort", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_CHAR = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putChar", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_INT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putInt", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_FLOAT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putFloat", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_LONG = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putLong", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_DOUBLE = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putDouble", NO_SIDE_EFFECT); - public static final SubstrateForeignCallDescriptor PUT_OBJECT = SnippetRuntime.findForeignCall(JSObjectAccess.class, "putObject", NO_SIDE_EFFECT); - private static final SubstrateForeignCallDescriptor[] FOREIGN_CALLS = { - GET_BOOLEAN, - GET_BYTE, - GET_SHORT, - GET_CHAR, - GET_INT, - GET_FLOAT, - GET_LONG, - GET_DOUBLE, - GET_OBJECT, - - PUT_BOOLEAN, - PUT_BYTE, - PUT_SHORT, - PUT_CHAR, - PUT_INT, - PUT_FLOAT, - PUT_LONG, - PUT_DOUBLE, - PUT_OBJECT, - }; - public static final Map GETTERS = new EnumMap<>(JavaKind.class); - public static final Map SETTERS = new EnumMap<>(JavaKind.class); - - static { - GETTERS.put(JavaKind.Boolean, GET_BOOLEAN); - GETTERS.put(JavaKind.Byte, GET_BYTE); - GETTERS.put(JavaKind.Short, GET_SHORT); - GETTERS.put(JavaKind.Char, GET_CHAR); - GETTERS.put(JavaKind.Int, GET_INT); - GETTERS.put(JavaKind.Float, GET_FLOAT); - GETTERS.put(JavaKind.Long, GET_LONG); - GETTERS.put(JavaKind.Double, GET_DOUBLE); - GETTERS.put(JavaKind.Object, GET_OBJECT); - - SETTERS.put(JavaKind.Boolean, PUT_BOOLEAN); - SETTERS.put(JavaKind.Byte, PUT_BYTE); - SETTERS.put(JavaKind.Short, PUT_SHORT); - SETTERS.put(JavaKind.Char, PUT_CHAR); - SETTERS.put(JavaKind.Int, PUT_INT); - SETTERS.put(JavaKind.Float, PUT_FLOAT); - SETTERS.put(JavaKind.Long, PUT_LONG); - SETTERS.put(JavaKind.Double, PUT_DOUBLE); - SETTERS.put(JavaKind.Object, PUT_OBJECT); - } - - @Override - public void registerForeignCalls(SubstrateForeignCallsProvider foreignCalls) { - foreignCalls.register(FOREIGN_CALLS); - } -} diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java index 6954af36340a..a1f252a489aa 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java @@ -38,28 +38,29 @@ import com.oracle.graal.pointsto.BigBang; import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.meta.AnalysisType; import com.oracle.svm.core.ParsingReason; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.nodes.SubstrateMethodCallTargetNode; import com.oracle.svm.hosted.FeatureImpl; import com.oracle.svm.hosted.ImageClassLoader; -import com.oracle.svm.hosted.webimage.JSObjectAccessFeature; import com.oracle.svm.hosted.webimage.codegen.node.InterceptJSInvokeNode; import com.oracle.svm.hosted.webimage.codegen.oop.ClassWithMirrorLowerer; +import com.oracle.svm.hosted.webimage.js.JSObjectAccessMethodSupport; import com.oracle.svm.hosted.webimage.util.ReflectUtil; import com.oracle.svm.util.ReflectionUtil; import com.oracle.svm.webimage.api.Nothing; import com.oracle.svm.webimage.platform.WebImageJSPlatform; -import jdk.graal.compiler.core.common.spi.ForeignCallDescriptor; -import jdk.graal.compiler.core.common.type.Stamp; import jdk.graal.compiler.core.common.type.StampFactory; -import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.core.common.type.StampPair; +import jdk.graal.compiler.nodes.CallTargetNode; import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.InvokeWithExceptionNode; import jdk.graal.compiler.nodes.ValueNode; -import jdk.graal.compiler.nodes.extended.ForeignCallNode; import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; import jdk.graal.compiler.nodes.graphbuilderconf.InlineInvokePlugin; @@ -140,37 +141,48 @@ public boolean handleStoreField(GraphBuilderContext b, ValueNode object, Resolve return false; } - private void genJSObjectFieldAccess(GraphBuilderContext b, ValueNode object, ResolvedJavaField field, ValueNode valueForStore) { - ResolvedJavaType fieldType = field.getType().resolve(null); - JavaKind fieldKind = fieldType.getJavaKind(); - ConstantNode fieldNameNode = ConstantNode.forConstant(b.getConstantReflection().forString(field.getName()), b.getMetaAccess(), b.getGraph()); + /** + * Replaces an access to {@link JSObject} fields with a call to an + * {@link com.oracle.svm.hosted.webimage.js.JSObjectAccessMethod accessor method} that + * performs the access on the underlying JavaScript object. + * + * @param valueForStore If {@code null} is this a load. Otherwise, the value to be + * written into the field. + * @see JSObjectAccessMethodSupport + */ + private static void genJSObjectFieldAccess(GraphBuilderContext b, ValueNode object, ResolvedJavaField field, ValueNode valueForStore) { + AnalysisMetaAccess metaAccess = (AnalysisMetaAccess) b.getMetaAccess(); + AnalysisField analysisField = (AnalysisField) field; boolean isLoad = valueForStore == null; - ForeignCallDescriptor bridgeMethod; + AnalysisMethod accessMethod; ValueNode[] arguments; if (isLoad) { - // This is a load access. - bridgeMethod = JSObjectAccessFeature.GETTERS.get(fieldKind); - arguments = new ValueNode[]{object, fieldNameNode}; + accessMethod = JSObjectAccessMethodSupport.singleton().lookupLoadMethod(metaAccess, analysisField); + arguments = new ValueNode[]{object}; } else { - // This is a store access. - bridgeMethod = JSObjectAccessFeature.SETTERS.get(fieldKind); - arguments = new ValueNode[]{object, fieldNameNode, valueForStore}; + accessMethod = JSObjectAccessMethodSupport.singleton().lookupStoreMethod(metaAccess, analysisField); + arguments = new ValueNode[]{object, valueForStore}; } - ValueNode access = new ForeignCallNode(bridgeMethod, arguments); + + JavaKind returnKind = accessMethod.getSignature().getReturnType().getJavaKind(); + StampPair returnStamp = StampPair.createSingle(StampFactory.forKind(returnKind)); + + SubstrateMethodCallTargetNode callTarget = new SubstrateMethodCallTargetNode(CallTargetNode.InvokeKind.Static, accessMethod, arguments, returnStamp); + /* + * Just use a null exception edge. The GraphBuilderContext takes care of wiring it + * up correctly. The exception edge is needed because the access may produce an + * exception during conversions, especially loads, which can cause a + * ClassCastException if JavaScript code stored a value with the wrong type in the + * field. + */ + InvokeWithExceptionNode invoke = new InvokeWithExceptionNode(callTarget, null, b.bci()); + if (isLoad) { - b.addPush(fieldKind, access); - // A checkcast is necessary to guard against invalid assignments in JavaScript - // code. - if (fieldKind.isObject()) { - b.pop(fieldKind); - Stamp classStamp = StampFactory.forDeclaredType(null, b.getMetaAccess().lookupJavaType(Class.class), true).getTrustedStamp(); - ConstantNode classConstant = b.add(new ConstantNode(b.getConstantReflection().asObjectHub(fieldType.resolve(null)), classStamp)); - b.genCheckcastDynamic(access, classConstant); - } + b.addPush(returnKind, invoke); } else { - b.add(access); + b.add(invoke); } } }); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/WebImageJSNodeLowerer.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/WebImageJSNodeLowerer.java index 451e141a0724..94c81b919378 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/WebImageJSNodeLowerer.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/WebImageJSNodeLowerer.java @@ -25,10 +25,6 @@ package com.oracle.svm.hosted.webimage.codegen; -import static com.oracle.svm.hosted.webimage.codegen.Runtime.BoxIfNeeded; -import static com.oracle.svm.hosted.webimage.codegen.Runtime.JavaScriptToJava; -import static com.oracle.svm.hosted.webimage.codegen.Runtime.JavaToJavaScript; -import static com.oracle.svm.hosted.webimage.codegen.Runtime.UnboxIfNeeded; import static jdk.graal.compiler.core.common.calc.CanonicalCondition.BT; import java.lang.reflect.Method; @@ -36,7 +32,6 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -78,13 +73,10 @@ import com.oracle.svm.hosted.webimage.codegen.value.ResolvedVarLowerer; import com.oracle.svm.hosted.webimage.codegen.wrappers.JSEmitter; import com.oracle.svm.hosted.webimage.js.JSBody; -import com.oracle.svm.hosted.webimage.js.JSStaticMethodDefinition; import com.oracle.svm.hosted.webimage.snippets.JSSnippets; import com.oracle.svm.webimage.JSKeyword; -import com.oracle.svm.webimage.api.JSObjectAccess; import com.oracle.svm.webimage.functionintrinsics.ImplicitExceptions; import com.oracle.svm.webimage.functionintrinsics.JSCallNode; -import com.oracle.svm.webimage.functionintrinsics.JSConversion; import com.oracle.svm.webimage.functionintrinsics.JSFunctionDefinition; import com.oracle.svm.webimage.functionintrinsics.JSSystemFunction; import com.oracle.svm.webimage.type.TypeControl; @@ -245,14 +237,6 @@ public class WebImageJSNodeLowerer extends NodeLowerer { public WebImageJSNodeLowerer(JSCodeGenTool codeGenTool) { super(codeGenTool); this.codeGenTool = codeGenTool; - ResolvedJavaMethod coerceJavaScriptToJava = null; - try { - Method coerceMethod = JSConversion.class.getDeclaredMethod("coerceJavaScriptToJava", Object.class, Class.class); - coerceJavaScriptToJava = codeGenTool.getProviders().getMetaAccess().lookupJavaMethod(coerceMethod); - } catch (NoSuchMethodException ex) { - VMError.shouldNotReachHere(ex); - } - this.coerceJavaScriptToJavaMethodDef = new JSStaticMethodDefinition(coerceJavaScriptToJava); } // ============================================================================ @@ -260,8 +244,6 @@ public WebImageJSNodeLowerer(JSCodeGenTool codeGenTool) { public static final boolean INT_PALADIN = true; - private final JSStaticMethodDefinition coerceJavaScriptToJavaMethodDef; - /** Denotes nodes that do not need to be lowered. */ public static final Set> IGNORED_NODE_TYPES = new HashSet<>(Arrays.asList( MergeNode.class, @@ -1510,72 +1492,7 @@ protected void lower(ForeignCall node) { SnippetRuntime.SubstrateForeignCallDescriptor substrateDescriptor = (SnippetRuntime.SubstrateForeignCallDescriptor) descriptor; ResolvedJavaMethod method = substrateDescriptor.findMethod(codeGenTool.getProviders().getMetaAccess()); - if (substrateDescriptor.getDeclaringClass() == JSObjectAccess.class) { - assert node.getArguments().size() == 2 || node.getArguments().size() == 3 : "Only expect 2 or 3 arguments, found = " + node.getArguments().size(); - assert node.getArguments().get(1) instanceof ConstantNode : "Field name should be a constant"; - - ValueNode receiverNode = node.getArguments().get(0); - JavaConstant fieldConst = (JavaConstant) ((ConstantNode) node.getArguments().get(1)).getValue(); - String field = Objects.requireNonNull(codeGenTool.getProviders().getSnippetReflection().asObject(String.class, fieldConst)); - IEmitter receiverJS = tool -> JavaToJavaScript.emitCall(tool, Emitter.of(receiverNode)); - IEmitter receiverSelect = tool -> tool.genPropertyAccess(receiverJS, Emitter.of(field)); - - /* - * Intrinsify JSObjectAccess calls for performance & simplify the job of Closure - * compiler. - * - * Otherwise, we will have to generate extern files to prevent Closure compiler from - * renaming fields of imported classes. Those fields are accessed via field names in - * JSObjectAccess in the form of `o['f']`. - * - * This also ensures that the user can use Closure compiler to compress the - * generated JS file in their own workflow without worrying about extern files and - * renaming. - */ - if (node.getArguments().size() == 2) { - // javaScriptToJava(javaToJavaScript(arg0).arg1) - IEmitter javaValueBeforeCoerce = tool -> JavaScriptToJava.emitCall(tool, receiverSelect); - - // coerce result - ResolvedJavaMethod target = substrateDescriptor.findMethod(codeGenTool.getProviders().getMetaAccess()); - JavaKind returnKind = target.getSignature().getReturnKind(); - Class clazz = switch (returnKind) { - case Boolean -> Boolean.class; - case Byte -> Byte.class; - case Short -> Short.class; - case Char -> Character.class; - case Int -> Integer.class; - case Float -> Float.class; - case Long -> Long.class; - case Double -> Double.class; - case Object -> Object.class; - default -> throw VMError.shouldNotReachHere("Unexpected return type void"); - }; - - TypeControl typeControl = codeGenTool.getJSProviders().typeControl(); - String hubName = typeControl.requestHubName(codeGenTool.getProviders().getMetaAccess().lookupJavaType(clazz)); - IEmitter coercedJavaValue = tool -> coerceJavaScriptToJavaMethodDef.emitCall(tool, javaValueBeforeCoerce, Emitter.of(hubName)); - - /* - * Unbox return value. - * - * Note that coercion and unboxing are two different steps: The former addresses - * the problem of interoperability between Java and JavaScript, while the latter - * adapts boxed and primitive types needed for Java semantics. - */ - UnboxIfNeeded.emitCall(codeGenTool, coercedJavaValue, Emitter.of(returnKind.ordinal())); - } else if (node.getArguments().size() == 3) { - ValueNode rhsNode = node.getArguments().last(); - - // Box of the 3rd argument if necessary - IEmitter rhs = tool -> BoxIfNeeded.emitCall(tool, Emitter.of(rhsNode), Emitter.of(rhsNode.getStackKind().ordinal())); - - // javaToJavaScript(arg0).arg1 = javaToJavaScript(arg2) - receiverSelect.lower(codeGenTool); - codeGenTool.genAssignment(); - JavaToJavaScript.emitCall(codeGenTool, rhs); - } - } else if (method.isStatic()) { + if (method.isStatic()) { codeGenTool.genStaticCall(method, Emitter.of(node.getArguments())); } else { throw GraalError.unimplemented("ForeignCallNode for non-static methods not implemented: " + node); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethod.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethod.java new file mode 100644 index 000000000000..4db6b5626dfb --- /dev/null +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethod.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.hosted.webimage.js; + +import java.lang.reflect.Modifier; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.HostedProviders; +import com.oracle.svm.hosted.code.NonBytecodeMethod; + +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.debug.GraalError; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; + +/** + * Stub method for accesses to {@link org.graalvm.webimage.api.JSObject} fields. + *

+ * Such accesses are delegated to JavaScript code that modifies the JS mirror object. + *

+ * This method is substituted by {@link JSSubstitutionProcessor} and treated as if it was implicitly + * annotated with {@link org.graalvm.webimage.api.JS @JS}. The jsbody contents are defined in + * {@link #buildJSCode()}. + * + * @see JSObjectAccessMethodSupport + */ +public class JSObjectAccessMethod extends NonBytecodeMethod { + private final ResolvedJavaField targetField; + private final boolean isLoad; + /** + * Computed lazily. + */ + private JSBody.JSCode jsCode = null; + + public JSObjectAccessMethod(String name, ResolvedJavaField targetField, boolean isLoad, ResolvedJavaType declaringClass, Signature signature, ConstantPool constantPool) { + super(name, true, declaringClass, signature, constantPool); + this.targetField = targetField; + this.isLoad = isLoad; + } + + /** + * Constructs the {@link org.graalvm.webimage.api.JS @JS} body code for this method. + *

+ * For loads: + * + *

{@code
+     * return object.;
+     * }
+ * + * For stores: + * + *
{@code
+     * object. = value;
+     * }
+ */ + private JSBody.JSCode buildJSCode() { + String fieldAccess = "object." + targetField.getName(); + String body; + if (isLoad) { + body = "return " + fieldAccess + ";"; + } else { + body = fieldAccess + " = value;"; + } + + return new JSBody.JSCode(new String[]{"object", "value"}, body); + } + + public JSBody.JSCode getJSCode() { + if (jsCode == null) { + this.jsCode = buildJSCode(); + } + return jsCode; + } + + public boolean isLoad() { + return isLoad; + } + + @Override + public StructuredGraph buildGraph(DebugContext debug, AnalysisMethod method, HostedProviders providers, Purpose purpose) { + throw GraalError.shouldNotReachHere("This should never be called. This is a 'native' method that is substituted"); + } + + @Override + public int getModifiers() { + return Modifier.NATIVE | super.getModifiers(); + } +} diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodHolder.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodHolder.java new file mode 100644 index 000000000000..2b421e3dfc89 --- /dev/null +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodHolder.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.hosted.webimage.js; + +/** + * Holder class for generated {@link org.graalvm.webimage.api.JSObject} access methods. + * + * @see JSObjectAccessMethodSupport + * @see JSObjectAccessMethod + */ +public final class JSObjectAccessMethodHolder { + private JSObjectAccessMethodHolder() { + } +} diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java new file mode 100644 index 000000000000..ff09c27abe00 --- /dev/null +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.hosted.webimage.js; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.webimage.api.JSObject; + +import com.oracle.graal.pointsto.infrastructure.ResolvedSignature; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; +import com.oracle.svm.core.util.UserError; + +import jdk.graal.compiler.debug.GraalError; +import jdk.graal.compiler.util.Digest; +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; + +/** + * Dynamically generates methods for accesses to {@link JSObject} fields. + * + * @see JSObjectAccessMethod + */ +@AutomaticallyRegisteredImageSingleton +public class JSObjectAccessMethodSupport { + public static JSObjectAccessMethodSupport singleton() { + return ImageSingletons.lookup(JSObjectAccessMethodSupport.class); + } + + record AccessorDescription(AnalysisField field, boolean isLoad) { + } + + private final Map accessMethods = new ConcurrentHashMap<>(); + + public AnalysisMethod lookupLoadMethod(AnalysisMetaAccess aMetaAccess, AnalysisField field) { + return lookup(aMetaAccess, field, true); + } + + public AnalysisMethod lookupStoreMethod(AnalysisMetaAccess aMetaAccess, AnalysisField field) { + return lookup(aMetaAccess, field, false); + } + + private AnalysisMethod lookup(AnalysisMetaAccess aMetaAccess, AnalysisField field, boolean isLoad) { + GraalError.guarantee(JSObject.class.isAssignableFrom(field.getDeclaringClass().getJavaClass()), "Field must be in JSObject class: %s", field); + GraalError.guarantee(!field.isStatic(), "Field must not be static: %s", field); + UserError.guarantee(!field.isFinal(), "Instance fields in subclasses of %s must not be final: %s", JSObject.class.getSimpleName(), field.format("%H.%n")); + JSObjectAccessMethod accessMethod = accessMethods.computeIfAbsent( + new AccessorDescription(field, isLoad), + key -> createAccessMethod(aMetaAccess, field, isLoad)); + + return aMetaAccess.getUniverse().lookup(accessMethod); + } + + private static JSObjectAccessMethod createAccessMethod(AnalysisMetaAccess metaAccess, AnalysisField field, boolean isLoad) { + ResolvedJavaType unwrappedFieldDeclaringClass = field.getDeclaringClass().getWrapped(); + ResolvedJavaType unwrappedFieldType = field.getType().getWrapped(); + + Signature signature; + if (isLoad) { + signature = ResolvedSignature.fromArray(new ResolvedJavaType[]{unwrappedFieldDeclaringClass}, unwrappedFieldType); + } else { + signature = ResolvedSignature.fromArray(new ResolvedJavaType[]{unwrappedFieldDeclaringClass, unwrappedFieldType}, metaAccess.lookupJavaType(void.class).getWrapped()); + } + + // Just use some constant pool. Should not actually be meaningfully accessed + ConstantPool pool = unwrappedFieldDeclaringClass.getDeclaredConstructors(false)[0].getConstantPool(); + + String name = accessMethodName(field, isLoad); + ResolvedJavaType unwrappedDeclaringClass = metaAccess.lookupJavaType(JSObjectAccessMethodHolder.class).getWrapped(); + return new JSObjectAccessMethod(name, field, isLoad, unwrappedDeclaringClass, signature, pool); + } + + /** + * The method the {@link JSObjectAccessMethod} gets. The only requirement here is that all + * method names are unique. + */ + private static String accessMethodName(AnalysisField field, boolean isLoad) { + String sb = field.getDeclaringClass().toClassName() + "." + field.getName(); + return (isLoad ? "get" : "set") + "_" + field.getDeclaringClass().getUnqualifiedName() + "_" + field.getName() + "_" + Digest.digest(sb); + } +} diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSStubMethod.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSStubMethod.java index 7cc3f87165ed..659003367113 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSStubMethod.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSStubMethod.java @@ -26,6 +26,7 @@ package com.oracle.svm.hosted.webimage.js; import java.util.List; +import java.util.Objects; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.webimage.api.JS; @@ -103,8 +104,29 @@ private static Stamp returnStamp(ResolvedSignature sig) { @Override public StructuredGraph buildGraph(DebugContext debug, AnalysisMethod method, HostedProviders providers, Purpose purpose) { - boolean rawCall = AnnotationAccess.isAnnotationPresent(method, JSRawCall.class); - boolean coercion = AnnotationAccess.isAnnotationPresent(method, JS.Coerce.class); + boolean rawCall; + boolean coercion; + JSBody.JSCode jsCode; + if (getOriginal() instanceof JSObjectAccessMethod jsObjectAccessMethod) { + rawCall = false; + /* + * Only the load return value should be coerced. For stores, only regular conversion + * should be applied. + * + * TODO GR-65036 We should coerce in both directions + */ + coercion = jsObjectAccessMethod.isLoad(); + jsCode = jsObjectAccessMethod.getJSCode(); + } else { + rawCall = AnnotationAccess.isAnnotationPresent(method, JSRawCall.class); + coercion = AnnotationAccess.isAnnotationPresent(method, JS.Coerce.class); + JS js = Objects.requireNonNull(AnnotationAccess.getAnnotation(method, JS.class)); + jsCode = new JSBody.JSCode(js, method); + } + return buildGraph(debug, method, providers, purpose, jsCode, coercion, rawCall); + } + + private static StructuredGraph buildGraph(DebugContext debug, AnalysisMethod method, HostedProviders providers, Purpose purpose, JSBody.JSCode jsCode, boolean coercion, boolean rawCall) { if (rawCall && coercion) { throw JVMCIError.shouldNotReachHere("Cannot use JS.Coerce and JSRawCall annotation simultaneously: " + method.format("%H.%n")); } @@ -135,13 +157,6 @@ public StructuredGraph buildGraph(DebugContext debug, AnalysisMethod method, Hos // Step 2: insert the JS body representation node. int bci = kit.bci(); - JSBody.JSCode jsCode; - JS js = AnnotationAccess.getAnnotation(method, JS.class); - if (js != null) { - jsCode = new JSBody.JSCode(js, method); - } else { - throw JVMCIError.shouldNotReachHere(); - } Stamp jsBodyStamp = rawCall ? returnStamp(method.getSignature()) : StampFactory.object(); AnalysisMethod exceptionHandler; diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSSubstitutionProcessor.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSSubstitutionProcessor.java index 48019b8aa0b2..5e55ea179df6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSSubstitutionProcessor.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSSubstitutionProcessor.java @@ -32,10 +32,17 @@ import org.graalvm.webimage.api.JS; import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor; +import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.svm.hosted.annotation.CustomSubstitutionMethod; import jdk.vm.ci.meta.ResolvedJavaMethod; +/** + * Methods annotated with {@link JS @JS} are substituted with {@link JSStubMethod}. + *

+ * The {@link JSObjectAccessMethod} is implicitly annotated with {@link JS @JS} and is also + * substituted here. + */ public class JSSubstitutionProcessor extends SubstitutionProcessor { private final Map callWrappers = new ConcurrentHashMap<>(); @@ -50,9 +57,8 @@ public ResolvedJavaMethod lookup(ResolvedJavaMethod method) { } private static boolean isJSStubMethod(ResolvedJavaMethod method) { - if (AnnotationAccess.isAnnotationPresent(method, JS.class)) { - return true; - } - return false; + // If AnalysisMethods appeared here, they would first need to be unwrapped + assert !(method instanceof AnalysisMethod) : method; + return method instanceof JSObjectAccessMethod || AnnotationAccess.isAnnotationPresent(method, JS.class); } } diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/api/JSObjectAccess.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/api/JSObjectAccess.java deleted file mode 100644 index e55fa1d9eff3..000000000000 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/api/JSObjectAccess.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package com.oracle.svm.webimage.api; - -import org.graalvm.webimage.api.JSObject; - -import com.oracle.svm.core.snippets.SubstrateForeignCallTarget; - -/** - * Methods for field accesses on subtypes of {@link JSObject}. - * - * The following methods are intrinsified in the compiler. The intrinsification is implemented in - * WebImageJSNodeLowerer during the lowering of foreign calls. - * - *

Why intrinsification?

- * - * The main reason is to make generated code Closure-friendly. Previously, the code uses a['prop'] - * to access object fields, which Closure cannot reason about and will result in errors without - * extern files. It's unreasonable to have extern files for code included with @JS.Code - * or @JS.Code.Include. That means users cannot generate a non-Closure image and use their own - * Closure-workflow to compress it even if the image contains all the JS code. The intrinsification - * solves the problem. - */ -public class JSObjectAccess { - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native boolean getBoolean(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native byte getByte(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native short getShort(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native char getChar(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native int getInt(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native float getFloat(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native long getLong(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native double getDouble(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native Object getObject(JSObject self, String field); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putBoolean(JSObject self, String field, boolean value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putByte(JSObject self, String field, byte value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putShort(JSObject self, String field, short value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putChar(JSObject self, String field, char value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putInt(JSObject self, String field, int value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putFloat(JSObject self, String field, float value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putLong(JSObject self, String field, long value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putDouble(JSObject self, String field, double value); - - @SubstrateForeignCallTarget(stubCallingConvention = false) - public static native void putObject(JSObject self, String field, Object value); -} From 4b52ca4e214b879303e30f1bda67a6c2a616bcdb Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 15 May 2025 20:47:37 +0200 Subject: [PATCH 2/4] Stop using final fields in JSObject It is not allowed by the API docs and now also forbidden in practice --- .../com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java index 5d14c94ec383..752d625a8489 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java @@ -162,6 +162,7 @@ private static void heapGeneratedObjects() { inspectHeapOnlyPoint("dummy"); } + // TODO GR-65036 This will have to be updated since the x and y values will be JS primitives @JS("const { x, y } = dummy.$vm.exports.com.oracle.svm.webimage.jtt.api.HeapOnlyPointStatics.getPoint(); console.log(x.$as('number')); console.log(y.$as('number'));") private static native void inspectHeapOnlyPoint(String dummy); } @@ -212,7 +213,7 @@ public String toString() { @JS.Export class SubclassOfImportedJSObject extends ImportedJSObject { - protected final int index; + protected int index; SubclassOfImportedJSObject(String importDeclaration, int index) { super(importDeclaration); From dabeec4661f9afb49945e74be6b8025e52a9ba8b Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 15 May 2025 21:28:50 +0200 Subject: [PATCH 3/4] Rework JSObject docs --- .../org/graalvm/webimage/api/JSObject.java | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/web-image/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java b/web-image/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java index da57c8c3ee20..f4c033fac7a0 100644 --- a/web-image/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java +++ b/web-image/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java @@ -30,7 +30,7 @@ * is that a JavaScript object is not normally an instance of any Java class, and it therefore * cannot be represented as a data-type in Java programs. When the JavaScript code (invoked by the * method annotated with the {@link JS} annotation) returns a JavaScript object, that object gets - * wrapped into JSObject instance. The JSObject allows the Java code to + * wrapped into a JSObject instance. The JSObject allows the Java code to * access the fields of the underlying JavaScript object using the get and * set methods. * @@ -123,30 +123,27 @@ * * Directly exposing a Java object to JavaScript code means that the JavaScript code is able to * manipulate the data within the object (e.g. mutate fields, add new fields, or redefine existing - * fields), which is not allowed by default for regular Java classes translated by Web Image. - * Extending {@link JSObject} furthermore allows the JavaScript code to instantiate objects of the - * {@link JSObject} subclass. One of the use-cases for these functionalities are JavaScript - * frameworks that redefine properties of JavaScript objects with custom getters and setters, with - * the goal of enabling data-binding or reactive updates. + * fields), which is not allowed by default for regular Java classes. Extending {@link JSObject} + * furthermore allows the JavaScript code to instantiate objects of the {@link JSObject} subclass. + * One of the use-cases for these functionalities are JavaScript frameworks that redefine properties + * of JavaScript objects with custom getters and setters, with the goal of enabling data-binding or + * reactive updates. * * In a subclass of {@link JSObject}, every JavaScript property directly corresponds to the Java * field of the same name. Consequently, all these properties point to native JavaScript values - * rather than Java values, so Web Image generates bridge methods that are called instead of - * property accesses and that convert native JavaScript values to their Java counterparts. The - * conversion rules are the same as in a {@link JS}-annotated method. Furthermore, note that - * JavaScript code can violate the Java type-safety by storing into some property a value that is - * not compatible with the corresponding Java field. For this reason, the bridge methods also - * generate check-casts on every access: if the JavaScript property that corresponds to the Java - * field does not contain a compatible value, a {@link ClassCastException} is thrown. - * - * There are several restrictions that Web Image imposes on {@link JSObject} subclasses: + * rather than Java values, so bridge methods are generated that are called for each property access + * and that convert native JavaScript values to their Java counterparts. The conversion rules are + * the same as in a {@link JS}-annotated method. Furthermore, note that JavaScript code can violate + * the Java type-safety by storing into some property a value that is not compatible with the + * corresponding Java field. For this reason, the bridge methods also generate check-casts on every + * access: if the JavaScript property that corresponds to the Java field does not contain a + * compatible value, a {@link ClassCastException} is thrown. + * + * There are several restrictions imposed on {@link JSObject} subclasses: *
    - *
  • Only public and protected fields are exposed to JavaScript. This restriction ensures - * encapsulation, and also ensures that users cannot introduce two private fields of the same name - * within the same inheritance lineage.
  • - *
  • Subclasses of this class are only allowed to have non-final fields. This restriction ensures - * that JavaScript code cannot inadvertently change the property that corresponds to a final - * field.
  • + *
  • Only public and protected fields are allowed to ensure encapsulation.
  • + *
  • Instance fields must not be {@code final}. This restriction ensures that JavaScript code + * cannot inadvertently change the property that corresponds to a final field.
  • *
* * Example: consider the following JSObject subclass: @@ -172,8 +169,8 @@ *
  * class Point {
  *     constructor(x, y){
- *     this.x=x;
- *     this.y=y;
+ *         this.x=x;
+ *         this.y=y;
  *     }
  *
  *     absolute() {
@@ -225,8 +222,7 @@
  * 
* * A {@link Class} object that represents {@link JSObject} can also be passed to JavaScript code. - * The {@link Class} object is converted to its JavaScript representation, which is the JavaScript - * class constructor. The JavaScript class constructor can be used inside a {@code new} expression + * The {@link Class} object is wrapped in a proxy, which can be used inside a {@code new} expression * to instantiate the object of the corresponding class from JavaScript. * * Example: the following code creates a {@code Point} object in JavaScript: From ae89a8adfd1d26768585687b0be9d33cb8e9622c Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 15 May 2025 21:44:52 +0200 Subject: [PATCH 4/4] Disallow private and package-private fields in JSObject subclasses This was the original intent. This way, not state is split between the JS and Java mirror. It is also the less confusing behavior, allowing private, fields, but not exposing them to JS, could be unexpected --- .../webimage/codegen/JSBodyFeature.java | 4 +-- .../webimage/codegen/oop/ClassLowerer.java | 5 ++- .../codegen/oop/ClassWithMirrorLowerer.java | 31 +++++++++++++++++-- .../js/JSObjectAccessMethodSupport.java | 2 ++ .../jtt/api/JSObjectSubclassTest.java | 4 +++ .../webimage/jtt/api/JavaDocExamplesTest.java | 2 +- 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java index a1f252a489aa..04ba9b557db8 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/JSBodyFeature.java @@ -125,7 +125,7 @@ private boolean canBeJavaScriptCall(AnalysisMethod method) { @Override public boolean handleLoadField(GraphBuilderContext b, ValueNode object, ResolvedJavaField field) { - if (ClassWithMirrorLowerer.isJSObjectSubtype(((AnalysisType) field.getDeclaringClass()).getJavaClass())) { + if (ClassWithMirrorLowerer.isFieldRepresentedInJavaScript(field)) { genJSObjectFieldAccess(b, object, field, null); return true; } @@ -134,7 +134,7 @@ public boolean handleLoadField(GraphBuilderContext b, ValueNode object, Resolved @Override public boolean handleStoreField(GraphBuilderContext b, ValueNode object, ResolvedJavaField field, ValueNode value) { - if (ClassWithMirrorLowerer.isJSObjectSubtype(((AnalysisType) field.getDeclaringClass()).getJavaClass())) { + if (ClassWithMirrorLowerer.isFieldRepresentedInJavaScript(field)) { genJSObjectFieldAccess(b, object, field, value); return true; } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassLowerer.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassLowerer.java index 36b0afe83dab..ddacc24e6734 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassLowerer.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassLowerer.java @@ -36,7 +36,6 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.graalvm.webimage.api.JSObject; import org.graalvm.webimage.api.JSResource; import com.oracle.svm.hosted.meta.HostedField; @@ -133,8 +132,8 @@ private void lowerClassHeader() { * we only collect the instance fields declared by this type, inherited fields are resolved * via a super call */ - if (!JSObject.class.isAssignableFrom(type.getJavaClass())) { - for (HostedField field : type.getInstanceFields(false)) { + for (HostedField field : type.getInstanceFields(false)) { + if (!ClassWithMirrorLowerer.isFieldRepresentedInJavaScript(field)) { genFieldInitialization(codeGenTool, masm, field); } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassWithMirrorLowerer.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassWithMirrorLowerer.java index 5622c54e3832..97949e92a508 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassWithMirrorLowerer.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/oop/ClassWithMirrorLowerer.java @@ -26,7 +26,9 @@ import static com.oracle.svm.hosted.webimage.codegen.RuntimeConstants.RUNTIME_SYMBOL; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -35,6 +37,7 @@ import org.graalvm.webimage.api.JS; import org.graalvm.webimage.api.JSObject; +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; import com.oracle.svm.hosted.meta.HostedClass; import com.oracle.svm.hosted.meta.HostedField; @@ -55,6 +58,7 @@ import jdk.graal.compiler.hightiercodegen.CodeBuffer; import jdk.graal.compiler.nodes.StructuredGraph; import jdk.graal.compiler.options.OptionValues; +import jdk.vm.ci.meta.ResolvedJavaField; import jdk.vm.ci.meta.ResolvedJavaMethod; import jdk.vm.ci.meta.Signature; @@ -189,6 +193,17 @@ public ClassWithMirrorLowerer(OptionValues options, DebugContext debug, JSCodeGe this.externClassDescriptor = null; } + /** + * Public and protected fields in {@link JSObject} subclasses are represented in the JavaScript + * mirror. + *

+ * Accesses to those fields must be intercepted. The fields also do not appear in the Java + * object. + */ + public static boolean isFieldRepresentedInJavaScript(ResolvedJavaField field) { + return !field.isStatic() && isJSObjectSubtype(OriginalClassProvider.getJavaClass(field.getDeclaringClass())); + } + /** * An imported Javascript class needs extern file for Closure compiler if the source code is not * included. @@ -205,6 +220,18 @@ public static boolean isJSObjectSubtype(Class cls) { return JSObject.class.isAssignableFrom(cls); } + public static List getOwnFieldOnJSSide(HostedType type) { + List fields = new ArrayList<>(); + + for (HostedField instanceField : type.getInstanceFields(false)) { + if (isFieldRepresentedInJavaScript(instanceField)) { + fields.add(instanceField); + } + } + + return fields; + } + @Override public void lower(WebImageTypeControl typeControl) { if (needExternDeclaration()) { @@ -228,7 +255,7 @@ protected void lowerPreamble(JSCodeGenTool tool) { if (needExternDeclaration()) { // We need to mark the fields in the externs file. - for (HostedField field : type.getInstanceFields(false)) { + for (HostedField field : getOwnFieldOnJSSide(type)) { externClassDescriptor.addProperty(field.getName()); } } @@ -302,7 +329,7 @@ private void genJavaScriptMirrorConstructor(JSCodeGenTool tool, JSCodeBuffer buf } // Initialize properties. - for (HostedField field : type.getInstanceFields(false)) { + for (HostedField field : getOwnFieldOnJSSide(type)) { tool.genResolvedVarDeclThisPrefix(field.getName()); genDefaultValue(tool, buffer, field); tool.genResolvedVarDeclPostfix(null); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java index ff09c27abe00..c99f1b078c70 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/js/JSObjectAccessMethodSupport.java @@ -72,6 +72,8 @@ private AnalysisMethod lookup(AnalysisMetaAccess aMetaAccess, AnalysisField fiel GraalError.guarantee(JSObject.class.isAssignableFrom(field.getDeclaringClass().getJavaClass()), "Field must be in JSObject class: %s", field); GraalError.guarantee(!field.isStatic(), "Field must not be static: %s", field); UserError.guarantee(!field.isFinal(), "Instance fields in subclasses of %s must not be final: %s", JSObject.class.getSimpleName(), field.format("%H.%n")); + UserError.guarantee(field.isPublic() || field.isProtected(), "Only public and protected instance fields in subclasses of %s are allowed: %s", JSObject.class.getSimpleName(), + field.format("%H.%n")); JSObjectAccessMethod accessMethod = accessMethods.computeIfAbsent( new AccessorDescription(field, isLoad), key -> createAccessMethod(aMetaAccess, field, isLoad)); diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java index 752d625a8489..a059ddce7639 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectSubclassTest.java @@ -30,6 +30,7 @@ public class JSObjectSubclassTest { public static final String[] OUTPUT = { + // jsObjectSubclass "Declared(made in Java)", "made in Java", "Declared(made in JavaScript)", @@ -56,12 +57,15 @@ public class JSObjectSubclassTest { "inner imported in Java", "InnerImported(inner exported from JavaScript)", "inner exported from JavaScript", + // exportedReturningObject "non-exported subclass", "non-exported", "non-exported subclass", "non-exported", + // heapGeneratedObjects "5", "8", + // nonInstantiatedImported "7.0", }; diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaDocExamplesTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaDocExamplesTest.java index 73f00082fb69..e054a416307d 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaDocExamplesTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaDocExamplesTest.java @@ -189,7 +189,7 @@ class Rectangle extends JSObject { @JS.Export class Randomizer extends JSObject { - private Random rng = new Random(719513L); + protected Random rng = new Random(719513L); public byte[] randomBytes(int length) { byte[] bytes = new byte[length];