Skip to content

Add iOS on-device debugging support#4999

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

Add iOS on-device debugging support#4999
shai-almog wants to merge 5 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: 1 alert(s) (1 errors, 0 warnings, 0 suggestions) (exit code 1) (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.89% (6753/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.66% (33976/351672), branch 4.16% (1389/33359), complexity 5.21% (1667/32007), method 9.04% (1354/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.89% (6753/56788 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.66% (33976/351672), branch 4.16% (1389/33359), complexity 5.21% (1667/32007), method 9.04% (1354/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 694.000 ms
Base64 CN1 encode 91.000 ms
Base64 encode ratio (CN1/native) 0.131x (86.9% faster)
Base64 native decode 720.000 ms
Base64 CN1 decode 257.000 ms
Base64 decode ratio (CN1/native) 0.357x (64.3% 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: 417 seconds

Build and Run Timing

Metric Duration
Simulator Boot 83000 ms
Simulator Boot (Run) 2000 ms
App Install 21000 ms
App Launch 11000 ms
Test Execution 345000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 863.000 ms
Base64 CN1 encode 2431.000 ms
Base64 encode ratio (CN1/native) 2.817x (181.7% slower)
Base64 native decode 569.000 ms
Base64 CN1 decode 1698.000 ms
Base64 decode ratio (CN1/native) 2.984x (198.4% slower)
Base64 SIMD encode 680.000 ms
Base64 encode ratio (SIMD/native) 0.788x (21.2% faster)
Base64 encode ratio (SIMD/CN1) 0.280x (72.0% faster)
Base64 SIMD decode 487.000 ms
Base64 decode ratio (SIMD/native) 0.856x (14.4% faster)
Base64 decode ratio (SIMD/CN1) 0.287x (71.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 77.000 ms
Image createMask (SIMD on) 14.000 ms
Image createMask ratio (SIMD on/off) 0.182x (81.8% faster)
Image applyMask (SIMD off) 155.000 ms
Image applyMask (SIMD on) 84.000 ms
Image applyMask ratio (SIMD on/off) 0.542x (45.8% faster)
Image modifyAlpha (SIMD off) 166.000 ms
Image modifyAlpha (SIMD on) 83.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.500x (50.0% faster)
Image modifyAlpha removeColor (SIMD off) 171.000 ms
Image modifyAlpha removeColor (SIMD on) 173.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.012x (1.2% slower)
Image PNG encode (SIMD off) 1684.000 ms
Image PNG encode (SIMD on) 1223.000 ms
Image PNG encode ratio (SIMD on/off) 0.726x (27.4% faster)
Image JPEG encode 982.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: 9683 ms

  • Hotspots (Top 20 sampled methods):

    • 22.64% java.lang.String.indexOf (383 samples)
    • 21.63% com.codename1.tools.translator.Parser.isMethodUsed (366 samples)
    • 11.94% java.util.ArrayList.indexOf (202 samples)
    • 5.97% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (101 samples)
    • 4.85% java.lang.Object.hashCode (82 samples)
    • 2.72% java.lang.System.identityHashCode (46 samples)
    • 2.36% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (40 samples)
    • 2.01% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (34 samples)
    • 1.83% com.codename1.tools.translator.ByteCodeClass.markDependent (31 samples)
    • 1.71% com.codename1.tools.translator.Parser.getClassByName (29 samples)
    • 1.48% com.codename1.tools.translator.Parser.addToConstantPool (25 samples)
    • 1.18% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (20 samples)
    • 1.12% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (19 samples)
    • 1.00% com.codename1.tools.translator.Parser.cullMethods (17 samples)
    • 0.95% java.lang.StringBuilder.append (16 samples)
    • 0.95% java.lang.StringCoding.encode (16 samples)
    • 0.83% org.objectweb.asm.ClassReader.readCode (14 samples)
    • 0.71% com.codename1.tools.translator.BytecodeMethod.optimize (12 samples)
    • 0.65% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (11 samples)
    • 0.53% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (9 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: 300 seconds

Build and Run Timing

Metric Duration
Simulator Boot 91000 ms
Simulator Boot (Run) 20000 ms
App Install 22000 ms
App Launch 9000 ms
Test Execution 363000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 892.000 ms
Base64 CN1 encode 2036.000 ms
Base64 encode ratio (CN1/native) 2.283x (128.3% slower)
Base64 native decode 640.000 ms
Base64 CN1 decode 2096.000 ms
Base64 decode ratio (CN1/native) 3.275x (227.5% slower)
Base64 SIMD encode 469.000 ms
Base64 encode ratio (SIMD/native) 0.526x (47.4% faster)
Base64 encode ratio (SIMD/CN1) 0.230x (77.0% faster)
Base64 SIMD decode 567.000 ms
Base64 decode ratio (SIMD/native) 0.886x (11.4% faster)
Base64 decode ratio (SIMD/CN1) 0.271x (72.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 85.000 ms
Image createMask (SIMD on) 11.000 ms
Image createMask ratio (SIMD on/off) 0.129x (87.1% faster)
Image applyMask (SIMD off) 195.000 ms
Image applyMask (SIMD on) 117.000 ms
Image applyMask ratio (SIMD on/off) 0.600x (40.0% faster)
Image modifyAlpha (SIMD off) 221.000 ms
Image modifyAlpha (SIMD on) 68.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.308x (69.2% faster)
Image modifyAlpha removeColor (SIMD off) 354.000 ms
Image modifyAlpha removeColor (SIMD on) 103.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.291x (70.9% faster)
Image PNG encode (SIMD off) 1684.000 ms
Image PNG encode (SIMD on) 1114.000 ms
Image PNG encode ratio (SIMD on/off) 0.662x (33.8% faster)
Image JPEG encode 608.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).
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