Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
77da14d
Make the clean target a usable program runtime, and let casts fail
shai-almog Aug 27, 2026
01b366d
Measure what a thread costs, and make the per-thread sizes tunable
shai-almog Aug 27, 2026
3d4b378
Stop discarding uncaught exceptions on the clean target
shai-almog Aug 28, 2026
2ed10e5
Fix two ParparVM portability bugs the backend build hit
shai-almog Aug 29, 2026
8d8e894
Add virtual threads to the VM
shai-almog Sep 1, 2026
7f85de9
Yield the virtual thread instead of sleeping its carrier
shai-almog Aug 31, 2026
7cfea66
Keep the virtual-thread API out of the conservative-roots block
shai-almog Sep 1, 2026
67a61b0
Capture errno before the GC safepoint in the Linux socket read
shai-almog Sep 1, 2026
15bd697
Stop waiting on a thread the GC cannot stop
shai-almog Sep 1, 2026
6e6a18a
Turn virtual threads on wherever the switch exists, and file .S as as…
shai-almog Sep 1, 2026
fe581d5
Assemble the .S the generated projects have been shipping unassembled
shai-almog Sep 1, 2026
be886b6
Close five review findings on the VM half
shai-almog Sep 2, 2026
03d2bf2
Make the direct JSON writer agree with the map path, and test that it…
shai-almog Sep 2, 2026
0119acd
Give java.io.File a Windows implementation, and stage the header it n…
shai-almog Sep 2, 2026
9b4b1ca
An archive must not write outside the directory it is unpacked into
shai-almog Sep 2, 2026
cea27a2
Finish the Windows java.io.File arm: the four gaps behind the first one
shai-almog Sep 2, 2026
ba22565
Zero a pthread_t portably, and stop declaring a local Windows cannot use
shai-almog Sep 2, 2026
80da86d
Close the VM-half review findings, and record the two that stay open
shai-almog Sep 2, 2026
764587f
Four more ways the direct JSON writer disagreed with the map path
shai-almog Sep 2, 2026
6155b5f
Enumerate a directory once, and clamp skip without overflowing first
shai-almog Sep 2, 2026
a112185
Resolve drive-relative Windows paths, and create files in one syscall
shai-almog Sep 2, 2026
eaec4ed
Decode argv and the environment as UTF-8 instead of widening bytes
shai-almog Sep 2, 2026
3451ef0
Decode argv and the environment in the PLATFORM's encoding, not alway…
shai-almog Sep 2, 2026
ea35b3b
Validate an array store's index before checking its element type
shai-almog Sep 2, 2026
2b9d12a
Revert marking a running virtual thread's state active -- it hangs th…
shai-almog Sep 2, 2026
48e84bb
Stop the array store check rejecting valid multidimensional stores
shai-almog Sep 2, 2026
287ab9d
Probe an unresponsive thread briefly instead of skipping its roots
shai-almog Sep 2, 2026
bb98371
Emit JSON null for an unset boxed Boolean or Character
shai-almog Sep 2, 2026
9f27f80
Report thread-table exhaustion instead of writing before the table
shai-almog Sep 2, 2026
99f9263
Mark active only what the collector can stop, and unbind TLS before f…
shai-almog Sep 2, 2026
e1ea719
Keep an unmapped reference field's wire type a string
shai-almog Sep 2, 2026
f13a53c
Honour close() on System.in
shai-almog Sep 2, 2026
fbc8ca8
Mark the unfinished VM surfaces EXPERIMENTAL, and keep checked casts …
shai-almog Sep 2, 2026
9896b1f
Record the registry-snapshot scope as an experimental gap, not a fix
shai-almog Sep 2, 2026
de54965
Make the uncaught-exception test's timeout reachable
shai-almog Sep 2, 2026
5694e15
File.renameTo renamed the destination onto itself
shai-almog Sep 2, 2026
2907915
Keep unmapped list elements raw -- a regression from the reference fix
shai-almog Sep 2, 2026
30d7662
Release a file stream's native handle if it is never closed
shai-almog Sep 2, 2026
bd2057f
Qualify rooted Windows paths, make close idempotent, list the snapsho…
shai-almog Sep 2, 2026
0c96b7b
Park the mutator around every blocking stdio call, not just read and …
shai-almog Sep 2, 2026
9f7d96b
Answer null for an environment name holding a NUL; decline the rest
shai-almog Sep 2, 2026
4729dea
Park the seeks too, and let the thread benchmark exit
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ad-cn1lib-ios-native-check.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
Expand All@@ -29,6 +30,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'

