Eclipse Debug Framework. This article describes how to implement debugging behavior in Eclipse.

1. Eclipse Debug Framework

1.1. Prerequisites for this tutorial

This tutorial assumes that you have basic understanding of development for the Eclipse platform. Please see Eclipse RCP tutorial or Eclipse Plug-in tutorial if you need any basic information.

In addition to the basics another requirement is the Defining custom launcher for the Eclipse IDE tutorial, which should be read beforehand.

1.2. Eclipse Debug Model

The Eclipse Debug Model is represented by a hierarchy of model interfaces, which are visualized by a the Debug and Variables view, which can usually be found in the Debug perspective of the Eclipse IDE.

Debug model in the UI

Every debug model element is derived from org.eclipse.debug.core.model.IDebugElement interface.

  • org.eclipse.debug.core.model.IDebugTarget

  • org.eclipse.debug.core.model.IThread

  • org.eclipse.debug.core.model.IStackFrame

  • org.eclipse.debug.core.model.IVariable

  • org.eclipse.debug.core.model.IValue

The debug framework also offers an abstract org.eclipse.debug.core.model.DebugElement class as default implementation of IDebugElement interface, which should should be used for implementing these custom debug model elements.

1.3. Debug Model Communication

The Debug Model Communication is based on events, due to the asynchronous nature of the communication between the model and the interpreter.

For example the IDebugTarget implements the org.eclipse.debug.core.model.ISuspendResume interface with method like suspend() and resume(), which are called synchronously, but are immediately returned. So the actual result comes back asynchronously by sending an org.eclipse.debug.core.DebugEvent.

Also custom events may be fired asynchronously, as long as this is done in different threads so that the UI won’t be blocked. But for convenience the org.eclipse.debug.core.DebugEvent objects should be used by deriving the debug model elements from org.eclipse.debug.core.model.DebugElement, which offers default implementations for firing DebugEvents.

1.4. Attach the IDebugTarget to the launch delegate

IDebugTargets are added to the launch procedure like this:

@Override
public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {

    // do launch and anything else...

    // ... and add a certain IDebugTarget in case the debug mode is used
    if (ILaunchManager.DEBUG_MODE.equals(mode)) {
        IDebugTarget target = // instanciate custom IDebugTarget
        launch.addDebugTarget(target);
    }
}

A general overview concerning the launcher framework can be found in the Defining custom launcher for the Eclipse IDE tutorial.

2. Breakpoints

2.1. Eclipse Breakpoint Debug Model

Breakpoints in general are used to suspend the execution of an application at a certain point.

In Eclipse breakpoint classes are derived from the org.eclipse.debug.core.model.IBreakpoint interface. As listed below there are also default implementation for breakpoints, as well as more specific breakpoints, e.g., ILineBreakpoint, which also informs about a certain position in a document.

Class hierarchy of IBreakpoint

2.2. Registering custom breakpoints

Eclipse provides the org.eclipse.debug.core.breakpoints extension point for registering custom IBreakpoint classes.

IBreakpoint extension point

In this definition a markerType attribute is used, because breakpoints do have a reference to an IMarker, which represents the breakpoint in the UI and it is also used for the persistence of breakpoints.

Therefore the org.eclipse.core.resources.markers extension point is used.

<!-- The id is the same as the referenced "markerType" in the org.eclipse.debug.core.breakpoints
    extension point -->
<extension id="example.breakpoint.marker" point="org.eclipse.core.resources.markers">
    <!-- persist the marker, so that the referenced breakpoint will be recreated -->
    <persistent value="true" />
    <!-- Additional attributes for the marker can be defined, which are also
        persisted -->
    <attribute name="CUSTOM_ADDITIONAL_ATTR_WHICH_IS_ALSO_PERSISTED" />
    <!-- The org.eclipse.debug.core.breakpointMarker super type indicated that
        this marker is used for a breakpoint -->
    <super type="org.eclipse.debug.core.breakpointMarker" />
</extension>

2.3. Creating breakpoints in the Eclipse IDE

The easiest approach to create custom breakpoints is to provide an implementation of org.eclipse.debug.ui.actions.IToggleBreakpointsTarget.

