js: add JSTopLevel annotation that allows to import top-level declarations

This commit is contained in:
Alexey Andreev 2024-04-11 20:29:57 +02:00
parent 9b41e3e814
commit 6a09f181c7
16 changed files with 290 additions and 46 deletions

View File

@ -109,3 +109,5 @@ let $rt_setThread = t => {
}
let $rt_apply = (instance, method, args) => instance[method].apply(instance, args);
let $rt_apply_topLevel = (method, args) => method.apply(null, args);

View File

@ -15,9 +15,9 @@
*/
package org.teavm.jso.browser;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
import org.teavm.jso.core.JSArray;
import org.teavm.jso.core.JSArrayReader;
import org.teavm.jso.dom.html.HTMLDocument;
@ -81,47 +81,47 @@ public abstract class Window implements JSObject, WindowEventTarget, StorageProv
@JSProperty
public abstract Window getTop();
@JSBody(params = "message", script = "alert(message);")
@JSTopLevel
public static native void alert(JSObject message);
@JSBody(params = "message", script = "alert(message);")
@JSTopLevel
public static native void alert(String message);
@JSBody(params = "message", script = "return confirm(message);")
@JSTopLevel
public static native boolean confirm(JSObject message);
@JSBody(params = "message", script = "return confirm(message);")
@JSTopLevel
public static native boolean confirm(String message);
public static String prompt(String message) {
return prompt(message, "");
}
@JSBody(params = { "message", "defaultValue" }, script = "return prompt(message, defaultValue);")
@JSTopLevel
public static native String prompt(String message, String defaultValue);
@JSBody(params = { "handler", "delay" }, script = "return setTimeout(handler, delay);")
@JSTopLevel
public static native int setTimeout(TimerHandler handler, int delay);
@JSBody(params = { "handler", "delay" }, script = "return setTimeout(handler, delay);")
@JSTopLevel
public static native int setTimeout(TimerHandler handler, double delay);
@JSBody(params = "timeoutId", script = "clearTimeout(timeoutId);")
@JSTopLevel
public static native void clearTimeout(int timeoutId);
@JSBody(params = { "handler", "delay" }, script = "return setInterval(handler, delay);")
@JSTopLevel
public static native int setInterval(TimerHandler handler, int delay);
@JSBody(params = { "handler", "delay" }, script = "return setInterval(handler, delay);")
@JSTopLevel
public static native int setInterval(TimerHandler handler, double delay);
@JSBody(params = "timeoutId", script = "clearInterval(timeoutId);")
@JSTopLevel
public static native void clearInterval(int timeoutId);
@JSBody(params = "callback", script = "return requestAnimationFrame(callback);")
@JSTopLevel
public static native int requestAnimationFrame(AnimationFrameCallback callback);
@JSBody(params = "requestId", script = "cancelAnimationFrame(requestId);")
@JSTopLevel
public static native void cancelAnimationFrame(int requestId);
public abstract void blur();
@ -170,30 +170,32 @@ public abstract class Window implements JSObject, WindowEventTarget, StorageProv
postMessage(message, JSArray.of(transfer));
}
@JSBody(script = "return window;")
@JSTopLevel
@JSProperty("window")
public static native Window current();
@JSBody(script = "return self;")
@JSTopLevel
@JSProperty("self")
public static native Window worker();
@JSBody(params = "uri", script = "return encodeURI(uri);")
@JSTopLevel
public static native String encodeURI(String uri);
@JSBody(params = "uri", script = "return encodeURIComponent(uri);")
@JSTopLevel
public static native String encodeURIComponent(String uri);
@JSBody(params = "uri", script = "return decodeURI(uri);")
@JSTopLevel
public static native String decodeURI(String uri);
@JSBody(params = "uri", script = "return decodeURIComponent(uri);")
@JSTopLevel
public static native String decodeURIComponent(String uri);
@JSProperty
public abstract double getDevicePixelRatio();
@JSBody(params = "s", script = "return atob(s);")
@JSTopLevel
public static native String atob(String s);
@JSBody(params = "s", script = "return btoa(s);")
@JSTopLevel
public static native String btoa(String s);
}

View File

