Over a million developers have joined DZone.

How to Reuse the NetBeans Platform Debugger Architecture

· Java Zone

Discover how AppDynamics steps in to upgrade your performance game and prevent your enterprise from these top 10 Java performance problems, brought to you in partnership with AppDynamics.

This tutorial will demonstrate creating a debugger for a custom programming language via the NetBeans Debugger Core API. The debugger built in this tutorial will not be fully functional, but it will respond to the basic commands from the NetBeans Platform architecture.

During this tutorial, we will:

  • Create a new Debugger Module

  • Make our Debugger “Omniscient”

An Omniscient debugger is one that stores state about a program as it executes, in order to execute it in reverse. For example, in the programming language we will create, you can "Step Over" like normal, but you can also "Step Back Over." Consider, for example, if you throw an exception. Usually, this would cause you to jump out of a program, or to log something. In our debugger, however, you simply roll back the program to that state, so you can see exactly what the state of the system was at the time.

This tutorial requires NetBeans 6.9 or above, due to recent changes in the debugger architecture. NetBeans 6.8 and lower does not support adding custom debugger buttons (e.g., omniscient debugger support), using the methods indicated here.


Getting Started

We will begin this tutorial by downloading the finished CMinus syntax highlighting tutorial from http://wiki.netbeans.org/SyntaxColoringANTLR

Click here to download the starting point of this tutorial.

Next, we will create our debugger module:

  1. Choose File > New Project (Ctrl + Shift + N). Under Categories, select NetBeans Modules. Under Projects, select Module.

  2. Type CMinus Debugger in the Project Name Field.

  3. Be sure that the radio button labeled Add to Module Suite is selected. Assuming you are in the CMinusEditor project, it should automatically be selected.

 

After pressing next, enter org.cminus.debugger for your code name base and select Generate XML Layer. Then press Finish.



Next, we want to set two dependencies:

  • Debugger Core API

  • Debugger Core – UI

And finally, we want to make a public API package for connecting to our debugger. Do this by right clicking on the CMinus Debugger module and selecting properties. Next. Select API versioning and select the check box labeled org.cminus.debugger under Public Packages.

 You should now have a CMinus Debugger Module. We’ll do more with this in a bit.


Setting up CMinus Projects

Since the CMinusEditor module only supported lexing and a few other features, we need to “turn these on” in our NetBeans module suite.

  1. Right click on the CminusEditor module suite and click properties.

  2. Under categories, select Libraries. Under Platform Modules, I have apisupport, ergonomics, harness, ide, java, platform, and websvccommon, and all of the sub-nodes, selected. Technically, you could just select all nodes and it would work fine, although it would take longer to build.


Next, follow the standard project creation tutorial to add CMinus project types:

http://platform.netbeans.org/tutorials/nbm-projecttype.html

In addition to the dependencies listed in the tutorial, we will need more:

  • Debugger Core API

  • Debugger Core – UI

  • CMinus Debugger

Once you have finished, you will need to add a few lines of code to CMinusProject to enable the debugger button:

First, add a dependency to the CMinus Project Type module for CMinus Debugger module. Then in ActionProviderImpl, add:

private String[] supported = new String[]{   
  ActionProvider.COMMAND_DELETE,
  ActionProvider.COMMAND_COPY, 
  ActionProvider.COMMAND_DEBUG //add me
};

and in the invoke action:

//add this, to handle debug commands from NetBeans
if (string.equalsIgnoreCase(ActionProvider.COMMAND_DEBUG)) {
    debugger.startDebugger();
}

And then enable the action by modifying isActionEnable(String, Lookup):

if (command.equals(ActionProvider.COMMAND_DEBUG)) {
   return true;
}

The project implementation for CMinus projects provided in the zip file at the end of this tutorial is hardly feature complete, but our goal here is not to make working CMinus projects, but to demonstrate how to use the NetBeans debugger architecture.


