Pages

Monday, 10 June 2013

10 Tips For Delphi Users

Working on some Delphi projects with some clients over recent weeks has thrown up a number of tips that have been well-received so I thought I’d write them up for a more global audience.

1) Running apps from network shares

Sometimes people like to put executables on network drives and run them from there – it allows potentially many users to launch a single executable. However if the program is running and the network connection breaks there is a more than reasonable chance that the program will crash is a quite ghastly manner, understandably enough.

Given users are at liberty to launch there apps like this it may be an idea to make habit a simple step that alleviates the aforementioned crash scenario. You can have the Delphi linker set a special bit in a 32-bit Windows executable header to instruct Windows that in the event of being launched off a network drive the executable should first be copied to the local paging file and then be launched from there. This flag was introduced in Windows NT 4.0 and described in this old MSJ article by Matt Pietrek from 1996.

This is quite straightforward. You can do this with a compiler directive in the project file or you can change the linker setting (at least in recent versions of Delphi).

To change the project file, you must make sure that Windows (or Winapi.Windows, if you prefer) is in the uses clause and then add in this compiler directive after the uses clause:

{$SetPEFlags IMAGE_FILE_NET_RUN_FROM_SWAP}

To set the linker option directly, open the project options dialog (Ctrl+Shift+F11), go to the Linking options, ensure you are looking at options for a 32-bit build configuration and then give a value of 2048 to the Set extra PE Header flags option. This is passed along to the compiler as the --peflags:2048 command-line switch and does the same thing.

You could work out the value to pass along yourself by looking in Winapi.Windows.pas and seeing that IMAGE_FILE_NET_RUN_FROM_SWAP has a value of $800, which in decimal in 2048.

Update: Just to clarify some points that have come up in comments, this linker setting does not affect operation of the program in terms of how .ini files shipped alongside the .exe get loaded or what the value is returned by Application.ExeName.

2) Inspecting an item in a TList

The old TList class is still a useful beast, even if in many cases we should be using the newer generic TList<T>. TList is useful when the list will potentially contain objects of different types, with suitable backup code that knows to check the type and cast appropriately.

In the debugger, if you want to inspect an item in a TList things can be a bit of a challenge. When you inspect your TList variable (Alt+F5) you get a debug inspector that hints at the items contained therein in its FList field.

TList1

If you right-click on FList and choose Descend (or Ctrl+D, to make this inspector inspect that field) or Inspect (or Ctrl+I, to launch a new inspector showing that field) you’ll see the addresses of the items in the list.

TList2

These aren’t much use though; being as this is the type-unsafe general TList these values are just pointers. Indeed if you look in the definition of TList you’ll see this FList field is a TPointerList – an array of pointers.

So what do we do to inspect the item underlying a pointer variable? Well, we cast the pointer to the right type and inspect that. To work out what type that is, add a watch that casts the address shown in the inspector to a TObject and call its ClassName method – don’t forget to ensure the watch is set to allow side effects and function calls when adding it.

TList3

This tells you the type. Now you can bring up an inspector for the currently unhelpful point by selecting it in the inspector and pressing Ctrl+I, or maybe just double-clicking it.

TList4

Then you can apply the typecast by right-clicking and choosing Type Cast... or pressing Ctrl+T, and entering the target type of TFoo (in this example’s case). And then you have what you sought:

TList5

For bonus points you can identify the target type in the CPU window’s memory pane rather than the watch window. Invoke the CPU window with Ctrl+Alt+C and pay attention just to the memory pane at the bottom left. In recent IDEs you can also just invoke a solitary memory pane using View, Debug Windows, CPU Windows, Memory 1 (Ctrl+Alt+E) or Memory 2 (Ctrl+Alt+2) or Memory 3 (Ctrl+Alt+3) or Memory 4 (Ctrl+Alt+4).

In the memory pane, firstly have the memory displayed in 8 byte chunks of data by right-clicking and choosing Display As, DWords.

Now right-click again and choose Go to address… (Ctrl+G) and enter a suitable expression based on the pointer value. In the case above the first pointer in the list had the value $27E2C20. This is an object reference – the address of an object’s instance data. So if we de-reference the pointer we’ll be at the start of the instance data. But the address of the class name string is a little way into the instance data, a distance given by the constant vmtClassName, so the required expression will be:

PInteger($27E2C20)^+vmtClassName

TList6

As mentioned, we’re now looking at the address of the class name, which is stored as a short string. To follow (de-reference) this address, right-click on it and choose Follow, Offset to Data or press Ctrl+D. This takes us to the information we seek: TFoo.

TList7

If you wish you can change the display back to bytes (right click, Display As, Bytes) and then the short string is more obvious – you can see the length prefix byte for the 4 character string is indeed 4.