IToggleBreakpointsTarget type hierarchy

For instance the IToggleBreakpointsTargetExtension interface provides a public boolean canToggleBreakpoints(IWorkbenchPart part, ISelection selection) ` method and a `public void toggleBreakpoints(IWorkbenchPart part, ISelection selection) throws CoreException; ` where a breakpoint can be toggled according to the current `ISelection and/or IWorkbenchPart.

These IToggleBreakpointsTarget implementations are often provided as an adapter for custom IDebugElements.

<extension point="org.eclipse.core.runtime.adapters">
    <factory adaptableType="com.example.CustomDebugElement"
        class="com.example.CustomDebugElementAdapterFactory">
        <adapter type="org.eclipse.debug.ui.actions.IToggleBreakpointsTarget">
        </adapter>
    </factory>
</extension>

2.4. Registering breakpoints in the IBreakpointManager

When an IBreakpoint is created, e.g., by a IToggleBreakpointsTarget, it must be registered to the workspace’s IBreakpointManager.

CustomBreakpoint breakpoint = // instanciate custom Breakpoint
DebugPlugin.getDefault().getBreakpointManager().addBreakpoint(breakpoint);

When adding or removing IBreakpoints from the IBreakpointManager its IBreakpointListener will be informed about it and can act upon the added or removed breakpoint.

An IDebugTarget is an instance of IBreakpointListener by default and is therefore in charge to manage changes concerning breakpoints.

3. Exercise: Creating the Debug Model Elements

3.1. Target

In this exercise an example debug model will be created, so that a debug session can be started.

In this debug session a given String will be printed to the console char by char. The running application can be suspended at the char, which is currently in the work queue.

This exercise is based on Launch Configuration UI and will make use of the tab and launch delegate, which is created in the Defining custom launcher for the Eclipse IDE tutorial.
Vogella launch UI

3.2. Create a common IDebugElement

As mentioned earlier each debug model class is derived from IDebugElement and contains methods, which are equal for every debug model class.

import org.eclipse.debug.core.model.DebugElement;
import org.eclipse.debug.core.model.IDebugTarget;

public abstract class ExampleDebugElement extends DebugElement {

    public ExampleDebugElement(IDebugTarget target) {
        super(target);
    }

    @Override
    public String getModelIdentifier() {
        return Activator.PLUGIN_ID;
    }
}
A custom model identifier may also be used. This means it is not necessary to use the plug-in’s id to make everything work correctly.

3.3. Create an IStackFrame

IStackFrames are only visible when a thread is suspended, which can also be seen in the following Create an IThread section.

For instance when debugging a Java application the list of IStackFrame objects under a thread show the currently suspended Java stacktrace.

package com.example.debug.core.model;

import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IRegisterGroup;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;

public class ExampleStackFrame extends ExampleDebugElement implements IStackFrame {

    private IThread thread;
    private int charPos;
    private String wordToBeSpelled;

    public ExampleStackFrame(IThread thread, String wordToBeSpelled, int charPos) {
        super(thread.getDebugTarget());
        this.thread = thread;
        this.wordToBeSpelled = wordToBeSpelled;
        this.charPos = charPos;
    }

    @Override
    public boolean canStepInto() {
        return thread.canStepInto();
    }

    @Override
    public boolean canStepOver() {
        return thread.canStepOver();
    }

    @Override
    public boolean canStepReturn() {
        return thread.canStepReturn();
    }

    @Override
    public boolean isStepping() {
        return thread.isStepping();
    }

    @Override
    public void stepInto() throws DebugException {
        thread.stepInto();
    }

    @Override
    public void stepOver() throws DebugException {
        thread.stepOver();
    }

    @Override
    public void stepReturn() throws DebugException {
        thread.stepReturn();
    }

    @Override
    public boolean canResume() {
        return thread.canResume();
    }

    @Override
    public boolean canSuspend() {
        return thread.canSuspend();
    }

    @Override
    public boolean isSuspended() {
        return thread.isSuspended();
    }

