A breakpoint with a side effect is a breakpoint that causes a task to be performed upon encountering the enabled breakpoint within your application. Of course, such a breakpoint only performs its side effect when your code is running with Delphi's debugger attached to the process. Nonetheless, this type of breakpoint can be extremely useful, especially when you are in the testing phase of your application development.
This article shows you how to create breakpoints that cause side effects. It begins with a general overview of breakpoints, and continues with a discussion of non-breaking breakpoints. Finally, how to create breakpoints to produce side effects is explained in detail.
Breakpoint Overview
When most Delphi developers think of breakpoints, they typically think of the debugger feature that interupts the execution of your code upon executing a specific line of your code. In reality, this definition is far from complete.
First of all, the only type of breakpoint that is associated with a line of your source code is a source breakpoint. There are three other types of breakpoints: address breakpoints, data breakpoints, and module breakpoints.
Address Breakpoints
Address breakpoints permit you to define a breakpoint that will trigger when an instruction at a particular memory address is executed. You can only add an address breakpoint when the debugger is loaded. Then you can select Run -> Add Breakpoint -> Address Breakpoint. Enter the address in the Address field of the Add Address Breakpoint dialog box.
You can also add an address breakpiont from the disassembly pane of the CPU window. With your project stopped in the debugger, display the CPU window by selecting View -> Debug Windows -> CPU. To add an address breakpoint, either click in the left gutter of the disassembly pane in the CPU window on the line associated with the instruction address, press Ctrl-B with your cursor on the instruction line, or right-click the instruction line and select Toggle breakpoint. An address breakpoint is shown in the CPU window in Figure 1.
Figure 1 An address breakpoint in the CPU window
Data Breakpoints
Data breakpoints are those that trigger when a particular memory address is written to. The memory address can be represented either as a hexadecimal number or as the variable used to refer to that memory address. Unlike other breakpoint types, data breakpoints last only for the current Delphi session.
To add a data breakpoint, invoke the debugger (either through an exception or by hitting a breaking breakpoint). Then select Run -> Add Breakpoint -> Data Breakpoint. Delphi responds by displaying the Add Data Breakpoint dialog box, as shown in Figure 2.
Figure 2 Adding a data breakpoint
At Address, enter the memory address of the data. Use Length to define the length of the data beginning at that address. If you are running Delphi 2007 or later, you can also enter the name of a variable in the Address field.
In earlier versions of Delphi, to add a data breakpoint based on a variable name, add the variable into the Watches window. (You display the Watches window by selecting View -> Debug Windows -> Watches from Delphi's main menu.) Then, right-click on the watch entry and select the option Break on change. For data breakpoints added using this technique, you can modify the breakpoint properties by right-clicking on the breakpoint in the Breakpoint List dialog box and selecting Properties. (You can display the Breakpoint List dialog box by pressing Ctrl-Alt-B.)
Module Breakpoints
Module load breakpoints are those that trigger when a specified module is being loaded into your application. For example, if you want to ensure that a particular DLL is not being used by your application, you can set an enabled breaking module breakpoint for it, ensuring that the debugger will load if for some reason that DLL gets loaded by your application.
To add a module breakpoint, select Run -> Add Breakpoint -> Module Load Breakpoint. Delphi responds by displaying the dialog box shown in Figure 3.
Figure 3 The Add Module Breakpiont dialog box
Enter the name of the module in the Module Name field. Click OK when you are done. This is the only way to set a module load breakpoint on a module that is not yet loaded (or even one that will never be loaded).
You can also create a module load breakpoint from the Modules window, but this is only useful if the project is in the debugger and the module has already been loaded. With the debugger loaded, open the Modules window by selecting View -> Debug Windows -> Modules. Then in the Module pane, right-click the module you want to break on, and select Break On Load. Modules that will trigger a module load breakpoint appear in the Module pane with a red dot to their left. To remove a module load breakpoint, right-click the specific module and select Break On Load once again.
Non-Breaking Breakpoints
Another aspect of the breakpoint definition provided at the top of this article that is not completely correct is the part where the breakpoint interupts the execution of your code. In fact, a breakpoint only interupts the execution of your code if it is a breaking breakpoint. Non-breaking breakpoints, like breaking breakpoints, trigger when they are active and are encountered by the debugger. Unlike breaking breakpoints, however, the do not stop code execution.
To define a non-breaking breakpoint, display the Breakpoint Propeties dialog box for the breakpoint. Click the button labeled Advanced >> to display the Advanced breakpoint properties, as shown in Figure 4, and then disable the Break checkbox. (Module breakpoints are the only breakpoints that do not support a non-breaking mode.)
Figure 4 The Advanced view of the Breakpoint Properties dialog box
At first you might wonder "What is the value of a breakpoint if it does not stop your code execution?". Let me tell you, the value can be very big indeed. Non-breaking breakpoints permit you to perform a variety of actions without interrupting the execution of your application.
Consider that a non-breaking breakpoint can be used to enable or disable one or more other breakpoints. For example, you can set a non-breaking breakpoint to execute on the second pass of a section of initialization code that you assume will only execute once. If that code is executed a second time, for whatever reason, you can enable a group of additional breakpoints that will load the debugger, permitting you to examine your application's variables in order to determine why the code is executing a second time.
For me, however, the most valuable capability of a non-breaking breakpoint is to perform a side effect.
Breakpoints with Side Effects
There are five types of side effects that a breakpoint can perform, whether or not it is a breaking breakpoint. It can enable or disable the loading of the debugger in response to an exception, it can write a message to the event log, it can evaluate an expression, it can enable or disable breakpoints associated with a particular group, and in the more recent versions of Delphi, it can write some or all of the call stack to the event log.
Of these, the side effect I consider the most valuable is the ability to evaluate an expression, so I will consider that side effect last. But first, let's consider the other side effects.
Controlling How The Debugger Handles Exceptions
If you check the Ignore subsequent exceptions checkbox, the integrated debugger will stop loading when an exception is raised, at least until another breakpoint that enables subsequent exceptions is encountered. To define a breakpoint that enables exceptions, place a second breakpoint (breaking or non-breaking), and check the Enabled subsequent exceptions checkbox.
Disabling and enabling exceptions using non-breaking breakpoints permits you to conveniently run a section of code from the IDE that raises exceptions, without interruption from the debugger. This technique is particularly useful for sections of code that raise exceptions for the purpose of displaying errors or warnings to the user, but which do not constitute bugs or other similar errors in your code.
For example, when client-side validation code (code that tests the validity of user input) raises an exception, it is really not an error, code-wise. Instead, it is to inform the user that validation has failed, suggest to the user how to correct the problem, and to abort any remaining validations or operations that would proceed had the user's input been valid. While testing your validation code it serves you no purpose to be interrupt by the integrated debugger.
Writing Messages to the Event Log
Use the Log message field to enter a string that will be written to the event log when the non-breaking breakpoint triggers. This option is a nice alternative to using the Windows API call OutputDebugString, which requires adding additional code to your project. After running your application in the IDE, or while the integrated debugger is loaded, you can view messages written to the event log using the Log message field by selecting View -> Debug Windows -> Event Log. Messages written to the event log using breakpoints are identified by the label "Breakpoint Message."
Enabling/Disabling Breakpoint Groups
A breakpoint can be used to enable and disable groups of breakpoints. A breakpoint group consists of one or more breakpoints to which you have assigned the same group name, using the Group field on the Breakpoint Properties dialog box (see Figure 4).
When a breakpoint group is disabled, none of the breakpoints in the group will trigger when encountered by code running in the IDE. Enabling a group restores the enabled property of any disabled breakpoints in the group.
Evaluating Expressions
The Eval expression field serves two purposes. First, it can be used to evaluate any expression. This expression can include any object, method, variable, constant, resource, function, or type that is within scope of the breakpoint, and can also include operators appropriate for those symbols. After having entered a value in Eval expression, you have the option to enabled the Log result checkbox. When Log result is checked, the value of the expression is written to the event log. As with the Log Message field, Eval expression results that are logged permit you to avoid writing expression to the event log using OutputDebugString.
So, finally, here is the main point of this article. My favorite use of Eval expression is to execute a function. Specifically, a function that has side-effects. For example, imagine that upon startup your application tests for the existence of a CDS file that is used by a ClientDataSet. If the file is absent, you call CreateDataSet to create the data structure that the ClientDataSet will use, after which that structure is written to a CDS file.
During testing, you may always want to begin your application without an existing CDS file, so that you can test whether or not the CDS file is being created properly. One way of doing this is the write a function that deletes the CDS file if it exists. You can then call this function with a breakpoint by adding the function name to the Eval expression field.
So long as this Eval expression is associated with a breakpoint that is encountered prior to the first time your application tests for the presence of the CDS file, the Eval expression function will always ensure that no file exists when the test occurs. Since breakpoints only trigger when the application is run from the IDE, a deployed application, or one executed without the debugger being enabled, will not destroy an existing CDS file.
In order to execute a function using the Eval expression field, that function must be included in your compiled code by the linker. But this is a bit more complicated than it sounds. Specifically, if the only call to your function that you create to produce your side effect is made by the breakpoint, Delphi's smart linker will not recognize that the function is used, and will therefore omit it from the compiled application. While this feature ensures that your applications only contain code that they use, thereby keeping your exe size to a minimum, it will have the effect of removing your side effect.
Fortunately, there is a work-around for this. Specifically, you can trick the smart linking into thinking that your side effect function might be called, in which case it will be obligated to include the function in your compiled code. You do this by creating a method that looks like an event hander, and include a call to your side effect function within that method.
Here is how you can do this. Create a published method. While in reality this method can have any signature, you might as well make it a TNotifyEvent, which is a procedure that takes a TObject parameter named Sender. Then, in the implementation of this method, include your call to your side effect function. The following code shows what the pseudo event handler, and the side effect function, might look like:
procedure TForm1.FakeEvent(Sender: TObject);
begin
CleanupCDS;
end;
function TForm1.CleanupCDS: String;
begin
if FileExists(ExtractFilePath(Application.ExeName) + 'data.cds') then
DeleteFile(ExtractFilePath(Application.ExeName) + 'data.cds');
end;
All you have left to do is to add the call to CleanupCDS to the Eval Expression property of the breakpoint, as shown in Figure 5. Assuming that the Break property of the breakpoint has been disabled, anytime that this breakpoint is encountered in your code while executing with the debugger active, the CDS file named DATA.CDS, located in the same directory as your application, will be silently deleted.
Figure 5 A non-breaking breakpoint with side effects