Implementing a Basic Debugger

Next, we focus our attention on connecting to the standard user interface for debuggers. To begin, we need to add two META-INF registrations, effectively the standard way to inform NetBeans that you are providing an additional service (e.g., a debugger, a project).

  1. Create a new folder in the CMinus Debugger module called META-INF/debugger/CMinusSession

  2. Add an empty file with the name org.netbeans.spi.debugger.ActionsProvider

  3. In this file, add the following text org.cminus.debugger.CMinusDebugger

  4. Add a second empty file with the name org.netbeans.spi.debugger.DebuggerEngineProvider

  5. In the second file add org.cminus.debugger.CMinusEngineProvider

These registrations inform the NetBeans platform that you have created a debugger implementation and that the actions the debugger allows can be queried from these classes. Specifically, the ActionsProvider provides support for common debugger actions (e.g., start, stop, step into, step over), while DebuggerEngineProvider tells NetBeans about the debugger for your custom language.

Sometimes, we may also want to track details about a debugger session, while we will not do anything with the sessions here, we can accomplish this by making a third registration, org.netbeans.api.debugger.LazyDebuggerManagerListener. In this registration, which goes under META-INF.debugger, we can put a custom SessionListener class: org.cminus.debugger.SessionListener.

Once we have our registrations in place, we need to implement three classes, CMinusDebugger, CMinusEngineProvider, and SessionListener.

I’ve attempted to comment these source files to make it as clear as possible what they do. Perhaps the most important point here is in the static startDebugger method in CMinusDebugger. Notice that NetBeans is told to start a debugger session by first getting an implementation of the DebuggerManager class (DebuggerManager.getDebuggerManager()). Next, we create a new DebuggerInfo, which tells NetBeans where our Session registrations are located. Notice that spelling errors in the string you return in getTypeID() will cause your debugger to not load, which can be confusing and hard to spot. Finally, by calling startDebugging on your debugger manager object, and passing it the DebuggerInfo you created, NetBeans will begin your debugger session, causing the debugger toolbar to fly out and the menu items to become active.

CMinusDebugger.java

/**
 * This class houses the primary implementation of our fake debugger.
 *
 * @author Andreas Stefik
 */
public class CMinusDebugger extends ActionsProviderSupport{

    /** Action constant for Step Over Action. */
    public static final Object ACTION_STEP_OVER = "stepOver";
    /** Action constant for Step Over Action. */
    public static final Object ACTION_STEP_BACK_OVER = "stepBackOver";
    /** Action constant for breakpoint hit action. */
    public static final Object ACTION_RUN_INTO_METHOD = "runIntoMethod";
    /** Action constant for Step Into Action. */
    public static final Object ACTION_STEP_INTO = "stepInto";
    /** Action constant for Step Into Action. */
    public static final Object ACTION_STEP_BACK_INTO = "stepBackInto";

    //Note that the above are for omniscient debugging, if your debugger
    //is so lucky as to know everything.

    /** Action constant for Step Out Action. */
    public static final Object ACTION_STEP_OUT = "stepOut";
    /** Action constant for Step Operation Action. */
    public static final Object ACTION_STEP_OPERATION = "stepOperation";
    /** Action constant for Continue Action. */
    public static final Object ACTION_CONTINUE = "continue";
    /** Action constant for Continue Action. */
    public static final Object ACTION_REWIND = "rewind";
    /** Action constant for Start Action. */
    public static final Object ACTION_START = "start";
    /** Action constant for Kill Action. */
    public static final Object ACTION_KILL = "kill";
    /** Action constant for Make Caller Current Action. */
    public static final Object ACTION_MAKE_CALLER_CURRENT = "makeCallerCurrent";
    /** Action constant for Make Callee Current Action. */
    public static final Object ACTION_MAKE_CALLEE_CURRENT = "makeCalleeCurrent";
    /** Action constant for Pause Action. */
    public static final Object ACTION_PAUSE = "pause";
    /** Action constant for Run to Cursor Action. */
    public static final Object ACTION_RUN_TO_CURSOR = "runToCursor";
    /** Action constant for Run to Cursor Action. */
    public static final Object ACTION_RUN_BACK_TO_CURSOR = "runBackToCursor";
    /** Action constant for Pop Topmost Call Action. */
    public static final Object ACTION_POP_TOPMOST_CALL = "popTopmostCall";
    /** Action constant for Fix Action. */
    public static final Object ACTION_FIX = "fix";
    /** Action constant for Restart Action. */
    public static final Object ACTION_RESTART = "restart";
    /** Action constant for Toggle Breakpoint Action. */
    public static final Object ACTION_TOGGLE_BREAKPOINT = "toggleBreakpoint";