    @Override
    public void resume() throws DebugException {
        thread.resume();
    }

    @Override
    public void suspend() throws DebugException {
        thread.suspend();
    }

    @Override
    public boolean canTerminate() {
        return thread.canTerminate();
    }

    @Override
    public boolean isTerminated() {
        return thread.isTerminated();
    }

    @Override
    public void terminate() throws DebugException {
        thread.terminate();
    }

    @Override
    public IThread getThread() {
        return thread;
    }

    @Override
    public IVariable[] getVariables() throws DebugException {
        return new IVariable[0];
    }

    @Override
    public boolean hasVariables() throws DebugException {
        return false;
    }

    @Override
    public int getLineNumber() throws DebugException {
        return charPos;
    }

    @Override
    public int getCharStart() throws DebugException {
        return charPos;
    }

    @Override
    public int getCharEnd() throws DebugException {
        return charPos + 1;
    }

    @Override
    public String getName() throws DebugException {
        return "Char '" + wordToBeSpelled.charAt(charPos) + "' position : " + getCharStart();
    }

    @Override
    public IRegisterGroup[] getRegisterGroups() throws DebugException {
        return new IRegisterGroup[0];
    }

    @Override
    public boolean hasRegisterGroups() throws DebugException {
        return false;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + charPos;
        result = prime * result + ((thread == null) ? 0 : thread.hashCode());
        result = prime * result + ((wordToBeSpelled == null) ? 0 : wordToBeSpelled.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ExampleStackFrame other = (ExampleStackFrame) obj;
        if (charPos != other.charPos)
            return false;
        if (thread == null) {
            if (other.thread != null)
                return false;
        } else if (!thread.equals(other.thread))
            return false;
        if (wordToBeSpelled == null) {
            if (other.wordToBeSpelled != null)
                return false;
        } else if (!wordToBeSpelled.equals(other.wordToBeSpelled))
            return false;
        return true;
    }
}

For convenience most of the actions in the ExampleStackFrame are delegated to the given IThread. For visualization the getName() method returns the character and position of the word, which should be spelled. In later sections IVariables will be added to this IStackFrame implementation, so that also available variables are shown in the Variables view.

IStackFrame classes should also override the equals and hashcode methods, because they are usually compared to each other. See getStackFrames() and getTopStackFrame() methods in Create an IThread .

3.4. Create an IThread

An IThread is supposed to be a wrapper around a usual Java thread, which runs within an IDebugTarget.

import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;

public class ExampleThread extends ExampleDebugElement implements IThread {

    // The different states, which this IThread can have
    private enum State {
        RUNNING, STEPPING, SUSPENDED, TERMINATED
    }

    private State currentState;

    private State requestedState;

    private Thread thread;

    private int charPos;

    private final String wordToBeSpelled;

    public ExampleThread(IDebugTarget target, String wordToBeSpelled) {
        super(target);
        this.wordToBeSpelled = wordToBeSpelled;
        thread = new Thread(new Resume());
        thread.start();
        fireCreationEvent();
    }

    @Override
    public boolean canResume() {
        return isSuspended();
    }

    @Override
    public boolean canSuspend() {
        return !isTerminated() && !isSuspended();
    }

    @Override
    public boolean isSuspended() {
        return State.SUSPENDED.equals(currentState);
    }

    @Override
    public void resume() throws DebugException {
        synchronized (this) {
            requestedState = State.RUNNING;
            thread = new Thread(new Resume());
            thread.start();
        }
    }

    @Override
    public void suspend() throws DebugException {
        synchronized (this) {
            requestedState = State.SUSPENDED;
            thread.interrupt();
        }
    }

    @Override
    public synchronized boolean isStepping() {
        return State.STEPPING.equals(currentState);
    }

    @Override
    public boolean canStepOver() {
        return isSuspended();
    }

    @Override
    public void stepOver() throws DebugException {
        synchronized (this) {
            requestedState = State.STEPPING;
            thread = new Thread(new StepOver());
            thread.start();
        }
    }

    @Override
    public boolean canStepInto() {
        return false;
    }

    @Override
    public boolean canStepReturn() {
        return false;
    }