@ -21,7 +21,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface JSModule {
String value();
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface JSTopLevel {
}

View File

@ -39,6 +39,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
import org.teavm.jso.JSTopLevel;
import org.teavm.model.AnnotationContainerReader;
import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader;
@ -991,9 +992,19 @@ class JSClassProcessor {
}
private Variable getCallTarget(InvokeInstruction invoke) {
return invoke.getInstance() != null
? invoke.getInstance()
: marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation());
if (invoke.getInstance() != null) {
return invoke.getInstance();
}
var cls = classSource.get(invoke.getMethod().getClassName());
var method = cls != null ? cls.getMethod(invoke.getMethod().getDescriptor()) : null;
var isTopLevel = (cls != null && cls.getAnnotations().get(JSTopLevel.class.getName()) != null)
|| (method != null && method.getAnnotations().get(JSTopLevel.class.getName()) != null);
if (isTopLevel) {
var methodAnnotations = method != null ? method.getAnnotations() : null;
return marshaller.moduleRef(invoke.getMethod().getClassName(), methodAnnotations, invoke.getLocation());
} else {
return marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation());
}
}
private boolean processConstructor(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {

View File

@ -63,19 +63,31 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
break;
case "get":
case "getPure":
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS);
renderProperty(context.getArgument(1), context);
}
break;
case "set":
case "setPure":
context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT.next());
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS.next());
renderProperty(context.getArgument(1), context);
}
writer.ws().append('=').ws();
context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT.next());
break;
case "invoke":
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.GROUPING);
renderProperty(context.getArgument(1), context);
}
writer.append('(');
for (int i = 2; i < context.argumentCount(); ++i) {
if (i > 2) {
@ -234,15 +246,30 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
}
}
private static boolean isNull(Expr expr) {
if (expr instanceof ConstantExpr) {
var constantExpr = (ConstantExpr) expr;
if (constantExpr.getValue() == null) {
return true;
}
}
return false;
}
private void applyFunction(InjectorContext context) {
if (tryApplyFunctionOptimized(context)) {
return;
}
var writer = context.getWriter();
if (isNull(context.getArgument(0))) {
writer.appendFunction("$rt_apply_topLevel").append("(");
writer.append(extractPropertyName(context.getArgument(1)));
} else {
writer.appendFunction("$rt_apply").append("(");
context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT);
writer.append(",").ws();
context.writeExpr(context.getArgument(1), Precedence.ASSIGNMENT);
}
writer.append(",").ws();
context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT);
writer.append(")");
@ -323,8 +350,12 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
private void applyFunctionOptimized(InjectorContext context, List<Expr> paramList) {
var writer = context.getWriter();
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.GROUPING);
renderProperty(context.getArgument(1), context);
}
writer.append('(');
for (int i = 0; i < paramList.size(); ++i) {
if (i > 0) {

View File

@ -22,6 +22,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSModule;
import org.teavm.jso.JSObject;
import org.teavm.model.AnnotationContainerReader;
import org.teavm.model.CallLocation;
import org.teavm.model.ClassReader;
import org.teavm.model.ClassReaderSource;
@ -36,6 +37,7 @@ import org.teavm.model.Variable;
import org.teavm.model.instructions.ClassConstantInstruction;
import org.teavm.model.instructions.InvocationType;
import org.teavm.model.instructions.InvokeInstruction;
import org.teavm.model.instructions.NullConstantInstruction;
import org.teavm.model.instructions.StringConstantInstruction;
class JSValueMarshaller {
@ -563,6 +565,10 @@ class JSValueMarshaller {
}
Variable classRef(String className, TextLocation location) {
return classRef(className, null, location);
}
Variable classRef(String className, AnnotationContainerReader annotations, TextLocation location) {
String name = null;
String module = null;
var cls = classSource.get(className);
@ -578,17 +584,37 @@ class JSValueMarshaller {
}
}
}
var jsModule = cls.getAnnotations().get(JSModule.class.getName());
if (jsModule != null) {
module = jsModule.getValue("value").getString();
}
module = moduleName(cls.getAnnotations());
}
if (name == null) {
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
}
if (module == null && annotations != null) {
module = moduleName(annotations);
}
return module != null ? moduleRef(module, name, location) : globalRef(name, location);
}
Variable moduleRef(String className, AnnotationContainerReader annotations, TextLocation location) {
String module = null;
var cls = classSource.get(className);
if (cls != null) {
module = moduleName(cls.getAnnotations());
}
if (module == null && annotations != null) {
module = moduleName(annotations);
}
return module != null ? moduleRef(module, location) : nullInstance(location);
}
private String moduleName(AnnotationContainerReader annotations) {
var jsModule = annotations.get(JSModule.class.getName());
if (jsModule != null) {
return jsModule.getValue("value").getString();
}
return null;
}
Variable globalRef(String name, TextLocation location) {
var invoke = new InvokeInstruction();
invoke.setType(InvocationType.SPECIAL);
@ -602,6 +628,10 @@ class JSValueMarshaller {
}
Variable moduleRef(String module, String name, TextLocation location) {
return dot(moduleRef(module, location), name, location);
}
Variable moduleRef(String module, TextLocation location) {
var moduleNameInsn = new StringConstantInstruction();
moduleNameInsn.setReceiver(program.createVariable());
moduleNameInsn.setConstant(module);
@ -616,14 +646,26 @@ class JSValueMarshaller {
invoke.setLocation(location);
replacement.add(invoke);
return invoke.getReceiver();
}
Variable dot(Variable instance, String name, TextLocation location) {
var get = new InvokeInstruction();
get.setType(InvocationType.SPECIAL);
get.setMethod(JSMethods.GET_PURE);
get.setReceiver(program.createVariable());
get.setArguments(invoke.getReceiver(), addJsString(name, location));
get.setArguments(instance, addJsString(name, location));
get.setLocation(location);
replacement.add(get);
return get.getReceiver();
}
Variable nullInstance(TextLocation location) {
var nullConstant = new NullConstantInstruction();
nullConstant.setReceiver(program.createVariable());
nullConstant.setLocation(location);
replacement.add(nullConstant);
return nullConstant.getReceiver();
}
}

View File

@ -20,6 +20,7 @@ import org.junit.runner.RunWith;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSTopLevel;
import org.teavm.junit.AttachJavaScript;
import org.teavm.junit.EachTestCompiledSeparately;
import org.teavm.junit.OnlyPlatform;
@ -77,6 +78,28 @@ public class CallTest {
assertEquals("a:23,b:q,va:6:7:8", TestClass.restVararg(23, "q", intArray));
}
@Test
@AttachJavaScript("org/teavm/jso/test/vararg.js")
public void topLevelVararg() {
assertEquals("tva:q:w", TestClass.topLevelVararg("q", "w"));
assertEquals("tva:23:42", TestClass.topLevelVarargInt(23, 42));
var array = new String[3];
for (var i = 0; i < array.length; ++i) {
array[i] = String.valueOf((char) ('A' + i));
}
assertEquals("tva:A:B:C", TestClass.topLevelVararg(array));
var intArray = new int[3];
for (var i = 0; i < array.length; ++i) {
intArray[i] = 6 + i;
}
assertEquals("tva:6:7:8", TestClass.topLevelVarargInt(intArray));
assertEquals("tva", TestClass.topLevelVararg());
assertEquals("tva", TestClass.topLevelVarargInt());
}
@JSClass
public static class TestClass implements JSObject {
public static native String allVararg(String... args);
@ -87,5 +110,12 @@ public class CallTest {
public static native String restVararg(String a, int b, String... args);
public static native String restVararg(int a, String b, int... args);
@JSTopLevel
public static native String topLevelVararg(String... args);
@JSTopLevel
@JSMethod("topLevelVararg")
public static native String topLevelVarargInt(int... args);
}
}

View File

@ -18,6 +18,7 @@ package org.teavm.jso.test;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass
public class ClassWithConstructor implements JSObject {
@ -33,4 +34,15 @@ public class ClassWithConstructor implements JSObject {
public native String bar();
public static native String staticMethod();
@JSTopLevel
public static native String topLevelFunction();
@JSTopLevel
@JSProperty
public static native String getTopLevelProperty();
@JSTopLevel
@JSProperty
public static native void setTopLevelProperty(String value);
}

View File

@ -19,6 +19,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSModule;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass(name = "ClassWithConstructor")
@JSModule("./testModule.js")
@ -33,4 +34,11 @@ public class ClassWithConstructorInModule implements JSObject {
public native int getFoo();
public native String bar();
@JSTopLevel
public static native String topLevelFunction();
@JSTopLevel
@JSProperty
public static native String getTopLevelProperty();
}

View File

@ -59,6 +59,22 @@ public class ImportClassTest {
assertEquals("static method called", ClassWithConstructor.staticMethod());
}
@Test
@AttachJavaScript("org/teavm/jso/test/classWithConstructor.js")
public void topLevel() {
assertEquals("top level", ClassWithConstructor.topLevelFunction());
assertEquals("top level prop", ClassWithConstructor.getTopLevelProperty());
ClassWithConstructor.setTopLevelProperty("update");
assertEquals("update", ClassWithConstructor.getTopLevelProperty());
assertEquals("top level", TopLevelDeclarations.topLevelFunction());
assertEquals("update", TopLevelDeclarations.getTopLevelProperty());
TopLevelDeclarations.setTopLevelProperty("update2");
assertEquals("update2", ClassWithConstructor.getTopLevelProperty());
}
@JSBody(script = "return {};")
private static native O create();

View File

@ -68,6 +68,14 @@ public class ImportModuleTest {
assertEquals(23, o.getFoo());
}
@Test
@JsModuleTest
@ServeJS(from = "org/teavm/jso/test/classWithConstructorInModule.js", as = "testModule.js")
public void topLevel() {
assertEquals("top level", ClassWithConstructorInModule.topLevelFunction());
assertEquals("top level prop", ClassWithConstructorInModule.getTopLevelProperty());
}
@JSBody(
script = "return testModule.foo();",
imports = @JSBodyImport(alias = "testModule", fromModule = "./testModule.js")

View File

@ -0,0 +1,36 @@
/*
* Copyright 2024 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.jso.test;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass
@JSTopLevel
public class TopLevelDeclarations implements JSObject {
private TopLevelDeclarations() {
}
public static native String topLevelFunction();
@JSProperty
public static native String getTopLevelProperty();
@JSProperty
public static native void setTopLevelProperty(String value);
}

View File

@ -31,3 +31,9 @@ class ClassWithConstructor {
return "static method called";
}
}
function topLevelFunction() {
return "top level";
}
let topLevelProperty = "top level prop";

View File

@ -27,3 +27,9 @@ export class ClassWithConstructor {
return "bar called";
}
}
export function topLevelFunction() {
return "top level";
}
export let topLevelProperty = "top level prop";

View File

@ -31,3 +31,11 @@ class TestClass {
return result;
}
}
function topLevelVararg(...args) {
let result = "tva";
for (const arg of args) {
result += ":" + arg;
}
return result;
}