Skip to content

Add iOS on-device debugging support#4999

Open
shai-almog wants to merge 8 commits into
masterfrom
on-device-debug-ios
Open

Add iOS on-device debugging support#4999
shai-almog wants to merge 8 commits into
masterfrom
on-device-debug-ios

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

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, and inspect locals + Strings on the
running app.

End-user documentation is in docs/developer-guide/On-Device-Debugging.asciidoc.

Architecture

Three pieces, each independent:

  1. Translator instrumentation. When -Dcn1.onDeviceDebug=true is
    set, the ParparVM translator emits per-method side-tables (locals
    address arrays, variable names, line tables) and a
    cn1-symbols.txt sidecar that the desktop proxy uses for name
    resolution. Release builds are completely unaffected — gated by a
    CN1_ON_DEVICE_DEBUG preprocessor define.

  2. 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: set/clear-bp, resume, step (into/over/out), get-stack,
    get-locals, get-object-class, get-string. Suspend/resume yields
    the GC bit so a paused thread doesn't block collection. The hot
    path in __CN1_DEBUG_INFO is one predictable load+branch when
    nothing is attached (__builtin_expect(cn1DebuggerActive, 0)).

  3. Desktop proxy. New Maven module maven/cn1-debug-proxy/
    contains a minimum-viable JDWP server (JdwpServer) that
    translates our custom protocol to/from JDWP. Covers the commands
    jdb actually issues — handshake, VM.Version/IDSizes/Capabilities,
    AllClasses[WithGeneric], Method.LineTable/VariableTable[WithGeneric],
    EventRequest.Set/Clear, Event.Composite (VM_START, BREAKPOINT,
    SINGLE_STEP, VM_DEATH), ThreadReference.Name/Frames/FrameCount/Status,
    StackFrame.GetValues, ObjectReference.ReferenceType, StringReference.Value.

Build hints (UX)

In common/codenameone_settings.properties:

codename1.arg.ios.onDeviceDebug=true
codename1.arg.ios.onDeviceDebug.proxyHost=127.0.0.1
codename1.arg.ios.onDeviceDebug.proxyPort=55333
# Optional: block the app at startup until the proxy is attached.
# codename1.arg.ios.onDeviceDebug.waitForAttach=true

For a physical device, set proxyHost to 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 (jdb -attach localhost:8000).
  • mvn cn1:buildIosOnDeviceDebug — cloud-build target that forces the
    on-device-debug flag on. Routes through the debug iOS pipeline (a
    new ios-on-device-debug ant target maps to debug cert / ad-hoc
    provisioning).

IPhoneBuilder reads ios.onDeviceDebug and (a) threads
-Dcn1.onDeviceDebug=true into the translator JVM and (b) injects
CN1ProxyHost / CN1ProxyPort / CN1ProxyWaitForAttach and an ATS
exemption into Info.plist when the flag is set.

What works today

  • Class loading: the IDE sees every class in the build.
  • Line breakpoints in user code and Codename One framework code.
  • Step into / over / out.
  • Full stack walking (including Codename One framework frames).
  • Primitive local inspection (int/long/float/double/boolean/byte/char/short).
  • java.lang.String value inspection — strings show as "hello".
  • Object refs — class name and identity, drill-in via the IDE's
    variables view.
  • Pause / resume from the IDE.

Known limitations (documented in the dev guide)

  • Method invocation from the debugger is not supported (so jdb's
    "evaluate expression" can read existing values but not call methods).
  • Hot-swap is not supported.
  • Local variable names depend on -g having been used at javac time
    — Codename One archetypes set this by default; classes built
    without it show up as v1, v2, ...

Performance

Release builds: zero overhead (no listener, no metadata, no per-line
callback — guarded by CN1_ON_DEVICE_DEBUG preprocessor define).

Debug builds, no debugger attached: one predictable load+branch per
source line (the existing __CN1_DEBUG_INFO for stack-trace line
recording is already there; we add a flag check).