    private CMinusEngineProvider engineProvider;
    private static final Set actions = new HashSet();

    public static final String CMINUS_DEBUGGER_INFO = "CMinusDebuggerInfo";
    public static final String CMINUS_SESSION = "CMinusSession";

    public CMinusDebugger(ContextProvider contextProvider) {
        //if we not enable the actions, our debugger will show them as greyed
        //out by default, in both the menus and the toolbar.
        engineProvider = (CMinusEngineProvider) contextProvider.lookupFirst(null, DebuggerEngineProvider.class);
        for (Iterator it = actions.iterator(); it.hasNext();) {
            setEnabled(it.next(), true);
        }
    }

    /**
     * Make an array of actions for convenience.
     */
    static {
        actions.add(ACTION_RUN_BACK_TO_CURSOR);
        actions.add(ACTION_STEP_BACK_INTO);
        actions.add(ACTION_STEP_BACK_OVER);
        actions.add(ACTION_REWIND);
        actions.add(ACTION_KILL);
        actions.add(ACTION_PAUSE);
        actions.add(ACTION_CONTINUE);
        actions.add(ACTION_START);
        actions.add(ACTION_STEP_INTO);
        actions.add(ACTION_STEP_OVER);
        actions.add(ACTION_RUN_TO_CURSOR);
    }

    /**
     * This method starts the debugger. Don't worry about creating a
     * similar stopDebugger method, as this is taken care of by our
     * set of defined actions. This method literally starts the debugger, by
     * passing a DebuggerInfo instance to NetBeans DebuggerManager class.
     */
    public static void startDebugger() {
        DebuggerManager manager = DebuggerManager.getDebuggerManager();
        DebuggerInfo info = DebuggerInfo.create(CMINUS_DEBUGGER_INFO,
                new Object[]{
                    new SessionProvider() {

                        @Override
                        public String getSessionName() {
                            return "CMinus Program";
                        }

                        @Override
                        public String getLocationName() {
                            return "localhost";
                        }

                        public String getTypeID() {
                            return CMINUS_SESSION;
                        }

                        public Object[] getServices() {
                            return new Object[]{};
                        }
                    }, null
                });

        manager.startDebugging(info);
    }

    /**
     * This is where we implement (or delegate), the implementation
     * of our debugger. In other words, this is where we tell
     * our debugger implementation to step over, into, stop, or to take
     * other custom operations.
     *
     * @param action
     */
    @Override
    public void doAction(Object action) {
        if (action == ACTION_RUN_BACK_TO_CURSOR) {
        } else if (action == ACTION_STEP_BACK_INTO) {
        } else if (action == ACTION_STEP_BACK_OVER) {
        } else if (action == ACTION_REWIND) {
        } else if (action == ACTION_KILL) {
            //this stops the debugger
            engineProvider.getDestructor().killEngine();
        } else if (action == ACTION_PAUSE) {
        } else if (action == ACTION_CONTINUE) {
        } else if (action == ACTION_START) {
        } else if (action == ACTION_STEP_INTO) {
        } else if (action == ACTION_STEP_OVER) {
        } else if (action == ACTION_RUN_TO_CURSOR) {
        }

        //print this out, since we don't actually have an implementation of
        //a CMinus Debugger.
        System.out.println("The debugger took the action: " + action);
    }