concurrency:
Expand DownExpand Up@@ -115,6 +117,11 @@ jobs:
# cn1_globals.h for them. Reproduce that here, using the port's own
# header so a change to those macros is caught too.
cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/"
# cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual
# thread rather than sleeping the carrier it runs on), so staging the
# first without the second stops the probe at "file not found" before it
# compiles a single line of the cn1lib.
cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/"
# Generated per translation from the app's class list; nothing in the
# ad bridges reads it, so an empty stand-in is enough to let
# cn1_globals.h parse on its own.
Expand Down
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/mapping/Mapper.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,35 @@ public interface Mapper<T> {
/// `JSONParser` and populates a fresh `T`.
T fromMap(Map<String, Object> map);

/// Optional: append `instance` as JSON directly, without building a map
/// first.
///
/// A generated mapper knows every property name and type at build time, so
/// it can append them in order rather than filling a `LinkedHashMap` -- with
/// a hash per key -- and having the writer walk it back rediscovering each
/// value's type. On a small object that map round trip is the majority of
/// the serialisation cost, not the escaping.
///
/// Measured through `Mappers#toJson` on a four-property object, output
/// asserted identical: **2.05x / 1.51x / 2.81x** faster (263 -> 128,
/// 214 -> 142, 224 -> 80 ns per call).
///
/// That measurement ran on Java SE, so the map it avoids is the JDK's
/// LinkedHashMap. On a translated device build the map is
/// `vm/JavaAPI`'s, which overrides the natives HashMap gets and costs about
/// 1.5x a HashMap to build -- so the saving there is at least this, not less.
///
/// Implemented as a separate interface rather than a method on `Mapper` so
/// hand-written mappers keep compiling; `Mappers#toJson` uses it when the
/// mapper offers it and falls back to `toMap` when it does not.
interface Direct<T> {

/// Appends `instance` as a JSON value -- an object, or the four
/// characters `null`. Must produce exactly what
/// `JSONWriter.toJson(toMap(instance))` would.
void toJson(T instance, StringBuilder out);
}

/// XML root element name (`@XmlRoot.value`, falling back to the class
/// simple name with a lowercase first character).
String xmlRootName();
Expand Down
131 changes: 130 additions & 1 deletion CodenameOne/src/com/codename1/mapping/Mappers.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,18 @@ public static String toJson(Object instance) {
if (m == null) {
throw missing(instance.getClass());
}
Map<String, Object> root = m.toMap(instance);
StringBuilder sb = new StringBuilder();
if (m instanceof Mapper.Direct) {
// The generated mapper knows its properties at build time and can
// append them in order. Skips a LinkedHashMap, a hash per key and a
// walk back over it that rediscovers each value's type -- which on a
// small object is most of the cost of serialising it.
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, sb);
return sb.toString();
}
Map<String, Object> root = m.toMap(instance);
writeJson(sb, root);
return sb.toString();
}
Expand DownExpand Up@@ -208,6 +218,112 @@ private static IllegalStateException missing(Class<?> type) {
// Tiny JSON writer
// ---------------------------------------------------------------

/// Appends any value a generated codec can hold, producing exactly what the
/// map path would.
///
/// Public because generated `toJson` methods call it for the property kinds
/// they cannot render inline -- a nested mapped object, a `Property`'s value,
/// a list element. A nested object goes through ITS mapper, taking that
/// mapper's `Mapper.Direct` route when it offers one, so nesting stays free
/// of intermediate maps all the way down.
///
/// Conversions match `Mapper#toMap` exactly, and must keep matching: a date
/// becomes its millisecond value and an enum its `name()`, because that is
/// what the map path puts in the map before the writer ever sees it.
public static void appendJsonValue(StringBuilder out, Object value) {
if (value == null) {
out.append("null");
return;
}
if (value instanceof java.util.Date) {
out.append(((java.util.Date) value).getTime());
return;
}
if (value instanceof String || value instanceof Boolean
|| value instanceof Number || value instanceof Map
|| value instanceof java.util.Collection) {
writeJson(out, value);
return;
}
// A mapped object, or something with no mapper at all -- appendJson
// decides, and falls back to the string form the map path would use.
appendJson(value, out);
}

/// Appends `instance` as a JSON object using its registered mapper, taking
/// the `Mapper.Direct` route when that mapper offers one.
///
/// Unlike `#toJson(Object)` this appends rather than returning a String, so
/// nesting does not build one String per level. An unmapped value falls back
/// to its `toString`, which is what `Mapper#toMap` does for the same case
/// rather than failing the whole document.
public static void appendJson(Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) BY_NAME.get(instance.getClass().getName());
if (m == null) {
writeJsonString(out, instance.toString());
return;
}
if (m instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) m;
d.toJson(instance, out);
return;
}
writeJson(out, m.toMap(instance));
}