Debug builds, debugger attached: ~2-3× slowdown in tight numeric
loops, consistent with -g overhead on other native VMs.

Verification

End-to-end smoke test against a real iOS Simulator app:

  • mvn cn1:buildIosXcodeProject -Dcodename1.arg.ios.onDeviceDebug=true
    generates an Xcode project; xcodebuild succeeds for
    iphonesimulator/arm64.
  • App boots in the simulator and dials in to the proxy. jdb -attach
    succeeds.
  • Setting stop at com.example.DebugApp:22, the breakpoint fires and
    jdb shows the full 9-frame stack walked up through
    Display.executeSerialCall / mainEDTLoop / CodenameOneThread.run.
  • locals shows name = "world", greeting = "hello, world", plus
    the Form and Button instances with class + identity.
  • print name returns name = "world" directly via
    StringReference.Value.

Companion BuildDaemon PR mirrors the cloud-build binding:
https://github.com/codenameone/BuildDaemon (separate PR to follow).

Test plan

  • Verify mvn -Plocal-dev-javase install still succeeds with the
    new cn1-debug-proxy module registered.
  • Verify release iOS builds are byte-for-byte unchanged from the
    current behaviour when ios.onDeviceDebug is unset.
  • Repeat the simulator + jdb smoke test on a fresh archetype
    project.
  • Confirm a cloud buildIosOnDeviceDebug build round-trips and
    the resulting .ipa connects to the proxy from a tethered
    device on the same Wi-Fi.
  • Spot-check the dev guide renders cleanly in the asciidoctor
    build.

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.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: 2 alert(s) (1 errors, 1 warnings, 0 suggestions) (exit code 1) (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool (advisory): 7698 advisory match(es) — top: CONSECUTIVE_SPACES (2388), MORFOLOGIK_RULE_EN_US (2330), COMMA_PARENTHESIS_WHITESPACE (864) (report)
  • Image references: No unused images detected (report)

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 20 screenshots: 20 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 110 screenshots: 110 matched.

Native Android coverage

  • 📊 Line coverage: 11.90% (6760/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.67% (34004/351672), branch 4.17% (1390/33359), complexity 5.21% (1666/32007), method 9.06% (1356/14973), class 14.72% (302/2051)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.90% (6760/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.67% (34004/351672), branch 4.17% (1390/33359), complexity 5.21% (1666/32007), method 9.06% (1356/14973), class 14.72% (302/2051)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1063.000 ms
Base64 CN1 encode 204.000 ms
Base64 encode ratio (CN1/native) 0.192x (80.8% faster)
Base64 native decode 787.000 ms
Base64 CN1 decode 222.000 ms
Base64 decode ratio (CN1/native) 0.282x (71.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 182 seconds

Build and Run Timing

Metric Duration
Simulator Boot 80000 ms
Simulator Boot (Run) 2000 ms
App Install 11000 ms
App Launch 5000 ms
Test Execution 288000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 746.000 ms
Base64 CN1 encode 1364.000 ms
Base64 encode ratio (CN1/native) 1.828x (82.8% slower)
Base64 native decode 291.000 ms
Base64 CN1 decode 961.000 ms
Base64 decode ratio (CN1/native) 3.302x (230.2% slower)
Base64 SIMD encode 452.000 ms
Base64 encode ratio (SIMD/native) 0.606x (39.4% faster)
Base64 encode ratio (SIMD/CN1) 0.331x (66.9% faster)
Base64 SIMD decode 497.000 ms
Base64 decode ratio (SIMD/native) 1.708x (70.8% slower)
Base64 decode ratio (SIMD/CN1) 0.517x (48.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 61.000 ms
Image createMask (SIMD on) 14.000 ms
Image createMask ratio (SIMD on/off) 0.230x (77.0% faster)
Image applyMask (SIMD off) 151.000 ms
Image applyMask (SIMD on) 60.000 ms
Image applyMask ratio (SIMD on/off) 0.397x (60.3% faster)
Image modifyAlpha (SIMD off) 125.000 ms
Image modifyAlpha (SIMD on) 66.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.528x (47.2% faster)
Image modifyAlpha removeColor (SIMD off) 154.000 ms
Image modifyAlpha removeColor (SIMD on) 96.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.623x (37.7% faster)
Image PNG encode (SIMD off) 1334.000 ms
Image PNG encode (SIMD on) 1122.000 ms
Image PNG encode ratio (SIMD on/off) 0.841x (15.9% faster)
Image JPEG encode 541.000 ms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 647 total, 0 failed, 2 skipped

Benchmark Results

  • Execution Time: 7754 ms

  • Hotspots (Top 20 sampled methods):

    • 21.66% java.lang.String.indexOf (300 samples)
    • 20.79% com.codename1.tools.translator.Parser.isMethodUsed (288 samples)
    • 17.33% java.util.ArrayList.indexOf (240 samples)
    • 5.99% java.lang.Object.hashCode (83 samples)
    • 5.27% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (73 samples)
    • 2.89% java.lang.System.identityHashCode (40 samples)
    • 2.38% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (33 samples)
    • 1.95% com.codename1.tools.translator.ByteCodeClass.markDependent (27 samples)
    • 1.73% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (24 samples)
    • 1.30% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (18 samples)
    • 1.23% java.lang.StringBuilder.append (17 samples)
    • 1.23% com.codename1.tools.translator.Parser.getClassByName (17 samples)
    • 1.08% com.codename1.tools.translator.Parser.cullMethods (15 samples)
    • 1.08% java.lang.StringCoding.encode (15 samples)
    • 0.72% com.codename1.tools.translator.BytecodeMethod.equals (10 samples)
    • 0.58% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (8 samples)
    • 0.58% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (8 samples)
    • 0.51% com.codename1.tools.translator.BytecodeMethod.optimize (7 samples)
    • 0.51% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (7 samples)
    • 0.43% sun.nio.fs.UnixNativeDispatcher.open0 (6 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 21, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 222 seconds

Build and Run Timing

Metric Duration
Simulator Boot 73000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 9000 ms
Test Execution 337000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1969.000 ms
Base64 CN1 encode 1926.000 ms
Base64 encode ratio (CN1/native) 0.978x (2.2% faster)
Base64 native decode 511.000 ms
Base64 CN1 decode 967.000 ms
Base64 decode ratio (CN1/native) 1.892x (89.2% slower)
Base64 SIMD encode 410.000 ms
Base64 encode ratio (SIMD/native) 0.208x (79.2% faster)
Base64 encode ratio (SIMD/CN1) 0.213x (78.7% faster)
Base64 SIMD decode 425.000 ms
Base64 decode ratio (SIMD/native) 0.832x (16.8% faster)
Base64 decode ratio (SIMD/CN1) 0.440x (56.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 61.000 ms
Image createMask (SIMD on) 10.000 ms
Image createMask ratio (SIMD on/off) 0.164x (83.6% faster)
Image applyMask (SIMD off) 175.000 ms
Image applyMask (SIMD on) 139.000 ms
Image applyMask ratio (SIMD on/off) 0.794x (20.6% faster)
Image modifyAlpha (SIMD off) 133.000 ms
Image modifyAlpha (SIMD on) 98.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.737x (26.3% faster)
Image modifyAlpha removeColor (SIMD off) 148.000 ms
Image modifyAlpha removeColor (SIMD on) 75.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.507x (49.3% faster)
Image PNG encode (SIMD off) 1076.000 ms
Image PNG encode (SIMD on) 822.000 ms
Image PNG encode ratio (SIMD on/off) 0.764x (23.6% faster)
Image JPEG encode 479.000 ms

- 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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant