Skip to content

Latest commit

 

History

History
414 lines (327 loc) · 14.1 KB

File metadata and controls

414 lines (327 loc) · 14.1 KB

Rust Debugging Guide

This guide covers Rust debugging support in the MCP Debugger using the CodeLLDB debug adapter.

Prerequisites

1. Rust Toolchain

Install Rust from rustup.rs:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2. CodeLLDB (Automatic)

The Rust adapter automatically downloads CodeLLDB when you build it:

cd packages/codelldb-common
npm run build:adapter  # Downloads and extracts CodeLLDB

CodeLLDB Vendoring

The Rust adapter bundles the CodeLLDB binaries into packages/codelldb-common/vendor/codelldb. Vendoring runs automatically when you install or build the workspace. The published CLI installs CodeLLDB via per-platform @debugmcp/codelldb-* optional dependencies — npm picks the one matching your os/cpu, so no manual setup is needed. If you installed with --omit=optional, set the CODELLDB_PATH environment variable to a local CodeLLDB installation (for example from the VSCode extension) or run pnpm --filter @debugmcp/codelldb-common run build:adapter from a cloned repository to download your platform binaries.

  • pnpm install (postinstall hook)
  • pnpm vendor or pnpm vendor:adapters
  • pnpm --filter @debugmcp/codelldb-common run build:adapter
  • node packages/codelldb-common/scripts/vendor-codelldb.js

Default behavior: Locally, the vendoring script downloads all supported platforms (win32-x64, linux-x64, linux-arm64, darwin-x64, darwin-arm64) so Docker builds can reuse the same artifacts. Set CODELLDB_VENDOR_ALL=false to vendor only the current host platform. In CI environments, the script vendors only the current platform unless CODELLDB_VENDOR_ALL=true is explicitly set.

Manual control

# Force a fresh download for the current platform
pnpm vendor:force

# Skip vendoring temporarily (for air-gapped dev shells)
SKIP_ADAPTER_VENDOR=true pnpm install

Useful environment flags

  • CODELLDB_VERSION: override the release tag (downloaded by the vendor script, which defaults to 1.11.8)
  • CODELLDB_FORCE_REBUILD=true: ignore cached binaries and re-download
  • CODELLDB_PLATFORMS=win32-x64,linux-x64,linux-arm64,darwin-x64,darwin-arm64: vendor specific platforms (comma-separated)
  • CODELLDB_VENDOR_ALL=false: opt out of the "vendor every platform" default and fall back to host-only downloads
  • CODELLDB_VENDOR_LOCAL_ONLY=true: disable network downloads entirely and fail if the requested platform isn't already vendored (used by Docker builds that copy pre-fetched artifacts)
  • CODELLDB_KEEP_TEMP=true: retain the downloaded VSIX and extracted temp folders for inspection
  • CODELLDB_EXTRACT_TIMEOUT_MS: watchdog for VSIX extraction (default 120000); a stalled unzip is aborted, retried, and surfaced as a failure instead of dying silently
  • SKIP_ADAPTER_VENDOR=true: opt out entirely (used by CI jobs that pre-bake artifacts)

Troubleshooting vendoring

  1. Run pnpm vendor:status to see which adapters are ready for the current machine.
  2. Re-run pnpm --filter @debugmcp/codelldb-common run build:adapter and check the [CodeLLDB vendor] logs:
    • HTTP response: ... confirms GitHub access.
    • Artifact magic header: 504b0304 (PK..) verifies a valid VSIX download.
  3. If you see Failed to vendor, re-run with CODELLDB_KEEP_TEMP=true to inspect the downloaded file under packages/codelldb-common/vendor/codelldb/temp.
  4. Network errors or 404 responses usually indicate a typo in CODELLDB_VERSION or a transient GitHub outage—try again with pnpm vendor:force.
  5. On Windows, ensure PowerShell is allowed to create executables in the workspace (no antivirus quarantine).

Features

Supported Capabilities

  • Breakpoints: Set breakpoints in Rust source files
  • Stepping: Step over, into, and out of functions
  • Variable inspection: View local variables, including complex types
  • Stack traces: Full call stack with Rust symbols
  • Cargo integration: Debug Cargo projects directly
  • Debug/Release builds: Support for both build configurations
  • Async debugging: Debug tokio and async-std applications

Rust-Specific Features

  1. Smart Type Display: Collections like Vec, HashMap, and String are displayed in a readable format
  2. Ownership Tracking: See borrowed vs owned values
  3. Pattern Matching: Step through match expressions
  4. Macro Expansion: Debug through macro-generated code

Quick Start

1. Create a Debug Session

{
  "tool": "create_debug_session",
  "arguments": {
    "language": "rust",
    "name": "My Rust Debug Session"
  }
}

2. Build Your Rust Project

# For debug build (recommended for debugging)
cargo build

# For release build (optimized, harder to debug)
cargo build --release

3. Set Breakpoints

{
  "tool": "set_breakpoint",
  "arguments": {
    "sessionId": "your-session-id",
    "file": "src/main.rs",
    "line": 10
  }
}

4. Start Debugging

For a simple binary:

{
  "tool": "start_debugging",
  "arguments": {
    "sessionId": "your-session-id",
    "scriptPath": "target/debug/my_program"
  }
}

For a Cargo project with arguments:

{
  "tool": "start_debugging",
  "arguments": {
    "sessionId": "your-session-id",
    "scriptPath": "target/debug/my_program",
    "args": ["--verbose", "input.txt"],
    "dapLaunchArgs": {
      "cargo": {
        "bin": "my_program",
        "release": false
      }
    }
  }
}

5. Understand What the Rust Example Helper Does

Our test suite uses tests/e2e/rust-example-utils.ts to make sure every rust smoke test compiles and reuses binaries deterministically. The helper does three things you can mirror in your own workflows:

  1. Build with the GNU toolchain when available. On Windows it looks up dlltool.exe (via @debugmcp/adapter-rust’s findDlltoolExecutable) and runs cargo +stable-gnu build --target x86_64-pc-windows-gnu. If that fails it logs a warning and falls back to an explicit MSVC-targeted build (+stable-msvc build --target x86_64-pc-windows-msvc). You can follow the same pattern manually:

    # GNU-preferred build on Windows
    rustup target add x86_64-pc-windows-gnu
    cargo +stable-gnu build --target x86_64-pc-windows-gnu
    # Non-Windows hosts usually only need the default debug build
    cargo build
  2. Resolve the correct binary path for start_debugging. Breakpoints should always reference the .rs source file (absolute paths avoid MCP resolution issues), but start_debugging.scriptPath must point to the compiled artifact:

    • Windows GNU: examples/rust/<name>/target/x86_64-pc-windows-gnu/debug/<name>.exe
    • Windows MSVC: examples/rust/<name>/target/x86_64-pc-windows-msvc/debug/<name>.exe
    • Windows generic fallback: examples/rust/<name>/target/debug/<name>.exe
    • Unix-like hosts: examples/rust/<name>/target/debug/<name>

    Tokio/async builds use exactly the same rule—the helper’s prepareRustExample('async_example') just compiles a different crate and returns { sourcePath, binaryPath } so the smoke tests can reuse those paths.

  3. Cache builds between tests. Once an example has been compiled, the helper stores both paths in-memory so the rest of the suite can run without triggering another cargo build. For local work you can emulate this with Cargo’s default incremental builds: as long as you relaunch the same target/<triple>/debug/<binary> the MCP start_debugging call does not care when the binary was produced.

Tip: The prepareRustExample function is an internal test helper, not a supported public utility. You can replicate its approach in your own build tooling. The important detail is that breakpoints reference source (src/main.rs) while start_debugging.scriptPath references the compiled executable. This is true for synchronous and async (Tokio) binaries alike.

Cargo Integration

The Rust adapter provides special support for Cargo projects:

Debug Configuration

{
  "dapLaunchArgs": {
    "cargo": {
      "bin": "my_program",   // Binary target name
      "release": false        // false for debug build, true for release
    },
    "args": ["--help"],       // Program arguments (top-level, not inside cargo)
    "env": {                  // Environment variables
      "RUST_LOG": "debug"
    }
  }
}

The cargo object supports these fields:

  • bin: Binary target name
  • example: Example target name
  • test: Test target name
  • release: Build in release mode (boolean, default: false)

Workspace Support

For Cargo workspaces with multiple packages:

{
  "dapLaunchArgs": {
    "cargo": {
      "bin": "my-crate"    // Specify which binary target to debug
    }
  }
}

Common Debugging Scenarios

1. Debug a Unit Test

# Build the test executable
cargo test --no-run

# Find the test executable (usually in target/debug/deps/)
ls target/debug/deps/*-*

# Debug it
{
  "tool": "start_debugging",
  "arguments": {
    "sessionId": "your-session-id",
    "scriptPath": "target/debug/deps/my_crate-abc123def456",
    "args": ["test_name", "--nocapture"]
  }
}

2. Debug with Environment Variables

{
  "tool": "start_debugging",
  "arguments": {
    "sessionId": "your-session-id",
    "scriptPath": "target/debug/my_program",
    "dapLaunchArgs": {
      "env": {
        "RUST_BACKTRACE": "1",
        "RUST_LOG": "debug",
        "DATABASE_URL": "postgres://localhost/mydb"
      }
    }
  }
}

3. Debug Async Code

When debugging async Rust code with tokio:

#[tokio::main]
async fn main() {
    // Set breakpoint here to debug async initialization
    // Your async code here
    my_async_function().await;
}

4. Inspect Complex Types

{
  "tool": "get_variables",
  "arguments": {
    "sessionId": "your-session-id",
    "scope": 5
  }
}

Response will show Rust types clearly:

{
  "variables": [
    {"name": "my_vec", "value": "[1, 2, 3, 4, 5]", "type": "Vec<i32>"},
    {"name": "my_string", "value": "\"Hello, Rust!\"", "type": "String"},
    {"name": "my_option", "value": "Some(42)", "type": "Option<i32>"},
    {"name": "my_result", "value": "Ok(\"success\")", "type": "Result<&str, Error>"}
  ]
}

Debugging host-built binaries in Docker

A binary built by cargo build on the host embeds host-absolute source paths (e.g. /home/user/proj/src/main.rs) in its DWARF debug info. When the mcp-debugger container mounts the project at /workspace, breakpoint requests use /workspace/... paths and CodeLLDB cannot match them — file+line breakpoints silently never bind (function breakpoints and pause still work; issue #363).

In container mode (MCP_CONTAINER=true) the adapter auto-derives a best-effort sourceMap for prebuilt binaries by scanning the binary for embedded host paths whose suffixes exist under the workspace mount (MCP_WORKSPACE_ROOT, default /workspace). If derivation misses, pass the mapping explicitly — a caller-supplied sourceMap always wins:

{
  "scriptPath": "/workspace/target/debug/myapp",
  "dapLaunchArgs": {
    "sourceMap": { "/home/user/proj": "/workspace" }
  }
}

Alternatively, build inside the container (or with RUSTFLAGS="--remap-path-prefix=/home/user/proj=/workspace") so the DWARF paths match the mount directly.

Troubleshooting

Breakpoints Not Hit

  1. Ensure debug symbols are included:

    • Use debug builds (cargo build) not release
    • Or add debug symbols to release:
      [profile.release]
      debug = true
  2. Check optimization level:

    • Optimizations can prevent breakpoints
    • Use opt-level = 0 for debugging
  3. Verify file paths:

    • Use absolute paths or paths relative to workspace root
    • Ensure the file exists and matches the compiled code

Variables Show as Optimized Out

This happens in release builds or with optimizations enabled:

[profile.dev]
opt-level = 0  # No optimization for better debugging

Can't Find CodeLLDB

If CodeLLDB is not found:

# Re-run the vendor script
cd packages/codelldb-common
npm run build:adapter

# Check if it was downloaded (fixed layout under vendor/codelldb/ with per-platform subdirectories)
ls vendor/codelldb/
# e.g., vendor/codelldb/win32-x64/, vendor/codelldb/linux-x64/, vendor/codelldb/darwin-arm64/

Async Code Debugging Issues

  • Use tokio::time::sleep instead of std::thread::sleep in async contexts
  • Set breakpoints inside async blocks, not on the async fn declaration
  • Use .await points as natural breakpoint locations

Performance Tips

  1. Use debug builds for development: They're slower but much easier to debug
  2. Limit scope of debugging: Use conditional breakpoints to avoid stopping too often
  3. Use logging: Combine debugging with env_logger or tracing for better insights
  4. Profile before optimizing: Use cargo flamegraph or perf to find bottlenecks

Advanced Features

Conditional Breakpoints

{
  "tool": "set_breakpoint",
  "arguments": {
    "sessionId": "your-session-id",
    "file": "src/main.rs",
    "line": 42,
    "condition": "counter > 100"
  }
}

Expression Evaluation

Evaluate Rust expressions in the current debug context. Note that CodeLLDB evaluates expressions through LLDB, so some Rust-specific syntax (e.g., closures, trait methods) may not be supported:

{
  "tool": "evaluate_expression",
  "arguments": {
    "sessionId": "your-session-id",
    "expression": "my_vec.len()"
  }
}

Examples

See the examples/rust/ directory for complete examples:

  • hello_world/: Basic Rust debugging
  • async_example/: Async/await with Tokio
  • More examples coming soon!

Known Limitations

  1. Macro debugging: Stepping through macros can be confusing due to expansion
  2. Inline functions: May not have breakpoint locations
  3. Generic functions: Need concrete instantiation for breakpoints
  4. Async stack traces: Can be deep due to runtime machinery

Resources