diff --git a/.github/workflows/website-docs.yml b/.github/workflows/website-docs.yml
index 708698e283..a23dc8ea3b 100644
--- a/.github/workflows/website-docs.yml
+++ b/.github/workflows/website-docs.yml
@@ -144,6 +144,11 @@ jobs:
WEBSITE_INCLUDE_DEVGUIDE: "true"
WEBSITE_INCLUDE_INITIALIZR: "auto"
WEBSITE_INCLUDE_PLAYGROUND: "auto"
+ # The Initializr's JavaScript app is built with the local ParparVM
+ # target, whose builder lives in the repo (8.0-SNAPSHOT) plugin rather
+ # than the pinned release. Bootstrap the local snapshot artifacts so
+ # the initializr (and the other site apps) build against repo HEAD.
+ WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS: "true"
# PR previews build with future-dated posts visible so reviewers
# can read posts staged for later in the week. Production deploys
# (push to master) keep the default so future posts only appear
diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java
new file mode 100644
index 0000000000..e919b3b908
--- /dev/null
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/NativeInterfaceBridge.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors.
+ * Licensed under the PolyForm Noncommercial License 1.0.0.
+ * You may use this file only in compliance with that license.
+ * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md.
+ */
+package com.codename1.impl.platform.js;
+
+/**
+ * Host bridge that dispatches Codename One {@code NativeInterface} method calls
+ * to their JavaScript implementation registered in {@code cn1_native_interfaces}.
+ *
+ *
The generated {@code Impl} classes (emitted by the JavaScript
+ * builder) delegate every interface method to one of the {@code call*} natives
+ * below, picked by the method's return type. These natives are runtime-
+ * implemented in {@code parparvm_runtime.js}: the worker suspends, the call is
+ * replayed on the main thread (via {@code browser_bridge.js}) where the
+ * developer-authored JS stub runs with full DOM access and completes the call
+ * through its callback, and the worker resumes with the result coerced to the
+ * declared Java type.
+ *
+ * Supported types mirror {@code NativeInterface}: all primitives, {@code String},
+ * primitive arrays plus {@code String[]} (via {@link #callArray}), and
+ * {@code com.codename1.ui.PeerComponent} (routed through {@link #callObject}).
+ *
+ * {@code iface} is the interface class name with dots replaced by underscores
+ * (the {@code cn1_native_interfaces} registry key), {@code method} is the
+ * trailing-underscore method key (e.g. {@code "isDarkMode_"}), and {@code args}
+ * holds the (boxed) Java arguments, or an empty array for a no-arg method.
+ */
+public final class NativeInterfaceBridge {
+ private NativeInterfaceBridge() {
+ }
+
+ public static native boolean callBoolean(String iface, String method, Object[] args);
+
+ public static native byte callByte(String iface, String method, Object[] args);
+
+ public static native short callShort(String iface, String method, Object[] args);
+
+ public static native int callInt(String iface, String method, Object[] args);
+
+ public static native char callChar(String iface, String method, Object[] args);
+
+ public static native long callLong(String iface, String method, Object[] args);
+
+ public static native float callFloat(String iface, String method, Object[] args);
+
+ public static native double callDouble(String iface, String method, Object[] args);
+
+ public static native String callString(String iface, String method, Object[] args);
+
+ public static native Object callObject(String iface, String method, Object[] args);
+
+ public static native void callVoid(String iface, String method, Object[] args);
+
+ /**
+ * Array-returning call. {@code componentToken} identifies the element type so
+ * the runtime can build the correctly-typed Java array: {@code "JAVA_INT"},
+ * {@code "JAVA_BYTE"}, {@code "JAVA_LONG"}, {@code "JAVA_DOUBLE"},
+ * {@code "JAVA_FLOAT"}, {@code "JAVA_BOOLEAN"}, {@code "JAVA_CHAR"},
+ * {@code "JAVA_SHORT"} or {@code "java_lang_String"}. The caller casts the
+ * result to the concrete array type.
+ */
+ public static native Object callArray(String iface, String method, Object[] args, String componentToken);
+}
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java
index 0afae0e7db..846514acad 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java
@@ -29,6 +29,10 @@
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -123,9 +127,17 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException
File portSources = locateJavaScriptPortSources(request);
File portClassesStaged = stageJavaScriptPort(request, portSources, stageClasses, portClasses);
+ // For every NativeInterface in the app, generate a Impl whose
+ // methods bridge to the developer's JS stub on the MAIN thread (via
+ // NativeInterfaceBridge -> browser_bridge.js -> cn1_native_interfaces). The
+ // launcher registers each impl with NativeLookup so create() resolves and the
+ // optimizer keeps the impl (it is otherwise only reached reflectively).
+ List> nativeInterfaces = findNativeInterfaces(stageClasses);
+ List generatedImpls = generateNativeInterfaceImpls(buildDir, nativeInterfaces);
+
String translatorAppName = sanitizeIdentifier(request.getMainClass()) + "JavaScriptMain";
- File launcherJava = writeLauncher(buildDir, translatorAppName, request.getPackageName(), request.getMainClass(), stageClasses);
- compileLauncher(launcherJava, stageClasses, portClassesStaged);
+ File launcherJava = writeLauncher(buildDir, translatorAppName, request.getPackageName(), request.getMainClass(), stageClasses, nativeInterfaces);
+ compileLauncher(launcherJava, generatedImpls, stageClasses, portClassesStaged);
File parparvmCompilerJar = extractParparVMCompiler();
@@ -166,6 +178,17 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException
}
private boolean checkUserLevel(BuildRequest request) {
+ // A logged-in Codename One account is the authorization for the local JS
+ // build, the same way it authorizes the cloud build. The credentials are
+ // written to the /com/codename1/ui preferences node by `cn1:set-user-token`
+ // (SetUserTokenMojo) -- e.g. set_cn1_user_token in the website build. Honor
+ // that login directly so the local target "just works" once you're logged in.
+ if (hasCodenameOneLogin()) {
+ log("Local JavaScript builder: authorized via logged-in Codename One account.");
+ return true;
+ }
+ // Fallback for direct CLI invocations that aren't logged in: an explicit
+ // Enterprise-or-higher user level still unlocks the build.
String raw = firstNonEmpty(
request.getArg("javascript.userLevel", null),
request.getArg("userLevel", null),
@@ -179,12 +202,26 @@ private boolean checkUserLevel(BuildRequest request) {
return true;
}
log("ERROR: The local JavaScript build is licensed only to Enterprise and higher tier users. "
- + "Set codename1.arg.javascript.userLevel=Enterprise (or a higher tier) in codenameone_settings.properties, "
+ + "Log in with `cn1:set-user-token -Duser= -Dtoken=`, "
+ + "set codename1.arg.javascript.userLevel=Enterprise (or a higher tier) in codenameone_settings.properties, "
+ "or define the CN1_USER_LEVEL environment variable, to enable this preview. "
+ "See https://www.codenameone.com/pricing.html for tier details.");
return false;
}
+ private boolean hasCodenameOneLogin() {
+ try {
+ java.util.prefs.Preferences prefs = java.util.prefs.Preferences.userRoot().node("/com/codename1/ui");
+ String user = prefs.get("user", null);
+ String token = prefs.get("token", null);
+ return user != null && user.trim().length() > 0
+ && token != null && token.trim().length() > 0;
+ } catch (Exception ex) {
+ // Preferences backing store unavailable -- fall through to the userLevel path.
+ return false;
+ }
+ }
+
private static int parseUserRank(String raw) {
if (raw == null) {
return 0;
@@ -346,7 +383,8 @@ private String resolveJavac() {
return "javac";
}
- private File writeLauncher(File workDir, String launcherName, String packageName, String mainClass, File stageClasses) throws IOException {
+ private File writeLauncher(File workDir, String launcherName, String packageName, String mainClass, File stageClasses,
+ List> nativeInterfaces) throws IOException {
// If the build-time SVG transcoder generated com.codename1.generated.svg.SVGRegistry
// for this app, register the transcoded SVGs at startup -- the JS-port analogue of
// JavaSEPort.init's reflective installGlobal(). A DIRECT call (not reflection) is
@@ -365,6 +403,16 @@ private File writeLauncher(File workDir, String launcherName, String packageName
if (hasGeneratedSvg) {
pw.println(" com.codename1.generated.svg.SVGRegistry.installGlobal();");
}
+ // Register the generated native interface implementations. The DIRECT
+ // class references (not reflection) also keep the optimizer from culling
+ // the *Impl classes, which are otherwise reached only via NativeLookup.
+ if (nativeInterfaces != null) {
+ for (Class> iface : nativeInterfaces) {
+ String ifaceName = iface.getName();
+ pw.println(" com.codename1.system.NativeLookup.register("
+ + ifaceName + ".class, " + ifaceName + "Impl.class);");
+ }
+ }
pw.println(" ParparVMBootstrap.bootstrap(new " + mainClass + "());");
pw.println(" }");
pw.println("}");
@@ -374,15 +422,287 @@ private File writeLauncher(File workDir, String launcherName, String packageName
return f;
}
- private void compileLauncher(File launcherJava, File stageClasses, File portClasses) throws Exception {
+ private void compileLauncher(File launcherJava, List generatedImpls, File stageClasses, File portClasses) throws Exception {
String javac = resolveJavac();
- boolean ok = exec(tmpDir, -1, javac, "-source", "8", "-target", "8",
- "-cp", stageClasses.getAbsolutePath() + File.pathSeparator + portClasses.getAbsolutePath(),
- "-d", stageClasses.getAbsolutePath(),
- launcherJava.getAbsolutePath());
+ List cmd = new ArrayList();
+ cmd.add(javac);
+ cmd.add("-source"); cmd.add("8");
+ cmd.add("-target"); cmd.add("8");
+ cmd.add("-cp"); cmd.add(stageClasses.getAbsolutePath() + File.pathSeparator + portClasses.getAbsolutePath());
+ cmd.add("-d"); cmd.add(stageClasses.getAbsolutePath());
+ cmd.add(launcherJava.getAbsolutePath());
+ if (generatedImpls != null) {
+ for (File impl : generatedImpls) {
+ cmd.add(impl.getAbsolutePath());
+ }
+ }
+ boolean ok = exec(tmpDir, -1, cmd.toArray(new String[cmd.size()]));
if (!ok) {
- throw new BuildException("Failed to compile JavaScript launcher class");
+ throw new BuildException("Failed to compile JavaScript launcher / native interface impl classes");
+ }
+ }
+
+ // ----- Native interface binding --------------------------------------------------
+ // Scans the staged app classes for com.codename1.system.NativeInterface subtypes and
+ // generates, per interface, a Impl whose methods delegate to
+ // NativeInterfaceBridge.call* (a HOST_HOOK native). At runtime those calls suspend the
+ // worker and run the developer's JS stub (cn1_native_interfaces[...][method_]) on the
+ // MAIN thread, then resume the worker with the result. Mirrors the cloud builder's
+ // JSStubGenerator + NativeLookup.register flow, adapted to the worker/host-call model.
+
+ private List> findNativeInterfaces(File stageClasses) {
+ List> result = new ArrayList>();
+ URLClassLoader loader = null;
+ try {
+ loader = new URLClassLoader(new URL[]{ stageClasses.toURI().toURL() },
+ JavaScriptBuilder.class.getClassLoader());
+ Class> niClass;
+ try {
+ niClass = loader.loadClass("com.codename1.system.NativeInterface");
+ } catch (Throwable t) {
+ log("com.codename1.system.NativeInterface not on the classpath; no native interfaces to bind");
+ return result;
+ }
+ List classFiles = new ArrayList();
+ collectClassFiles(stageClasses, classFiles);
+ for (File cf : classFiles) {
+ // Cheap pre-filter: only classes whose bytes mention the marker interface
+ // are candidates (native interfaces extend it directly). Avoids loading the
+ // thousands of unrelated core/runtime classes.
+ byte[] bytes;
+ try {
+ bytes = java.nio.file.Files.readAllBytes(cf.toPath());
+ } catch (Throwable t) {
+ continue;
+ }
+ if (!new String(bytes, StandardCharsets.ISO_8859_1).contains("com/codename1/system/NativeInterface")) {
+ continue;
+ }
+ String cn = classNameFor(stageClasses, cf);
+ if (cn == null) {
+ continue;
+ }
+ try {
+ Class> c = loader.loadClass(cn);
+ if (c.isInterface() && !c.equals(niClass) && niClass.isAssignableFrom(c)) {
+ result.add(c);
+ log("Found native interface: " + c.getName());
+ }
+ } catch (Throwable ignore) {
+ // class not loadable in isolation (missing deps) -- not a native interface we can bind
+ }
+ }
+ } catch (Throwable t) {
+ log("Failed scanning for native interfaces: " + t);
+ } finally {
+ if (loader != null) {
+ try {
+ loader.close();
+ } catch (Throwable ignore) {
+ }
+ }
+ }
+ return result;
+ }
+
+ private static void collectClassFiles(File dir, List out) {
+ File[] children = dir.listFiles();
+ if (children == null) return;
+ for (File f : children) {
+ if (f.isDirectory()) {
+ collectClassFiles(f, out);
+ } else if (f.getName().endsWith(".class") && f.getName().indexOf('$') < 0) {
+ out.add(f);
+ }
+ }
+ }
+
+ private static String classNameFor(File root, File classFile) {
+ String rootPath = root.getAbsolutePath();
+ String filePath = classFile.getAbsolutePath();
+ if (!filePath.startsWith(rootPath)) {
+ return null;
+ }
+ String rel = filePath.substring(rootPath.length());
+ if (rel.startsWith(File.separator)) {
+ rel = rel.substring(1);
+ }
+ if (!rel.endsWith(".class")) {
+ return null;
+ }
+ rel = rel.substring(0, rel.length() - ".class".length());
+ return rel.replace(File.separatorChar, '.').replace('/', '.');
+ }
+
+ private List generateNativeInterfaceImpls(File buildDir, List> nativeInterfaces) throws IOException {
+ List generated = new ArrayList();
+ if (nativeInterfaces == null || nativeInterfaces.isEmpty()) {
+ return generated;
+ }
+ File genDir = new File(buildDir, "generated-native-impls");
+ genDir.mkdirs();
+ for (Class> iface : nativeInterfaces) {
+ File jf = writeNativeInterfaceImpl(genDir, iface);
+ if (jf != null) {
+ generated.add(jf);
+ }
+ }
+ return generated;
+ }
+
+ private File writeNativeInterfaceImpl(File genDir, Class> iface) throws IOException {
+ String pkg = iface.getPackage() != null ? iface.getPackage().getName() : "";
+ String simpleImpl = iface.getSimpleName() + "Impl";
+ String registryKey = iface.getName().replace('.', '_');
+
+ File pkgDir = pkg.isEmpty() ? genDir : new File(genDir, pkg.replace('.', File.separatorChar));
+ pkgDir.mkdirs();
+ File out = new File(pkgDir, simpleImpl + ".java");
+
+ StringBuilder sb = new StringBuilder();
+ if (!pkg.isEmpty()) {
+ sb.append("package ").append(pkg).append(";\n\n");
+ }
+ sb.append("public class ").append(simpleImpl)
+ .append(" implements ").append(iface.getName()).append(" {\n");
+ sb.append(" private static final String __NI = \"").append(registryKey).append("\";\n\n");
+
+ for (Method m : iface.getMethods()) {
+ if (Modifier.isStatic(m.getModifiers())) {
+ continue;
+ }
+ appendNativeInterfaceImplMethod(sb, m);
+ }
+ sb.append("}\n");
+
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8));
+ try {
+ pw.print(sb.toString());
+ } finally {
+ pw.close();
+ }
+ return out;
+ }
+
+ private void appendNativeInterfaceImplMethod(StringBuilder sb, Method m) {
+ Class>[] params = m.getParameterTypes();
+ Class> ret = m.getReturnType();
+ String methodKey = nativeInterfaceMethodKey(m);
+
+ sb.append(" public ").append(ret.getCanonicalName()).append(" ").append(m.getName()).append("(");
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(params[i].getCanonicalName()).append(" p").append(i);
+ }
+ sb.append(") {\n");
+
+ // Build the boxed argument array.
+ StringBuilder args = new StringBuilder();
+ if (params.length == 0) {
+ args.append("new Object[0]");
+ } else {
+ args.append("new Object[]{ ");
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) args.append(", ");
+ args.append(boxArgExpression(params[i], "p" + i));
+ }
+ args.append(" }");
+ }
+
+ String call = "com.codename1.impl.platform.js.NativeInterfaceBridge.";
+ String invokeArgs = "__NI, \"" + methodKey + "\", " + args.toString();
+
+ if (ret == void.class) {
+ sb.append(" ").append(call).append("callVoid(").append(invokeArgs).append(");\n");
+ } else if (ret == boolean.class) {
+ sb.append(" return ").append(call).append("callBoolean(").append(invokeArgs).append(");\n");
+ } else if (ret == int.class) {
+ sb.append(" return ").append(call).append("callInt(").append(invokeArgs).append(");\n");
+ } else if (ret == long.class) {
+ sb.append(" return ").append(call).append("callLong(").append(invokeArgs).append(");\n");
+ } else if (ret == double.class) {
+ sb.append(" return ").append(call).append("callDouble(").append(invokeArgs).append(");\n");
+ } else if (ret == float.class) {
+ sb.append(" return ").append(call).append("callFloat(").append(invokeArgs).append(");\n");
+ } else if (ret == byte.class) {
+ sb.append(" return ").append(call).append("callByte(").append(invokeArgs).append(");\n");
+ } else if (ret == short.class) {
+ sb.append(" return ").append(call).append("callShort(").append(invokeArgs).append(");\n");
+ } else if (ret == char.class) {
+ sb.append(" return ").append(call).append("callChar(").append(invokeArgs).append(");\n");
+ } else if (ret == String.class) {
+ sb.append(" return ").append(call).append("callString(").append(invokeArgs).append(");\n");
+ } else if (ret.isArray()) {
+ // Primitive arrays + String[]: callArray builds the correctly-typed
+ // Java array from the JS array the host returns (componentToken picks
+ // the element type).
+ sb.append(" return (").append(ret.getCanonicalName()).append(") ")
+ .append(call).append("callArray(").append(invokeArgs)
+ .append(", \"").append(arrayComponentToken(ret.getComponentType())).append("\");\n");
+ } else if ("com.codename1.ui.PeerComponent".equals(ret.getName())) {
+ // The stub returns a native element (delivered to the worker as a
+ // host-ref); wrap it as a Codename One peer component.
+ sb.append(" return com.codename1.ui.PeerComponent.create(")
+ .append(call).append("callObject(").append(invokeArgs).append("));\n");
+ } else {
+ sb.append(" return (").append(ret.getCanonicalName()).append(") ")
+ .append(call).append("callObject(").append(invokeArgs).append(");\n");
+ }
+ sb.append(" }\n\n");
+ }
+
+ private static String boxArgExpression(Class> type, String var) {
+ // Pass a PeerComponent's underlying native element (a host-ref) to the
+ // stub, not the Java peer wrapper.
+ if ("com.codename1.ui.PeerComponent".equals(type.getName())) {
+ return var + ".getNativePeer()";
+ }
+ if (type == int.class) return "Integer.valueOf(" + var + ")";
+ if (type == long.class) return "Long.valueOf(" + var + ")";
+ if (type == double.class) return "Double.valueOf(" + var + ")";
+ if (type == float.class) return "Float.valueOf(" + var + ")";
+ if (type == boolean.class) return "Boolean.valueOf(" + var + ")";
+ if (type == byte.class) return "Byte.valueOf(" + var + ")";
+ if (type == short.class) return "Short.valueOf(" + var + ")";
+ if (type == char.class) return "Character.valueOf(" + var + ")";
+ return var;
+ }
+
+ // Mirrors StubGenerator's JS stub key: methodName + "_" + ("_" + xmlvmType) per param.
+ private static String nativeInterfaceMethodKey(Method m) {
+ StringBuilder key = new StringBuilder(m.getName()).append("_");
+ for (Class> p : m.getParameterTypes()) {
+ if ("com.codename1.ui.PeerComponent".equals(p.getName())) {
+ key.append("_com_codename1_ui_PeerComponent");
+ } else {
+ key.append("_").append(xmlvmTypeName(p));
+ }
+ }
+ return key.toString();
+ }
+
+ // Runtime newArray() component-class token for an array's element type.
+ private static String arrayComponentToken(Class> component) {
+ if (component == int.class) return "JAVA_INT";
+ if (component == long.class) return "JAVA_LONG";
+ if (component == double.class) return "JAVA_DOUBLE";
+ if (component == float.class) return "JAVA_FLOAT";
+ if (component == boolean.class) return "JAVA_BOOLEAN";
+ if (component == byte.class) return "JAVA_BYTE";
+ if (component == short.class) return "JAVA_SHORT";
+ if (component == char.class) return "JAVA_CHAR";
+ if (component == String.class) return "java_lang_String";
+ return component.getName().replace('.', '_');
+ }
+
+ private static String xmlvmTypeName(Class> type) {
+ if (type.isArray()) {
+ return xmlvmTypeName(type.getComponentType()) + "_1ARRAY";
+ }
+ if (type.isPrimitive()) {
+ return type.getName();
}
+ return type.getName().replace('.', '_');
}
private File extractParparVMCompiler() throws BuildException {
diff --git a/scripts/initializr/README.adoc b/scripts/initializr/README.adoc
index 2ccc909727..d18ca6ce90 100644
--- a/scripts/initializr/README.adoc
+++ b/scripts/initializr/README.adoc
@@ -59,7 +59,7 @@ mvn -DskipTests install
[source,bash]
----
cd ../scripts/initializr
-./mvnw package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript -Dcn1.localWorkspace=true
+./mvnw package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript -Dcn1.localWorkspace=true
----
This switches Initializr to `8.0-SNAPSHOT` so JavaScript builds use your local Codename One code.
diff --git a/scripts/initializr/build.bat b/scripts/initializr/build.bat
index 622d02e706..40893d8b27 100644
--- a/scripts/initializr/build.bat
+++ b/scripts/initializr/build.bat
@@ -22,7 +22,7 @@ goto :EOF
goto :EOF
:javascript
-!MVNW! package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=javascript -U -e
+!MVNW! package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=local-javascript -U -e
goto :EOF
:android
diff --git a/scripts/initializr/build.sh b/scripts/initializr/build.sh
index 196d9f1339..54b63ee6e7 100755
--- a/scripts/initializr/build.sh
+++ b/scripts/initializr/build.sh
@@ -12,7 +12,7 @@ function windows_desktop {
}
function javascript {
- "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=javascript" "-U" "-e"
+ "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=local-javascript" "-U" "-e"
}
function android {
diff --git a/scripts/initializr/common/codenameone_settings.properties b/scripts/initializr/common/codenameone_settings.properties
index a53c18b350..f2da507a85 100644
--- a/scripts/initializr/common/codenameone_settings.properties
+++ b/scripts/initializr/common/codenameone_settings.properties
@@ -12,6 +12,11 @@ codename1.arg.and.themeMode=modern
codename1.arg.desktop.titleBar=native
codename1.arg.desktop.interactiveScrollbars=true
codename1.arg.java.version=8
+# Local ParparVM JavaScript build (codename1.buildTarget=local-javascript) is
+# gated to Enterprise-tier accounts in the released plugin. The website build
+# logs in via set_cn1_user_token; declare the tier so the released plugin's
+# license gate is satisfied. Newer plugins also accept the login directly.
+codename1.arg.javascript.userLevel=Enterprise
codename1.displayName=Initializr
codename1.icon=icon.png
codename1.ios.appid=Q5GHSKAL2F.com.codename1.initializr
diff --git a/scripts/initializr/common/src/main/resources/skill/SKILL.md b/scripts/initializr/common/src/main/resources/skill/SKILL.md
index d71c510541..4c6ca31dcb 100644
--- a/scripts/initializr/common/src/main/resources/skill/SKILL.md
+++ b/scripts/initializr/common/src/main/resources/skill/SKILL.md
@@ -253,10 +253,13 @@ mvn -pl common cn1:debug
# Execute the CN1 test runner
mvn -pl common cn1:test
-# Cloud build for Android/iOS/JS (requires CN1 build server creds)
+# Cloud build for Android/iOS (requires CN1 build server creds)
mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device
mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-device
-mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript
+
+# JavaScript / web bundle, built locally via the ParparVM → JS translator (Enterprise-gated).
+# Use -Dcodename1.buildTarget=javascript instead for the cloud builder.
+mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript
```
See `references/build-and-run.md` for the local-vs-cloud matrix, automated-build mode (Enterprise), iOS local-build prerequisites, and the complete goal list. The full `codename1.arg.*` index lives in `references/build-hints.md`.
diff --git a/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md b/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md
index 8f3597f324..059bbab3b3 100644
--- a/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md
+++ b/scripts/initializr/common/src/main/resources/skill/references/build-and-run.md
@@ -13,7 +13,7 @@ A Codename One project can produce four kinds of artifacts. Some build entirely
| iOS app | Cloud, **or** locally as an Xcode project via `ios-source` | `mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-device` (cloud) or `…-Dcodename1.buildTarget=ios-source` (local Xcode project) |
| Mac Native app (AOT-compiled, same pipeline as iOS) | Cloud, **or** locally as an Xcode project via `mac-source` | `mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=mac-os-x-native` (cloud) or `…-Dcodename1.buildTarget=mac-source` (local Xcode project) |
| Native Windows `.exe` (`win32`, ParparVM → clang-cl, no JVM) | Cloud (Linux build server cross-compiles); **also** locally on Windows, or as a project via `windows-source` | `mvn -pl common package -Dcodename1.platform=windows -Dcodename1.buildTarget=windows-device` (cloud) or `…-Dcodename1.buildTarget=local-windows-device` (local). A regular build returns x64 + arm64 release exes; add the `windows.debug` build hint for a single x64 debug exe. |
-| JavaScript / web bundle | Cloud | `mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript` |
+| JavaScript / web bundle | Local (ParparVM → JavaScript translator; Enterprise-gated). Cloud still available via `…-Dcodename1.buildTarget=javascript`. | `mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript` |
The two big "local-only" outputs are the **simulator** and **tests** — those are everything you need for ordinary development and CI feedback loops. You only invoke the cloud builds when you want a deployable native artifact.
@@ -102,8 +102,9 @@ mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=mac-source
# Native Android APK/AAB. Cloud-built by default.
mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device
-# JavaScript / web bundle. Cloud-built.
-mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript
+# JavaScript / web bundle. Built locally via the ParparVM → JavaScript translator (Enterprise-gated).
+# Append -Dcodename1.buildTarget=javascript instead to use the cloud builder.
+mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript
# Standalone Mac / Windows / Linux desktop app. Cloud-built.
mvn -pl javase package -Dcodename1.platform=javase -Dcodename1.buildTarget=mac-os-x-desktop
diff --git a/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md b/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md
index 775489ff22..37edc5cfce 100644
--- a/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md
+++ b/scripts/initializr/common/src/main/resources/skill/references/native-interfaces.md
@@ -70,7 +70,7 @@ mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source
mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device -Dautomated=true
# JavaScript — produces a web bundle; open dev tools and confirm the JS impl is included.
-mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript
+mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript
# Desktop simulator — just run cn1:run and observe the bridge boots without errors.
mvn -pl common cn1:run
diff --git a/scripts/initializr/javascript/pom.xml b/scripts/initializr/javascript/pom.xml
index a68c0251d5..4bba4f16eb 100644
--- a/scripts/initializr/javascript/pom.xml
+++ b/scripts/initializr/javascript/pom.xml
@@ -18,7 +18,7 @@
1.8
javascript
javascript
- javascript
+ local-javascript
diff --git a/scripts/initializr/pom.xml b/scripts/initializr/pom.xml
index 95e782606b..d8552381be 100644
--- a/scripts/initializr/pom.xml
+++ b/scripts/initializr/pom.xml
@@ -117,8 +117,14 @@
true
+
- 7.0.250
+ 8.0-SNAPSHOT
+ 8.0-SNAPSHOT
diff --git a/scripts/run-javascript-headless-browser.mjs b/scripts/run-javascript-headless-browser.mjs
index c229bf161b..db60110d6e 100755
--- a/scripts/run-javascript-headless-browser.mjs
+++ b/scripts/run-javascript-headless-browser.mjs
@@ -45,7 +45,20 @@ let finalizeProfile = async () => {};
const launchArgs = [
'--autoplay-policy=no-user-gesture-required',
'--disable-web-security',
- '--allow-file-access-from-files'
+ '--allow-file-access-from-files',
+ // Headless pages count as hidden, so Chromium's background-timer machinery
+ // (IntensiveWakeUpThrottling in particular) batches re-armed setTimeout
+ // chains to ~one firing per MINUTE once the page's wake-up budget drains.
+ // The ParparVM worker schedules every Thread.sleep / Object.wait(timeout)
+ // through host timers, so the whole green-thread scheduler stalls in
+ // 12-60s bursts during quiet (no-host-event) phases -- observed as the
+ // screenshot suite crawling ~60s/test through the theme cluster with every
+ // thread parked past its wake deadline. Disable the throttling: this
+ // harness IS the foreground workload.
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-renderer-backgrounding',
+ '--disable-features=IntensiveWakeUpThrottling'
];
if (profileWorker) {
launchArgs.push(`--remote-debugging-port=${remoteDebugPort}`);
@@ -212,6 +225,18 @@ try {
append(`goto:${url}`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
+ // VM liveness nudge from the Node side. Headless Chromium intensively
+ // throttles page AND worker timers (re-armed setTimeout chains batch to
+ // ~1/min once the hidden page's wake-up budget drains), which starves the
+ // ParparVM scheduler's sleep/wait wakeups and crawls the suite. CDP
+ // Runtime.evaluate is exempt from that throttling, so a Node interval
+ // pinging the bridge's __cn1NudgeVm (worker postMessage 'timer-wake' ->
+ // drain -> fire due wakeups) keeps the VM clock honest regardless of the
+ // browser's visibility heuristics.
+ const nudgeTimer = setInterval(() => {
+ page.evaluate('window.__cn1NudgeVm && window.__cn1NudgeVm()').catch(() => {});
+ }, 250);
+ nudgeTimer.unref?.();
await page.waitForTimeout(2000);
const start = Date.now();
diff --git a/scripts/website/build.sh b/scripts/website/build.sh
index 4f10267f5e..156f3d31f5 100755
--- a/scripts/website/build.sh
+++ b/scripts/website/build.sh
@@ -48,11 +48,18 @@ bootstrap_local_cn1_snapshots() {
return
fi
+ # Each site app (initializr/playground/skindesigner) calls this; the full
+ # reactor build is expensive, so run setup-workspace.sh only once per build.
+ if [ "${__CN1_SNAPSHOTS_BOOTSTRAPPED:-}" = "true" ]; then
+ return
+ fi
+
echo "Bootstrapping local Codename One snapshot Maven artifacts..." >&2
(
cd "${REPO_ROOT}"
SKIP_CN1_ARCHETYPES=1 ./scripts/setup-workspace.sh -q -DskipTests
)
+ __CN1_SNAPSHOTS_BOOTSTRAPPED="true"
}
activate_bootstrapped_java17() {
@@ -569,6 +576,13 @@ build_initializr_for_site() {
return
fi
+ # The initializr builds the JavaScript app with the local ParparVM target
+ # (codename1.buildTarget=local-javascript). That builder lives in the repo's
+ # 8.0-SNAPSHOT plugin, not the pinned release, so bootstrap the local
+ # snapshots and build with -Dcn1.localWorkspace=true (the cn1-local-workspace
+ # profile then overrides cn1.version/cn1.plugin.version to the repo build).
+ bootstrap_local_cn1_snapshots
+
echo "Building Initializr JavaScript bundle for website..." >&2
(
cd "${REPO_ROOT}/scripts/initializr"
@@ -581,7 +595,13 @@ build_initializr_for_site() {
fi
}
- if [ -n "${JAVA_HOME_8_X64:-}" ]; then
+ local initializr_workspace_args=()
+ if [ "${WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS}" = "true" ]; then
+ # Local ParparVM JS build runs the translator + javac; use the
+ # bootstrapped JDK 17 (matching the Playground/Skin Designer path).
+ activate_bootstrapped_java17
+ initializr_workspace_args+=(-Dcn1.localWorkspace=true)
+ elif [ -n "${JAVA_HOME_8_X64:-}" ]; then
export JAVA_HOME="${JAVA_HOME_8_X64}"
export PATH="${JAVA_HOME}/bin:${PATH}"
fi
@@ -589,6 +609,7 @@ build_initializr_for_site() {
# Ensure attached classifier artifact initializr-ZipSupport:jar:common is present
# in the local Maven repo before building modules that depend on it (e.g. initializr-common).
run_initializr_mvn -q -U -pl cn1libs/ZipSupport -am \
+ "${initializr_workspace_args[@]}" \
-DskipTests \
-Dcodename1.platform=javascript \
install
@@ -596,6 +617,7 @@ build_initializr_for_site() {
set_cn1_user_token "Initializr"
run_initializr_mvn -q -U -pl javascript -am \
+ "${initializr_workspace_args[@]}" \
-DskipTests \
-Dautomated=true \
-Dcodename1.platform=javascript \
@@ -617,10 +639,30 @@ build_initializr_for_site() {
mkdir -p "${output_dir}"
unzip -q -o "${result_zip}" -d "${output_dir}"
+ # The cloud result.zip is flat (index.html at the root), but the local
+ # ParparVM build (codename1.buildTarget=local-javascript) wraps the bundle in
+ # a single top-level directory (e.g. Initializr-js/). Flatten that wrapper so
+ # the served layout is identical regardless of which builder produced the zip.
+ if [ ! -f "${output_dir}/index.html" ]; then
+ local inner_dir
+ inner_dir="$(find "${output_dir}" -mindepth 1 -maxdepth 1 -type d | head -n1 || true)"
+ if [ -n "${inner_dir}" ] && [ -f "${inner_dir}/index.html" ]; then
+ ( cd "${inner_dir}" && tar cf - . ) | ( cd "${output_dir}" && tar xf - )
+ rm -rf "${inner_dir}"
+ fi
+ fi
+
if [ ! -f "${output_dir}/index.html" ]; then
echo "Initializr website bundle is missing index.html after extraction." >&2
exit 1
fi
+
+ # The Initializr page (layouts/_default/initializr.html) shows the app icon
+ # from /initializr-app/icon.png. The cloud bundle shipped one; the local
+ # ParparVM bundle does not, so copy the project icon in when it is absent.
+ if [ ! -f "${output_dir}/icon.png" ] && [ -f "${REPO_ROOT}/scripts/initializr/common/icon.png" ]; then
+ cp "${REPO_ROOT}/scripts/initializr/common/icon.png" "${output_dir}/icon.png"
+ fi
}
build_playground_for_site() {
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
index 0bbaba57fe..9cde045e3e 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
@@ -179,10 +179,48 @@ public int compare(ByteCodeClass a, ByteCodeClass b) {
for (int i = 0; i < leadCount; i++) {
String suffix = leadCount >= 10 ? String.format("_%02d", i + 1) : String.format("_%d", i + 1);
Files.write(new File(outputDirectory, "translated_app" + suffix + ".js").toPath(),
- hoistStringConstants(chunks.get(i).toString()).getBytes(StandardCharsets.UTF_8));
+ minifyJs(hoistStringConstants(chunks.get(i).toString())).getBytes(StandardCharsets.UTF_8));
}
Files.write(new File(outputDirectory, "translated_app.js").toPath(),
- hoistStringConstants(tail.toString()).getBytes(StandardCharsets.UTF_8));
+ minifyJs(hoistStringConstants(tail.toString())).getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Strips the translator's pretty-printing indentation and blank lines from the
+ * emitted application JS. The translator emits one statement per line with
+ * generous indentation for readability; for a deployed bundle that is ~20% dead
+ * weight that the browser must still download and parse. We keep one statement
+ * per line (newlines preserved) so the transform is safe regardless of ASI or
+ * {@code //} comments -- only leading/trailing line whitespace and empty lines
+ * are removed. Set {@code -Dparparvm.js.pretty=true} to keep the readable form
+ * for debugging the generated code.
+ */
+ private static String minifyJs(String code) {
+ if (System.getProperty("parparvm.js.pretty") != null) {
+ return code;
+ }
+ int n = code.length();
+ StringBuilder out = new StringBuilder(n);
+ int i = 0;
+ while (i < n) {
+ int eol = code.indexOf('\n', i);
+ if (eol < 0) {
+ eol = n;
+ }
+ int start = i;
+ int end = eol;
+ while (start < end && code.charAt(start) <= ' ') {
+ start++;
+ }
+ while (end > start && code.charAt(end - 1) <= ' ') {
+ end--;
+ }
+ if (end > start) {
+ out.append(code, start, end).append('\n');
+ }
+ i = eol + 1;
+ }
+ return out.toString();
}
/**
@@ -536,6 +574,10 @@ private static void writeWorker(File outputDirectory) throws IOException {
// call) but *after* other runtime helpers / native shims.
if (name.startsWith("translated_app_") && name.endsWith(".js")) {
classChunkScripts.add(name);
+ } else if (isNativeInterfaceStub(file)) {
+ // Native-interface implementations run on the MAIN thread (index.html),
+ // not in the worker -- they need DOM access. Skip them here.
+ continue;
} else {
nativeScripts.add(name);
}
@@ -560,7 +602,53 @@ private static void writeWorker(File outputDirectory) throws IOException {
}
private static void writeIndex(File outputDirectory) throws IOException {
- writeResource(outputDirectory, "index.html", "index.html");
+ String index = loadResource("index.html");
+ StringBuilder stubs = new StringBuilder();
+ for (String stub : collectNativeInterfaceStubs(outputDirectory)) {
+ stubs.append("\n");
+ }
+ index = index.replace("", stubs.toString().trim());
+ Files.write(new File(outputDirectory, "index.html").toPath(), index.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Native-interface JS implementations self-register into
+ * {@code cn1_native_interfaces} (they end with {@code })(cn1_get_native_interfaces());}).
+ * They run on the MAIN thread so their DOM access works, and are dispatched from the
+ * worker via the host-call bridge. Identify them by that content marker so the worker
+ * importScripts list excludes them and index.html loads them on the page instead.
+ */
+ private static List collectNativeInterfaceStubs(File outputDirectory) {
+ List stubs = new ArrayList();
+ File[] files = outputDirectory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.getName().endsWith(".js") && isNativeInterfaceStub(file)) {
+ stubs.add(file.getName());
+ }
+ }
+ }
+ Collections.sort(stubs);
+ return stubs;
+ }
+
+ private static boolean isNativeInterfaceStub(File jsFile) {
+ String name = jsFile.getName();
+ if ("parparvm_runtime.js".equals(name)
+ || "translated_app.js".equals(name)
+ || "worker.js".equals(name)
+ || "sw.js".equals(name)
+ || "port.js".equals(name)
+ || "browser_bridge.js".equals(name)
+ || name.startsWith("translated_app_")) {
+ return false;
+ }
+ try {
+ String content = new String(Files.readAllBytes(jsFile.toPath()), StandardCharsets.UTF_8);
+ return content.contains("cn1_get_native_interfaces");
+ } catch (IOException ex) {
+ return false;
+ }
}
private static void writeBrowserBridge(File outputDirectory) throws IOException {
@@ -651,4 +739,47 @@ private static String loadResource(String resourceName) throws IOException {
input.close();
}
}
+
+ /**
+ * Every {@code cn1_*} token referenced as a string literal by the
+ * hand-written bridge JS (parparvm_runtime.js, browser_bridge.js and
+ * the JavaScript port's port.js). These are names the bridge resolves
+ * by string at runtime -- and, in the {@code bindNative} /
+ * {@code bindCiFallback} case, REPLACES with {@code function*}
+ * overrides. The suspension analysis must treat the named methods as
+ * suspending: a translated caller that skipped {@code yield*} (because
+ * the static body looked synchronous) would receive the installed
+ * override's raw generator object as its "result" and the override
+ * body would never run.
+ */
+ static Set collectBridgeReferencedCn1Tokens() {
+ Set tokens = new HashSet();
+ List sources = new ArrayList();
+ for (String res : new String[]{ "parparvm_runtime.js", "browser_bridge.js" }) {
+ try {
+ sources.add(loadResource(res));
+ } catch (IOException ignore) {
+ // resource absent -- skip
+ }
+ }
+ try {
+ Path webApp = locateJavaScriptPortWebApp();
+ if (webApp != null) {
+ Path portJs = webApp.resolve("port.js");
+ if (Files.exists(portJs)) {
+ sources.add(new String(Files.readAllBytes(portJs), StandardCharsets.UTF_8));
+ }
+ }
+ } catch (Exception ignore) {
+ // port.js unavailable -- skip
+ }
+ java.util.regex.Pattern literal = java.util.regex.Pattern.compile("[\"'](cn1_[A-Za-z0-9_]+)[\"']");
+ for (String src : sources) {
+ java.util.regex.Matcher m = literal.matcher(src);
+ while (m.find()) {
+ tokens.add(m.group(1));
+ }
+ }
+ return tokens;
+ }
}
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
index d76a812071..d1793dd95b 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
@@ -3388,13 +3388,14 @@ private static boolean appendStraightLineInvokeInstruction(StringBuilder out, In
// ``resolveVirtual`` handles inheritance without per-class alias
// entries.
String dispatchId = JavascriptNameUtil.dispatchMethodIdentifier(invoke.getName(), invoke.getDesc());
+ boolean suspending = isInvokeSuspending(invoke);
if (hasReturn) {
out.append(" {\n");
- appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues);
+ appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, true, target, false, argValues, suspending);
out.append(" ").append(ctx.push("__result")).append(";\n");
out.append(" }\n");
} else {
- appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues);
+ appendCompactVirtualDispatch(out, " ", dispatchId, argValues.length, false, target, false, argValues, suspending);
}
return true;
}
@@ -4799,6 +4800,13 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
}
if (invoke.getOpcode() == Opcodes.INVOKEVIRTUAL || invoke.getOpcode() == Opcodes.INVOKEINTERFACE) {
+ // CHA verdict for this call site: a sync signature uses the
+ // non-generator ``cn1_ivs*`` family with no ``yield*`` (so a
+ // method whose every virtual call is sync can itself be a plain
+ // ``function``); a suspending signature keeps ``yield* cn1_iv*``.
+ boolean susp = isInvokeSuspending(invoke);
+ String iv = susp ? "cn1_iv" : "cn1_ivs";
+ String yk = susp ? "yield* " : "";
// Fast path for 0-arg virtual dispatch: inline the
// target pop into the iv0 call. Pops TOS inside the
// invoke's arg list, so the full block collapses to a
@@ -4806,9 +4814,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
// call sites.
if (argCount == 0) {
if (hasReturn) {
- out.append(" stack.p(yield* cn1_iv0(stack.q(), \"").append(dispatchId).append("\"));\n");
+ out.append(" stack.p(").append(yk).append(iv).append("0(stack.q(), \"").append(dispatchId).append("\"));\n");
} else {
- out.append(" yield* cn1_iv0(stack.q(), \"").append(dispatchId).append("\");\n");
+ out.append(" ").append(yk).append(iv).append("0(stack.q(), \"").append(dispatchId).append("\");\n");
}
out.append(" pc = ").append(index + 1).append("; break;\n");
return;
@@ -4816,9 +4824,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
if (argCount == 1) {
out.append(" { let __arg0 = stack.q(); ");
if (hasReturn) {
- out.append("stack.p(yield* cn1_iv1(stack.q(), \"").append(dispatchId).append("\", __arg0));");
+ out.append("stack.p(").append(yk).append(iv).append("1(stack.q(), \"").append(dispatchId).append("\", __arg0));");
} else {
- out.append("yield* cn1_iv1(stack.q(), \"").append(dispatchId).append("\", __arg0);");
+ out.append(yk).append(iv).append("1(stack.q(), \"").append(dispatchId).append("\", __arg0);");
}
out.append(" pc = ").append(index + 1).append("; break; }\n");
return;
@@ -4826,9 +4834,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
if (argCount == 2) {
out.append(" { let __arg1 = stack.q(); let __arg0 = stack.q(); ");
if (hasReturn) {
- out.append("stack.p(yield* cn1_iv2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1));");
+ out.append("stack.p(").append(yk).append(iv).append("2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1));");
} else {
- out.append("yield* cn1_iv2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1);");
+ out.append(yk).append(iv).append("2(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1);");
}
out.append(" pc = ").append(index + 1).append("; break; }\n");
return;
@@ -4836,9 +4844,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
if (argCount == 3) {
out.append(" { let __arg2 = stack.q(); let __arg1 = stack.q(); let __arg0 = stack.q(); ");
if (hasReturn) {
- out.append("stack.p(yield* cn1_iv3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2));");
+ out.append("stack.p(").append(yk).append(iv).append("3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2));");
} else {
- out.append("yield* cn1_iv3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2);");
+ out.append(yk).append(iv).append("3(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2);");
}
out.append(" pc = ").append(index + 1).append("; break; }\n");
return;
@@ -4846,9 +4854,9 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
if (argCount == 4) {
out.append(" { let __arg3 = stack.q(); let __arg2 = stack.q(); let __arg1 = stack.q(); let __arg0 = stack.q(); ");
if (hasReturn) {
- out.append("stack.p(yield* cn1_iv4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3));");
+ out.append("stack.p(").append(yk).append(iv).append("4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3));");
} else {
- out.append("yield* cn1_iv4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3);");
+ out.append(yk).append(iv).append("4(stack.q(), \"").append(dispatchId).append("\", __arg0, __arg1, __arg2, __arg3);");
}
out.append(" pc = ").append(index + 1).append("; break; }\n");
return;
@@ -4863,7 +4871,7 @@ private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, in
out.append(" {\n");
appendInvocationArgumentBindings(out, argCount, " ", "stack.q()");
out.append(" let __target = stack.q();\n");
- appendCompactVirtualDispatch(out, " ", dispatchId, argCount, hasReturn, "__target", true);
+ appendCompactVirtualDispatch(out, " ", dispatchId, argCount, hasReturn, "__target", true, isInvokeSuspending(invoke));
out.append(" pc = ").append(index + 1).append("; break;\n");
out.append(" }\n");
return;
@@ -5015,32 +5023,46 @@ private static void appendInvocationArgumentBindings(StringBuilder out, int argC
* the arg expressions directly (straight-line path).
*/
private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId,
- int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack) {
- appendCompactVirtualDispatch(out, indent, methodId, argCount, hasReturn, targetExpr, argsFromStack, null);
+ int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, boolean suspending) {
+ appendCompactVirtualDispatch(out, indent, methodId, argCount, hasReturn, targetExpr, argsFromStack, null, suspending);
}
+ /**
+ * @param suspending CHA verdict for the dispatched signature. When true the
+ * call goes through the generator family
+ * ({@code yield* cn1_iv*}) so a blocking override can
+ * suspend the cooperative scheduler. When false the
+ * analysis proved every impl is synchronous, so we emit
+ * the synchronous family ({@code cn1_ivs*}, no
+ * {@code yield*}) -- this is what allows a caller that
+ * makes only non-suspending virtual calls to itself be a
+ * plain {@code function}.
+ */
private static void appendCompactVirtualDispatch(StringBuilder out, String indent, String methodId,
- int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions) {
+ int argCount, boolean hasReturn, String targetExpr, boolean argsFromStack, String[] argExpressions,
+ boolean suspending) {
+ String base = suspending ? "cn1_iv" : "cn1_ivs";
+ String yieldKw = suspending ? "yield* " : "";
String helper;
boolean variadic = false;
switch (argCount) {
- case 0: helper = "cn1_iv0"; break;
- case 1: helper = "cn1_iv1"; break;
- case 2: helper = "cn1_iv2"; break;
- case 3: helper = "cn1_iv3"; break;
- case 4: helper = "cn1_iv4"; break;
+ case 0: helper = base + "0"; break;
+ case 1: helper = base + "1"; break;
+ case 2: helper = base + "2"; break;
+ case 3: helper = base + "3"; break;
+ case 4: helper = base + "4"; break;
default:
- helper = "cn1_ivN";
+ helper = base + "N";
variadic = true;
break;
}
out.append(indent);
if (hasReturn && argsFromStack) {
- out.append("stack.p(yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
+ out.append("stack.p(").append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
} else if (hasReturn) {
- out.append("let __result = yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
+ out.append("let __result = ").append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
} else {
- out.append("yield* ").append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
+ out.append(yieldKw).append(helper).append("(").append(targetExpr).append(", \"").append(methodId).append("\"");
}
if (variadic) {
out.append(", [");
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java
index efa15290cd..2545ceab6b 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java
@@ -117,7 +117,23 @@ enum NativeCategory {
"cn1_java_util_TimeZone_getTimezoneRawOffset_java_lang_String_R_int",
"cn1_java_util_TimeZone_isTimezoneDST_java_lang_String_long_R_boolean",
"cn1_com_codename1_impl_platform_js_VMHost_getLastEventCode_R_int",
- "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int"
+ "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int",
+ // NativeInterface bridge: runtime-implemented in parparvm_runtime.js
+ // (bindNative). Each forwards to the main thread via the
+ // __cn1_native_interface_call__ host hook and coerces the result to
+ // the declared Java type (createJavaString / _LfromNumber / newArray).
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callBoolean_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_boolean",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callByte_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_byte",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callShort_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_short",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callInt_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_int",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callChar_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_char",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callLong_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_long",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callFloat_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_float",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callDouble_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_double",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callString_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_String",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callObject_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_Object",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callArray_java_lang_String_java_lang_String_java_lang_Object_1ARRAY_java_lang_String_R_java_lang_Object",
+ "cn1_com_codename1_impl_platform_js_NativeInterfaceBridge_callVoid_java_lang_String_java_lang_String_java_lang_Object_1ARRAY"
));
private static final Set HOST_HOOK_PREFIXES = new HashSet(Arrays.asList(
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java
index 2de31ee74f..cda51f131f 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptSuspensionAnalysis.java
@@ -38,12 +38,13 @@
* It is declared {@code synchronized} — monitor acquisition can block.
* Its bytecode contains {@code monitorenter} or {@code monitorexit}
* (synchronized block) — same reason.
- * It contains any {@code invokevirtual} / {@code invokeinterface}
- * instruction — the dispatch goes through {@code cn1_iv*} which is
- * a generator, so the caller must be ready to {@code yield*}. We
- * treat ALL virtuals as suspending rather than doing
- * override-set CHA, which keeps the analysis portable and safe
- * against future-inherited suspending overrides.
+ * It contains an {@code invokevirtual} / {@code invokeinterface}
+ * whose dispatched signature has AT LEAST ONE suspending impl in
+ * the class hierarchy (override-set CHA). Such sites are emitted as
+ * {@code yield* cn1_iv*}; sites whose every impl is synchronous use
+ * the {@code cn1_ivs*} sync dispatcher and do NOT make their caller
+ * suspending. The suspending-sig set is computed by fixed-point in
+ * {@link #propagate} and exported via {@link #exportedSuspendingSigs}.
* It contains any {@code invokestatic} / {@code invokespecial} whose
* resolved target is itself suspending (recursive closure via
* fixed-point iteration).
@@ -58,6 +59,11 @@
final class JavascriptSuspensionAnalysis {
private final Map byName = new HashMap();
private final Set suspending = Collections.newSetFromMap(new IdentityHashMap());
+ // Sigs whose runtime impl can be a bindNative-installed generator the
+ // static concrete-impl scan cannot see: declared (possibly abstractly)
+ // on JSO-bridge classes, or string-referenced by the bridge JS (see
+ // seedBridgeReferenced). Unconditionally suspending.
+ private final Set jsoDeclaredSigs = new java.util.HashSet();
// Sigs (name + descriptor) whose concrete impl set contains AT
// LEAST ONE suspending method. Populated during ``propagate``
@@ -73,6 +79,7 @@ static int run(List classes) {
JavascriptSuspensionAnalysis a = new JavascriptSuspensionAnalysis();
a.index(classes);
a.seedDirectlySuspending(classes);
+ a.seedBridgeReferenced(classes);
a.propagate(classes);
return a.applyResults(classes);
}
@@ -105,6 +112,17 @@ private void seedDirectlySuspending(List classes) {
for (ByteCodeClass cls : classes) {
boolean clsIsJso = jsoBridgeClasses.contains(cls.getClsName());
for (BytecodeMethod m : cls.getMethods()) {
+ // JSO-declared SIGNATURES must be suspending even when the
+ // declaration is abstract (interface methods like
+ // ``Window.getDocument()`` have NO translated impl at all --
+ // the only "impl" is the ``function*`` override bindNative
+ // installs at runtime, which the concrete-impl scan in
+ // ``propagate`` can never see). Record the sig here so
+ // ``propagate`` folds it into ``suspendingSigs`` and every
+ // dispatching call site keeps its ``yield*``.
+ if (clsIsJso && !m.isEliminated() && !m.isStatic() && !m.isConstructor()) {
+ jsoDeclaredSigs.add(m.getMethodName() + m.getSignature());
+ }
if (m.isEliminated() || m.isAbstract()) {
continue;
}
@@ -119,25 +137,74 @@ private void seedDirectlySuspending(List classes) {
// with ``cn1_ivAdapt`` wrappers at every hand-written
// ``yield* translatedFn(args)`` call site.
//
- // ``hasVirtualDispatch`` is required in the seed
- // because the emitter hardcodes ``yield* cn1_iv*`` at
- // every INVOKEVIRTUAL / INVOKEINTERFACE call site (see
- // ``JavascriptMethodGenerator.appendVirtualDispatch``
- // -- there is no ``cn1_ivs*`` synchronous virtual
- // dispatcher, and 3 prior attempts to add one all hit
- // runtime errors per
- // ``project_jsport_suspension_tightening_failure``
- // memory). A method emitted as plain ``function``
- // cannot contain ``yield*``, so any method with even
- // ONE virtual call must be a generator. Tightening
- // the sync set further requires landing the sync
- // virtual dispatcher first.
+ // Virtual dispatch is NO LONGER an unconditional seed.
+ // The emitter now has a synchronous virtual-dispatch
+ // family (``cn1_ivs0..N`` in parparvm_runtime.js) that
+ // it selects (via ``isInvokeSuspending`` consulting
+ // ``exportedSuspendingSigs``) for any INVOKEVIRTUAL /
+ // INVOKEINTERFACE whose CHA impl set is entirely
+ // synchronous. So a method whose only virtual calls
+ // target non-suspending sigs can itself be a plain
+ // ``function``. Suspension still propagates through
+ // virtual dispatch in ``propagate``: if ANY impl of a
+ // called sig is suspending, that sig is suspending and
+ // every caller of it is marked suspending there. The
+ // earlier sync-dispatcher attempts failed by letting a
+ // generator leak as a value; ``cn1_ivs*`` drives a
+ // one-shot and throws a named error on a true gap
+ // instead (see the runtime helper).
if (m.isNative()
|| m.isSynchronizedMethod()
|| hasMonitorOps(m)
- || clsIsJso
- || hasVirtualDispatch(m)) {
+ || clsIsJso) {
+ suspending.add(m);
+ }
+ }
+ }
+ }
+
+ /**
+ * Any method whose emitted identifier (or its {@code __impl} body, or
+ * its class-free dispatch id) appears as a string literal in the
+ * hand-written bridge JS must be suspending. Those strings are how
+ * {@code bindNative} / {@code bindCiFallback} (port.js,
+ * parparvm_runtime.js, browser_bridge.js) locate translated methods
+ * to REPLACE with {@code function*} overrides at runtime. The static
+ * body may look trivially synchronous, but the override that actually
+ * runs is a generator -- a caller that skipped {@code yield*} would
+ * receive the raw generator object as its "result" and the override
+ * would never execute (observed as the screenshot runner's
+ * done-callback silently never firing). Over-protecting names the
+ * bridge merely CALLS (it wraps those in {@code cn1_ivAdapt}, which
+ * tolerates sync) costs a handful of generators; under-protecting
+ * breaks the bridge contract silently, so blanket-protect every
+ * string-referenced name.
+ */
+ private void seedBridgeReferenced(List classes) {
+ Set tokens = JavascriptBundleWriter.collectBridgeReferencedCn1Tokens();
+ if (tokens.isEmpty()) {
+ return;
+ }
+ for (ByteCodeClass cls : classes) {
+ for (BytecodeMethod m : cls.getMethods()) {
+ if (m.isEliminated() || m.isAbstract()) {
+ continue;
+ }
+ String full = JavascriptNameUtil.methodIdentifier(cls.getClsName(), m.getMethodName(), m.getSignature());
+ boolean referenced = tokens.contains(full) || tokens.contains(full + "__impl");
+ boolean dispatchable = !m.isStatic() && !m.isConstructor();
+ if (!referenced && dispatchable
+ && tokens.contains(JavascriptNameUtil.dispatchMethodIdentifier(m.getMethodName(), m.getSignature()))) {
+ referenced = true;
+ }
+ if (referenced) {
suspending.add(m);
+ if (dispatchable) {
+ // Virtual dispatch can land on the runtime-installed
+ // override too -- protect the whole signature, same
+ // as the JSO-declared sigs.
+ jsoDeclaredSigs.add(m.getMethodName() + m.getSignature());
+ }
}
}
}
@@ -190,23 +257,6 @@ private static boolean hasMonitorOps(BytecodeMethod m) {
return false;
}
- private static boolean hasVirtualDispatch(BytecodeMethod m) {
- List instructions = m.getInstructions();
- if (instructions == null) {
- return false;
- }
- for (Instruction instr : instructions) {
- if (!(instr instanceof Invoke)) {
- continue;
- }
- int op = instr.getOpcode();
- if (op == Opcodes.INVOKEVIRTUAL || op == Opcodes.INVOKEINTERFACE) {
- return true;
- }
- }
- return false;
- }
-
private void propagate(List classes) {
// Build two reverse indexes so a method becoming suspending
// can propagate to all its callers without rescanning every
@@ -243,6 +293,11 @@ private void propagate(List classes) {
}
}
}
+ // JSO-bridge declared sigs are suspending regardless of their (often
+ // absent / abstract) translated impls -- see seedDirectlySuspending.
+ // Must be folded in BEFORE the caller scan below so dispatching
+ // callers get escalated.
+ suspendingSigs.addAll(jsoDeclaredSigs);
for (ByteCodeClass cls : classes) {
for (BytecodeMethod caller : cls.getMethods()) {
if (caller.isEliminated() || caller.isAbstract()) {
diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
index 9967922bff..f5a4463d43 100644
--- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
+++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
@@ -164,6 +164,74 @@
}
};
+ // ---- Native interface dispatch -------------------------------------------------
+ // Codename One NativeInterface calls arrive here (on the MAIN thread) from the
+ // worker via the generated Impl -> NativeInterfaceBridge.call* host-hooks.
+ // We look up the developer's JS implementation in cn1_native_interfaces (the
+ // registry the stub self-registers into, populated on the main thread by the
+ //
+
+