    @Override
    public void stepInto() throws DebugException {
    }

    @Override
    public void stepReturn() throws DebugException {
    }

    @Override
    public boolean canTerminate() {
        return !isTerminated();
    }

    @Override
    public boolean isTerminated() {
        return State.TERMINATED.equals(currentState);
    }

    @Override
    public void terminate() throws DebugException {
        synchronized (this) {
            requestedState = State.TERMINATED;
            if (isSuspended()) {
                // run to termination
                thread = new Thread(new Resume());
                thread.start();
            } else {
                thread.interrupt();
            }
        }
    }

    @Override
    public IStackFrame getTopStackFrame() throws DebugException {
        synchronized (this) {
            if (isSuspended()) {
                return new ExampleStackFrame(this, wordToBeSpelled, charPos);
            }
        }
        return null;
    }

    @Override
    public IStackFrame[] getStackFrames() throws DebugException {
        synchronized (this) {
            if (isSuspended()) {
                if (charPos < 1) {
                    return new IStackFrame[] { new ExampleStackFrame(this, wordToBeSpelled, charPos) };
                }
                IStackFrame[] frames = new IStackFrame[charPos + 1];
                for (int i = charPos; i >= 0; i--) {
                    frames[i] = new ExampleStackFrame(this, wordToBeSpelled, charPos - i);
                }
                return frames;
            }
        }
        return new IStackFrame[0];
    }

    @Override
    public boolean hasStackFrames() throws DebugException {
        // An IThread only has stack frames when it is suspended.
        return isSuspended();
    }

    @Override
    public int getPriority() throws DebugException {
        return 0;
    }

    @Override
    public String getName() throws DebugException {
        return "Example Thread with the word " + wordToBeSpelled;
    }

    @Override
    public IBreakpoint[] getBreakpoints() {
        return new IBreakpoint[0];
    }

    class StepOver implements Runnable {
        @Override
        public void run() {
            synchronized (ExampleThread.this) {
                currentState = State.STEPPING;
            }
            fireResumeEvent(DebugEvent.STEP_OVER);
            int event = doNextStep();
            int detail = DebugEvent.UNSPECIFIED;
            synchronized (ExampleThread.this) {
                // update state
                switch (event) {
                case DebugEvent.BREAKPOINT:
                    currentState = State.SUSPENDED;
                    detail = DebugEvent.BREAKPOINT;
                    break;
                case DebugEvent.UNSPECIFIED:
                    currentState = State.SUSPENDED;
                    detail = DebugEvent.STEP_END;
                    break;
                case DebugEvent.TERMINATE:
                    currentState = State.TERMINATED;
                    break;
                case DebugEvent.SUSPEND:
                    currentState = State.SUSPENDED;
                    detail = DebugEvent.CLIENT_REQUEST;
                    break;
                }
            }
            switch (currentState) {
            case SUSPENDED:
                fireSuspendEvent(detail);
                break;
            case TERMINATED:
                fireTerminateEvent();
                ExampleDebugTarget target = (ExampleDebugTarget) getDebugTarget();
                target.dispose();
                break;
            default:
                break;
            }
        }
    }

    class Resume implements Runnable {
        @Override
        public void run() {
            synchronized (ExampleThread.this) {
                currentState = State.RUNNING;
            }
            fireResumeEvent(DebugEvent.CLIENT_REQUEST);
            int detail = DebugEvent.UNSPECIFIED;
            int event = DebugEvent.UNSPECIFIED;
            while (event == DebugEvent.UNSPECIFIED) {
                event = doNextStep();
            }
            synchronized (ExampleThread.this) {
                // update state
                switch (event) {
                case DebugEvent.BREAKPOINT:
                    currentState = State.SUSPENDED;
                    detail = DebugEvent.BREAKPOINT;
                    break;
                case DebugEvent.TERMINATE:
                    currentState = State.TERMINATED;
                    break;
                case DebugEvent.SUSPEND:
                    currentState = State.SUSPENDED;
                    detail = DebugEvent.CLIENT_REQUEST;
                    break;
                }
            }
            switch (currentState) {
            case SUSPENDED:
                fireSuspendEvent(detail);
                break;
            case TERMINATED:
                fireTerminateEvent();
                ExampleDebugTarget target = (ExampleDebugTarget) getDebugTarget();
                target.dispose();
                break;
            default:
                break;
            }
        }

    }