    @Override
    public Set getActions() {
        return actions;
    }
}

CMinusEngineProvider.java

/**
 * This class provides details on our custom debugger engine.
 * 
 * @author Andreas Stefik
 */
public class CMinusEngineProvider extends DebuggerEngineProvider{

    private DebuggerEngine.Destructor destructor;

    @Override
    public String[] getLanguages() {
        return new String[] {"CMinus"};
    }

    @Override
    public String getEngineTypeID() {
        return "CMinusDebuggerEngine";
    }

    @Override
    public Object[] getServices() {
        return new Object[]{};
    }

    @Override
    public void setDestructor (DebuggerEngine.Destructor destructor) {
        this.destructor = destructor;
    }

    public DebuggerEngine.Destructor getDestructor () {
        return destructor;
    }

}

SessionListener.java

/**
 * You can do something with the session here if you wish.
 * 
 * @author Andreas Stefik
 */
public class SessionListener extends DebuggerManagerAdapter{

    @Override
    public void sessionAdded( Session session ) {
        super.sessionAdded(session);
    }

    @Override
    public void sessionRemoved( Session session ) {
        super.sessionRemoved(session);
    }

}

Making your Debugger Omniscient

While the functionality provided by the NetBeans architecture allows for a great deal more functionality then I have mentioned so far, occasionally we want to add new buttons to our debugger. In the case of the Sodbeans project, our debugger is omniscient, meaning it stores state about a program as it executes and allows the user to navigate a program forward and backward. This can be handy, as programs run in an omniscient debugger never “crash,” they become aware that they are about to crash, inform the user, and then let the user backup to see what is wrong.

Creating an implementation of an omniscient debugger is not trivial, but, fortunately, post NetBeans 6.9, adding the actions for omniscient debuggers is. You can create your own buttons for an omniscient debugger (or any other kind of button), using the following layer registrations, which go into your layer file:

    <folder name="Actions">
        <folder name="Debug">
            <file name="org-cminus-debugger-RunBackToCursorAction.instance">
                <attr name="instanceClass" stringvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction"/>
                <attr name="instanceOf" stringvalue="javax.swing.Action"/>
                <attr name="instanceCreate" methodvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction.createAction"/>
                <attr name="action" stringvalue="runBackToCursor"/>
                <attr name="name" stringvalue="Run Back to Cursor"/>
                <attr name="iconBase" stringvalue="org/cminus/debugger/resources/RunBackToCursor.png"/>
            </file>
            <file name="org-cminus-debugger-RewindDebuggerAction.instance">
                <attr name="instanceClass" stringvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction"/>
                <attr name="instanceOf" stringvalue="javax.swing.Action"/>
                <attr name="instanceCreate" methodvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction.createAction"/>
                <attr name="action" stringvalue="rewind"/>
                <attr name="name" stringvalue="Rewind"/>
                <attr name="iconBase" stringvalue="org/cminus/debugger/resources/Rewind.png"/>
            </file>
            <file name="org-cminus-debugger-StepBackIntoAction.instance">
                <attr name="instanceClass" stringvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction"/>
                <attr name="instanceOf" stringvalue="javax.swing.Action"/>
                <attr name="instanceCreate" methodvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction.createAction"/>
                <attr name="action" stringvalue="stepBackInto"/>
                <attr name="name" stringvalue="Step Back Into"/>
                <attr name="iconBase" stringvalue="org/cminus/debugger/resources/StepBackInto.png"/>
            </file>
            <file name="org-cminus-debugger-StepBackOverAction.instance">
                <attr name="instanceClass" stringvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction"/>
                <attr name="instanceOf" stringvalue="javax.swing.Action"/>
                <attr name="instanceCreate" methodvalue="org.netbeans.modules.debugger.ui.actions.DebuggerAction.createAction"/>
                <attr name="action" stringvalue="stepBackOver"/>
                <attr name="name" stringvalue="Step Back Over"/>
                <attr name="iconBase" stringvalue="org/cminus/debugger/resources/StepBackOver.png"/>
            </file>
        </folder>
    </folder>

    <folder name="Menu">
        <folder name="RunProject">
            <attr name="position" intvalue="801"/>
            <file name="org-cminus-debugger-RewindDebuggerAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-RewindDebuggerAction.instance"/>
                <attr name="position" intvalue="850"/>
            </file>
            <file name="org-cminus-debugger-RunBackToCursorAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-RunBackToCursorAction.instance"/>
                <attr name="position" intvalue="1110"/>
            </file>
            <file name="org-cminus-debugger-StepBackIntoAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-StepBackIntoAction.instance"/>
                <attr name="position" intvalue="1120"/>
            </file>
            <file name="org-cminus-debugger-StepBackOverAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-StepBackOverAction.instance"/>
                <attr name="position" intvalue="1130"/>
            </file>
        </folder>
    </folder>
    <folder name="Toolbars">
        <folder name="Debug">
            <file name="org-cminus-debugger-RunBackToCursorAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-RunBackToCursorAction.instance"/>
                <attr name="position" intvalue="10"/>
            </file>
            <file name="org-cminus-debugger-StepBackOverAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-StepBackOverAction.instance"/>
                <attr name="position" intvalue="20"/>
            </file>
            <file name="org-cminus-debugger-StepBackIntoAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-StepBackIntoAction.instance"/>
                <attr name="position" intvalue="30"/>
            </file>
            <file name="org-cminus-debugger-RewindDebuggerAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Debug/org-cminus-debugger-RewindDebuggerAction.instance"/>
                <attr name="position" intvalue="40"/>
            </file>
        </folder>
    </folder>

