Monday, September 27, 2004

Don’t miss the Delphi launch in Oslo!

Borland Nordic have started sending out invitations to the launch event of the next Delphi version. Based on the sneak-peak URL, the final name for Diamondback might be Delphi 2005. The event in Oslo will be Tuesday October 12th at Fabrikken (The Factory), address Nedre gate 7.

I'll be there - will you?
Register here.

Tuesday, September 14, 2004

Object-to-object casts

In object-oriented code, probably the most common type of cast is between object types.

This cast is not really needed at all, as the compiler will implicitly convert from a descendent class up to a base class. This is conceptually done each time you call an inherited method, pass values into TObject parameters, add objects to a TObjectList and so on. Although the compiler will implicitly handle up-casts, it does allow the programmer to explicitly apply up-casts as well, but they will be optimised away into no-ops:

TParent = class
TChild = class(TParent)
Parent: TParent;
Child: TChild;
Child := TChild.Create;
Parent := Child; // Implicit up-cast
Parent := TParent(Child); // Explicit hard-cast
Parent := Child as TParent; // Explicit as-cast
if Child is TParent then // is-test

The code above demonstrates the syntax used for hard-casts, run-time checked as-casts and the is-check. The Win32 compiler assumes that you are not deliberately breaking the type-system, so it optimises the as-cast into a no-op and the is-test into an Assigned-test.

This means that if you play it dirty and use a hard down-cast to store a TObject in a TChild reference (effectively breaking the type system), the casts and is-check will still succeed;

O: TObject;
// ...
Child := TChild(TObject.Create); // Dirty trick - don't do this
if Child is TParent then // is-test
O := Child;
if not (O is TParent) then // is-test

The.NET runtime makes it virtually impossible to bypass the type-system (bar unsafe or unmanaged code). While as-casts and is-tests work identically in Win32 and .NET, hard-casts that fail at runtime will return nil in .NET and break the type-system in Win32. So on .NET the code above will actually store nil in Child (because a TObject is not a TChild). A down-cast is casting from a parent-class reference to a child-class reference. This is normally what we mean with object-casts.

Safe vs. unsafe casts
In Win32, hard casts are unsafe, because the compiler will not complain if you perform obviously "illegal" or "impossible" casts. Hard-casts is a way of telling the Win32 compiler;

"Relax, I know what I'm doing. Just close your eyes and reinterpret these bits as the type I'm telling you it is".

So there are no checks and no conversions going on (there are a couple of exceptions as we shall see in a later blog post). In .NET even hard-casts are safe, in the sense that the compiler and runtime will check that the cast is valid. For object-to-object hard-casts, the CLR will check that the source is compatible with the target type - if not, nil is returned instead. Conceptually, in .NET hard-casts like this:

  Target := TTargetClass(Source); 

work like this:

if Source is TTargetClass then
Target := Source
Target := nil;

As-casts are safe on both platforms. If the cast does not succeed, an exception will be raised, so:

  Target := Source as TTargetClass;

is conceptually equivalent with

  if Source is TTargetClass then 
Target := Source
raise EInvalidCast.Create;

Performance issues
In Win32, there is a certain performance overhead with doing an is- or as-check - the compiler and RTL traverses the inheritance chain linearly to check all the class' parents to search for a match. For most code the overhead is negligible. A common need is to covert a reference to a specific type, but to avoid exceptions in the failing cases, so often you'll see code like this:

if Instance is TMyClass then 
MyObject := Instance as TMyClass;

Experienced Delphi programmers will often balk at this construct, noting that the additional test performed by the as-cast is redundant. Again, for most code the overhead is insignificant, but never-the-less you'll often find it rewritten like this:

if Instance is TMyClass then 
MyObject := TMyClass (Instance);
// ... xxx

Here the safe is-cast is performed, followed by the unsafe hard-cast. But combined with the preceding is-cast, the hard-cast is actually safe! In Win32, only one check of the inheritance chain is performed. In .NET however, the hard-cast performs another is-check (returning nil if it fails, which should be never in the above code). So performance aficionados might be tempted to change the code into this:

  MyObject := TMyClass(Instance);
if Assigned(MyObject) then
// ...