    private int doNextStep() {
        if (State.TERMINATED.equals(requestedState)) {
            return DebugEvent.TERMINATE;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            switch (requestedState) {
            case TERMINATED:
                return DebugEvent.TERMINATE;
            case SUSPENDED:
                return DebugEvent.SUSPEND;
            default:
                break;
            }
        }
        System.out.print(wordToBeSpelled.charAt(charPos));
        charPos++;
        if (charPos > wordToBeSpelled.length() - 1) {
            System.out.println();
            charPos = 0;
        }

        return DebugEvent.UNSPECIFIED;
    }
}

3.5. Create an IDebugTarget

The IDebugTarget is supposed to be applied to the org.eclipse.debug.core.ILaunch object, which is passed to an org.eclipse.debug.core.model.LaunchConfigurationDelegate, by using its addDebugTarget(IDebugTarget target) method. .

It can contain several IThreads, which run during a debug session.

import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IThread;

public class ExampleDebugTarget extends ExampleDebugElement implements IDebugTarget {

    private IProcess process;
    private ILaunch launch;

    private ExampleThread thread;

    // the IProcess and ILaunch are provided by an IDebugTarget and therefore
    // they should be passed. The getProcess() and getLaunch() method should be
    // overridden accordingly. See DebugElement implementation
    public ExampleDebugTarget(IProcess process, ILaunch launch, String wordToBeSpelled) {
        super(null);
        this.process = process;
        this.launch = launch;
        this.thread = new ExampleThread(this, wordToBeSpelled);

        // notify that this element has been created
        fireCreationEvent();
    }

    // must be overridden since "this" cannot be passed in a constructor
    @Override
    public IDebugTarget getDebugTarget() {
        return this;
    }

    @Override
    public IProcess getProcess() {
        return this.process;
    }

    @Override
    public ILaunch getLaunch() {
        return launch;
    }

    @Override
    public boolean canTerminate() {
        return thread.canTerminate();
    }

    @Override
    public boolean isTerminated() {
        return thread.isTerminated();
    }

    @Override
    public void terminate() throws DebugException {
        thread.terminate();
    }

    @Override
    public boolean canResume() {
        return thread.canResume();
    }

    @Override
    public boolean canSuspend() {
        return thread.canSuspend();
    }

    @Override
    public boolean isSuspended() {
        return thread.isSuspended();
    }

    @Override
    public void resume() throws DebugException {
        thread.resume();
    }

    @Override
    public void suspend() throws DebugException {
        thread.suspend();
    }

    @Override
    public void breakpointAdded(IBreakpoint breakpoint) {
    }

    @Override
    public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
    }

    @Override
    public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
    }

    @Override
    public boolean canDisconnect() {
        return false;
    }

    @Override
    public void disconnect() throws DebugException {
    }

    @Override
    public boolean isDisconnected() {
        return false;
    }

    @Override
    public boolean supportsStorageRetrieval() {
        return false;
    }

    @Override
    public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
        return null;
    }

    @Override
    public IThread[] getThreads() throws DebugException {
        if (isTerminated()) {
            return new IThread[0];
        }
        return new IThread[]{thread};
    }

    @Override
    public boolean hasThreads() throws DebugException {
        return true;
    }

    @Override
    public String getName() throws DebugException {
        return "Example Debug target";
    }

    @Override
    public boolean supportsBreakpoint(IBreakpoint breakpoint) {
        return false;
    }

    public void dispose() {
        fireTerminateEvent();
    }
}

3.6. Validate

To validate the result start the ExampleLaunchDelegate, switch to the debug perspective and use the toolbar controls below to suspend, resume, step over or terminate the debug session.

Debug toolbar buttons

When passing in "vogella" as String the result should look similar to this:

Debug view result

4. Eclipse Debug Framework Resources