The salient points here with the layer registrations are two fold. First we tell NetBeans’ debugger APIs that we want to add our own custom actions into the debugger by declaring these actions under Actions/Debug. Note that the attr name = action, name, and iconBase are of your own design and can be whatever you like. However, instanceOf, instanceCreate, instanceClass, must be exactly as shown here, as this tells the system to register your actions as debugger actions. Second, the last two sections of layer registrations are telling the system to put your new buttons into the proper toolbars and menus.

If you completed everything correctly, you should now have working controls for an omniscient debugger. Clicking on these buttons, or using the shortcuts in the menus, should output what they do to the console.

 

For all of the sources used at this stage in the tutorial, click here. Click here to download a simple CMinus project. To try out the debugger, run the suite you have created and open the simple CMinus project. If you open it and have it selected, you will be able to click the normal debug button on the toolbar and run it. If you then click the debugger buttons (e.g., step over), you can see it will output "step over" in the output window of NetBeans IDE. Stop, similarly, closes the debugger session like normal. Keep in mind that that CMinus project doesn't really do anything (like running CMinus code), but it does register that it has a debugger and allow you to implement that debugger.

More Debugger Features

This tutorial has only scratched the surface of what a NetBeans debugger must do. Realistically, we need a local variable window, a watch window, breakpoints, persistence, and other attributes. While we will not discuss these topics here, adding these features into an existing debugger is not terribly difficult. For more information, see any of the debugger implementations in the NetBeans source code, or take a look at the Hop Debugger module in the Sodbeans project.

For more information on our tools or source code, see:

https://sourceforge.net/apps/trac/sodbeans/

The Java Zone is brought to you in partnership with AppDynamics. AppDynamics helps you gain the fundamentals behind application performance, and implement best practices so you can proactively analyze and act on performance problems as they arise, and more specifically with your Java applications. Start a Free Trial.

Topics:

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}