/// Appends `value` exactly as `JSONWriter` would render it if it had been put
/// into the map that `Mapper#toMap` builds.
///
/// This is deliberately NOT `#appendJsonValue`: that one is smarter, turning a
/// `Date` into epoch milliseconds and a mapped object into nested JSON. Where a
/// generated mapper is reproducing what the map path stored RAW -- a `Property`
/// value is the case that matters -- being smarter is being different, and
/// `Mapper.Direct` promises identical output rather than better output.
public static void appendJsonRaw(StringBuilder out, Object value) {
writeJson(out, value);
}

/// Appends `instance` through the mapper the CALLER names, rather than the one
/// registered for the instance's runtime class.
///
/// The distinction is polymorphism. A field declared `Base` holding an instance
/// of an unmapped subclass finds no mapper by runtime class, and
/// `#appendJson(Object, StringBuilder)` then falls back to the quoted
/// `toString`. `Mapper#toMap` looks the mapper up by the DECLARED type and
/// serialises the subclass as an object, so a generated mapper reproducing the
/// map path has to ask the same question. Mirrors what the map path does with a
/// null mapper too: the raw value, which renders as its quoted `toString`.
public static void appendJsonUsing(Mapper<?> mapper, Object instance, StringBuilder out) {
if (instance == null) {
out.append("null");
return;
}
if (mapper == null) {
// toString(), not the raw value. emitFieldToMap stores `_v.toString()`
// when the declared type has no registered mapper, so JSONWriter quotes
// it -- an Object field holding an Integer comes out as "5". Passing the
// instance to writeJson would emit 5, changing the field's wire TYPE the
// day its mapper gains a direct writer.
writeJsonString(out, instance.toString());
return;
}
if (mapper instanceof Mapper.Direct) {
@SuppressWarnings("unchecked")
Mapper.Direct<Object> d = (Mapper.Direct<Object>) mapper;
d.toJson(instance, out);
return;
}
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) mapper;
writeJson(out, m.toMap(instance));
}