TList8

3) Loop failure iteration detection

You’ve doubtless encountered this scenario while debugging. There’s an issue in a loop. The loop executes *loads* of times but you need to look into the problem so you need to know how many times round the loop the problem occurs. Stepping through the loop, or using a breakpoint and pressing F9 or clicking on the last line of the loop and pressing F4, counting in your head – all such options are mind-numbing and hugely prone to losing count or messing up in some regard, so how about letting the system work it out for you?

Place a breakpoint at the end of the loop with a massive pass count – say 1000000. Now run the app and make the problem occur. Now look in the breakpoints window and check the current pass count – it will tell you how many times the breakpoint has been passed without actually breaking.

Once you know how many iterations it has been through, modify the properties of the breakpoint and set the pass count to that value. Restart the program and the when the breakpoint triggers the error will occur on the next iteration of the loop. Nice!

4) Breakpoint logging

Breakpoints had their advanced properties added many, many versions ago. You can use advanced breakpoint properties for a variety of cunning debugging scenarios, but a simple usage is to enable cheap logging without writing additional code statements.

Wherever you want to log some information, add a breakpoint and bring up the breakpoint properties. Press the Advanced button, set the breakpoint to not actually break and then specify the log message. For bonus points you can also choose to have an expression evaluated and (optionally) logged at the same time.

AdvancedBreakpoints

This log information will be added to the Event Log window during your debug session in the same way that OutputDebugString messages are, but with the benefit of not having written any code. If you have the IDE set to save your project desktop then the breakpoints will be saved when you close the project and reappear next time you open it.

5) Leaving RTL assembler code

When stepping through code with Debug DCUs enabled you often inadvertently end up in RTL assembly code. In most instances Shift+F8 (Run, Run Until Return) will run the rest of the code through and take you back to where you were so you can try F7 again.

Shift+F8 doesn’t always work. For example if you use F7 on a call to a dynamic method you end up in the System unit’s _CallDynaInst routine. This doesn’t exit in the normal manner, but by jumping off to the located method address stored in the ESI register.

CallDynaInst

The best way forward here is to click on the JMP ESI line, press F4 to run up to that point and then press F7 – that will take you into the dynamic method that was called.

6) Find declaration while debugging

Seasoned Delphi developers know that you can locate the definition of a symbol by right-clicking it in the editor and choosing Find Declaration. Rather annoyingly there is no shortcut listed next to it, but it is commonly known that if you hold down Ctrl and wave your mouse around the editor, anything that the IDE thinks it can locate the definition/declaration of turns into a hyperlink – this Ctrl+click hyperlink feature is Code Browsing and links to the Code Browsing History keystrokes of Alt+← and Alt+→ that allow you to go back and forth though the links you’ve clicked.

The problem with Ctrl+click is that it doesn’t work in a debug session. Many times it would be handy to find the definition of a symbol in a debug session but Ctrl+click just doesn’t cut it. Fortunately, however, Alt+↑ *does* work in a debug session (yay!)… (or at least should do – it generally does for me). Why Alt+↑ isn’t listed in the editor context menu against Find Declaration baffles me. It’s a very little-known but useful shortcut.

7) Non-destructively locally disable a compiler option

Often-times a build configuration has range-checking enabled to ensure problems show up when they occur as tangible issues rather than vague Access Violations through memory overwrites way down the line during an execution. That said, it can still be important to selectively turn off range-checking for specific small sections of code that are valid, but will flag up range-check errors thanks to, say, the pointer types involved.

Clearly you can turn range-checking (or overflow-checking or whatever) off and on using local compiler directives, as in:

{$R-}
//code that requires range-checking off
{$R+}

but what about if you then build the code with a build configuration that has range-checking globally disabled? This use of compiler directives means the file in question will have range-checking *enabled* from that point onwards, despite the intent being to have it disabled throughout the project.

This is where this tip comes in. You can selectively change a compiler option using code like this, where this example disables range-checking if it was enabled, and then ensures it gets enabled again if appropriate:

{$IFOPT R+}
  {$DEFINE RANGE_CHECKING_WAS_ON}
  {$R-}
{$ENDIF}
//code that requires range-checking to be disabled
{$IFDEF RANGE_CHECKING_WAS_ON}
  {$R+}
{$ENDIF}

Here we define a conditional symbol if we toggle range-checking off, and we use that symbol’s existence to decide whether to enable range-checking later.

8) Building a Delphi project at the command-line

For quite a few versions now, Delphi has used the MSBuild format for its project files and project group files. This means you can use MSBuild to build your projects and project groups, using appropriate MSBuild command-lines to pass in options or specify build targets (the targets include clean, make and build). So if you have a project Foo.dproj and you want to clean up the previously produced binaries (compiled units and executable output) and then build it in Release and Debug configurations for Win32 and Win64 you could run these commands in a RAD Studio Command Prompt:

msbuild -t:clean -p:Config=Debug -p:Platform=Win32 Foo.dproj
msbuild -t:build -p:Config=Debug -p:Platform=Win32 Foo.dproj
msbuild -t:clean -p:Config=Release -p:Platform=Win32 Foo.dproj
msbuild -t:build -p:Config=Release -p:Platform=Win32 Foo.dproj
msbuild -t:clean -p:Config=Debug -p:Platform=Win64 Foo.dproj
msbuild -t:build -p:Config=Debug -p:Platform=Win64 Foo.dproj
msbuild -t:clean -p:Config=Release -p:Platform=Win64 Foo.dproj
msbuild -t:build -p:Config=Release -p:Platform=Win64 Foo.dproj

9) Formatting entire projects

Recent versions of Delphi have offered pretty printing, as it used to be called, or source formatting as the process is now described. It’s invoked by Edit, Format Source (or Ctrl+D) and can be fully customised in the Tools, Options dialog. Your custom settings can be saved into formatter profile files.

It’s less well known that the formatter is also available through a command-line tool. You can use this to run it across all files in a project directory tree.

Assuming you’ve saved your custom IDE formatter options in a formatter profile called Formatter_MyProfile.config then you start by running up a RAD Studio Command Prompt. Now issue some commands along these lines and it’s all done for you:

set CONFIG="Formatter_MyProfile.config"
set LOG=Format.log
del %LOG%
formatter -delphi -config %CONFIG% -r –d . > %LOG%

10) Case toggle

If you are not a touch typist and occasionally type a whole bunch of code in with CAPS LOCK on you should become familiar with the keystroke that toggles the case of a marked block: Ctrl+O, U.

11) Bonus tip: disable Error Insight to lower blood pressure

I’ve said this before in talks and in writing. I’m very happy to say it again. Please, *please*, don’t suffer the shame of Error Insight and its hopeless and hapless efforts in working out what might compile and what might not. Yes, it gets the simple ones right. But any vaguely interesting project that you work within is pretty much guaranteed to be besmirched with lots of red squiggles over perfectly good code and perfectly locatable units in the uses clause all within a fully compilable code base. The fact that Error Insight continues to be enabled by default, despite the growing problem of its inaccurate problem detection is really rather surprising. I urge you to disable Error Insight by going to Tools, Options… and then locating the Code Insight options within the Editor Options.

Yes, yes, there are reasons why it gets it wrong – it’s not using the exact same code as the compiler does and doesn’t know all the things that the compiler does. But this has been the case for years, and it still offends my eyes with its tenacity of squiggling over code that has just compiled cleanly.

Ease your annoyance at Error Insight by turning it off. Then pick any of the plethora of Error Insight bugs logged on QC and vote for it/them.

Seriously though, on very large projects, disabling anything that executes and isn’t useful/productive is a benefit. When working with clients’ large projects I disable Error Insight, Tooltip Help Insight and automatic Code Completion.

8 comments:

  1. Thanks for sharing your knowledge!

    ReplyDelete
  2. Good post, thanks!

    ReplyDelete
  3. Great tips, Brian! I didn't know Alt+Up works while debugging!

    ReplyDelete
  4. Thanks for sharing your knwoledge!
    Just a question: when doing like you described in the first point, what will return Application.ExeName? The original directory on the network or the local directory?
    Thanks again and regards,
    Klaus

    ReplyDelete
    Replies
    1. Klaus
      It doesn't affect the execution of the app. ExeName will still be the same - the location of the original binary. Really it affects what happens when more of the binary image needs to be paged in - it comes from the local paging file.
      Cheers
      - Brian

      Delete
    2. IMAGE_FILE_NET_RUN_FROM_SWAP has really helped us.
      Q: Is the local copy freed as soon as the app exits?
      I'm asking because on some systems, after downloading our latest release, clients restart our app and get the error 'file ... is either corrupt or not a windows binary'. I assume this is due to caching (because the local copy no longer matches the networked file). It happens even though we have confirmed all users had quit the program before getting the release.
      Any suggestions? We're at a loss.

      Delete
    3. Stéphane
      Glad you found it useful. Alas I haven't encountered this issue. I would expect the paging file to be updated immediately on running an app that differs with different size, version info etc. Does it occur if the clients in question restart the machine they run the app on after updating the executable? That's about the most sensible suggestion that immediately comes to mind right now, I'm afraid....

      Delete
  5. BTW, the Matt Pietrek article seems to have moved from http://www.microsoft.com/msj/archive/S413.aspx to https://www.microsoft.com/msj/archive/S413.aspx

    ReplyDelete