That will work nicely without overhead in .NET, but it will fail (typically crash) in Win32 when the cast is invalid. The examples above gracefully handle the case where the cast fails at runtime. Other times you consider it a programming or configuration error for the cast to fail. This is when you'll typically use the as-cast - it will raise an EInvalidCast exception (alias to System. InvalidCastException in .NET) if it fails. This is fairly common to do in event-handlers, for instance. The generic Sender parameter is TObject, so you might need to type cast it to the actual component type (say TDrawGrid). This is particularly useful when you share a common event handler for multiple components. One way to do this is:

procedure TMyForm.GridsDrawCell(Sender: TObject; ...);
Grid: TDrawGrid;
Grid := Sender as TDrawGrid;
// ...

This performs the as-check for each draw-cell operation for all the grids in the form. But the only way for the cast to fail is if you have somehow assigned the event handler to a non-DrawGrid component. Why burden the release version of your application with a check that is bound to succeed every time? An alternative solution is to turn the check into an Assert, like this:

procedure TMyForm.GridsDrawCell(Sender: TObject;  ...); 
Grid: TDrawGrid;
Assert(Sender is TDrawGrid);
Grid := TDrawGrid (Sender);
// ...

Now in the release builds, the runtime check will be gone (except on .NET), while silly programmer mistakes will still be caught in the internal debug builds (with assertions enabled). Be careful you're not over-applying this optimisation, however. You do want to keep as-casts or is-tests in code where the runtime type of the object may vary according to external input (user interaction, importing data files, communicating over TCP/IP etc).

The protected-access trick
I've talked about this trick earlier. It basically revolves around performing an invalid hard down-cast to a locally defined class. This is done to get access to the protected parts of the object. For instance:

TControlAccess = class(TControl);

Click is a protected method of TControl, so normally we cannot call it on a control instance. To fool the type-system, we declare an empty local class that inherits from TControl. Because our code is in the same unit as this new type, we get access to all its protected members. Although MyControl is not really a TControlAccess instance, we cast it into one, to call the Click method. This does works in Win32, but it is really a hack, IMO.

This hack is so common, and many VCL components and Delphi applications rely on it, that Borland felt the need to somehow support this technique even in .NET. It does work in .NET as long as the accessed member resides in the same assembly as the calling code. The compiler marks all protected members as the CLR access level protected-or-assembly. This means that at the IL-level, all protected members are directly accessible to all code in the same assembly.

The Delphi compiler does not allow access to these members unless you perform the casting-trick, however. When it encounters the cast, it simply removes it - there is no trace of it in the IL code. The CLR will allow the code as long as it is intra-assembly. The Delphi compiler will also prevent the call across assemblies, of course. For instance if you try the call above when referencing (and not linking in) the Delphi.Vcl.dll assembly the compiler will emit the following compile time error:

[Error] Cross-assembly protected reference to [Borland.Vcl]TControl.Click in Upcasts.Upcasts

Friday, September 10, 2004

Blog description updated

FWIW, I've updated the blog description in the header of my blog, to better reflect what I'm actually writing about ;). The new description reads: "Confessions of a Delphi hacker - hacks, programming tips and Delphi stuff." Oh, btw, I've just returned the corrected proofs of my upcoming Delphi Magazine article to the editor - it looks good! Make sure you have that subscription updated... ;).

Win32 vs. .NET casting issues in Delphi

In a small series of blog articles I'm planning to talk about some of the different types of casts and conversions that Delphi supports, and mention any differences in casting logic and implementation between Win32 and .NET.

In general, using casts in your code indicates an impendence mismatch between your design and the problem domain. Sometimes they cannot be easily avoided, other times it might be better to refactor your design and code to remove or reduce the number of casts. Until true generic classes become available (as they probably will - at least for .NET 2.0), it is close to impossible to have no casts anywhere in the code. They can (and should) be encapsulated to a few core locations, however - typically in type-specific collection classes.

For the purposes of this discussion, I've identified 9 separate categories of casts, in terms of the types of the entities we are casting to and from.

Types of casts:

1. Object-to-object upcast

2. Object- to-object downcast

3. Object-to-interface cast

4. Interface-to-interface cast

5. Interface-to-object cast

6. Valuetype-to-valuetype cast

7. Valuetype-to-object cast

8. Object-to-valuetype cast

9. Casts to and from compiler-managed types

The upcoming blog posts will each discuss one or two of these casting categories.

Copyright © 2004-2007 by Hallvard Vassbotn