Add iOS on-device debugging support#4999
Merged
Merged
Conversation
Adds a JDWP-compatible debugger for ParparVM-built iOS apps so jdb /
IntelliJ / VS Code can attach to a real device or the iOS Simulator
and set breakpoints, walk the stack, and inspect locals + Strings.
Three pieces:
- ParparVM translator emits per-method side-tables (locals addresses,
variable names, line tables) and a cn1-symbols.txt sidecar when
-Dcn1.onDeviceDebug=true is set. Release builds are unaffected.
- A listener thread (Ports/iOSPort/nativeSources/cn1_debugger.{h,m})
is compiled into debug builds, dials out to a desktop proxy over
TCP, and services set/clear-bp, resume, step, get-stack/locals,
get-object-class, and get-string commands. The hot path in
__CN1_DEBUG_INFO is one predictable load+branch when nothing is
attached.
- A new Maven module (cn1-debug-proxy) bridges that custom protocol
to JDWP so any standard Java debugger speaks to it. Includes a
minimum-viable JDWP implementation covering everything jdb needs
for breakpoint, where, locals, and String inspection.
Maven goals: cn1:ios-on-device-debugging (launches the proxy) and
cn1:buildIosOnDeviceDebug (cloud build target).
Build-hint UX: codename1.arg.ios.onDeviceDebug=true plus
proxyHost/proxyPort. End-user docs live in
docs/developer-guide/On-Device-Debugging.asciidoc.
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Contributor
Cloudflare Preview
|
Collaborator
Author
|
Compared 20 screenshots: 20 matched. |
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Collaborator
Author
|
Compared 109 screenshots: 109 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
- Force-off ios.onDeviceDebug on release builds (ios.buildType=release)
in both the translator JVM flag and the Info.plist injection, so a
stray hint in codenameone_settings.properties can't leak the debug
listener thread into an App Store binary.
- Document the new hints (ios.onDeviceDebug, .proxyHost, .proxyPort,
.waitForAttach) in the iOS build hints table in
Advanced-Topics-Under-The-Hood.asciidoc.
- Drop unused Parser.getClasses() that triggered MS_EXPOSE_REP.
- Rework the dev-guide chapter: remove the {cn1-release-version}
sentence from Prerequisites, drop the "macOS with Xcode required"
claim (the cloud build path works equally), drop the redundant
JDWP-debugger line, collapse the duplicated build instructions
into one step that points at the normal build flow, switch to
build-hint vocabulary, and strip the codename1.arg. prefixes from
the user-facing hint names.
- Fix Vale prose-linter regressions (contractions, first-person,
Latinisms).
# Conflicts: # maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Quality-of-life improvements that emerged while running the proxy end-to-end
locally against the iOS simulator.
Device-side runtime (cn1_debugger.m + .h):
- cn1_debugger_start() no longer blocks the AppDelegate on
didFinishLaunchingWithOptions. The proxy connection runs on its own
thread regardless of CN1ProxyWaitForAttach, so UIKit can finish boot,
draw the launch transition, and -- when waitForAttach is on -- present
a translucent "Waiting for debugger..." overlay UIWindow. The previous
behaviour left the user staring at the splash with no signal that the
app was waiting on anything.
- New cn1_debugger_run_when_ready(block) API lets the AppDelegate defer
the VM start callback until the proxy reports the IDE has attached.
When waitForAttach is off (or on-device-debug is disabled at build
time) the block runs synchronously and behaves identically to the
pre-change boot flow.
GLAppDelegate:
- Calls cn1_debugger_run_when_ready around the VM callback so wait-mode
no longer races against splash dismissal, and captures the location
launch option into the block so it survives the deferral.
JDWP proxy (JdwpServer.java):
- acceptAndServe() now loops on accept() so the developer can detach and
reattach the IDE without restarting the proxy. Per-attach state is
reset via closeJdwpSession(); breakpoint registrations persist across
attaches.
- After handshake completes the proxy schedules an auto-resume that
releases the device-side waitForAttach gate 500 ms later. The delay
gives IntelliJ / VS Code time to register breakpoints before the app
races past them; without this the app sat on the waiting overlay
forever because most JDWP debuggers don't auto-send VM.Resume.
Misc:
- Add /artifacts/ to .gitignore (build wrapper drop-zone used by the
new ios-on-device-debugging mojo).
Three additions that turn the proxy from "stack-trace viewer" into a real interactive debugger. 1. Instance-field inspection Translator: ByteCodeClass.appendOnDeviceDebugFieldTable emits a per-class field-offset table (fieldId, offsetof, JVM type char, name) in each generated .m file, behind a CN1_ON_DEVICE_DEBUG guard. An __attribute__((constructor)) shim registers the table with cn1_debugger at process load. Parser.writeSymbolSidecar also emits `field <classId> <fieldId> <name> <descriptor> <accessFlags>` rows so the proxy can answer JDWP ClassType.Fields / FieldsWithGeneric without a device round-trip. Device: new CMD_GET_OBJECT_FIELDS handler walks the registered field table for the object's runtime classId and reads each field straight out of the struct using offsetof. Replies as EVT_OBJECT_FIELDS (count, then [type-char, value] tuples). Proxy: JdwpServer.handleObject case 2 (GetValues), handleStackFrame case 3 (ThisObject), and ClassType cases 4/14 (Fields, FieldsWithGeneric) now read real data instead of returning empty stubs. ThisObject piggybacks on the existing GetLocals path, reading slot 0 as an object reference for instance methods (the JVM always parks `this` there on entry); for statics slot 0 is zero, which is the correct JDWP reply. `dump this` in jdb against a running ParparVM-built iOS app now shows `tickCount: 4, pointerCount: 0` etc. — actual field values. 2. Native stdout / stderr forwarding cn1_debugger_start dup2()'s STDOUT_FILENO and STDERR_FILENO to pipes, then runs two streamCaptureThreads that chunk the pipes by newline. Each completed line is mirrored back to the original FD (so xcrun simctl log / Xcode console still works) and, if a proxy is connected, sent as EVT_STDOUT_LINE / EVT_STDERR_LINE. Proxy: handles the events and prints them prefixed with `[device]` to its own stdout (IntelliJ surfaces this in the proxy's debug console when it's launched as an IDE run config). System.out.println, Log.p, printf, fprintf(stderr,...) all flow through. NSLog on the iOS Simulator works too; on a real device NSLog may bypass stderr (documented as a limit). 3. CN1 core class breakpoints Confirmed working — sidecar already covers framework classes the same way it covers user code. The missing piece was just docs: On-Device-Debugging.asciidoc now describes how to attach the CodenameOne/src source directory in IntelliJ / jdb so the source pane resolves while stepping through framework code. Also tightened the "What works today" list and added a fresh "Known limitations" entry for static-field reads, plus a note that NSLog on a real device may bypass the stdout forwarder. Wire protocol additions: CMD_GET_OBJECT_FIELDS (0x0D), EVT_OBJECT_FIELDS (0x8A), EVT_STDOUT_LINE (0x8B), EVT_STDERR_LINE (0x8C). SymbolTable additions: FieldInfo, ClassInfo.instanceFields, fieldById, fieldCount. Verified end-to-end on iPhone 17 Pro / iOS 26.3 simulator under Xcode 26 against a minimal CN1 app: BP in framework's Display.edtLoopImpl fires, list shows the actual source, locals populate with their real names, and `dump this` walks the instance-field table.
Real bug found while testing stepping in IntelliJ: jdb's `next`
(step-over) returned `JDWP Error: 103` because the EventRequest.Set
modifier-loop walked past the end of the payload when JDI sent its
default ClassExclude modifiers (`java.*`, `javax.*`, `sun.*`,
`com.sun.*`, `jdk.internal.*`, etc. — JDI auto-attaches these to
every step request) and our switch's `default` branch set
`off = p.length`, then the next iteration read `p[p.length]` and
ArrayIndexOutOfBounds'd. The IDE's view: step request rejected → no
step event → app keeps running → looks indistinguishable from
"continue".
Fix: every modifier-case now bounds-checks before reading and bails
the loop via a `badModifier` flag if anything's off. Added the
missing modifier kinds JDI emits but we hadn't seen on the wire
yet (`FieldOnly`=9, `SourceNameMatch`=12), and changed the unknown-
kind branch to abort the loop with a `[jdwp]` log instead of trying
to guess the width.
Also fixed a related NPE: when the IDE detached mid-session, the
device-disconnect path tried to send VM_DEATH on an already-null
out stream and crashed the listener thread. writeEventCommand now
no-ops when out is null.
Added `[jdwp] STEP request` / `STEP_COMPLETE` / `VM.Resume` /
`Thread.Resume` log lines so future debugging of stepping is just
a matter of reading the proxy console.
Docs: documented the IntelliJ run-config trick for surfacing
device output ([device] lines) in the IDE console — launch the
proxy as an Application configuration alongside the Remote JVM
Debug attach and group them with a Compound run config. Without
this, the proxy's stdout (where device prints end up) only shows
up if the user runs the proxy from a terminal.
Verified end-to-end on iPhone 17 Pro / iOS 26.3 sim:
- jdb's `next` from a BP at heartbeat line 41 lands on line 42
(STEP_COMPLETE logged).
- jdb's `step` from a BP at line 42 enters the Log.p method
(STEP_COMPLETE for methodId=13294, line=242).
- Proxy survives an IDE detach and accepts the next reattach.
The IDE can now call any framework / user method on a paused VM and
have the result come back as a real object reference (or a value, or
a thrown Throwable). This is what makes `print
Display.getInstance().getCurrent().getTitle()` work in jdb, and
what makes IntelliJ's "Evaluate Expression" pop-up usable for chains
that involve method calls.
Translator (BytecodeMethod / ByteCodeClass / Parser):
- Emits one C "invoke thunk" per non-eliminated method, per class,
under CN1_ON_DEVICE_DEBUG. The thunk has a uniform signature
(tsd, this, args, *result), unpacks the args from a union into
the typed C parameters the translated method expects, dispatches
through virtual_<sym>() / <sym>() depending on virtuality, and
packs the return value back into the result union. The call is
wrapped in a catch-all try block so an uncaught Throwable
round-trips as result.type='X' instead of longjmp-ing past
suspendCurrent's cond_wait.
- Skips classes whose hand-written native impl has fallen out of
sync with the translator's calling convention: java.io.*,
java.net.*, java.nio.*, com.codename1.impl.*. The other system
packages (java.lang, java.util, ...) are fine because their
native impls are in nativeMethods.m and use the modern names.
- When on-device-debug is on, the unused-method optimiser keeps
every instance method of java.lang.Object alive — jdb's `print`
formats every object through Object.toString, so silently
dropping it earlier made every evaluation return "<void value>".
- Sidecar `method` rows now carry an isStatic flag; `class` rows
carry their superclass id so the proxy can answer
ClassType.Superclass and let JDI walk to inherited methods.
Device runtime (cn1_debugger.h/m):
- New cn1_invoke_arg union + cn1_invoke_result struct (JVM type-
char plus value slot), and a cn1_invoke_thunk_t function-pointer
type that the translator-emitted thunks all match.
- cn1_debugger_register_invoke_thunk(methodId, thunk) registry,
array-indexed by methodId for O(1) lookup.
- New CMD_INVOKE_METHOD handler. It queues the call on the
target thread's sus_state, signals s->cv, and blocks the
listener thread on a result-ready predicate. suspendCurrent's
cond_wait loop services the request on the suspended Java
thread (so the call runs in a valid tsd / GC context), then
goes back to waiting.
- New EVT_INVOKE_RESULT response carrying (type-char, 8-byte value).
Proxy (WireProtocol / DeviceConnection / SymbolTable / JdwpServer):
- WireProtocol: CMD_INVOKE_METHOD=0x0E, EVT_INVOKE_RESULT=0x8D.
- DeviceConnection: invokeMethod(threadId, methodId, thisObj,
argTypes[], argValues[]) and onInvokeResult(type, value)
callback wired through the listener.
- SymbolTable: MethodInfo.isStatic, ClassInfo.superId, and
extended `method`/`class` row parsing (still tolerates older
4-column rows).
- JdwpServer:
* ClassType.InvokeMethod (cmd 3) and
ObjectReference.InvokeMethod (cmd 6) parse the JDWP args,
forward as a CMD_INVOKE_METHOD, and pack the device's
EVT_INVOKE_RESULT into a JDWP returnValue + exception slot.
* ClassType.Superclass now returns the actual sidecar superId
so JDI walks the hierarchy properly instead of stopping at
every class.
* Methods / MethodsWithGeneric set the JDWP STATIC bit when
the sidecar marks the method static — without it jdb's
expression parser refuses to resolve `Class.method()`.
Verified end-to-end on iPhone 17 Pro / iOS 26.3 simulator. Single-
invoke and chained:
print com.codename1.ui.Display.getInstance() -> Display ref
print Display.getInstance().getCurrent() -> Form ref
print Display.getInstance().getCurrent().getTitle() -> String "hello, world"
Object.toString round-trips through nativeMethods.m, so jdb's
default object-display formatting also works.
…sues When IntelliJ's step behaviour diverges from jdb's (different default class-exclude modifiers, additional pre-step queries, etc.), the proxy needs a way to surface every JDWP command it receives so the wire- level difference is visible without rebuilding. Add a --trace-jdwp flag that toggles a single-line log per inbound command (cmd-set / cmd / id / payload length). Off by default — release sessions shouldn't pay the log overhead.
JDWP modifier-kind values per the spec: 1 Count 2 ConditionalExpr (deprecated) 3 ThreadOnly 4 ClassOnly 5 ClassMatch 6 ClassExclude 7 LocationOnly 8 ExceptionOnly 9 FieldOnly 10 Step 11 InstanceOnly 12 SourceNameMatch The previous switch had every kind shifted by one — 2 was treated as ThreadOnly (really Conditional, deprecated), 3 as ClassOnly (really ThreadOnly), and the string-payload kinds were 4/5 instead of 5/6. The practical bite: IntelliJ auto-attaches a handful of ClassExclude modifiers (`java.*`, `javax.*`, `sun.*`, `com.sun.*`, `jdk.internal.*`) to every StepRequest. With kind=6 unrecognised the parser bailed mid-payload, sent the modKind=6 warning to the proxy log, and truncated the modifier list — IntelliJ then either retries the step or shows "Source code does not match the bytecode" depending on exactly which modifiers it expected to be honoured. Caught from a JDWP packet trace where IntelliJ sent a 561-byte EventRequest.Set with 27 modifiers; the proxy logged "unknown modKind=6 — ignoring remaining 26 modifiers" right before each step. Also added an explicit case for modKind=2 (Conditional, deprecated) even though no current debugger sends it, so the parser handles every value in the spec without falling to the default branch.
Frames panel was sticking to the previous suspend location after each step because fetchStackForThread short-circuits on a populated cache. onBreakpointHit eagerly fetches the stack and populates that cache; onStepComplete had no eager fetch but also didn't invalidate, so when IntelliJ asked for Frames after a step it got back the BP_HIT stack — e.g. user sets BP on line 48, presses F8 once, the cursor / Frames / double-click all still point to line 48 while the device is actually suspended on line 49. Same bug affected locals (StackFrame.GetValues short-circuits via its own pendingLocals cache, kept across suspensions of the same frameIdx) — the IDE evaluated expressions against a stale frame. Fix: introduce invalidateStack() / invalidateLocals() helpers and call them in every state-changing handler: - onBreakpointHit (before the eager fetch) - onStepComplete (no eager fetch — IDE will pull) - VM.Resume (everything cached is now meaningless) - Thread.Resume (same) End result: Frames panel and Variables panel both refresh after every step / resume cycle without the IDE having to explicitly re-suspend.
ArrayList's `elementData` (and any other Object[] / int[] / etc.) was
showing as a single ref in the Variables view with no children — the
proxy stubbed both ArrayReference commands with NOT_IMPLEMENTED, so
IntelliJ couldn't expand the array.
Device side (cn1_debugger.m):
- CMD_GET_ARRAY_LENGTH reads ((JAVA_ARRAY)obj)->length.
- CMD_GET_ARRAY_VALUES walks ->data with element width chosen from
primitiveSize + the element class's clsName (so byte/boolean
arrays don't get conflated, neither do int/float and long/double).
Element bytes go on the wire as packed big-endian — same layout
JDWP expects for ArrayRegion primitive payloads.
- CMD_GET_OBJECT_CLASS now reports the class struct's `isArray`
flag as a trailing byte. Older proxies that only read 4 bytes
still work.
Wire protocol additions: CMD_GET_ARRAY_LENGTH (0x0F),
CMD_GET_ARRAY_VALUES (0x10), EVT_ARRAY_LENGTH (0x8E),
EVT_ARRAY_VALUES (0x8F).
Proxy:
- JdwpServer.handleArray wires JDWP ArrayReference.Length (cmd 1)
and GetValues (cmd 2) to the device commands above. For object
arrays the GetValues reply tags each element 'L' inline (JDWP's
ArrayRegion encoding for non-primitive arrays); for primitives
we forward the packed bytes verbatim.
- DeviceConnection.onObjectClass now carries an isArray flag from
the device. JdwpServer's ObjectReference.ReferenceType uses
TYPE_TAG_ARRAY (3) for array instances so IntelliJ knows to
issue ArrayReference commands rather than treating the object
as a class instance and giving up.
Verified: dropping a BP in user code, expanding an ArrayList in the
Variables view now shows `elementData[0..size-1]` with values, not
just an opaque object reference.
Update the "What works today" list:
- Add a bullet for array inspection (length + per-index values for
Object[] and primitive arrays, including ArrayList.elementData
drilldown).
- Expand the method-invocation bullet to mention the
Display.getInstance().getCurrent().getTitle() chain that motivated
the implementation, plus the catch-all try block that keeps an
uncaught throw from tearing down the session.
Vale: move punctuation inside the quoted phrase and rephrase the
first-person plural in the troubleshooting section.
After merging master (which promoted LanguageTool to a hard quality gate via #5007) the developer guide had 7 grammar-checker matches attributable to this branch's chapter plus 1 pre-existing match elsewhere. This drives the count back to 0. Chapter-local fixes (docs/developer-guide/On-Device-Debugging.asciidoc): - Replace "source dir" with "source directory" so LanguageTool's morfologik English dictionary stops flagging the abbreviation. - Rephrase the "Watching expressions follow the same rule" sentence so QUESTION_MARK no longer fires (LanguageTool reads the "ing" opening as a rhetorical fragment expecting a question mark). - Rephrase the "What doesn't work:" lead so the wh-word doesn't trip QUESTION_MARK on the surrounding sentence. Accept-list additions (docs/developer-guide/languagetool-accept.txt): - jdb (the JDK command-line debugger), - loopback (the networking term), - rethrow / rethrows (standard exception-handling vocabulary). These are well-known technical terms LanguageTool's default dictionary doesn't recognise but appear unaltered in any reasonable developer-facing prose. Pre-existing match (docs/developer-guide/Maven-Getting-Started.adoc): Replace "(aka X. aka Y)" with "(also known as X or Y)" — the period between the two "aka"s made LanguageTool flag the second "aka" as a sentence-start lowercase letter. Master's docs job is PR-trigger-only so this never showed up there; it surfaced here because the merge brought in the gate.
Wire on-device debugging into the cn1app-archetype so a fresh project
already has everything needed for the IDE-driven flow, not just the
command-line one:
- common/codenameone_settings.properties carries the four
`ios.onDeviceDebug.*` build hints commented out, with a one-line
pointer to the developer guide. Discoverable from the file every
Codename One developer already edits.
- archetype-resources/.idea/runConfigurations/CN1_Debug_Proxy.xml is
a Maven run config that invokes `codenameone:ios-on-device-debugging`
from the project root — same proxy, same defaults, no path
hardcoding.
- archetype-resources/.idea/runConfigurations/CN1_Attach_iOS.xml is a
Remote JVM Debug config attaching to `localhost:8000`, scoped to
the `${rootArtifactId}-common` module so IntelliJ's source
resolution lines up with the user's code.
- .gitignore un-ignores the archetype's .idea/ subtree so these
configs stay versioned (it still ignores everyone else's local
.idea/).
Developer guide:
- Re-orders so the IntelliJ Quick start leads, the Maven /
command-line + jdb flow becomes a smaller section after it. The
text now explicitly says to **Run** (not Debug) the proxy config
to avoid accidentally attaching IntelliJ's own debugger to the
proxy process.
- Renames "iOS Simulator" to "native iOS simulator" wherever the
Codename One simulator could otherwise be implied.
- Rewrites the "Working from a clone of this repository" bullet —
promotes the Maven sources-jar approach (the path almost every
reader will take), explains the cloned-CodenameOne-repo alternative
by name, and links to the GitHub repository instead of implying
the reader is already inside it.
Misc:
- `cn1_debugger.h` now wraps the `cn1_globals.h` include inside the
`CN1_ON_DEVICE_DEBUG` ifdef so release builds don't pull in the
ParparVM globals header at all.
- Single Microsoft.Auto vale warning fixed ("auto-resume" →
"resume automatically").
The build-hints table already carried entries for
`ios.onDeviceDebug` and the three companion hints; updated one
parenthetical to say "native iOS simulator" for consistency.
The previous commit added the two run-config XMLs under archetype-resources/.idea/runConfigurations/ but git silently dropped them because of a second `.idea/` line further down in .gitignore that re-applied the ignore after my un-ignore exception. Move the exception below both ignore rules so the files actually land in the tree.
8 tasks
shai-almog
added a commit
that referenced
this pull request
May 23, 2026
* Add Android on-device debugging support Mirrors the iOS on-device-debug flow from #4999 for Android, taking advantage of the fact that Dalvik/ART already speaks JDWP — no desktop proxy needed. The Codename One Maven plugin orchestrates adb: install the APK, mark it as the debug-app, launch the Activity, poll for the PID, forward JDWP onto localhost, and tail logcat. The existing IntelliJ workflow extends: there is now a CN1 Android On-Device Debug + CN1 Attach Android pair under the same On-Device Debug folder as the iOS configs. Pieces: - AndroidGradleBuilder parses a new android.onDeviceDebug build hint that flips the generated manifest to debuggable=true and disables R8/proguard so symbols and locals survive the build. Release builds (anything without the hint) are unaffected. - cn1:android-on-device-debugging — new Mojo that drives the adb session end-to-end, with autodetection of adb from ANDROID_HOME / ANDROID_SDK_ROOT / standard Studio SDK paths / $PATH, optional adb connect <ip:port> for wireless devices (Android 11+ adb pair / adb connect and legacy adb tcpip both covered), APK autodetection from target/, am set-debug-app -w for wait-for- attach, and a logcat stream prefixed [device] for the lifetime of the session. Cleans up adb forward on shutdown. - cn1:buildAndroidOnDeviceDebug — wrapper that force-sets the hint and invokes the existing android-device cloud build, so the IDE menu has a one-click entry that doesn't depend on the project's codenameone_settings.properties. Archetype: - common/codenameone_settings.properties carries the new hint commented out, next to the existing ios.onDeviceDebug.* lines. - .idea/runConfigurations/CN1_Android_OnDeviceDebug.xml — Maven run config that invokes the new Mojo from the project root. - .idea/runConfigurations/CN1_Attach_Android.xml — Remote JVM Debug to localhost:5005, scoped to the -common module. Developer guide: - New On-Device-Debugging-Android.asciidoc chapter: IntelliJ quick start, wireless debugging (both Android 11+ and legacy paths), Maven CLI flow, flag table, source-resolution for both codenameone-core and codenameone-android sources jars, and troubleshooting. Explicitly calls out that JNI C/C++ is out of scope (use Android Studio + LLDB for that, can run alongside). - Advanced-Topics-Under-The-Hood gets an android.onDeviceDebug entry next to the existing android.debug / android.release rows. * Address CI feedback on Android on-device debugging Three gates failed on the original push: - SpotBugs DE_MIGHT_IGNORE in AndroidOnDeviceDebuggingMojo's shutdown hook (catch-all swallowed every Exception silently). Narrow the catch to MojoFailureException — the only checked exception runAdb can throw — and surface the message to stderr so a stuck adb forward is at least visible at shutdown. Process.destroy() doesn't declare a checked exception, so the surrounding try/catch is no longer needed for that call. - Vale (Microsoft.Contractions + Microsoft.Adverbs) on On-Device-Debugging-Android.adoc: "it is" → "it's"; drop "rarely" by rewriting the command-line-flags lead sentence; drop "silently" from the breakpoint-not-firing troubleshooter. - LanguageTool typo flags: "codepath" → "code path" in the waitForAttach=false note; allowlist "adb", "logcat", and "pidof" in languagetool-accept.txt under a new "Android tooling" section so future Android docs don't re-trip the same rule. * android.onDeviceDebug: also disable ProGuard and pin to debug-only Two follow-ups to the initial hint binding: - Force off android.enableProguard alongside disableR8. ProGuard runs on the non-Gradle-8 legacy path, and even on Gradle 8 it can layer on top of R8 when android.enableProguard=true; either way the user loses method names and locals. The hint now disables both obfuscators so symbols survive the build regardless of which Gradle line the project is on. - Force android.release=false and android.debug=true. Android's manifest is shared between the release and debug variants, so android:debuggable="true" propagates to both. The cloud build also produces both APKs by default (android.release defaults to true). Without this pin, a stray hint left in codenameone_settings.properties would silently ship a release-signed APK marked debuggable=true — a serious security problem since any device that side-loads it would be exposing a JDWP socket. Pinning to debug-only means the release APK is simply not produced when the hint is on, and the developer guide row for the hint now spells that out. Companion change in the BuildDaemon's AndroidGradleBuilder (separate repo) applies the same guard for the cloud build path. * docs: contraction fixes in android.onDeviceDebug hint row Vale Microsoft.Contractions flagged the expanded row on the second CI run: "cannot" → "can't", "that is" → "that's". Same fix pattern as the earlier batch of contraction tweaks in the Android chapter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a JDWP-compatible debugger for ParparVM-built iOS apps so
jdb,IntelliJ IDEA, VS Code, Eclipse, NetBeans — anything that speaks
JDWP — can attach to a real device or the iOS Simulator and set
breakpoints, walk the stack, inspect locals + fields + arrays,
invoke methods, and surface native stdout/stderr in the IDE console
on the running app.
End-user documentation is in
docs/developer-guide/On-Device-Debugging.asciidoc.Architecture
Three pieces, each independent:
Translator instrumentation. When
-Dcn1.onDeviceDebug=trueisset, the ParparVM translator emits per-method side-tables (locals
address arrays, variable names, line tables, per-class field
offset tables, per-method invoke thunks) and a
cn1-symbols.txtsidecar (classes + their superclass, methods + isStatic, lines,
local-vars, fields with JVM descriptors and JDWP access flags)
that the desktop proxy uses for name resolution. Release builds
are unaffected — gated by a
CN1_ON_DEVICE_DEBUGpreprocessordefine and a separate filter that keeps
java.io.*/java.net.*/
java.nio.*/com.codename1.impl.*out of the invoke-thunkset (those packages have hand-written native shims that have
drifted from the modern calling convention).
Device runtime.
Ports/iOSPort/nativeSources/cn1_debugger.{h,m}is compiled into debug builds only. It dials out to a desktop
proxy over TCP, then services the wire protocol from a listener
thread. The hot path in
__CN1_DEBUG_INFOis one predictableload+branch when nothing is attached (
__builtin_expect(cn1DebuggerActive, 0)).doesn't block collection.
dup2-based stdout/stderr capture that forwards lines asEVT_STDOUT_LINE/EVT_STDERR_LINEevents.__attribute__((constructor))shims — used to answerCMD_GET_OBJECT_FIELDSbyoffsetof-walking the C struct.suspended Java thread (so it runs in a valid
tsdcontext),wraps the underlying call in a catch-all
setjmpso uncaughtthrows round-trip back as a
result.type='X'instead oflongjmp-ing past
suspendCurrent's cond_wait.JavaArrayPrototypestruct with element type-tag derived from
primitiveSizeplusthe element class's
clsName.CN1ProxyWaitForAttach=YES.Desktop proxy. Maven module
maven/cn1-debug-proxy/containsa minimum-viable JDWP server (
JdwpServer) that translates ourcustom protocol to/from JDWP. Coverage:
VirtualMachine: Version, IDSizes, Capabilities[New], AllThreads,AllClasses[WithGeneric], TopLevelThreadGroups, ClassPaths,
Suspend, Resume, Exit, Dispose.
ReferenceType: Signature, ClassLoader, Modifiers, Fields,FieldsWithGeneric, Methods, MethodsWithGeneric, SourceFile,
Status, Interfaces, ClassObject.
ClassType: Superclass, InvokeMethod (static).Method: LineTable, VariableTable[WithGeneric], Bytecodes (stub),IsObsolete.
ObjectReference: ReferenceType (incl.TYPE_TAG_ARRAYforarrays), GetValues (instance fields), InvokeMethod,
IsCollected.
ArrayReference: Length, GetValues.StringReference: Value.ThreadReference: Name, Suspend, Resume, Status, ThreadGroup,Frames, FrameCount, SuspendCount.
ThreadGroupReference: Name, Parent, Children.StackFrame: GetValues, ThisObject.EventRequest: Set, Clear, ClearAllBreakpoints — full modifierparser covering all 12 JDWP modifier kinds (Count, Conditional,
ThreadOnly, ClassOnly, ClassMatch, ClassExclude, LocationOnly,
ExceptionOnly, FieldOnly, Step, InstanceOnly, SourceNameMatch).
Event.Composite: VM_START, BREAKPOINT, SINGLE_STEP, VM_DEATH.--trace-jdwpflag dumps every inbound JDWP command for diagnosingIDE-specific issues. Stack and locals caches invalidate on every
suspend (BP_HIT / STEP_COMPLETE) and every resume so the IDE's
Frames / Variables panels always reflect the current thread state.
Build hints (UX)
In
common/codenameone_settings.properties:For a physical device, set
proxyHostto the laptop's LAN IP.New Maven goals
mvn cn1:ios-on-device-debugging— autodetects the symbol sidecar,launches the proxy, and prints attach instructions.
mvn cn1:buildIosOnDeviceDebug— cloud-build target that forces theon-device-debug flag on. Routes through the debug iOS pipeline (a
new
ios-on-device-debugant target maps to debug cert / ad-hocprovisioning).
IPhoneBuilderreadsios.onDeviceDebugand (a) threads-Dcn1.onDeviceDebug=trueinto the translator JVM and (b) injectsCN1ProxyHost/CN1ProxyPort/CN1ProxyWaitForAttachand an ATSexemption into
Info.plistwhen the flag is set.What works today
(
com.codename1.ui.*,com.codename1.io.*, etc.).IntelliJ's default class excludes don't truncate the request).
java.lang.Stringvalue inspection — strings show inline.drill-down. The proxy walks the ParparVM struct layout to read
instance fields directly at known offsets, so
dump this/"Variables" shows the same fields you'd see in a simulator debug
session.
Object[],int[],byte[], etc. reporttheir length and per-index values, so expanding an
ArrayList'selementDatashows actual element references rather than anopaque pointer. Element type tags round-trip properly so
byte[]andboolean[]don't get conflated.print myForm.getTitle()/Display.getInstance().getCurrent().getTitle()all run on thesuspended Java thread inside a catch-all try block.
System.out.println,Log.p, andprintf/NSLogare surfaced in the proxy console (and the IDERun window when the proxy is launched as a run config) prefixed
with
[device].Known limitations (documented in the dev guide)
java.io.*,java.net.*,java.nio.*,com.codename1.impl.*is unsupported — the translator skipsinvoke-thunk generation for those packages because their
hand-written native shims drift from the modern calling convention
and would break the link.
-ghaving been used at javac time— Codename One archetypes set this by default.
NSLogon a real device may bypass the stdout forwarder (it routesto
os_logwhich doesn't always mirror to stderr);printf/fprintf/Log.pare unaffected.Performance
Release builds: zero overhead (no listener, no metadata, no per-line
callback, no invoke thunks, no field tables — guarded by
CN1_ON_DEVICE_DEBUG).Debug builds, no debugger attached: one predictable load+branch per
source line (the existing
__CN1_DEBUG_INFOfor stack-trace linerecording is already there; the on-device-debug flag check is the
one added branch).
Debug builds, debugger attached: ~2-3× slowdown in tight numeric
loops, consistent with
-goverhead on other native VMs.Verification
End-to-end smoke test against an iPhone 17 Pro / iOS 26.3 simulator
under Xcode 26 with a minimal CN1 app:
com.example.DebugApp.heartbeat:41(tickCount++) firesevery second;
dump thisreturns{ tickCount: 4, pointerCount: 0 }with the actual field values.
com.codename1.ui.Display.edtLoopImplfires on every EDTcycle;
listrenders the liveDisplay.javasource; namedlocals (
current,qt,ie) populate.Log.plands at the next executable line;F7 step-into lands at line 242 of
com.codename1.io.Log.p.com.codename1.ui.Display.getInstance().getCurrent().getTitle()returns the live form's title string.
ArrayListin the Variables view showselementData[0..size-1]with their actual values.System.out.println,Log.p, andNSLoglines appear in theproxy's console prefixed with
[device].Companion BuildDaemon PR mirrors the cloud-build binding:
https://github.com/codenameone/BuildDaemon (separate PR).
Test plan
mvn -Plocal-dev-javase installstill succeeds with the newcn1-debug-proxymodule registered.ios.onDeviceDebugis unset.archetype project; confirm Frames panel, Variables panel,
Evaluate Expression, and array drill-down all populate.
buildIosOnDeviceDebuground-trip — resulting.ipaconnects to the proxy from a tethered device.
clean).