static void writeJson(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
Expand DownExpand Up@@ -249,6 +365,19 @@ static void writeJson(StringBuilder sb, Object value) {
writeJsonString(sb, value.toString());
}

/// Appends `s` as an escaped JSON string, or `null`.
///
/// Public because GENERATED mappers call it: a direct writer has to escape
/// exactly the way the map path does, and the only way to guarantee that is
/// for both to use this method rather than each having its own copy.
public static void appendJsonString(StringBuilder sb, String s) {
if (s == null) {
sb.append("null");
return;
}
writeJsonString(sb, s);
}

private static void writeJsonString(StringBuilder sb, String s) {
sb.append('"');
int len = s.length();
Expand Down
100 changes: 74 additions & 26 deletions Ports/JavaSE/src/com/codename1/impl/javase/UnzipUtility.java
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
/*
* Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All@@ -23,42 +46,67 @@ public class UnzipUtility {
public void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
destDir.mkdirs();
}
// Canonical, because that is what resolves the "../" an archive can carry.
Path destRoot = destDir.getCanonicalFile().toPath();
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
try {
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
// ZIP SLIP. An entry name is attacker-controlled and may be
// "../../something": concatenating it onto the destination writes
// wherever the archive says, which for these two callers means an
// arbitrary file overwritten under the user's account while they
// believe they are unpacking Groovy or JavaFX. Refuse anything that
// does not land inside the destination.
//
// Path.startsWith compares COMPONENT-wise, not character-wise, so
// "/tmp/dest-evil" is correctly rejected against "/tmp/dest" -- a
// plain String.startsWith accepts it unless the prefix is given a
// trailing separator, and then it wrongly rejects the destination
// itself. Neither trap exists here.
Path target = new File(destDir, entry.getName()).getCanonicalFile().toPath();
if (!target.startsWith(destRoot)) {
throw new IOException("Zip entry escapes the destination directory: "
+ entry.getName());
}
File targetFile = target.toFile();
if (!entry.isDirectory()) {
// Nested entries can arrive before the directory that holds
// them, and FileOutputStream will not create it.
File parent = targetFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
extractFile(zipIn, targetFile);
} else {
targetFile.mkdirs();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} finally {
zipIn.close();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @param target
* @throws IOException
*/
private void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
private void extractFile(ZipInputStream zipIn, File target) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
try {
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
} finally {
bos.close();
}
bos.close();
}
}



8 changes: 7 additions & 1 deletion Ports/LinuxPort/nativeSources/cn1_linux_socket.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
CN1Socket* s = (CN1Socket*) (intptr_t) socket;
char* data;
ssize_t n;
int readErrno;
if (!s || s->fd < 0 || buffer == JAVA_NULL || length <= 0) {
return -1;
}
Expand All@@ -119,14 +120,19 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_socketRead___long_byte_1ARRAY_int_
* syscall. Every other CN1 port's blocking I/O does the same. */
CN1_YIELD_THREAD;
n = read(s->fd, data + offset, (size_t) length);
/* Captured before CN1_RESUME_THREAD. The resume is a GC safepoint and can park
* this thread on a timed wait, which overwrites errno -- so lastError below
* reported the WAIT's outcome rather than the read's, handing Java a misleading
* error for a failure that had nothing to do with it. */
readErrno = errno;
CN1_RESUME_THREAD;
/* Keep the buffer array object reachable across the parked read: only `data` (an
* interior pointer) is used, so the optimizer may drop `buffer` and the concurrent GC
* -- scanning this parked thread -- can sweep it mid-read (a use-after-free; see the
* Windows port where this manifested on the cn1ss WebSocket reader). Force liveness. */
CN1_SOCKET_KEEP_ALIVE(buffer);
if (n <= 0) {
s->lastError = n < 0 ? errno : 0;
s->lastError = n < 0 ? readErrno : 0;
if (n == 0) {
s->connected = 0;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -572,9 +572,18 @@ List<String> stageWatchTranslation(BuildRequest request, File tmpFile, File appS
// Swift phase fixup, which globs <Main>-src/**/*.swift, would have swept the watch
// copy into the PHONE target instead. It is excluded from that glob for the same
// reason (see IPhoneBuilder's swift fixup).
// .S belongs here too, for the same reason .swift does. The virtual-thread
// runtime's context switch has to be assembly (glibc aborts a cross-stack
// longjmp under _FORTIFY_SOURCE and musl has no makecontext), and it is the
// first .S the translator emits -- so copying cn1_virtual_thread.c without
// cn1_virtual_thread_asm.S left the WATCH target compiling a caller whose
// callee did not exist: "_cn1VirtualThreadSwitch, referenced from
// _cn1VirtualThreadYield ... symbol(s) not found". The phone target linked
// fine, so only a watch-enabled build shows it.
boolean source = name.endsWith(".m") || name.endsWith(".c")
|| name.endsWith(".swift") || name.endsWith(".mm")
|| name.endsWith(".cpp") || name.endsWith(".cc");
|| name.endsWith(".cpp") || name.endsWith(".cc")
|| name.endsWith(".S") || name.endsWith(".s");
if (!source && !name.endsWith(".h")) {
// Only the code. The watch bundle's plist, resources and project file are written
// by this builder against the PHONE project -- taking the second translation's
Expand Down
Loading
Loading