diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBigInt.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBigInt.java index 34f188a994d1..911a7e7e24ac 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBigInt.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBigInt.java @@ -67,16 +67,8 @@ public String typeof() { return "bigint"; } - @JS("return conversion.toProxy(toJavaString(this.toString()));") - private native String javaString(); - - @Override - protected String stringValue() { - return javaString(); - } - private BigInteger bigInteger() { - return new BigInteger(javaString()); + return new BigInteger(stringValue()); } @Override @@ -116,14 +108,14 @@ public BigInteger asBigInteger() { @Override public boolean equals(Object that) { - if (that instanceof JSBigInt) { - return this.javaString().equals(((JSBigInt) that).javaString()); + if (that instanceof JSBigInt otherBigInt) { + return this.stringValue().equals(otherBigInt.stringValue()); } return false; } @Override public int hashCode() { - return javaString().hashCode(); + return stringValue().hashCode(); } } diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBoolean.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBoolean.java index dca0cbc05cb3..f58d8857489f 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBoolean.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSBoolean.java @@ -66,11 +66,6 @@ public String typeof() { @JS("return conversion.toProxy(conversion.createJavaBoolean(this));") private native Boolean javaBoolean(); - @Override - protected String stringValue() { - return String.valueOf(javaBoolean()); - } - @Override public Boolean asBoolean() { return javaBoolean(); diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSNumber.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSNumber.java index 9cd131153c5e..989c5246ba1e 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSNumber.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSNumber.java @@ -62,11 +62,6 @@ public String typeof() { @JS("return conversion.toProxy(conversion.createJavaDouble(this));") private native Double javaDouble(); - @Override - protected String stringValue() { - return String.valueOf(javaDouble()); - } - @Override public Byte asByte() { return javaDouble().byteValue(); diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java index 8554b18d1863..510075f332c5 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSObject.java @@ -409,10 +409,6 @@ public String typeof() { return typeofString().asString(); } - @Override - @JS("return conversion.toProxy(toJavaString(this.toString()));") - protected native String stringValue(); - /** * Returns the value of the key passed as the argument in the JavaScript object. * @@ -457,7 +453,7 @@ public R get(Object key, Class type) { * underlying JavaScript function * @return The result of the JavaScript function, converted to the corresponding Java value */ - @JS("return this.apply(this, conversion.extractJavaScriptArray(args[runtime.symbol.javaNative]));") + @JS("return this.apply(this, [...args]);") public native Object invoke(Object... args); /** @@ -469,109 +465,9 @@ public R get(Object key, Class type) { * underlying JavaScript function * @return The result of the JavaScript function, converted to the corresponding Java value */ - @JS("return this.apply(thisArg, conversion.extractJavaScriptArray(args[runtime.symbol.javaNative]));") + @JS("return this.apply(thisArg, [...args]);") public native Object call(Object thisArg, Object... args); - private ClassCastException classCastException(String targetType) { - return new ClassCastException(this + " cannot be coerced to '" + targetType + "'."); - } - - @JS("if (this.constructor === Uint8Array) { this.hub = booleanArrayHub; return conversion.toProxy(this); } else { return null; };") - private native boolean[] extractBooleanArray(); - - @Override - public boolean[] asBooleanArray() { - boolean[] array = extractBooleanArray(); - if (array != null) { - return array; - } - throw classCastException("boolean[]"); - } - - @JS("if (this.constructor === Int8Array) { this.hub = byteArrayHub; return conversion.toProxy(this); } else { return null; };") - private native byte[] extractByteArray(); - - @Override - public byte[] asByteArray() { - byte[] array = extractByteArray(); - if (array != null) { - return array; - } - throw classCastException("byte[]"); - } - - @JS("if (this.constructor === Int16Array) { this.hub = shortArrayHub; return conversion.toProxy(this); } else { return null; };") - private native short[] extractShortArray(); - - @Override - public short[] asShortArray() { - short[] array = extractShortArray(); - if (array != null) { - return array; - } - throw classCastException("short[]"); - } - - @JS("if (this.constructor === Uint16Array) { this.hub = charArrayHub; return conversion.toProxy(this); } else { return null; };") - private native char[] extractCharArray(); - - @Override - public char[] asCharArray() { - char[] array = extractCharArray(); - if (array != null) { - return array; - } - throw classCastException("char[]"); - } - - @JS("if (this.constructor === Int32Array) { this.hub = intArrayHub; return conversion.toProxy(this); } else { return null; };") - private native int[] extractIntArray(); - - @Override - public int[] asIntArray() { - int[] array = extractIntArray(); - if (array != null) { - return array; - } - throw classCastException("int[]"); - } - - @JS("if (this.constructor === Float32Array) { this.hub = floatArrayHub; return conversion.toProxy(this); } else { return null; };") - private native float[] extractFloatArray(); - - @Override - public float[] asFloatArray() { - float[] array = extractFloatArray(); - if (array != null) { - return array; - } - throw classCastException("float[]"); - } - - @JS("if (this.constructor === BigInt64Array) { this.hub = longArrayHub; initComponentView(this); return conversion.toProxy(this); } else { return null; };") - private native long[] extractLongArray(); - - @Override - public long[] asLongArray() { - long[] array = extractLongArray(); - if (array != null) { - return array; - } - throw classCastException("long[]"); - } - - @JS("if (this.constructor === Float64Array) { this.hub = doubleArrayHub; return conversion.toProxy(this); } else { return null; };") - private native double[] extractDoubleArray(); - - @Override - public double[] asDoubleArray() { - double[] array = extractDoubleArray(); - if (array != null) { - return array; - } - throw classCastException("double[]"); - } - @JS("return conversion.coerceToFacadeClass(this, cls);") private native T coerceToFacadeClass(Class cls); diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSString.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSString.java index 4c55ee45cc26..f844abd315f2 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSString.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSString.java @@ -57,30 +57,26 @@ public String typeof() { return "string"; } - @JS("return conversion.toProxy(toJavaString(this));") - private native String javaString(); - + /** + * Cannot use {@link #stringValue()} because the implementation of that method calls this + * method. The manual extraction of the Java string here ensures that no infinite recursion + * occurs. + */ @Override - protected String stringValue() { - return javaString(); - } - - @Override - public String asString() { - return javaString(); - } + @JS("return conversion.toProxy(toJavaString(this));") + public native String asString(); @Override public boolean equals(Object that) { - if (that instanceof JSString) { - return this.javaString().equals(((JSString) that).javaString()); + if (that instanceof JSString otherString) { + return this.stringValue().equals(otherString.stringValue()); } return false; } @Override public int hashCode() { - return javaString().hashCode(); + return stringValue().hashCode(); } @JS.Coerce diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSSymbol.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSSymbol.java index 2e8bca9680a6..1fc8b8da7ae8 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSSymbol.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSSymbol.java @@ -63,14 +63,6 @@ public String typeof() { return "symbol"; } - @JS("return conversion.toProxy(toJavaString(this.toString()));") - private native String javaString(); - - @Override - protected String stringValue() { - return javaString(); - } - @Override public boolean equals(Object that) { if (that instanceof JSSymbol) { @@ -81,7 +73,7 @@ public boolean equals(Object that) { @Override public int hashCode() { - return javaString().hashCode(); + return stringValue().hashCode(); } @JS(value = "return Symbol.for(key);") diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSUndefined.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSUndefined.java index ebe57002270b..bbc099a0e09e 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSUndefined.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSUndefined.java @@ -64,9 +64,4 @@ public boolean isUndefined() { public String typeof() { return "undefined"; } - - @Override - protected String stringValue() { - return "undefined"; - } } diff --git a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSValue.java b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSValue.java index 573103e45e3f..3be8bdfc8785 100644 --- a/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSValue.java +++ b/sdk/src/org.graalvm.webimage.api/src/org/graalvm/webimage/api/JSValue.java @@ -105,7 +105,15 @@ public static JSUndefined undefined() { public abstract String typeof(); - protected abstract String stringValue(); + /** + * Returns the JS string representation of this value (by calling the JS {@code toString} method + * on it) and {@code "undefined"} for the JS {@code undefined} value. + * + * @since 25.1 + */ + @JS.Coerce + @JS("return this?.toString()?? 'undefined';") + public final native String stringValue(); public Boolean asBoolean() { throw classCastError("Boolean"); @@ -147,38 +155,6 @@ public String asString() { throw classCastError("String"); } - public boolean[] asBooleanArray() { - throw classCastError("boolean[]"); - } - - public byte[] asByteArray() { - throw classCastError("byte[]"); - } - - public short[] asShortArray() { - throw classCastError("short[]"); - } - - public char[] asCharArray() { - throw classCastError("char[]"); - } - - public int[] asIntArray() { - throw classCastError("int[]"); - } - - public float[] asFloatArray() { - throw classCastError("float[]"); - } - - public long[] asLongArray() { - throw classCastError("long[]"); - } - - public double[] asDoubleArray() { - throw classCastError("double[]"); - } - /** * Coerces this JavaScript value to the requested Java type. See {@link JS.Coerce} for the * JavaScript to Java coercion rules. @@ -219,33 +195,6 @@ public T as(Class cls) { if (String.class.equals(cls)) { return (T) asString(); } - if (cls.isArray() && cls.getComponentType().isPrimitive()) { - // Dispatch to primitive array casts. - if (int[].class.equals(cls)) { - return (T) asIntArray(); - } - if (float[].class.equals(cls)) { - return (T) asFloatArray(); - } - if (long[].class.equals(cls)) { - return (T) asLongArray(); - } - if (double[].class.equals(cls)) { - return (T) asDoubleArray(); - } - if (byte[].class.equals(cls)) { - return (T) asByteArray(); - } - if (short[].class.equals(cls)) { - return (T) asShortArray(); - } - if (char[].class.equals(cls)) { - return (T) asCharArray(); - } - if (boolean[].class.equals(cls)) { - return (T) asBooleanArray(); - } - } if (Character.class.equals(cls)) { return (T) asChar(); } diff --git a/web-image/mx.web-image/suite.py b/web-image/mx.web-image/suite.py index 3572f3082a5c..782556594894 100644 --- a/web-image/mx.web-image/suite.py +++ b/web-image/mx.web-image/suite.py @@ -159,6 +159,7 @@ "substratevm:SVM", "WEBIMAGE_LIBRARY_SUPPORT", "mx:JUNIT", + "mx:JUNIT-JUPITER-API", "NET_JAVA_HTML", "NET_JAVA_HTML_BOOT", "NET_JAVA_HTML_JSON", @@ -237,6 +238,7 @@ "sourceDirs": ["src"], "dependencies": [ "mx:JUNIT", + "mx:JUNIT-JUPITER-API", "compiler:GRAAL_TEST", "com.oracle.svm.webimage.jtt", "com.oracle.svm.hosted.webimage", @@ -389,6 +391,7 @@ ], "exclude": [ "mx:JUNIT", + "mx:JUNIT-JUPITER-API", ], "maven": False, "testDistribution": True, @@ -404,6 +407,7 @@ ], "exclude": [ "mx:JUNIT", + "mx:JUNIT-JUPITER-API", ], "maven": False, "testDistribution": True, diff --git a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/spec/JS_JTT_JSAnnotation.java b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/spec/JS_JTT_JSAnnotation.java index 9a372734b294..7f4a3682c38a 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/spec/JS_JTT_JSAnnotation.java +++ b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/spec/JS_JTT_JSAnnotation.java @@ -27,23 +27,24 @@ import java.nio.file.Path; -import com.oracle.svm.webimage.jtt.api.JSNumberTest; -import com.oracle.svm.webimage.jtt.api.JSObjectTest; -import com.oracle.svm.webimage.jtt.api.JSStringTest; -import com.oracle.svm.webimage.jtt.api.JSSymbolTest; import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import com.oracle.svm.hosted.webimage.test.util.JTTTestSuite; import com.oracle.svm.hosted.webimage.test.util.WebImageTestOptions; +import com.oracle.svm.webimage.jtt.api.ArrayProxyTest; import com.oracle.svm.webimage.jtt.api.CoercionConversionTest; import com.oracle.svm.webimage.jtt.api.HtmlApiExamplesTest; import com.oracle.svm.webimage.jtt.api.JSErrorsTest; +import com.oracle.svm.webimage.jtt.api.JSNumberTest; import com.oracle.svm.webimage.jtt.api.JSObjectConversionTest; import com.oracle.svm.webimage.jtt.api.JSObjectSubclassTest; +import com.oracle.svm.webimage.jtt.api.JSObjectTest; import com.oracle.svm.webimage.jtt.api.JSPrimitiveConversionTest; import com.oracle.svm.webimage.jtt.api.JSRawCallTest; +import com.oracle.svm.webimage.jtt.api.JSStringTest; +import com.oracle.svm.webimage.jtt.api.JSSymbolTest; import com.oracle.svm.webimage.jtt.api.JavaDocExamplesTest; import com.oracle.svm.webimage.jtt.api.JavaProxyConversionTest; import com.oracle.svm.webimage.jtt.api.JavaProxyTest; @@ -80,28 +81,26 @@ public void rawCallTest() { @Test public void coercionConversion() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC - Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(CoercionConversionTest.OUTPUT, CoercionConversionTest.class.getName()); } @Test public void javaDocExamples() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-62854 Enable once JSObject subtyping is supported in WasmGC Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JavaDocExamplesTest.OUTPUT, JavaDocExamplesTest.class.getName()); } @Test public void jsObjectConversion() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-62854 Enable once JSObject subtyping is supported in WasmGC Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JSObjectConversionTest.OUTPUT, JSObjectConversionTest.class.getName()); } @Test public void jsObjectSubclass() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-62854 Enable once JSObject subtyping is supported in WasmGC Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JSObjectSubclassTest.OUTPUT, JSObjectSubclassTest.class.getName()); } @@ -113,7 +112,7 @@ public void jsPrimitiveConversion() { @Test public void javaProxyConversion() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-71902 Enable once WasmGC supports vm.as Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JavaProxyConversionTest.OUTPUT, JavaProxyConversionTest.class.getName()); } @@ -130,7 +129,7 @@ public void jsErrors() { @Test public void htmlApiExamplesTest() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-62854 Enable once JSObject subtyping is supported in WasmGC Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(HtmlApiExamplesTest.OUTPUT, HtmlApiExamplesTest.class.getName()); } @@ -142,7 +141,7 @@ public void jsNumberTest() { @Test public void jsStringTest() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC + // TODO GR-71902 Enable once WasmGC supports vm.as Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JSStringTest.class.getName()); } @@ -154,8 +153,11 @@ public void jsSymbolTest() { @Test public void jsObjectTest() { - // TODO GR-60603 Enable once JS annotation is supported in WasmGC - Assume.assumeFalse(WebImageTestOptions.isWasmGCBackend()); testFileAgainstNoBuild(JSObjectTest.class.getName()); } + + @Test + public void arrayProxyTest() { + testFileAgainstNoBuild(ArrayProxyTest.class.getName()); + } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/RuntimeModificationLowerer.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/RuntimeModificationLowerer.java index 8bcdc1fb50ea..ed650ea4828d 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/RuntimeModificationLowerer.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/RuntimeModificationLowerer.java @@ -26,7 +26,6 @@ import static com.oracle.svm.webimage.hightiercodegen.Emitter.of; -import java.lang.reflect.Array; import java.math.BigInteger; import java.util.HashSet; import java.util.Set; @@ -110,7 +109,6 @@ private static void assignHubProperty(JSCodeGenTool tool, TypeControl typeContro private static void indexHubs(JSCodeGenTool tool) { WebImageTypeControl typeControl = tool.getJSProviders().typeControl(); tool.genComment("Create an index between class names and hubs in the image."); - MetaAccessProvider meta = tool.getProviders().getMetaAccess(); for (HostedType type : typeControl.emittedTypes()) { assignJsClassToHub(tool, typeControl, type); if (COMPULSORY_RUNTIME_HUBS.contains(type.getJavaClass())) { @@ -118,12 +116,6 @@ private static void indexHubs(JSCodeGenTool tool) { assignToRuntimeHubs(tool, typeControl, type, type.getJavaClass().getName()); } } - for (JavaKind kind : JavaKind.values()) { - if (kind.isPrimitive() && kind != JavaKind.Void) { - Class arrayClass = Array.newInstance(kind.toJavaClass(), 0).getClass(); - assignToRuntimeHubs(tool, typeControl, (HostedType) meta.lookupJavaType(arrayClass), kind.getJavaName() + "[]"); - } - } } /** 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 62965f20fe58..f9a6be3947ce 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 @@ -357,7 +357,7 @@ private void genJavaScriptMirrorConstructor(JSCodeGenTool tool, JSCodeBuffer buf // We use the ProxyHandler's overload resolution. buffer.emitConstDeclPrefix("handler"); buffer.emitText("conversion.getOrCreateProxyHandler("); - tool.genTypeName(type); + buffer.emitText(codeGenTool.getJSProviders().typeControl().requestHubName(type)); buffer.emitText(");"); buffer.emitNewLine(); buffer.emitText("handler._getJavaConstructorMethod()(this, ...args);"); @@ -417,7 +417,7 @@ private void genBridgeMethod(CodeBuffer buffer, String name, boolean isStatic) { buffer.emitScopeBegin(); codeGenTool.genResolvedConstDeclPrefix("handler"); buffer.emitText("conversion.getOrCreateProxyHandler("); - codeGenTool.genTypeName(type); + buffer.emitText(codeGenTool.getJSProviders().typeControl().requestHubName(type)); buffer.emitText(");"); buffer.emitNewLine(); buffer.emitText("return handler."); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion-js.js b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion-js.js index a4facd28f2f8..04e2a79eebae 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion-js.js +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion-js.js @@ -78,24 +78,6 @@ class JSConversion extends Conversion { return jlstring.toJSString(); } - /** - * Converts a Java array to a JavaScript array that contains JavaScript values - * that correspond to the Java values of the input array. - * - * @param jarray A Java array - * @returns {*} The resulting JavaScript array - */ - extractJavaScriptArray(jarray) { - const length = $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["lengthOf"](jarray); - const jsarray = new Array(length); - for (let i = 0; i < length; i++) { - jsarray[i] = $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["javaToJavaScript"]( - jarray[i] - ); - } - return jsarray; - } - // JavaScript-to-Java conversions (standard Java classes) /** @@ -270,11 +252,31 @@ class JSConversion extends Conversion { return isA(true, obj, hub); } - getOrCreateProxyHandler(constructor) { - if (!constructor.hasOwnProperty(runtime.symbol.javaProxyHandler)) { - constructor[runtime.symbol.javaProxyHandler] = new JSProxyHandler(constructor); + isSupertype(supertype, subtype) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["isSupertype"](supertype, subtype); + } + + getHub(obj) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["getClass"](obj); + } + + getSupertype(hub) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["getSuperclass"](hub); + } + + getComponentHub(hub) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["getComponentType"](hub); + } + + getTypeNameAsJavaString(hub) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["getClassName"](hub); + } + + getOrCreateProxyHandler(hub) { + if (!hub.hasOwnProperty(runtime.symbol.javaProxyHandler)) { + hub[runtime.symbol.javaProxyHandler] = new JSProxyHandler(hub); } - return constructor[runtime.symbol.javaProxyHandler]; + return hub[runtime.symbol.javaProxyHandler]; } _getProxyHandlerArg(obj) { @@ -322,46 +324,12 @@ class JSConversion extends Conversion { return BigInt(bs); case "string": return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptString"](o); - case "object": - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptObject"](o); case "function": const sam = proxyHandler._getSingleAbstractMethod(proxy); if (sam !== undefined) { return (...args) => proxyHandler._applyWithObject(proxy, args); } this.throwClassCastException(o, tpe); - case Uint8Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptUint8Array"]( - o - ); - case Int8Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptInt8Array"]( - o - ); - case Uint16Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m[ - "coerceToJavaScriptUint16Array" - ](o); - case Int16Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptInt16Array"]( - o - ); - case Int32Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["coerceToJavaScriptInt32Array"]( - o - ); - case Float32Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m[ - "coerceToJavaScriptFloat32Array" - ](o); - case BigInt64Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m[ - "coerceToJavaScriptBigInt64Array" - ](o); - case Float64Array: - return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m[ - "coerceToJavaScriptFloat64Array" - ](o); default: this.throwClassCastException(o, tpe); } @@ -378,7 +346,7 @@ class JSConversion extends Conversion { typeHub = runtime.hubs[type]; } else if (typeof type === "object") { const javaType = type[runtime.symbol.javaNative]; - if (javaType !== undefined && javaType.constructor === runtime.classHub) { + if (javaType !== undefined && this.isJavaLangClass(javaType)) { typeHub = javaType; } } @@ -390,8 +358,8 @@ class JSConversion extends Conversion { // Check if the current object is a Java Proxy, in which case no coercion is possible. let javaValue = javaScriptValue[runtime.symbol.javaNative]; if (javaValue !== undefined) { - const valueHub = runtime.hubOf(javaValue); - if (runtime.isSupertype(typeHub, valueHub)) { + const valueHub = this.getHub(javaValue); + if (this.isSupertype(typeHub, valueHub)) { return javaValue; } else { throw new Error("Cannot coerce Java Proxy of type '" + valueHub + "' to the type '" + typeHub + "'"); @@ -401,38 +369,92 @@ class JSConversion extends Conversion { javaValue = this.javaScriptToJava(javaScriptValue); return this.javaToJavaScript(javaValue.$t["org.graalvm.webimage.api.JSValue"].$m["as"](javaValue, typeHub)); } -} -// Java Proxies + getArrayLength(javaArray) { + return $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["lengthOf"](javaArray); + } -/** - * Handler for JavaScript Proxies that wrap Java objects. - */ -class JSProxyHandler extends ProxyHandler { - constructor(javaScriptConstructor) { - super(); - this.javaScriptConstructor = javaScriptConstructor; + loadBooleanArrayElement(javaArray, idx) { + return !!javaArray[idx]; } - _getClassMetadata() { - return this.javaScriptConstructor[runtime.symbol.classMeta]; + loadByteArrayElement(javaArray, idx) { + return javaArray[idx]; } - _getClassName() { - return this.javaScriptConstructor.name; + loadShortArrayElement(javaArray, idx) { + return javaArray[idx]; } - _linkMethodPrototype() { - if (this.javaScriptConstructor !== $t["java.lang.Object"]) { - const parentConstructor = Object.getPrototypeOf(this.javaScriptConstructor); - const parentProxyHandler = conversion.getOrCreateProxyHandler(parentConstructor); - Object.setPrototypeOf(this._getMethods(), parentProxyHandler._getMethods()); - } + loadCharArrayElement(javaArray, idx) { + return javaArray[idx]; + } + + loadIntArrayElement(javaArray, idx) { + return javaArray[idx]; } - _getSingleAbstractMethod(javaScriptJavaProxy) { - const javaThis = javaScriptJavaProxy[runtime.symbol.javaNative]; - return javaThis.constructor[runtime.symbol.classMeta].singleAbstractMethod; + loadFloatArrayElement(javaArray, idx) { + return javaArray[idx]; + } + + loadLongArrayElement(javaArray, idx) { + return javaArray[idx]; + } + + loadDoubleArrayElement(javaArray, idx) { + return javaArray[idx]; + } + + loadObjectArrayElement(javaArray, idx) { + return javaArray[idx]; + } + + storeBooleanArrayElement(javaArray, idx, jsBoolean) { + javaArray[idx] = jsBoolean ? 1 : 0; + } + + storeByteArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeShortArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeCharArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeIntArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeFloatArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeLongArrayElement(javaArray, idx, jsBigInt) { + javaArray[idx] = jsBigInt; + } + + storeDoubleArrayElement(javaArray, idx, jsNumber) { + javaArray[idx] = jsNumber; + } + + storeObjectArrayElement(javaArray, idx, javaObjectValue) { + javaArray[idx] = javaObjectValue; + } +} + +// Java Proxies + +/** + * Handler for JavaScript Proxies that wrap Java objects. + */ +class JSProxyHandler extends ProxyHandler { + _getClassMetadata() { + return this.javaHub[runtime.symbol.jsClass]?.[runtime.symbol.classMeta]; } _createInstance(hub) { @@ -443,10 +465,4 @@ class JSProxyHandler extends ProxyHandler { const conversion = new JSConversion(); -runtime.classHub = $t["java.lang.Class"]; - -runtime.hubOf = $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["hubOf"]; - -runtime.isSupertype = $t["com.oracle.svm.webimage.functionintrinsics.JSConversion"].$m["isSupertype"]; - vm.as = (...args) => conversion.coerceJavaScriptToJavaType(...args); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion.js b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion.js index e934fd2700d5..6b8ee861ffdf 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion.js +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/jsconversion.js @@ -80,17 +80,6 @@ class Conversion { throw new Error("Unimplemented: Conversion.extractJavaScriptString"); } - /** - * Converts a Java array to a JavaScript array that contains JavaScript values - * that correspond to the Java values of the input array. - * - * @param jarray A Java array - * @returns {*} The resulting JavaScript array - */ - extractJavaScriptArray(jarray) { - throw new Error("Unimplemented: Conversion.extractJavaScriptArray"); - } - // JavaScript-to-Java conversions (standard Java classes) /** @@ -351,13 +340,55 @@ class Conversion { } isJavaLangClass(obj) { - throw new Error("Unimplemented: Conversion.isJavaLangClassHub"); + throw new Error("Unimplemented: Conversion.isJavaLangClass"); } + /** + * Checks if the given object is an instance of the given class. null values also return true. + */ isInstance(obj, hub) { throw new Error("Unimplemented: Conversion.isInstance"); } + /** + * @return {*} The result of obj.getClass() + */ + getHub(obj) { + throw new Error("Unimplemented: Conversion.getHub"); + } + + /** + * Returns the supertype of the type represented by the given hub or null + * if and only if the hub represents java.lang.Object. + * + * This function must only be called with instance or array classes (no + * primitive or interface classes). + */ + getSupertype(hub) { + throw new Error("Unimplemented: Conversion.getSupertype"); + } + + /** + * @return {*|null} The component type of the given hub, or null if the hub does not represent an array type. + */ + getComponentHub(hub) { + throw new Error("Unimplemented: Conversion.getComponentHub"); + } + + /** + * Returns hub.getTypeName(). + */ + getTypeNameAsJavaString(hub) { + throw new Error("Unimplemented: Conversion.getTypeNameAsJavaString"); + } + + /** + * Returns hub.getTypeName() as a JS string. + */ + getTypeName(hub) { + return conversion.extractJavaScriptString(this.getTypeNameAsJavaString(hub)); + } + /** * Copies own fields from source to destination. * @@ -382,18 +413,10 @@ class Conversion { /** * Obtains or creates the proxy handler for the given Java class */ - getOrCreateProxyHandler(arg) { + getOrCreateProxyHandler(hub) { throw new Error("Unimplemented: Conversion.getOrCreateProxyHandler"); } - /** - * For proxying the given object returns value that should be passed to - * getOrCreateProxyHandler. - */ - _getProxyHandlerArg(obj) { - throw new Error("Unimplemented: Conversion._getProxyHandlerArg"); - } - /** * Creates a proxy that intercepts messages that correspond to Java method calls and Java field accesses. * @@ -401,7 +424,7 @@ class Conversion { * @return {*} The proxy around the Java object */ toProxy(obj) { - let proxyHandler = this.getOrCreateProxyHandler(this._getProxyHandlerArg(obj)); + let proxyHandler = this.getOrCreateProxyHandler(this.getHub(obj)); // The wrapper is a temporary object that allows having the non-identifier name of the target function. // We declare the property as a function, to ensure that it is constructable, so that the Proxy handler's construct method is callable. let targetWrapper = { @@ -534,6 +557,97 @@ class Conversion { return this.toProxy(rawJavaMirror); } + loadArrayElement(javaArray, componentKindOrdinal, idx) { + switch (componentKindOrdinal) { + case 0: + return this.loadBooleanArrayElement(javaArray, idx); + case 1: + return this.loadByteArrayElement(javaArray, idx); + case 2: + return this.loadShortArrayElement(javaArray, idx); + case 3: + return this.loadCharArrayElement(javaArray, idx); + case 4: + return this.loadIntArrayElement(javaArray, idx); + case 5: + return this.loadFloatArrayElement(javaArray, idx); + case 6: + return this.loadLongArrayElement(javaArray, idx); + case 7: + return this.loadDoubleArrayElement(javaArray, idx); + case 8: + return this.javaToJavaScript(this.loadObjectArrayElement(javaArray, idx)); + } + } + + checkNumericType(arrayType, jsValue) { + const tpe = typeof jsValue; + if (tpe !== "number" && tpe !== "bigint") { + throw new TypeError(`Invalid type ${tpe} for insertion into ${arrayType} array`); + } + } + + checkIntegerValueRange(arrayType, jsValue, lowValue, highValue) { + const tpe = typeof jsValue; + this.checkNumericType(arrayType, jsValue); + + if (tpe === "number" && !Number.isInteger(jsValue)) { + throw new RangeError(`Non-integer number ${jsValue} for insertion into ${arrayType} array`); + } + + if (jsValue > highValue || jsValue < lowValue) { + throw new RangeError(`Out of range value ${jsValue} for insertion into ${arrayType} array`); + } + } + + storeArrayElement(javaArray, componentKindOrdinal, idx, jsValue) { + if (idx < 0 || idx >= conversion.getArrayLength(javaArray)) { + // Silently ignore out of bounds array stores. This matches the + // behavior of TypedArray + return; + } + const tpe = typeof jsValue; + switch (componentKindOrdinal) { + case 0: + if (tpe !== "boolean") { + throw new TypeError(`Invalid type ${tpe} for insertion into boolean array`); + } + this.storeBooleanArrayElement(javaArray, idx, jsValue); + break; + case 1: + this.checkIntegerValueRange("byte", jsValue, -128, 127); + this.storeByteArrayElement(javaArray, idx, Number(jsValue)); + break; + case 2: + this.checkIntegerValueRange("short", jsValue, -32768, 32767); + this.storeShortArrayElement(javaArray, idx, Number(jsValue)); + break; + case 3: + this.checkIntegerValueRange("char", jsValue, 0, 65535); + this.storeCharArrayElement(javaArray, idx, Number(jsValue)); + break; + case 4: + this.checkIntegerValueRange("int", jsValue, -2147483648, 2147483647); + this.storeIntArrayElement(javaArray, idx, Number(jsValue)); + break; + case 5: + this.checkNumericType("float", jsValue); + this.storeFloatArrayElement(javaArray, idx, Number(jsValue)); + break; + case 6: + this.checkIntegerValueRange("long", jsValue, -9223372036854775808n, 9223372036854775807n); + this.storeLongArrayElement(javaArray, idx, BigInt(jsValue)); + break; + case 7: + this.checkNumericType("double", jsValue); + this.storeDoubleArrayElement(javaArray, idx, Number(jsValue)); + break; + case 8: + this.storeObjectArrayElement(javaArray, idx, conversion.javaScriptToJava(jsValue)); + break; + } + } + /** * Coerce the specified JavaScript value to the specified Java type. * @@ -542,6 +656,181 @@ class Conversion { coerceJavaScriptToJavaType(javaScriptValue, type) { throw new Error("Unimplemented: Conversion.coerceJavaScriptToJavaType"); } + + /** + * Reads the length of a Java array and returns it as a JS number. + */ + getArrayLength(_javaArray) { + throw new Error("Unimplemented: Conversion.getArrayLength"); + } + + /** + * @param {number} _idx In bounds index + * @return {boolean} Boolean value at index idx as a JS boolean + */ + loadBooleanArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadBooleanArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Byte value at index idx as a JS number + */ + loadByteArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadByteArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Short value at index idx as a JS number + */ + loadShortArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadShortArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Char value at index idx as a JS number + */ + loadCharArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadCharArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Int value at index idx as a JS number + */ + loadIntArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadIntArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Float value at index idx as a JS number + */ + loadFloatArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadFloatArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {bigint} Long value at index idx as a JS bigint + */ + loadLongArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadLongArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {number} Double value at index idx as a JS number + */ + loadDoubleArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadDoubleArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @return {*} Java object at index idx (caller is responsible for conversion to JS value). + */ + loadObjectArrayElement(_javaArray, _idx) { + throw new Error("Unimplemented: loadObjectArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {boolean} _jsBoolean JS Boolean value to store at index idx. + */ + storeBooleanArrayElement(_javaArray, _idx, _jsBoolean) { + throw new Error("Unimplemented: storeByteArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeByteArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeByteArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeShortArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeShortArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeCharArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeCharArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeIntArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeIntArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeFloatArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeFloatArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {bigint} _jsBigInt JS bigint value to store at index idx. + */ + storeLongArrayElement(_javaArray, _idx, _jsBigInt) { + throw new Error("Unimplemented: storeLongArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {number} _jsNumber JS number value to store at index idx. + */ + storeDoubleArrayElement(_javaArray, _idx, _jsNumber) { + throw new Error("Unimplemented: storeDoubleArrayElement"); + } + + /** + * @param {number} _idx In bounds index + * @param {*} _javaObjectValue Java object instance to store at index idx (caller is responsible for conversion to Java object). + */ + storeObjectArrayElement(_javaArray, _idx, _javaObjectValue) { + throw new Error("Unimplemented: storeObjectArrayElement"); + } +} + +/** + * Checks if the given string is an array index and returns the numeric index + * (otherwise undefined). + * + * Proxied accesses always get a string property (even for indexed accesses) so + * we need to check if the property access is an indexed access. + * + * A property is an index if the numeric index it is refering to has the same + * string representation as the original property. + * E.g the string '010' is a property while '10' is an index. + */ +function getArrayIndex(propertyName) { + try { + const idx = Number(propertyName); + if (idx.toString() === propertyName) { + return idx; + } + } catch (e) { + // Catch clause because not all property keys (e.g. symbols) can be + // converted to a number. + } + return undefined; } /** @@ -564,16 +853,24 @@ class Conversion { * objects respectively, but no coercion is done. */ class ProxyHandler { - constructor() { + constructor(javaHub) { + if (javaHub === null || javaHub === undefined) { + throw new Error("Got undefined or null javaHub"); + } this._initialized = false; - this._methods = {}; + this._methods = null; this._staticMethods = {}; this._javaConstructorMethod = null; + this.javaHub = javaHub; + this.componentHub = conversion.getComponentHub(javaHub); + this.isArray = this.componentHub !== null; + this.componentKindOrdinal = this.isArray ? conversion.getHubKindOrdinal(this.componentHub) : -1; } - ensureInitialized() { + _ensureInitialized() { if (!this._initialized) { this._initialized = true; + this._methods = this._createLinkedMethodsObject(); // Function properties derived from accessible Java methods. this._createProxyMethods(); // Default function properties. @@ -582,17 +879,17 @@ class ProxyHandler { } _getMethods() { - this.ensureInitialized(); + this._ensureInitialized(); return this._methods; } _getStaticMethods() { - this.ensureInitialized(); + this._ensureInitialized(); return this._staticMethods; } _getJavaConstructorMethod() { - this.ensureInitialized(); + this._ensureInitialized(); return this._javaConstructorMethod; } @@ -615,14 +912,24 @@ class ProxyHandler { * String that can be printed as part of the toString and valueOf functions. */ _getClassName() { - throw new Error("Unimplemented: ProxyHandler._getClassName"); + return conversion.getTypeName(this.javaHub); } /** - * Link the methods object to the prototype chain of the methods object of the superclass' proxy handler. + * Creates an empty object for the _methods field with the prototype being + * the _methods object from the superclass' proxy handler. */ - _linkMethodPrototype() { - throw new Error("Unimplemented: ProxyHandler._linkMethodPrototype"); + _createLinkedMethodsObject() { + // Link the prototype chain of the superclass' proxy handler, to include super methods. + const parentClass = conversion.getSupertype(this.javaHub); + if (parentClass === null) { + // This is the handler for java.lang.Object, no linking to supertype necessary. + return {}; + } else { + const parentProxyHandler = conversion.getOrCreateProxyHandler(parentClass); + // Link the prototype chain of the superclass' proxy handler, to include super methods. + return Object.create(parentProxyHandler._getMethods()); + } } _createProxyMethods() { @@ -670,8 +977,6 @@ class ProxyHandler { ); }; } - - this._linkMethodPrototype(); } /** @@ -741,12 +1046,10 @@ class ProxyHandler { const proxyHandlerThis = this; const asProperty = function (tpe) { - // Note: this will be bound to the Proxy object. + // Note: 'this' will usually be bound to the Proxy object. return conversion.coerceJavaProxyToJavaScriptType(proxyHandlerThis, this, tpe); }; - if (!("$as" in this._methods)) { - this._methods["$as"] = asProperty; - } + this._methods["$as"] = asProperty; this._methods[runtime.symbol.javaScriptCoerceAs] = asProperty; const vmProperty = vm; @@ -856,33 +1159,47 @@ class ProxyHandler { } get(target, key) { + const javaObject = target(runtime.symbol.javaNative); if (key === runtime.symbol.javaNative) { - return target(runtime.symbol.javaNative); - } else { - const javaObject = target(runtime.symbol.javaNative); - // TODO GR-60603 Deal with arrays in WasmGC backend - if (Array.isArray(javaObject) && typeof key === "string") { - const index = Number(key); - if (0 <= index && index < javaObject.length) { - return conversion.javaToJavaScript(javaObject[key]); - } else if (key === "length") { - return javaObject.length; + return javaObject; + } else if (this.isArray) { + const componentKindOrdinal = this.componentKindOrdinal; + const length = conversion.getArrayLength(javaObject); + if (key === "length") { + return length; + } else if (key === Symbol.iterator) { + return function () { + return (function* () { + for (let i = 0; i < length; i++) { + yield conversion.loadArrayElement(javaObject, componentKindOrdinal, i); + } + })(); + }; + } + + const potentialIdx = getArrayIndex(key); + + if (potentialIdx !== undefined) { + if (potentialIdx < 0 || potentialIdx >= length) { + return undefined; } + return conversion.loadArrayElement(javaObject, componentKindOrdinal, potentialIdx); } } return this._loadMethod(target, key); } - set(target, key, value, receiver) { - const javaObject = target(runtime.symbol.javaNative); - // TODO GR-60603 Deal with arrays in WasmGC backend - if (Array.isArray(javaObject)) { - const index = Number(key); - if (0 <= index && index < javaObject.length) { - javaObject[key] = conversion.javaScriptToJava(value); + set(target, key, value) { + if (this.isArray) { + const potentialIdx = getArrayIndex(key); + + if (potentialIdx !== undefined) { + const javaObject = target(runtime.symbol.javaNative); + conversion.storeArrayElement(javaObject, this.componentKindOrdinal, potentialIdx, value); return true; } } + return false; } @@ -932,13 +1249,20 @@ class ProxyHandler { "Cannot invoke the 'new' operator. The 'new' operator can only be used on Java Proxies that represent the 'java.lang.Class' type." ); } + + if (conversion.getComponentHub(javaThis) !== null) { + throw new TypeError( + "Cannot invoke the 'new' operator on a Java proxy that represents the 'java.lang.Class' instance for an array." + ); + } + // Allocate the Java object from Class instance const javaInstance = this._createInstance(javaThis); // Lookup constructor method of the target class. // This proxy handler is for java.lang.Class while javaThis is a // java.lang.Class instance for some object type for which we want to // lookup the constructor. - const instanceProxyHandler = conversion.getOrCreateProxyHandler(conversion._getProxyHandlerArg(javaInstance)); + const instanceProxyHandler = conversion.getOrCreateProxyHandler(conversion.getHub(javaInstance)); const javaConstructorMethod = instanceProxyHandler._getJavaConstructorMethod(); // Convert the Java instance to JS (usually creates a proxy) const javaScriptInstance = conversion.javaToJavaScript(javaInstance); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/runtime.js b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/runtime.js index 5ace34d03e76..9012af6428ea 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/runtime.js +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/runtime/runtime.js @@ -261,25 +261,10 @@ class Runtime { }, }); - // Conversion-related functions and values. - // The following values are set or used by the jsconversion module. - /** * The holder of JavaScript mirror class for JSObject subclasses. */ this.mirrors = {}; - /** - * Reference to the hub of the java.lang.Class class. - */ - this.classHub = null; - /** - * Function that retrieves the hub of the specified Java object. - */ - this.hubOf = null; - /** - * Function that checks if the first argument hub is the supertype or the same as the second argument hub. - */ - this.isSupertype = null; } /** diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java index 7cf6e66f33e9..505fa8424930 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/ast/id/GCKnownIds.java @@ -25,8 +25,11 @@ package com.oracle.svm.hosted.webimage.wasmgc.ast.id; +import java.util.ArrayList; +import java.util.Collections; import java.util.EnumMap; import java.util.List; +import java.util.Locale; import com.oracle.svm.hosted.webimage.wasm.ast.Export; import com.oracle.svm.hosted.webimage.wasm.ast.id.KnownIds; @@ -178,7 +181,7 @@ public class GCKnownIds extends KnownIds { public final WasmGCJSBodyTemplates.ExtractJSValue extractJSValueTemplate; public final WasmGCJSBodyTemplates.IsJavaObject isJavaObjectTemplate; - private final List functionExports; + private final List functionExports = new ArrayList<>(); public GCKnownIds(WasmIdFactory idFactory) { super(idFactory); @@ -250,18 +253,19 @@ public GCKnownIds(WasmIdFactory idFactory) { this.extractJSValueTemplate = new WasmGCJSBodyTemplates.ExtractJSValue(idFactory); this.isJavaObjectTemplate = new WasmGCJSBodyTemplates.IsJavaObject(idFactory); - functionExports = List.of( - Export.forFunction(unsafeCreateTemplate.requestFunctionId(), "unsafe.create", "Create uninitialized instance of given class"), - Export.forFunction(wrapExternTemplate.requestFunctionId(), "extern.wrap", "Wrap externref in WasmExtern"), - Export.forFunction(toExternTemplate.requestFunctionId(), "extern.unwrap", "Unwrap Java object to externref"), - Export.forFunction(toExternTemplate.requestFunctionId(), "extern.isjavaobject", "Check if reference is a Java Object"), - Export.forFunction(arrayLoadTemplate.requestFunctionId(JavaKind.Char), "array.char.read", "Read element of char array"), - Export.forFunction(arrayLoadTemplate.requestFunctionId(JavaKind.Object), "array.object.read", "Read element of Object array"), - Export.forFunction(arrayLengthTemplate.requestFunctionId(), "array.length", "Length of a Java array"), - Export.forFunction(arrayStoreTemplate.requestFunctionId(JavaKind.Char), "array.char.write", "Write element of char array"), - Export.forFunction(arrayStoreTemplate.requestFunctionId(JavaKind.Object), "array.object.write", "Write element of Object array"), - Export.forFunction(arrayCreateTemplate.requestFunctionId(char.class), "array.char.create", "Create char array"), - Export.forFunction(arrayCreateTemplate.requestFunctionId(String.class), "array.string.create", "Create String array")); + this.functionExports.add(Export.forFunction(unsafeCreateTemplate.requestFunctionId(), "unsafe.create", "Create uninitialized instance of given class")); + this.functionExports.add(Export.forFunction(wrapExternTemplate.requestFunctionId(), "extern.wrap", "Wrap externref in WasmExtern")); + this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.unwrap", "Unwrap Java object to externref")); + this.functionExports.add(Export.forFunction(toExternTemplate.requestFunctionId(), "extern.isjavaobject", "Check if reference is a Java Object")); + this.functionExports.add(Export.forFunction(arrayLengthTemplate.requestFunctionId(), "array.length", "Length of a Java array")); + this.functionExports.add(Export.forFunction(arrayCreateTemplate.requestFunctionId(char.class), "array.char.create", "Create char array")); + this.functionExports.add(Export.forFunction(arrayCreateTemplate.requestFunctionId(String.class), "array.string.create", "Create String array")); + + for (JavaKind kind : arrayComponentKinds) { + String prefix = "array." + kind.name().toLowerCase(Locale.ROOT) + "."; + this.functionExports.add(Export.forFunction(arrayLoadTemplate.requestFunctionId(kind), prefix + "read", "Read element of " + kind + " array")); + this.functionExports.add(Export.forFunction(arrayStoreTemplate.requestFunctionId(kind), prefix + "write", "Write element of " + kind + " array")); + } } /** @@ -306,6 +310,6 @@ public List> getLateFunctionTemplates() { @Override public List getExports() { - return functionExports; + return Collections.unmodifiableList(functionExports); } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/runtime/jsconversion-wasmgc.js b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/runtime/jsconversion-wasmgc.js index 597a5644aa3c..2643deeba600 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/runtime/jsconversion-wasmgc.js +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasmgc/codegen/runtime/jsconversion-wasmgc.js @@ -80,15 +80,6 @@ class WasmGCConversion extends Conversion { return charArrayToString(proxyCharArray(getExport("string.tochars")(jlstring))); } - extractJavaScriptArray(jarray) { - const length = getExport("array.length")(jarray); - const jsarray = new Array(length); - for (let i = 0; i < length; i++) { - jsarray[i] = this.javaToJavaScript(getExport("array.object.read")(jarray, i)); - } - return jsarray; - } - createJavaBoolean(x) { return getExport("box.boolean")(x); } @@ -209,15 +200,27 @@ class WasmGCConversion extends Conversion { return getExport("object.isinstance")(obj, hub); } - getOrCreateProxyHandler(clazz) { - if (!this.proxyHandlers.has(clazz)) { - this.proxyHandlers.set(clazz, new WasmGCProxyHandler(clazz)); - } - return this.proxyHandlers.get(clazz); + getHub(obj) { + return getExport("object.getclass")(obj); } - _getProxyHandlerArg(obj) { - return getExport("object.getclass")(obj); + getSupertype(hub) { + return getExport("class.superclass")(hub); + } + + getComponentHub(hub) { + return getExport("class.componenttype")(hub); + } + + getTypeNameAsJavaString(hub) { + return getExport("class.getname")(hub); + } + + getOrCreateProxyHandler(hub) { + if (!this.proxyHandlers.has(hub)) { + this.proxyHandlers.set(hub, new WasmGCProxyHandler(hub)); + } + return this.proxyHandlers.get(hub); } javaToJavaScript(x) { @@ -246,36 +249,100 @@ class WasmGCConversion extends Conversion { switch (tpe) { case "boolean": // Due to Java booleans being numbers, the double-negation is necessary. - return !!this.#unwrapExtern(getExport("convert.coerce.boolean")(o)); + return !!getExport("convert.coerce.boolean")(o); case "number": - return this.#unwrapExtern(getExport("convert.coerce.number")(o)); + return getExport("convert.coerce.number")(o); case "bigint": const bs = this.#unwrapExtern(getExport("convert.coerce.bigint")(o)); return BigInt(bs); case "string": return this.#unwrapExtern(getExport("convert.coerce.string")(o)); - case "object": - return this.#unwrapExtern(getExport("convert.coerce.object")(o)); case "function": const sam = proxyHandler._getSingleAbstractMethod(proxy); if (sam !== undefined) { return (...args) => proxyHandler._applyWithObject(proxy, args); } this.throwClassCastException(o, tpe); - case Uint8Array: - case Int8Array: - case Uint16Array: - case Int16Array: - case Int32Array: - case Float32Array: - case BigInt64Array: - case Float64Array: - // TODO GR-60603 Support array coercion - throw new Error("Coercion to arrays is not supported yet"); default: this.throwClassCastException(o, tpe); } } + + getArrayLength(javaArray) { + return getExport("array.length")(javaArray); + } + + loadBooleanArrayElement(javaArray, idx) { + return !!getExport("array.boolean.read")(javaArray, idx); + } + + loadByteArrayElement(javaArray, idx) { + return getExport("array.byte.read")(javaArray, idx); + } + + loadShortArrayElement(javaArray, idx) { + return getExport("array.short.read")(javaArray, idx); + } + + loadCharArrayElement(javaArray, idx) { + return getExport("array.char.read")(javaArray, idx); + } + + loadIntArrayElement(javaArray, idx) { + return getExport("array.int.read")(javaArray, idx); + } + + loadFloatArrayElement(javaArray, idx) { + return getExport("array.float.read")(javaArray, idx); + } + + loadLongArrayElement(javaArray, idx) { + return getExport("array.long.read")(javaArray, idx); + } + + loadDoubleArrayElement(javaArray, idx) { + return getExport("array.double.read")(javaArray, idx); + } + + loadObjectArrayElement(javaArray, idx) { + return getExport("array.object.read")(javaArray, idx); + } + + storeBooleanArrayElement(javaArray, idx, jsBoolean) { + getExport("array.boolean.write")(javaArray, idx, jsBoolean ? 1 : 0); + } + + storeByteArrayElement(javaArray, idx, jsNumber) { + getExport("array.byte.write")(javaArray, idx, jsNumber); + } + + storeShortArrayElement(javaArray, idx, jsNumber) { + getExport("array.short.write")(javaArray, idx, jsNumber); + } + + storeCharArrayElement(javaArray, idx, jsNumber) { + getExport("array.char.write")(javaArray, idx, jsNumber); + } + + storeIntArrayElement(javaArray, idx, jsNumber) { + getExport("array.int.write")(javaArray, idx, jsNumber); + } + + storeFloatArrayElement(javaArray, idx, jsNumber) { + getExport("array.float.write")(javaArray, idx, jsNumber); + } + + storeLongArrayElement(javaArray, idx, jsBigInt) { + getExport("array.long.write")(javaArray, idx, jsBigInt); + } + + storeDoubleArrayElement(javaArray, idx, jsNumber) { + getExport("array.double.write")(javaArray, idx, jsNumber); + } + + storeObjectArrayElement(javaArray, idx, javaObjectValue) { + getExport("array.object.write")(javaArray, idx, javaObjectValue); + } } const METADATA_PREFIX = "META."; @@ -285,11 +352,6 @@ const METADATA_SEPARATOR = " "; class WasmGCProxyHandler extends ProxyHandler { #classMetadata = null; - constructor(clazz) { - super(); - this.clazz = clazz; - } - #lookupClass(name) { const clazz = getExport("conversion.classfromencoding")(toJavaString(name)); if (!clazz) { @@ -314,7 +376,7 @@ class WasmGCProxyHandler extends ProxyHandler { } const classId = parts[0]; - if (this.#lookupClass(classId) == this.clazz) { + if (this.#lookupClass(classId) == this.javaHub) { const methodName = parts[1]; const returnTypeId = parts[2]; const argTypeIds = parts.slice(3); @@ -370,19 +432,6 @@ class WasmGCProxyHandler extends ProxyHandler { return methodTable; } - _getClassName() { - return conversion.extractJavaScriptString(getExport("class.getname")(this.clazz)); - } - - _linkMethodPrototype() { - // Link the prototype chain of the superclass' proxy handler, to include super methods. - if (!getExport("class.isjavalangobject")(this.clazz)) { - const parentClass = getExport("class.superclass")(this.clazz); - const parentProxyHandler = conversion.getOrCreateProxyHandler(parentClass); - Object.setPrototypeOf(this._getMethods(), parentProxyHandler._getMethods()); - } - } - _createInstance(hub) { return getExport("unsafe.create")(hub); } diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/ArrayProxyTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/ArrayProxyTest.java new file mode 100644 index 000000000000..12a2213c539d --- /dev/null +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/ArrayProxyTest.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 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.jtt.api; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Array; +import java.math.BigInteger; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +import org.graalvm.webimage.api.JS; +import org.graalvm.webimage.api.JSBigInt; +import org.graalvm.webimage.api.JSBoolean; +import org.graalvm.webimage.api.JSNumber; +import org.graalvm.webimage.api.JSObject; +import org.graalvm.webimage.api.JSString; +import org.graalvm.webimage.api.JSUndefined; +import org.graalvm.webimage.api.JSValue; +import org.graalvm.webimage.api.ThrownFromJavaScript; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +public class ArrayProxyTest { + public static void main(String[] args) { + checkNoCoercion(); + System.out.println("checkNoCoercion DONE"); + checkArrayLoad(); + System.out.println("checkArrayLoad DONE"); + checkArrayStore(); + System.out.println("checkArrayStore DONE"); + checkArrayStoreMismatchedType(); + System.out.println("checkArrayStoreMismatchedType DONE"); + checkProxyIterator(); + System.out.println("checkProxyIterator DONE"); + checkProxyJSIteration(); + System.out.println("checkProxyJSIteration DONE"); + } + + /// Checks that Java arrays don't undergo coercion to JS array types. + public static void checkNoCoercion() { + assertNotCoercedToArrayOrTyped(new Object[1]); + assertNotCoercedToArrayOrTyped(new boolean[2]); + assertNotCoercedToArrayOrTyped(new short[3]); + assertNotCoercedToArrayOrTyped(new char[4]); + assertNotCoercedToArrayOrTyped(new int[5]); + assertNotCoercedToArrayOrTyped(new long[6]); + assertNotCoercedToArrayOrTyped(new float[7]); + assertNotCoercedToArrayOrTyped(new double[8]); + } + + /// Checks proper behavior of loads from Java primitive arrays in JS code. + /// + /// Loads from primitive arrays should undergo coercion and return a JS value. + public static void checkArrayLoad() { + boolean[] boolArray = new boolean[]{true, false, true}; + assertOutOfRangeLoadsAreUndefined(boolArray); + for (int i = 0; i < boolArray.length; i++) { + JSBoolean bool = assertInstanceOf(JSBoolean.class, arrayGet(boolArray, i), "Wrong type at index " + i + " for boolean array"); + assertEquals(boolArray[i], bool.asBoolean(), "at index " + i + " for boolean array"); + } + + short[] shortArray = new short[]{-1, 12, 0}; + assertOutOfRangeLoadsAreUndefined(shortArray); + for (int i = 0; i < shortArray.length; i++) { + JSNumber n = assertInstanceOf(JSNumber.class, arrayGet(shortArray, i), "Wrong type at index " + i + " for short array"); + assertEquals(shortArray[i], n.asShort(), "at index " + i + " for short array"); + } + + char[] charArray = new char[]{'a', '\n', 0}; + assertOutOfRangeLoadsAreUndefined(charArray); + for (int i = 0; i < charArray.length; i++) { + JSNumber n = assertInstanceOf(JSNumber.class, arrayGet(charArray, i), "Wrong type at index " + i + " for char array"); + assertEquals(charArray[i], n.asChar(), "at index " + i + " for char array"); + } + + int[] intArray = new int[]{1, 2, 3, Integer.MAX_VALUE}; + assertOutOfRangeLoadsAreUndefined(intArray); + for (int i = 0; i < intArray.length; i++) { + JSNumber num = assertInstanceOf(JSNumber.class, arrayGet(intArray, i), "Wrong type at index " + i + " for int array"); + assertEquals(intArray[i], num.asInt(), "at index " + i + " for int array"); + } + + long[] longArray = new long[]{-1, 12, Long.MIN_VALUE, Long.MAX_VALUE}; + assertOutOfRangeLoadsAreUndefined(longArray); + for (int i = 0; i < longArray.length; i++) { + JSBigInt bool = assertInstanceOf(JSBigInt.class, arrayGet(longArray, i), "Wrong type at index " + i + " for long array"); + assertEquals(longArray[i], bool.asLong(), "at index " + i + " for long array"); + } + + float[] floatArray = new float[]{Float.NaN, 1.2233450123572345f, -1, Float.NEGATIVE_INFINITY, Float.MIN_NORMAL, Float.MAX_VALUE, Float.MIN_VALUE}; + assertOutOfRangeLoadsAreUndefined(floatArray); + for (int i = 0; i < floatArray.length; i++) { + JSNumber n = assertInstanceOf(JSNumber.class, arrayGet(floatArray, i), "Wrong type at index " + i + " for float array"); + assertEquals(floatArray[i], n.asFloat(), "at index " + i + " for float array"); + } + + double[] doubleArray = new double[]{Double.NaN, 1.2233450123572345, -1, Double.NEGATIVE_INFINITY, Double.MIN_NORMAL, Double.MAX_VALUE, Double.MIN_VALUE}; + assertOutOfRangeLoadsAreUndefined(doubleArray); + for (int i = 0; i < doubleArray.length; i++) { + JSNumber n = assertInstanceOf(JSNumber.class, arrayGet(doubleArray, i), "Wrong type at index " + i + " for double array"); + assertEquals(doubleArray[i], n.asDouble(), "at index " + i + " for double array"); + } + } + + /// Checks proper behavior when storing into Java primitive array in JS code. + /// + /// Any number of BigInt value that's in range for the target type should be allowed. + public static void checkArrayStore() { + boolean[] boolArray = new boolean[]{true, false, true}; + arraySet(boolArray, 0, JSBoolean.of(false)); + arraySet(boolArray, 1, JSBoolean.of(true)); + arraySet(boolArray, 2, JSBoolean.of(false)); + assertArrayEquals(new boolean[]{false, true, false}, boolArray, "Stores did not take effect in boolean array"); + + short[] shortArray = new short[]{1, 2, 3, 4}; + short[] originalShortArray = shortArray.clone(); + arraySet(shortArray, -1, JSNumber.of(12)); + arraySet(shortArray, shortArray.length, JSNumber.of(33)); + assertArrayEquals(originalShortArray, shortArray, "short array was changed by out of bounds store"); + arraySet(shortArray, 0, JSNumber.of(5)); + // Arrays should also accept in-range BigInt value + arraySet(shortArray, 1, JSBigInt.of(12)); + arraySet(shortArray, 2, JSBigInt.of(Short.MAX_VALUE)); + assertArrayEquals(new short[]{5, 12, Short.MAX_VALUE, 4}, shortArray, "Stores did not take effect in short array"); + + char[] charArray = new char[]{0, 12, 99}; + arraySet(charArray, 0, JSNumber.of(Character.MAX_VALUE)); + arraySet(charArray, 1, JSNumber.of(0)); + arraySet(charArray, 2, JSBigInt.of(128)); + assertArrayEquals(new char[]{Character.MAX_VALUE, 0, 128}, charArray, "Stores did not take effect in char array"); + + int[] intArray = new int[]{0, 12, 99}; + arraySet(intArray, 0, JSNumber.of(Integer.MAX_VALUE)); + arraySet(intArray, 1, JSNumber.of(Integer.MIN_VALUE)); + arraySet(intArray, 2, JSBigInt.of(128)); + assertArrayEquals(new int[]{Integer.MAX_VALUE, Integer.MIN_VALUE, 128}, intArray, "Stores did not take effect in int array"); + + long[] longArray = new long[]{1, 2, 3, 4}; + arraySet(longArray, 0, JSBigInt.of(12)); + // Long arrays should also accept integer values + arraySet(longArray, 1, JSNumber.of(-100)); + arraySet(longArray, 2, JSNumber.of(12)); + assertArrayEquals(new long[]{12, -100, 12, 4}, longArray, "Stores did not take effect in long array"); + + float[] floatArray = new float[]{1, 2, 3, 4}; + arraySet(floatArray, 0, JSNumber.of(1.2f)); + // Floating point arrays should accept all BigInt values with potential loss of precision + arraySet(floatArray, 1, JSBigInt.of(-100)); + // This is the first integer that a 32-bit float can't accurately represent, should lose + // precision when stored + BigInteger largeBigIntFloat = BigInteger.valueOf(16_777_217); + arraySet(floatArray, 2, JSBigInt.of(largeBigIntFloat)); + assertArrayEquals(new float[]{1.2f, -100, largeBigIntFloat.floatValue(), 4}, floatArray, "Stores did not take effect in float array"); + + double[] doubleArray = new double[]{1, 2, 3, 4}; + arraySet(doubleArray, 0, JSNumber.of(1.2)); + // doubleing point arrays should accept all BigInt values with potential loss of precision + arraySet(doubleArray, 1, JSBigInt.of(-100)); + // This is the first integer that a 64-bit double can't accurately represent, should lose + // precision when stored + BigInteger largeBigIntDouble = BigInteger.valueOf(JSNumber.maxSafeInteger() + 1); + arraySet(doubleArray, 2, JSBigInt.of(largeBigIntDouble)); + assertArrayEquals(new double[]{1.2d, -100, largeBigIntDouble.doubleValue(), 4}, doubleArray, "Stores did not take effect in double array"); + } + + /// Checks that the proper errors are thrown when inserting incompatible JS values into Java + /// primitive arrays. + /// + /// The store should throw a JS `RangeError` whenever the inserted value is not inside the range + /// of valid types of the target type. For integer types that are out of range or non-integer + /// floating point values. `float` and `double` arrays accept any number value, though + /// possibly with a loss of precision. + public static void checkArrayStoreMismatchedType() { + boolean[] boolArray = new boolean[]{true}; + int[] intArray = new int[]{12}; + long[] longArray = new long[]{Long.MAX_VALUE}; + + assertArraySetThrowsError("TypeError", "Invalid type number for insertion into boolean array", boolArray, 0, JSNumber.of(0), "number"); + assertArraySetThrowsError("TypeError", "Invalid type bigint for insertion into boolean array", boolArray, 0, JSBigInt.of(0), "bigint"); + assertArraySetThrowsError("TypeError", "Invalid type undefined for insertion into boolean array", boolArray, 0, JSValue.undefined(), "undefined"); + + assertArraySetThrowsError("TypeError", "Invalid type string for insertion into int array", intArray, 0, JSString.of("foo"), "string"); + assertArraySetThrowsError("RangeError", "Non-integer number 0.5 for insertion into int array", intArray, 0, JSNumber.of(0.5), "non-integer"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSNumber.of(JSNumber.maxSafeInteger()), "too big"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSNumber.of(JSNumber.minSafeInteger()), "too small"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSNumber.of(Integer.MIN_VALUE - 1.0), "too small"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSNumber.of(Integer.MAX_VALUE + 1.0), "too big"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSBigInt.of(JSNumber.maxSafeInteger()), "too big"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSBigInt.of(JSNumber.minSafeInteger()), "too small"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSBigInt.of(Integer.MIN_VALUE - 1L), "too small"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into int array", intArray, 0, JSBigInt.of(Integer.MAX_VALUE + 1L), "too big"); + + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into long array", longArray, 0, JSBigInt.of(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)), "too big"); + assertArraySetThrowsError("RangeError", "Out of range value %s for insertion into long array", longArray, 0, JSBigInt.of(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE)), + "too small"); + assertArraySetThrowsError("RangeError", "Non-integer number NaN for insertion into long array", longArray, 0, JSNumber.of(Double.NaN), "non-integer"); + assertArraySetThrowsError("RangeError", "Non-integer number -Infinity for insertion into long array", longArray, 0, JSNumber.of(Double.NEGATIVE_INFINITY), "non-integer"); + } + + /// Checks that the proxy for Java arrays conforms to the [iterable + /// protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). + /// + /// The proxy has to have a `[Symbol.iterator]()` method, returning an iterator on which + /// `next()` can be called to get the next iteration result. + public static void checkProxyIterator() { + byte[] byteArray = new byte[]{1, 2, 3, 4}; + JSObject iterator = assertInstanceOf(JSObject.class, getProxyIterator(byteArray)); + JSObject nextFunction = iterator.get("next", JSObject.class); + + for (int i = 0; i < byteArray.length; i++) { + Object iteratorResult = nextFunction.call(iterator); + // Put into final variable to be used in lambda + final int index = i; + assertFullIteratorResult(i, iteratorResult, value -> { + JSNumber num = assertInstanceOf(JSNumber.class, value); + byte byteValue = num.asByte(); + assertEquals(byteArray[index], byteValue, "Wrong value at index " + index); + }); + } + + assertIterationDone(nextFunction.call(iterator)); + assertIterationDone(nextFunction.call(iterator)); + assertIterationDone(nextFunction.call(iterator)); + } + + /** + * Checks that the proxy for Java arrays can be used with JS iteration (`for` loop, spread + * iterator). + */ + public static void checkProxyJSIteration() { + int[] intArray = new int[]{1, 2, 99}; + + int sum = IntStream.of(intArray).sum(); + int product = IntStream.of(intArray).reduce(0, (a, b) -> a * b); + + assertEquals(sum, sumInLoop(intArray), "Sum mismatch"); + assertEquals(product, productUsingReduce(intArray), "Product mismatch"); + } + + private static void assertFullIteratorResult(int index, Object iterationResult, Consumer valueMatcher) { + JSObject result = assertInstanceOf(JSObject.class, iterationResult, "Iterator result for index " + index + " is not a JSObject"); + assertTrue(hasKey(result, "done"), "Iterator result for index " + index + " does not have 'done' property"); + assertTrue(hasKey(result, "value"), "Iterator result for index " + index + " does not have 'value' property"); + boolean done = result.get("done", Boolean.class); + assertFalse(done, "Iterator was done at index " + index); + valueMatcher.accept(result.get("value")); + } + + private static void assertIterationDone(Object iterationResult) { + JSObject result = assertInstanceOf(JSObject.class, iterationResult, "Iterator result is not a JSObject"); + assertTrue(hasKey(result, "done"), "Iterator result does not have 'done' property"); + boolean done = result.get("done", Boolean.class); + assertTrue(done, "Iterator was not done"); + } + + /// Inserts the given JS value into the given Java array at the given index and asserts it + /// throws a JS exception (e.g. `RangeError`). + /// + /// @param expectedType Expected value of the Error.name property + /// @param expectedMessagePattern Expected Error.message property. Can contain a %s conversion + /// that is replaced using [String#formatted(Object...) ]. + /// @param message Context message that describes what's supposed to go wrong. Added to every + /// assertion + /// + private static void assertArraySetThrowsError(String expectedType, String expectedMessagePattern, Object arr, int idx, JSValue value, String message) { + String fullMessage = message + " in " + arr.getClass().getTypeName(); + String errorMessage = assertThrowsJSError(expectedType, () -> arraySet(arr, idx, value), fullMessage); + String expectedMessage = expectedMessagePattern.formatted(value.stringValue()); + assertEquals(expectedMessage, errorMessage, fullMessage); + } + + /// @return The `Error.message` property + private static String assertThrowsJSError(String expectedType, Executable e, String message) { + ThrownFromJavaScript jsExc = assertThrows(ThrownFromJavaScript.class, e, message); + JSObject thrownObject = assertInstanceOf(JSObject.class, jsExc.getThrownObject(), message); + assertTrue(isError(thrownObject), "Thrown object " + thrownObject + " is not an error: " + message); + String name = thrownObject.get("name", String.class); + assertEquals(expectedType, name, message); + return thrownObject.get("message", String.class); + } + + private static void assertNotCoercedToArrayOrTyped(Object o) { + Assertions.assertNotNull(o, "Object to check is null"); + assertFalse(isCoercedToArray(o), "Object of type " + o.getClass() + " is coerced to JS Array"); + assertFalse(isCoercedToTypedArray(o), "Object of type " + o.getClass() + " is coerced to JS Typed Array"); + } + + private static void assertOutOfRangeLoadsAreUndefined(Object arr) { + int length = Array.getLength(arr); + + assertInstanceOf(JSUndefined.class, arrayGet(arr, -1), "Array of type " + arr + " does not return undefined at index -1"); + assertInstanceOf(JSUndefined.class, arrayGet(arr, length), "Array of type " + arr + " does not return undefined at index " + length); + } + + @JS.Coerce + @JS("return o instanceof Error;") + private static native boolean isError(Object o); + + @JS.Coerce + @JS("return Array.isArray(o);") + private static native boolean isCoercedToArray(Object o); + + @JS.Coerce + @JS("return ArrayBuffer.isView(o) && !(o instanceof DataView);") + private static native boolean isCoercedToTypedArray(Object o); + + @JS.Coerce + @JS("return arr[idx];") + private static native Object arrayGet(Object arr, int idx); + + @JS.Coerce + @JS("arr[idx] = value;") + private static native Object arraySet(Object arr, int idx, JSValue value); + + @JS("return arr[Symbol.iterator]();") + private static native Object getProxyIterator(Object arr); + + @JS.Coerce + @JS("return Reflect.has(obj, key);") + private static native boolean hasKey(JSObject obj, String key); + + @JS.Coerce + @JS(""" + let sum = 0; + for (var x of arr) { + sum += x; + } + + return sum; + """) + private static native int sumInLoop(Object arr); + + @JS.Coerce + @JS("return [...arr].reduce((a, c) => a * c, 0);") + private static native int productUsingReduce(Object arr); +} diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/CoercionConversionTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/CoercionConversionTest.java index 33de429db6a1..749a258fea1c 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/CoercionConversionTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/CoercionConversionTest.java @@ -55,7 +55,7 @@ public class CoercionConversionTest { "102030405060", "bigint", "9876543210", "bigint", "freestyla", "string", - "1,2,3,4,5", "object", + "[Java Proxy: int[]]", "function", "Tuple(x=3, y=4)", "function", // Return-value coercion tests. "true", @@ -63,7 +63,6 @@ public class CoercionConversionTest { "B", "9876543210", "MC", - "100,101,102", "Tuple(x=1, y=2)", "ghost in the virtual machine", "undefined", @@ -123,11 +122,11 @@ private static void argumentCoercion() { log("freestyla"); typeof("rock da microphone"); + // All other types are not coerced, and are exposed as Java Proxies. int[] ints = new int[]{1, 2, 3, 4, 5}; logToString(ints); typeof(ints); - // All other types are not coerced, and are exposed as Java Proxies. log(new Tuple(3, 4)); typeof(new Tuple(3, 4)); } @@ -151,14 +150,6 @@ private static void returnValueCoercion() { System.out.println(b()); System.out.println(bigDaddy("9876543210")); System.out.println(bomfunk()); - byte[] bytes = primitivo(); - for (int i = 0; i < bytes.length; i++) { - if (i > 0) { - System.out.print(','); - } - System.out.print(bytes[i]); - } - System.out.println(); System.out.println(returnSame(new Tuple(1, 2))); System.out.println(returnCharSequence(new StringBuilder("ghost in the virtual machine"))); System.out.println(returnUndefined().typeof()); @@ -185,10 +176,6 @@ private static void returnValueCoercion() { @JS("return 'MC';") private static native String bomfunk(); - @JS.Coerce - @JS("let xs = new Int8Array(3); xs[0] = 100; xs[1] = 101; xs[2] = 102; return xs;") - private static native byte[] primitivo(); - @JS.Coerce @JS("return tuple;") private static native Object returnSame(Tuple tuple); diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSNumberTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSNumberTest.java index 3d1c41dfba98..98f5bea5d437 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSNumberTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSNumberTest.java @@ -25,14 +25,14 @@ package com.oracle.svm.webimage.jtt.api; -import org.graalvm.webimage.api.JSNumber; -import org.graalvm.webimage.api.JSObject; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import org.graalvm.webimage.api.JSNumber; +import org.graalvm.webimage.api.JSObject; + public class JSNumberTest { static final double DOUBLE_SMALL = 3.14159; @@ -195,9 +195,9 @@ public static void testToStringRadix() { JSNumber pi = JSNumber.of(DOUBLE_SMALL); JSNumber neg = JSNumber.of(NEG_INT); - assertEquals("JavaScript", hex.toString()); + assertEquals("JavaScript", hex.toString()); assertEquals("JavaScript", pi.toString()); - assertEquals("JavaScript", neg.toString()); + assertEquals("JavaScript", neg.toString()); assertEquals("11111111", hex.toString(2)); assertEquals("ff", hex.toString(16)); assertEquals("377", hex.toString(8)); diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectConversionTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectConversionTest.java index 3476093e7107..d4b8d8b74025 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectConversionTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JSObjectConversionTest.java @@ -32,23 +32,7 @@ public class JSObjectConversionTest { public static final String[] OUTPUT = { "JavaScript", "[object Object]", "JavaScript", "true", "true", "false", - "JavaScript", "1,0,0,0", "true", - "JavaScript cannot be coerced to 'double[]'.", - "JavaScript", "11,0,0,0,0,0,0,0,0,0", "11", "78", - "JavaScript cannot be coerced to 'short[]'.", - "JavaScript", "14,0,0,0", "14", - "JavaScript cannot be coerced to 'int[]'.", - "JavaScript", "65,0,0,0", "A", - "JavaScript cannot be coerced to 'boolean[]'.", - "JavaScript", "123456,0,0,0", "123456", - "JavaScript cannot be coerced to 'long[]'.", - "JavaScript", "-22.5,0,0", "-22.5", - "JavaScript cannot be coerced to 'byte[]'.", - "JavaScript", "1000123456,0,0", "1000123456", - "JavaScript cannot be coerced to 'char[]'.", - "JavaScript", "0.0625,0,0", "0.0625", - "JavaScript cannot be coerced to 'float[]'.", - "JavaScript", "JavaScript", + "JavaScript", "JavaScript", "12", "js value", "Field type modified!", @@ -59,7 +43,6 @@ public class JSObjectConversionTest { public static void main(String[] args) { passingAnonymousObject(); - objectToArrayConversion(); functionPassingAndInvoking(); jsObjectInvalidFieldAccess(); } @@ -81,126 +64,6 @@ private static void passingAnonymousObject() { @JS("return x;") private static native Object id(Object x); - private static void objectToArrayConversion() { - // Object-to-array conversion. - JSObject booleanArrayObject = createBooleanArray(JSNumber.of(4)); - System.out.println(booleanArrayObject); - log(booleanArrayObject); - boolean[] booleans = booleanArrayObject.asBooleanArray(); - System.out.println(booleans[0]); - try { - booleanArrayObject.asDoubleArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject byteArrayObject = createByteArray(JSNumber.of(10)); - System.out.println(byteArrayObject); - log(byteArrayObject); - byte[] bytes = byteArrayObject.asByteArray(); - System.out.println(bytes[0]); - bytes[0] = 78; - System.out.println(getByte0(byteArrayObject).asByte()); - try { - byteArrayObject.asShortArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject shortArrayObject = createShortArray(JSNumber.of(4)); - System.out.println(shortArrayObject); - log(shortArrayObject); - short[] shorts = shortArrayObject.asShortArray(); - System.out.println(shorts[0]); - try { - shortArrayObject.asIntArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject charArrayObject = createCharArray(JSNumber.of(4)); - System.out.println(charArrayObject); - log(charArrayObject); - char[] chars = charArrayObject.asCharArray(); - System.out.println(chars[0]); - try { - charArrayObject.asBooleanArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject intArrayObject = createIntArray(JSNumber.of(4)); - System.out.println(intArrayObject); - log(intArrayObject); - int[] ints = intArrayObject.asIntArray(); - System.out.println(ints[0]); - try { - intArrayObject.asLongArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject floatArrayObject = createFloatArray(JSNumber.of(3)); - System.out.println(floatArrayObject); - log(floatArrayObject); - float[] floats = floatArrayObject.asFloatArray(); - System.out.println(floats[0]); - try { - floatArrayObject.asByteArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject longArrayObject = createLongArray(JSNumber.of(3)); - System.out.println(longArrayObject); - log(longArrayObject); - long[] longs = longArrayObject.asLongArray(); - System.out.println(longs[0]); - try { - longArrayObject.asCharArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - JSObject doubleArrayObject = createDoubleArray(JSNumber.of(3)); - System.out.println(doubleArrayObject); - log(doubleArrayObject); - double[] doubles = doubleArrayObject.asDoubleArray(); - System.out.println(doubles[0]); - try { - doubleArrayObject.asFloatArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - } - - @JS("const arr = new Uint8Array(length); arr[0] = 1; return arr;") - private static native JSObject createBooleanArray(JSNumber length); - - @JS("const arr = new Int8Array(length); arr[0] = 11; return arr;") - private static native JSObject createByteArray(JSNumber length); - - @JS("return byteArray[0];") - private static native JSNumber getByte0(JSObject byteArray); - - @JS("const arr = new Int16Array(length); arr[0] = 14; return arr;") - private static native JSObject createShortArray(JSNumber length); - - @JS("const arr = new Uint16Array(length); arr[0] = 65; return arr;") - private static native JSObject createCharArray(JSNumber length); - - @JS("const arr = new Int32Array(length); arr[0] = 123456; return arr;") - private static native JSObject createIntArray(JSNumber length); - - @JS("const arr = new Float32Array(length); arr[0] = -22.5; return arr;") - private static native JSObject createFloatArray(JSNumber length); - - @JS("const arr = new BigInt64Array(length); arr[0] = BigInt('1000123456'); return arr;") - private static native JSObject createLongArray(JSNumber length); - - @JS("const arr = new Float64Array(length); arr[0] = 0.0625; return arr;") - private static native JSObject createDoubleArray(JSNumber length); - private static void functionPassingAndInvoking() { // Function creation, passing through boundaries and invoking. JSObject add = addFunction(); 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 e054a416307d..d1a1a5de9472 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 @@ -37,7 +37,7 @@ public class JavaDocExamplesTest { "User message: Initialization completed.", "3", // JSObject "3.2", "4.8", "5.4", - "JavaScript", + "JavaScript", "0.3", "0.4", "0.5", "Type mismatch: 'whoops' cannot be coerced to 'Double'.", "1.5, 2.5", "0.0, 0.0", "1.25, 0.5", "640x480", diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaProxyConversionTest.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaProxyConversionTest.java index c4c718945d8d..0e6f24f13fe3 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaProxyConversionTest.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/api/JavaProxyConversionTest.java @@ -41,6 +41,7 @@ public class JavaProxyConversionTest { public static final String[] OUTPUT = { + "passingProxies", "Cell(10)", "Cell(10)", "Cell(10)", @@ -71,10 +72,12 @@ public class JavaProxyConversionTest { "1.125", "1.0625", "Guess who.", + + "proxyToBasicTypeConversion", "JavaScript", "true", "'true' cannot be coerced to a JavaScript 'function'.", - "JavaScript", + "JavaScript", "121", "'4141.0' cannot be coerced to a JavaScript 'string'.", "JavaScript", @@ -82,20 +85,10 @@ public class JavaProxyConversionTest { "JavaScript", "JavaScript", "Go proxy!", - "'Proxy power!' cannot be coerced to a JavaScript 'object'.", - "true", - "JavaScript cannot be coerced to 'double[]'.", "Lambda call! Can't touch this.", "It's raining proxies.", - "true", - "true", - "true", - "true", - "true", - "true", - "true", - "true", - "cannot be coerced to a JavaScript 'Float32Array'.", + + "proxyCalls", "Rock'n'roll", "3", "Rock'n'roll", @@ -105,10 +98,16 @@ public class JavaProxyConversionTest { "[717: Integer]", "[717: Integer]", "717", + + "proxyNewOperator", "[]", "[Great success!, For the win!]", + + "passingLambdaToVirtual", "JavaScript", "JavaScript", + + "indexedAccessProxiedArray", }; public static void main(String[] args) { @@ -121,6 +120,7 @@ public static void main(String[] args) { } private static void passingProxies() { + System.out.println("passingProxies"); Cell cell = new Cell(10); logJavaProxy(cell); System.out.println(id(cell)); @@ -216,6 +216,7 @@ private static void passingProxies() { private static native double idDouble(double d); private static void proxyToBasicTypeConversion() { + System.out.println("proxyToBasicTypeConversion"); JSBoolean trueValue = coerceToBoolean(true); System.out.println(trueValue); log(trueValue); @@ -242,64 +243,12 @@ private static void proxyToBasicTypeConversion() { JSString string = coerceToString("Go proxy!"); System.out.println(string); log(string); - try { - failedCoerceToString("Proxy power!"); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } - - byte[] bytes = new byte[3]; - bytes[0] = 3; - byte[] bytes0 = coerceToObject(bytes).asByteArray(); - System.out.println(bytes == bytes0); - try { - coerceToObject(bytes).asDoubleArray(); - } catch (ClassCastException e) { - System.out.println(e.getMessage()); - } JSObject fun = coerceRunnableToFunction(() -> System.out.println("Lambda call! Can't touch this.")); fun.invoke(); JSObject fun1 = coerceConsumerToFunction(x -> System.out.println("It's raining " + x + ".")); fun1.invoke("proxies"); - - boolean[] booleans = new boolean[3]; - booleans[0] = true; - boolean[] booleans1 = coerceToTypedArray(booleans).asBooleanArray(); - System.out.println(booleans == booleans1); - - byte[] bytes1 = coerceToTypedArray(bytes).asByteArray(); - System.out.println(bytes == bytes1); - - short[] shorts = new short[3]; - short[] shorts1 = coerceToTypedArray(shorts).asShortArray(); - System.out.println(shorts == shorts1); - - char[] chars = new char[3]; - char[] chars1 = coerceToTypedArray(chars).asCharArray(); - System.out.println(chars == chars1); - - int[] ints = new int[3]; - int[] ints1 = coerceToTypedArray(ints).asIntArray(); - System.out.println(ints == ints1); - - float[] floats = new float[3]; - float[] floats1 = coerceToTypedArray(floats).asFloatArray(); - System.out.println(floats == floats1); - - long[] longs = new long[3]; - long[] longs1 = coerceToTypedArray(longs).asLongArray(); - System.out.println(longs == longs1); - - double[] doubles = new double[3]; - double[] doubles1 = coerceToTypedArray(doubles).asDoubleArray(); - System.out.println(doubles == doubles1); - try { - failedCoerceToTypedFloatArray(doubles); - } catch (ClassCastException e) { - System.out.println(e.getMessage().substring(e.getMessage().indexOf("cannot"))); - } } @JS("console.log(x.toString());") @@ -335,34 +284,8 @@ private static void proxyToBasicTypeConversion() { @JS("return r.$as('function');") private static native JSObject coerceConsumerToFunction(Consumer r); - @JS("return booleans.$as(Uint8Array);") - private static native JSObject coerceToTypedArray(boolean[] booleans); - - @JS("return bytes.$as(Int8Array);") - private static native JSObject coerceToTypedArray(byte[] bytes); - - @JS("return shorts.$as(Int16Array);") - private static native JSObject coerceToTypedArray(short[] shorts); - - @JS("return chars.$as(Uint16Array);") - private static native JSObject coerceToTypedArray(char[] chars); - - @JS("return ints.$as(Int32Array);") - private static native JSObject coerceToTypedArray(int[] ints); - - @JS("return floats.$as(Float32Array);") - private static native JSObject coerceToTypedArray(float[] floats); - - @JS("return longs.$as(BigInt64Array);") - private static native JSObject coerceToTypedArray(long[] longs); - - @JS("return doubles.$as(Float64Array);") - private static native JSObject coerceToTypedArray(double[] doubles); - - @JS("return doubles.$as(Float32Array);") - private static native JSObject failedCoerceToTypedFloatArray(double[] doubles); - private static void proxyCalls() { + System.out.println("proxyCalls"); String text = " Rock'n'roll "; System.out.println(text.trim()); System.out.println(text.indexOf("o", 0)); @@ -400,6 +323,7 @@ private static void proxyCalls() { private static native Object get(SubCell cell); private static void proxyNewOperator() { + System.out.println("proxyNewOperator"); System.out.println(new ArrayList<>()); ArrayList list = createArrayList(ArrayList.class); list.add("Great success!"); @@ -413,11 +337,13 @@ private static void proxyNewOperator() { private static Cell[] cells = new Cell[]{new Cell("content"), new SubCell("subcontent")}; private static void passingLambdaToVirtual() { + System.out.println("passingLambdaToVirtual"); System.out.println(cells[0].jsName()); System.out.println(cells[1].jsName()); } private static void indexedAccessProxiedArray() { + System.out.println("indexedAccessProxiedArray"); Object[] arr = new Object[]{"abc", 5}; for (int i = 0; i < arr.length; i++) { Object result = getIndexedProxyArray(arr, JSNumber.of(i)); diff --git a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/testdispatcher/JSAnnotationTests.java b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/testdispatcher/JSAnnotationTests.java index 1c904555cf6e..98a2321e53b6 100644 --- a/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/testdispatcher/JSAnnotationTests.java +++ b/web-image/src/com.oracle.svm.webimage.jtt/src/com/oracle/svm/webimage/jtt/testdispatcher/JSAnnotationTests.java @@ -26,6 +26,7 @@ import java.util.Arrays; +import com.oracle.svm.webimage.jtt.api.ArrayProxyTest; import com.oracle.svm.webimage.jtt.api.CoercionConversionTest; import com.oracle.svm.webimage.jtt.api.HtmlApiExamplesTest; import com.oracle.svm.webimage.jtt.api.JSErrorsTest; @@ -76,6 +77,8 @@ public static void main(String[] args) { JSObjectTest.main(remainingArgs); } else if (checkClass(JSObjectCoercionTest.class, className)) { JSObjectCoercionTest.main(remainingArgs); + } else if (checkClass(ArrayProxyTest.class, className)) { + ArrayProxyTest.main(remainingArgs); } else { throw new IllegalArgumentException("unexpected class name"); } diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/fs/FileSystemInitializer.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/fs/FileSystemInitializer.java index 12913273feea..166f753e96a0 100644 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/fs/FileSystemInitializer.java +++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/fs/FileSystemInitializer.java @@ -37,7 +37,6 @@ import org.graalvm.webimage.api.JSObject; import org.graalvm.webimage.api.JSString; -import com.oracle.svm.webimage.platform.WebImageWasmGCPlatform; import com.oracle.svm.webimage.platform.WebImageWasmLMPlatform; import jdk.graal.compiler.debug.GraalError; @@ -91,12 +90,10 @@ static void populate(FileSystem fileSystem) { Files.createFile(fileSystem.getPath("/dev", "urandom")); /* - * In the WasmLM and WasmGC backend, the @JS annotation is not supported and thus, - * prefetched libraries neither because they can't be loaded from the JavaScript code. - * - * TODO GR-60603 Support @JS annotation in WasmGC backend + * In the WasmLM backend, the @JS annotation is not supported and thus, prefetched + * libraries neither because they can't be loaded from the JavaScript code. */ - if (!Platform.includedIn(WebImageWasmLMPlatform.class) && !Platform.includedIn(WebImageWasmGCPlatform.class)) { + if (!Platform.includedIn(WebImageWasmLMPlatform.class)) { // Store the prefetched libraries into the file system. JSObject prefetchedLibraryNames = prefetchedLibraryNames(); diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/functionintrinsics/JSConversion.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/functionintrinsics/JSConversion.java index 5802a15e9f67..81575030383f 100644 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/functionintrinsics/JSConversion.java +++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/functionintrinsics/JSConversion.java @@ -25,21 +25,23 @@ package com.oracle.svm.webimage.functionintrinsics; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.math.BigInteger; +import java.util.Objects; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platforms; import org.graalvm.webimage.api.JS; import org.graalvm.webimage.api.JSBigInt; import org.graalvm.webimage.api.JSBoolean; -import org.graalvm.webimage.api.ThrownFromJavaScript; import org.graalvm.webimage.api.JSNumber; import org.graalvm.webimage.api.JSObject; import org.graalvm.webimage.api.JSString; import org.graalvm.webimage.api.JSSymbol; import org.graalvm.webimage.api.JSUndefined; import org.graalvm.webimage.api.JSValue; +import org.graalvm.webimage.api.ThrownFromJavaScript; import com.oracle.svm.core.util.VMError; import com.oracle.svm.webimage.JSExceptionSupport; @@ -105,6 +107,32 @@ public static int getKindOrdinal(Class clazz) { return JavaKind.Object.ordinal(); } + @WasmExport(value = "object.getclass", comment = "Get Class from object") + @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) + public static Class getClass(Object o) { + return o.getClass(); + } + + @WasmExport(value = "class.superclass", comment = "Gets superclass of given non-primitive non-object class") + @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) + public static Class getSuperclass(Class clazz) { + Class superclass = clazz.getSuperclass(); + assert superclass != null || clazz == Object.class : "Cannot get superclass of " + clazz + ". Only instance and array classes are allowed"; + return superclass; + } + + @WasmExport(value = "class.componenttype", comment = "The component type of the array class") + @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) + public static Class getComponentType(Class clazz) { + return clazz.getComponentType(); + } + + @WasmExport(value = "class.getname", comment = "Get fully qualified class name") + @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) + public static String getClassName(Class clazz) { + return clazz.getTypeName(); + } + // Creation of JSValue objects /** @@ -206,6 +234,10 @@ public static double extractJavaScriptNumber(Double d) { public abstract void setJavaScriptNativeImpl(JSValue self, Object jsNative); public static void setJavaScriptNative(JSValue self, Object jsNative) { + if (jsNative == null || JSFunctionIntrinsics.isUndefined(jsNative)) { + throw VMError.shouldNotReachHere("null and undefined cannot be used as the jsNative value for " + self.getClass()); + } + Objects.requireNonNull(jsNative, "Cannot use null as jsNative value"); instance().setJavaScriptNativeImpl(self, jsNative); } @@ -376,122 +408,21 @@ public static Object coerceToJavaScriptString(Object obj) { throw throwClassCastException(obj, "string"); } - /** - * Coerces a Java array to the corresponding typed array. - * - * All other Java classes cannot be coerced. This method must not be called with {@link JSValue} - * subclasses -- only Java objects that become Java Proxies are valid arguments. - */ - @WasmExport("convert.coerce.object") - @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) - public static Object coerceToJavaScriptObject(Object obj) { - if (obj instanceof boolean[] || obj instanceof byte[] || obj instanceof char[] || obj instanceof short[] || obj instanceof int[] || obj instanceof float[] || obj instanceof long[] || - obj instanceof double[]) { - return obj; - } - throw throwClassCastException(obj, "object"); - } - - /** - * Coerces a Java {@code boolean[]} object to a JavaScript Uint8Array. - */ - public static Object coerceToJavaScriptUint8Array(Object obj) { - if (obj instanceof boolean[]) { - return obj; - } - throw throwClassCastException(obj, "Uint8Array"); - } - - /** - * Coerces a Java {@code byte[]} object to a JavaScript Int8Array. - */ - public static Object coerceToJavaScriptInt8Array(Object obj) { - if (obj instanceof byte[]) { - return obj; - } - throw throwClassCastException(obj, "Int8Array"); - } - - /** - * Coerces a Java {@code char[]} object to a JavaScript Uint16Array. - */ - public static Object coerceToJavaScriptUint16Array(Object obj) { - if (obj instanceof char[]) { - return obj; - } - throw throwClassCastException(obj, "Uint16Array"); - } - - /** - * Coerces a Java {@code short[]} object to a JavaScript Int16Array. - */ - public static Object coerceToJavaScriptInt16Array(Object obj) { - if (obj instanceof short[]) { - return obj; - } - throw throwClassCastException(obj, "Int16Array"); - } - - /** - * Coerces a Java {@code int[]} object to a JavaScript Int32Array. - */ - public static Object coerceToJavaScriptInt32Array(Object obj) { - if (obj instanceof int[]) { - return obj; - } - throw throwClassCastException(obj, "Int32Array"); - } - - /** - * Coerces a Java {@code float[]} object to a JavaScript Float32Array. - */ - public static Object coerceToJavaScriptFloat32Array(Object obj) { - if (obj instanceof float[]) { - return obj; - } - throw throwClassCastException(obj, "Float32Array"); - } - - /** - * Coerces a Java {@code long[]} object to a JavaScript BigInt64Array. - */ - public static Object coerceToJavaScriptBigInt64Array(Object obj) { - if (obj instanceof long[]) { - return obj; - } - throw throwClassCastException(obj, "BigInt64Array"); - } - public static char[] createCharArray(int length) { return new char[length]; } - /** - * Coerces a Java {@code double[]} object to a JavaScript Float64Array. - */ - public static Object coerceToJavaScriptFloat64Array(Object obj) { - if (obj instanceof double[]) { - return obj; - } - throw throwClassCastException(obj, "Float64Array"); - } - // Various other helper methods. /** * Returns the length of the specified array. - * + *

* This method is intended to be called from JavaScript. */ - public static int lengthOf(Object[] array) { - return array.length; - } - - /** - * Returns the hub of the specified object. - */ - public static Class hubOf(Object x) { - return x.getClass(); + @WasmExport("convert.arraylength") + @Platforms({WebImageJSPlatform.class, WebImageWasmGCPlatform.class}) + public static int lengthOf(Object array) { + return Array.getLength(array); } /** @@ -638,15 +569,6 @@ public static Object coerceJavaToJavaScript(Object x) { return JSNumber.of(c); } - // Step 7: check if the object is a primitive array. - Class cls = x.getClass(); - if (cls.isArray() && cls.getComponentType().isPrimitive()) { - // Note: in this case, x is also a valid JavaScript object, because Java primitive - // arrays are encoded as JavaScript typed arrays. - // TODO GR-60603 This is not the case for the WasmGC backend - return createJSObject(x); - } - // No coercion rule applies, return the original object. return x; } diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/WebImageHttpHandlerSubstitutions.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/WebImageHttpHandlerSubstitutions.java index 752da6035a0c..d2ace2bbfbae 100644 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/WebImageHttpHandlerSubstitutions.java +++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/WebImageHttpHandlerSubstitutions.java @@ -25,7 +25,6 @@ package com.oracle.svm.webimage.substitute; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.Proxy; @@ -35,10 +34,12 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.webimage.api.JS; +import org.graalvm.webimage.api.JSObject; +import org.graalvm.webimage.api.JSString; +import org.graalvm.webimage.api.JSValue; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -import com.oracle.svm.webimage.annotation.JSRawCall; /** * Handle HTTP and HTTPS URLs using XMLHttpRequest (instead of TCP sockets). The handlers are used @@ -69,7 +70,7 @@ static URLConnection openConnection(URL u, Proxy p) throws IOException { // The first call to connect() creates the request, sends it, and saves the response. // The saved response is used to implement getInputStream(). private static final class XhrUrlConnection extends URLConnection { - private byte[] content; + private JSObject content; XhrUrlConnection(URL url) { super(url); @@ -80,59 +81,72 @@ public void connect() throws IOException { if (content != null) { return; } - if (jsConnect(url.toExternalForm())) { - content = jsGetContent(); - } else { - throw new IOException(jsGetMessage()); + JSValue connectResult = jsConnect(url.toExternalForm()); + switch (connectResult) { + case JSString jsString -> throw new IOException(jsString.asString()); + case JSObject jsByteArray -> content = jsByteArray; + default -> throw new IOException("Got unexpected return value from jsConnect: " + connectResult); } } @Override public InputStream getInputStream() throws IOException { connect(); - return new ByteArrayInputStream(content); + return new JSByteArrayInputStream(content); } - // Creates the request and sends it. In addition to the return value, - // a temporary JavaScript field is set and can be retrieved using - // jsGetContent (if this method returns true) or - // jsGetMessage (if this method returns false). - // @formatter:off - @JSRawCall - @JS("" - + "try {\n" - + " if('window' in self) {\n" // are we on the main thread and not worker? - + " this.r = 'HTTP(S) URL connections are not allowed on main thread';\n" - + " return false;\n" - + " }\n" - + " var xhr = new XMLHttpRequest();\n" - + " xhr.open('GET', urlString.toJSString(), false);\n" - + " xhr.responseType = 'arraybuffer';\n" - + " xhr.send();\n" - + " if(xhr.status === 200) {\n" - + " this.r = new Int8Array(xhr.response);\n" - + " return true;\n" - + " } else {\n" - + " this.r = xhr.status + ' ' + xhr.statusText;\n" // e.g. "404 Not Found" - + " return false;\n" - + " }\n" - + "} catch(e) {\n" - + " this.r = e.toString();\n" - + " return false;\n" - + "}\n" - ) - // @formatter:on - private native boolean jsConnect(String urlString); - - // Returns and clears the field set by jsConnect. + /// Creates the request and sends it. + /// + /// @return If the sending returned a 200 status code, returns a [JSObject] that wraps a + /// `Uint8Array`, otherwise returns a `JSString` containing the error message. @JS.Coerce - @JS("var request = this[runtime.symbol.javaNative]; var r = request.r; request.r = null; return r;") - private native byte[] jsGetContent(); + @JS(""" + try { + if('window' in self) { + return 'HTTP(S) URL connections are not allowed on main thread'; + } + var xhr = new XMLHttpRequest(); + xhr.open('GET', urlString, false); + xhr.responseType = 'arraybuffer'; + xhr.send(); + if(xhr.status === 200) { + return new Uint8Array(xhr.response); + } else { + return xhr.status + ' ' + xhr.statusText; + } + } catch(e) { + return e.toString(); + } + """) + private native JSValue jsConnect(String urlString); + } - // Returns and clears the field set by jsConnect. - @JS.Coerce - @JS("var request = this[runtime.symbol.javaNative]; var r = request.r; request.r = null; return r;") - private native String jsGetMessage(); + /// [InputStream] implementation that is backed by a `Uint8Array` from JavaScript. + private static final class JSByteArrayInputStream extends InputStream { + private final JSObject jsUint8Array; + private final int length; + private int idx = 0; + + private JSByteArrayInputStream(JSObject jsUint8Array) { + this.jsUint8Array = jsUint8Array; + this.length = jsUint8Array.get("length", Integer.class); + } + + @Override + public int read() throws IOException { + if (idx >= length) { + return -1; + } + + int i = jsUint8Array.get(idx, Integer.class); + + if (i < 0 || i > 255) { + throw new IOException("Invalid byte value at index " + idx + ": " + i); + } + + idx++; + return i; + } } } diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/wasmgc/WasmGCJSConversion.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/wasmgc/WasmGCJSConversion.java index 2bde9cd8d584..98c204023f1c 100644 --- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/wasmgc/WasmGCJSConversion.java +++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/wasmgc/WasmGCJSConversion.java @@ -26,8 +26,8 @@ package com.oracle.svm.webimage.wasmgc; import org.graalvm.nativeimage.Platforms; -import org.graalvm.webimage.api.ThrownFromJavaScript; import org.graalvm.webimage.api.JSValue; +import org.graalvm.webimage.api.ThrownFromJavaScript; import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; import com.oracle.svm.core.util.VMError; @@ -46,16 +46,6 @@ public class WasmGCJSConversion extends JSConversion { public static final WasmForeignCallDescriptor SET_JS_NATIVE = new WasmForeignCallDescriptor("setJSNative", void.class, new Class[]{JSValue.class, WasmExtern.class}); public static final WasmForeignCallDescriptor EXTRACT_JS_NATIVE = new WasmForeignCallDescriptor("extractJSNative", WasmExtern.class, new Class[]{JSValue.class}); - @WasmExport(value = "object.getclass", comment = "Get Class from object") - public static Class getClass(Object o) { - return o.getClass(); - } - - @WasmExport(value = "class.getname", comment = "Get fully qualified class name") - public static String getClassName(Class clazz) { - return clazz.getName(); - } - @WasmExport(value = "conversion.classfromencoding", comment = "Lookup class instance from metadata encoding") public static Class classFromEncoding(String encoding) { return WasmGCMetadata.lookupClass(encoding); @@ -74,23 +64,6 @@ public static boolean isInstance(Object o, Class clazz) { return clazz.isAssignableFrom(o.getClass()); } - @WasmExport(value = "class.isjavalangobject", comment = "Checks whether the given class is Object.class") - public static boolean isJavaLangObject(Class clazz) { - return clazz == Object.class; - } - - @WasmExport(value = "class.isjavalangclass", comment = "Checks whether the given class is Class.class") - public static boolean isJavaLangClass(Class clazz) { - return clazz == Class.class; - } - - @WasmExport(value = "class.superclass", comment = "Gets superclass of given non-primitive non-object class") - public static Class getSuperClass(Class clazz) { - assert !clazz.isPrimitive() : "Cannot get superclass of primitive class: " + clazz; - assert clazz != Object.class : "Cannot get superclass of java.lang.Object"; - return clazz.getSuperclass(); - } - @WasmExport(value = "class.getboxedhub", comment = "Boxed class for given primitive class") public static Class getBoxedHub(Class clazz) { if (clazz == Boolean.TYPE) { @@ -180,15 +153,6 @@ public static void throwExceptionFromJS(Object thrownObject) throws Throwable { @Override public void setJavaScriptNativeImpl(JSValue self, Object jsNative) { - if (jsNative.getClass().isArray()) { - /* - * TODO GR-60603 Deal with coercion rules for arrays. The JS backend and the existing - * conversion code assumes that Java arrays are also JS objects, which they're not in - * WasmGC. - */ - throw VMError.unsupportedFeature("Cannot coerce arrays: " + jsNative.getClass().getTypeName()); - } - assert jsNative instanceof WasmExtern : "Tried to store non-JS value in " + self.getClass() + ": " + jsNative.getClass(); setJSNative0(SET_JS_NATIVE, self, (WasmExtern) jsNative); }