Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Wednesday, May 30, 2007

DN4DP#10: With a little help from your friends

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book. Last time we looked at how you can import identifiers that happens to conflict with Pascal keywords. This post includes the section on class helpers.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"With a little help from your friends

When Borland started to port the Delphi compiler, RTL and VCL to the .NET platform, they soon faced a problem. While the object model and naming conventions of the FCL classes and methods were strikingly similar with native Delphi, some common methods (such as Free and ClassName) were missing from the .NET classes. Basically, Borland had three options;

Not content with becoming a shadow, and not willing to break everybody’s existing, clean object oriented code, they decided to invent something. And they invented class helpers. A class helper is a compiler trick used to inject new members into an existing class and all its descendants.

With this solution TObject is defined as a type-alias for System.Object and it has an accompanying class helper, TObjectHelper, which injects[1] the missing methods into TObject and all its descendants. The effect is that the Free and ClassName methods are now available for all Delphi and .NET classes. Similar tricks have been done to implement VCL for .NET classes such as TPersistent, TComponent and Variant.

To define a class helper you use the syntax

type
TMyClassHelper = class helper(TBaseClassHelper) for TExternalClass
procedure NewInjectedMethod;
end;

where TBaseClassHelper is the name of an optional class helper that you inherit from. This is useful when you want to help an already helped class.

A class helper can contain instance methods, class methods and class fields, but you cannot add instance fields. This limitation can be circumvented by using a class var HashTable keyed by the Self instance implicitly received by the class helper methods. In some cases you can reuse the helped object's general storage mechanism. For instance, the TComponentHelper uses the Site property of Component to store the per-component properties Tag, Components and Owner.

Here is an excerpt from the ClassHelpers project that shows all the different kind of members that can be injected.

type 
TMyClassHelper = class helper(TObjectHelper) for TExternalClass
private
class constructor Create; overload;
class var
FNewClassVar: string;
public
constructor NewConstructor(const AName: string);
procedure NewInjectedMethod;
procedure NewVirtualMethod; virtual;
procedure NewDynamicMethod; dynamic;
class procedure NewClassMethod;
class procedure NewVirtualClassMethod; virtual;
class procedure NewClassStaticMethod; static;
property NewProperty: integer read GetNewProperty write SetNewProperty;
class property NewClassProperty: string read FNewClassVar
write SetNewClassProperty;
end;


Caution: In general, class helpers can be useful to close the gap between different platforms or component sets, but should normally not be used as a design element. If you have full control of a class, you should not inject methods into it by using a class helper; you should change the class itself (or derive from or aggregate it).





[1] For all the gory details, read Marcel van Brakel’s in-depth article Delphi for .NET Class Helpers Inside Out in The Delphi Magazine issue 108 (August 2004).




"


Update: C# has since (in the 3.0 beta version) adopted a similar technique called Extension Methods.

Wednesday, April 11, 2007

Subversion in Delphi's Tools menu

Joe White writes about how to add Tools menu items with Subversion commands
here.

For some reason, the Submit button didn't work on his Comments page (running IE6), so I'll just write my comment here:

Nice, thanks!

I've been meaning to do the same thing for a while, but kept postponing it. Shame about the space-before-macro requirement in the Tools menu - you should probably log it in QC.

I didn't have Ruby installed so I wrote a simple online .Bat file instead:

"c:/program files/tortoisesvn/bin/tortoiseproc.exe" /command:%1 /path:%2 /notempfile

Then I create the Tools items with:
Program: c:\windows\system32\cmd.exe
Parameters: /C C:\SvnPas\Utils\Batch\SvnCmd.Bat diff $EDNAME $SAVEALL

Works fine!

Saturday, March 31, 2007

DN4DP#5: Redefining the operators

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book. Last time we talked about the new record syntax. This time we will look at the exciting new operator overloading capabilities.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Redefining the operators

Long-time Delphi programmers have looked at the operator overloading features of other languages (such as C++) with both fear and envy. The ability for user code to redefine the meaning of operators applied to an instance of a specific class is powerful and elegant if used properly, but it can be highly confusing and erroneous if used improperly.

Delphi now supports operator overloading. This is an advanced concept where a class or (more commonly, and the only kind supported in Win32) a record can have a special method called when a specific operator (such as +, -, /, *, div, mod, etc) is applied to an instance of the class or record. To define operator overloads you must define class operator functions with specific names for each operator. See the Delphi language documentation for the full list of class operator names.

In addition to normal operators, implicit and explicit casts (or conversions) can be implemented. The OperatorOverloading project demonstrates the potentially confusing aspects of operator overloading. It defines a TStrangeInt record where the operator semantics have been reversed:

type
TStrangeInt = record
public
Value: Integer;
class operator Add(const Left, Right: TStrangeInt):
TStrangeInt; inline;
class operator Subtract(const Left, Right: TStrangeInt):
TStrangeInt; inline;
class operator Multiply(const Left, Right: TStrangeInt):
TStrangeInt; inline;
class operator Divide(const Left, Right: TStrangeInt):
TStrangeInt; inline;
class operator Implicit(const AValue: Integer):
TStrangeInt; inline;
class operator Implicit(const AValue: TStrangeInt):
Integer; inline;
class operator Implicit(const AValue: TStrangeInt):
string; inline;
class operator Explicit(const AValue: Double):
TStrangeInt; inline;
end;

The implementation of a class operator method should create and return a new record or class instance and not modify any of the parameters. Often operator methods are very short and simple and thus perfect inline candidates[1] (inline routines are covered later in this Chapter).

class operator TStrangeInt.Add(const Left, Right: 
TStrangeInt): TStrangeInt;
begin
Result.Value := Left.Value - Right.Value;
end;

Invoking the overloaded operators is just a matter of declaring an instance of the record type and using the standard Delphi operators on it.

var
Strange: TStrangeInt;
StrangeResult: TStrangeInt;
begin
Strange := 42;
StrangeResult := Strange + Strange * 3;
end;

Because of the reversed TStrangeInt implementation, StrangeResult will be 28 instead of the expected 168. See Chapter 7 for more details on operator overloading.

Tip For a more complete example of operator overloading, see the Borland.Vcl.Complex .NET unit (or the Vassbotn.Vcl.Complex unit in the Demos\DelphiWin32\VCLWin32\ComplexNumbers\ folder for the corresponding Win32 unit).




[1] Marking overloaded operators with inline is purely optional, of course. Currently (Delphi 2006) it seems like the compiler manages to inline all simple operators except the Implicit and Explicit conversion operators."

Wednesday, March 21, 2007

Delphi 2007 ESD arrived - one gotcha

We ordered Delphi 2007 ESD from the local Norwegian CodeGear distributor alfacode.no a few days ago and earlier today the email with download link and licence keys arrived (the DVD will arrive in a couple of weeks, they say).

I downloaded the installer stub that then downloads and installs the required binaries - it all worked very smoothly. After the install was done Delphi 2007 ran fine. But I was a little worried for a while - because when I tried to start BDS 2006, I got an error dialog with the following unnerving message:

---------------------------
bds.exe - Entry Point Not Found
---------------------------
The procedure entry point @Uxtheme@BufferedPaintSetAlpha$qqruip11Types@TRectuc could not be located in the dynamic link library rtl100.bpl.
---------------------------
OK
---------------------------

After scratching my head for a while, I searched by harddisk for all rtl*.bpl files. Both Delphi 2007 and BDS 2006 use a binary called rtl100.bpl - and normally it resides in the Windows\System32 directory. The Delphi 2007 installer updates this (and most other VCL bpls) when it installs. The problem is that I had a second copy of this file in the BDS 2006 bin directory (called C:\Delphi2006\bin on my system). This was entirely my own fault because I had copied it there and patched it using Peter Vones tool to hack away startup time as I've blogged about before.

The reason this happens is that Delphi 2007 installer upgrades all the *100.bpls in the System32 directory, but BDS 2006 tries to load the old rtl100.bpl from the bds.exe startup directory that is not compatible with the new vcl100.bpls now residing in System32. The solution was simply to delete or rename the rtl100.bpl in the Delphi2006\bin directory.

Lesson learned: it is dangerous to live on the edge - and you better know (and remember) what you are doing.... ;).

Thursday, March 01, 2007

Review: Delphi 2007 for Win32 (Beta) - part one

Nick Hodges of CodeGear contacted me and gave me permission to talk about the upcoming Delphi 2007 for Win32 product - codenamed Spacely. Note that this review is based on a pre-release Beta build 2063 from mid February. Anything you see here is subject to change in the release version.


The installer


Here are a few screenshots of how the new installer looks like - click them to see them in full size. The installer has been created with InstallAware (which is also bundled with Delphi 2007) and it should allow CodeGear to release patches and potentially new versions in an easier and more integrated way.


 


 


Delphi 2007 is a non-breaking version 


What does it mean that Delphi 2007 is a non-breaking version? Well, according to CodeGear roadmaps there will be a new BDS (or CDS?) version later this year code named Highlander. To reduce the pain (for developers, 3rd party component providers etc) of having two new full Delphi versions in less than a year, CodeGear decided to release Delphi 2007 as a non-breaking version. The result is that you should be able to use code and components designed for and compiled in BDS 2006 or Turbo Delphi 2006 without even recompiling any .dcu (delphi compiled unit) files. So even .dcu only shareware components should continue to just work.


Delphi 2007 is special in that it is (mostly) a non-breaking version in relation to BDS 2006 and Turbo Delphi 2006. This means that it is binary compatible at the .dcu level and no breaking changes has been made to the interface of any units. This means that there are no changes to existing classes or routines, for instance. Note that the Delphi unit interface checks are granular enough to allow new classes and identifiers to be added, as long as no existing identifiers are modified in any way.


A side-effect is that while it is relatively easy to fix RTL and VCL bugs (that only need changes in the implementation section), and to introduce new classes and components, it is very hard to introduce new functionality, methods and properties on existing classes. Hard, but not impossible, of course. CodeGear has not been shy to apply the odd hack or two to add native support for extended Vista Glass functionality in the TForm class, for instance. Allen Bauer has already spilled the beans on this and how it was achieved. We will take a closer look at the RTL and VCL changes in Delphi 2007 (relative to BDS 2006) further down in the review.


What's new in the compiler?


It also means that the compiler has not been updated with new features that would break binary compatibility, so there are no generics support yet, for instance. However, a little investigation shows that there have been improvements to the compiler. For instance, there are a slew of new compiler hints, warnings and errors. It also looks like the compiler has spiffed up its XML documentation generation capabilities. Some of the new error messages are probably due to bug fixes. Code that would earlier generate internal errors now generate clean compile-time errors with explanations of what is wrong with the code. Here is an example list of the new hints, warnings and errors that I've identified:


New Hints:



  • H2445 Inline function '%s' has not been expanded because its unit '%s' is specified in USES statement of IMPLEMENTATION section and current function is inline function or being inline function
  • H2451 Narrowing given WideChar constant (#$%04X) to AnsiChar lost information
  • H2456 Inline function '%s' has not been expanded because contained unit '%s' uses compiling unit '%s'

New Warnings:



  • W1055 PUBLISHED caused RTTI ($M+) to be added to type '%s'
  • W1201 XML comment on '%s' has badly formed XML -- 'Whitespace is not allowed at this location.'
  • W1202 XML comment on '%s' has badly formed XML -- 'Reference to undefined entity '%s'.'
  • W1203 XML comment on '%s' has badly formed XML -- 'A name was started with an invalid character.'
  • W1204 XML comment on '%s' has badly formed XML -- 'A name contained an invalid character.'
  • W1205 XML comment on '%s' has badly formed XML -- 'The character '%c' was expected.'
  • W1206 XML comment on '%s' has cref attribute '%s' that could not be resolved
  • W1207 XML comment on '%s' has a param tag for '%s', but there is no parameter by that name
  • W1208 Parameter '%s' has no matching param tag in the XML comment for '%s' (but other parameters do)

New Errors:



  • E2447 Duplicate symbol '%s' defined in namespace '%s' by '%s' and '%s'
  • E2448 An attribute argument must be a constant expression, typeof expression or array constructor
  • E2449 Inlined nested routine '%s' cannot access outer scope variable '%s'
  • E2450 There is no overloaded version of array property '%s' that can be used with these arguments

Linker errors:



  • F2446 Unit '%s' is compiled with unit '%s' in '%s' but different version '%s' found

What's new in the IDE


In general the IDE feels very fast and nice to work with - it has noticeably less flickering when switching between debug and design layouts, for instance.


Here are a couple of screen shots showing some of the news in the IDE.



I've marked the changes with numbers:



  1. I've added a new toolbar button to toggle a debugger setting called Notify on Language Exceptions
  2. The new TForm property GlassFrame to control the Vista Glass effect on the form's area. This is achieved cunningly with class helpers, property injectors and some extra hacks.
  3. The Tool Palette has improved partial search matching. Any component that contains the string you're typing will be included in the list. So starting to type "but" will show all button components. This is great!

Here is a another screen shot.




  1. The new File Browser view is very handy. It works like a mini Explorer, reducing the need to use an external Explorer or File | Open all the time. And of course, this view can be docked anywhere - or you can keep it floating, or unpin it so that it scrolls away when you're not using it.
  2. The project's options for Compiler, Compiler Warnings, Linker and Directories/Conditionals can now be set separately for a Release and Debug configurations. In addition you can create more named configurations. Finally!
  3. Here you can again see the new compiler warnings that can be toggled on or off
  4. I added a Windows Vista specific component called TFileOpenDialog - the compiler issues a platform warning for this. Nice.

This review will be updated with more information and details in the coming few days. Stay tuned ;)

Thursday, November 23, 2006

Borland and CodeGear

As most of you will already know, Borland has now moved its developer tools group into a separate company called CodeGear. CodeGear, which will still be wholly owned by Borland, will continue to develop, sell and support developer tools like Delphi, C++Builder, JBuilder and Interbase. There are also indications that they are moving into the webspace with support for languages such as PHP, Pyhon, Ruby and Ajax technology.

While some people have been waiting anxiously for a complete separation from Borland in a operation to sell the IDE group to a separate entity, I think that the current situation is better for the stability and long-term viability of CodeGear in general and the future of Delphi in particular.

The CodeGear management will be in a position to grow the IDE business and pour the revenue stream back into the development of new versions and products. I think this is good news, but as always we should keep our heads calm and keep our options open ;).

Good thing we called our user group Oslo Delphi Club and not NoBUG (Norwegian Borland User Group) or something similar ;)).

Friday, September 29, 2006

Hack#13: Access globals faster ($ImportedData)

There is an aura of magic around Delphi packages. Packages allow you to share Delphi code at a much higher level than is possible with plain old DLLs. Writing a DLL-based library API involves writing flat non-OOP global routines and avoiding any data types more advanced than integers, doubles, static arrays, PChar and records. You cannot share or exchange classes, objects, global variables or let alone strings (unless the client is a Delphi app and both the DLL and client use ShareMem).

Delphi Package Magic
Enter the magic of Delphi packages. Now you no longer have to worry about what you can share between module boundaries, because you can share everything! Packages is just a way of dividing up the logical code into physical deployment modules. Everything just works - somehow. The somehow is the (mainly undocumented) magic.

We're going to look at a small part of that magic - how global variables can be accessed across the application/package boundary. Lets start with the simplest possible case first .

Lets say we have a Unit1 that defines a GlobalVar: integer and some code in the same unit that sets it. Here is the generated assembly and machine code.

Unit1.GlobalVar := 13;
004081B1 C705989240000D00 mov [GlobalVar],$0000000d

That is a single assembly instruction with no indirections or pointer access going on. Notice that both the address of the global variable and the immediate value 13 (encoded as a 16-bit shortint, $000D) is encoded directly inside the machine code instruction. When I evaluate the address of the global variable in the Ctrl+F7 evaluator, @GlobalVar, I get $409298. This is the big-endian value I have highlighted in yellow above.


Then let's look at the package magic situation. If we have a PackageA containing a Unit1 that defines a GlobalVar: integer and a DemoApplication that links to PackageA and with code that access the GlobalVar, the compiler will compile the code into something like this:

Unit1.GlobalVar := 42;
00403389 A1A4404000 mov eax,[$004040a4]
0040338E C7002A000000 mov [eax],$0000002a

Notice the indirection there? Global variables are accessed through a pointer that is fixed up to point to the right address inside the package when the package is loaded. That is cool and a good thing. The global variable itself resides inside the package/DLL module, while the application has a magic, "invisible" global pointer variable that points to the actual package variable. This pointer is fixed up during the magic static loading of packages (it's value is probably set to the result of a GetProcAddress API call).


Global variables in standalone apps
Now lets step back a little and look at another common situation. We have looked at the intra-unit situation and the inter-module situation - the two most extreme conditions. But what happens when you access a global variable between units inside the same application? Surely, then the indirection should not be needed? Lets test it.


We write a simple console application consisting of two units, Unit1 and Unit2, and the main program.

unit Unit1;

interface

var
GlobalVar: integer = 42;

procedure SetGlobalLocally;

implementation

procedure SetGlobalLocally;
begin
asm int 3 end; // will break here, look in CPU view
Unit1.GlobalVar := 13;
end;

end.
unit Unit2;

interface

procedure SetGlobalIndirect;

implementation

uses Unit1;

procedure SetGlobalIndirect;
begin
asm int 3 end; // will break here, look in CPU view
Unit1.GlobalVar := 42;
end;

end.
program TestImportedData;
{$APPTYPE CONSOLE}
uses
Unit1 in 'Unit1.pas',
Unit2 in 'Unit2.pas';
begin
SetGlobalLocally;
SetGlobalInDirect;
end.

Notice the cute little hand-coded breakpoints? Debugger breakpoints are actually implemented by overwriting a byte of your code with the software breakpoint instruction (int 3) which is encoded in a machine code byte $CC. When you run this program with debugging enabled from the IDE, it will break on the two int 3 instructions. Neat, huh? ;)


The first breakpoint is inside Unit1 just above the code that modifies the global variable. This is the case we showed first above. If you run it and follow the instructions to look at the code in the CPU view, you will see that there is no indirection.

Unit1.GlobalVar := 13;
004081B1 C705989240000D00 mov [GlobalVar],$0000000d

The second breakpoint is in Unit2 where we are modifying the global variable from an "external" unit (from a different unit than were the global was declared). Notice that we are still in a single EXE file here, so there should be no need for indirection, as it was in the package case. Run the code again and when it hits the second breakpoint, look at the machine code in the CPU again.

Unit1.GlobalVar := 42;
00403389 A1A4404000 mov eax,[$004040a4]
0040338E C7002A000000 mov [eax],$0000002a

Looks like we still have indirection here...! What is going on? The reason for this (small) inefficiency is that a unit can be moved into or out of a package without being recompiled. So the compiler has to be conservative and generate the unit's code to support the "worst" case that it will be exported for use from a package. The important thing to learn from this is:



By default, all cross-unit global variable access is performed indirectly via a pointer


The implication is that if you have performance critical code that access a global variable, you should cache it in a local variable inside the loop. You can only do this if you do not need to "see" updates to the global variable while the loop is running (think multithreaded code).


For standalone applications that do not use packages at all, it is a bit irritating to know that the compiler generates sub-optimal code for global variable access. While both the performance and size overhead of the indirection should be negligible in most situations, it would still be nice to force the compiler to get rid of this indirection.


$ImportedData Off
And as it happens there is a compiler directive that does exactly this. Enter the $ImportedData (aka $G) directive. This is what the Delphi help file has to say:



"Type Switch
Syntax {$G+} or {$G-}
{$IMPORTEDDATA ON} or {$IMPORTEDDATA OFF}
Default {$G+} {$IMPORTEDDATA ON}
Scope Local
Remarks


The {$G-} directive disables creation of imported data references. Using {$G-} increases memory-access efficiency, but prevents a packaged unit where it occurs from referencing variables in other packages."


By default the setting is {$ImportedData On} - this enables the pointer indirection in code that access unit-external global variables. By using the {$ImportedData On} directive in a unit, you turn off this indirection, resulting in tighter faster code for global variable access. Notice that you have to use it in every unit referencing global variables, not just in the unit declaring it.


Lets add a third unit to our test bench program.

unit Unit3;

interface

procedure SetGlobalDirect;

implementation

uses Unit1;

{$IMPORTEDDATA OFF}

procedure SetGlobalDirect;
begin
asm int 3 end; // will break here, look in CPU view
Unit1.GlobalVar := 42;
end;

end.

The code here is identical to the code in Unit2, but now we have included the $ImportedData Off directive to tell the compiler generate optimized code for globals. Run the program again and when it stops at the breakpoint in Unit3, look in the CPU view.

Unit1.GlobalVar := 42; 
004033D1 C705A04040002A00 mov [GlobalVar],$0000002a

Hurrah! We got rid of the indirection.



The {$ImportedData Off} compiler directive forces the compiler to generate efficient global variable access


This works fine in Delphi 6 and 7. As it happens it no longer works in Delphi 2006 (and probably not 2005). Looks like a bug has sneaked into the compiler here. The directive is effectively ignored, still producing the indirection as we saw for Unit2 above. Hopefully Borland/DevCo will be able to fix this little issue in a future version of the compiler[1].


IDE Compiler Options
Knowing that $ImportedData Off generates better code for global variable access in a standalone application, we would of course like to set it for all the units in the application as a whole. Unfortunately that is not so easy to do. You see the Project | Options | Compiler Options dialog page does not give access to this option... :(.


Because of this issue you have to use one of the following solutions:



  • Manually insert {$IMPORTEDDATA OFF} at the top of all units in the project
  • Manually insert an {$ MyAppSettings.Inc} file in all units. The .inc file would contain settings such as {$IMPORTEDDATA OFF}
  • Compile using Dcc32.exe setting the option from the command-line

In other words it is a bit awkward to enable this for a given application.



It is hard to set {$ImportedData Off} from the IDE.


I hope that they will be able to make this option available from the compiler options in the IDE in a future version. In fact, I went ahead and reported this as a enhancement request in QC - (please vote for it!):



"Can we please have the $G switch (AKA $IMPORTEDDATA) promoted to appear in the Project Options | Compiler dialog? It makes it easier to switch it off globally for those of us who never use run-time packages."


While it can be argued that introducing this IDE option runs the danger of users producing unusable packages, I don't think this is reason to deny the large number of people writing standalone applications easy access to this option.

The solution could be to only enable this option for projects that are compiled with run-time packages turned off (on the Project Options, Packages page).

Still, if a unit is compiled with IMPORTEDDATA OFF and then moved to a package, it has to be recompiled (i.e. the package project must be rebuilt).


To get around this problem you could:


a) Have the compiler detect this case and recompile all units with the wrong IMPROTEDDATA setting

b) Document the issue and require the user to rebuild himself

c) Combined with b) make the option less obvious and make the IDE compiler options future proof by adding a free-text field for additional compiler options. The user must manually type IMPORTEDDATA OFF in this field, supposedly this makes him qualified to know what he is doing.


The following Dcc32.exe compiler options are currently unavailable from the IDE Compiler Options:


G+ Use imported data references
M- Runtime type info
W- Generate stack frames
Z1 Minimum size of enum types"


If you think this is worthwhile to implement, please vote on QC#34650 here!


Score: Delphi 7 vs. Delphi 2006: 1-1 ;)


[1] I found that this issue has indeed been discovered independently and reported by Frederic Vanmol in Quality Central:



Report No: 33464 Status: Open
{$IMPORTEDDATA OFF} does not seem to work
http://qc.borland.com/wc/qcmain.aspx?d=33464


PS. Just as I had finished this piece, I started experimenting some more...;) In Delphi 7 it is actually possible (but still awkward) to compile an application with $ImportedData Off for all units without messing with each unit or the command line compiler.


You only need to



  • close the project
  • manually edit the project's .dof file
  • change "-G+" to "-G-"
  • save and close the .dof file
  • reopen the project
  • rebuild the application

If you compile from the command line you can update the .cfg file in a similar manner.


It is probably possible to do the same in Delphi 2006 by manually editing the XML in the .bdsproj file, changing



<Compiler Name="G">1</Compiler>


to



<Compiler Name="G">0</Compiler>


But it is a little hard to check, because of the Delphi 2006 bug of ignoring the directive. In both cases, the manually set compiler options are preserved even when changing compiler options from the IDE.

Tuesday, September 12, 2006

DN4DP#2: Protecting your privates

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book. Last time we learned about the new kinds of class members that have become available; class fields, class static methods, class properties and class constructors. This time we will look at the new class member visibility specifiers that are available, abstract classes and final methods.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Protecting your privates

Native Delphi already had four class member visibility levels; private, protected, public and published[1]. One quirk with these is that private and protected members are fully visible to all the code in the unit they are declared in[2], not just the class they are part of (almost like an implicit version of the C++ friend concept). To match .NET's concept of truly private and protected, two new access levels named strict private and strict protected, were introduced. The AppendixDfn\PrivateParts project demonstrates this

type
TFoo = class
strict private
FCantTouchMe: integer;
FAnyOneAndDelphiRTTI: integer;
private
FClassAndUnit: integer;
strict protected
FClassAndDescendants: integer;
protected
FClassDescendantsAndUnit: integer;
public
FAnyOne: integer;
constructor Create(Report: boolean = True);
published
property AnyOneAndDelphiRTTI: integer read FAnyOneAndDelphiRTTI
write FAnyOneAndDelphiRTTI;
end;

Delphi classes can now be explicitly sealed and abstract. The syntax here has the mildly surprising order class sealed and class abstract. The main reason for this is to avoid reserving more language keywords than absolutely necessary – sealed and abstract are directives that only have special meaning after the class reserved word. This means that existing code that already use these identifiers will not break. A sealed class cannot be inherited from and an abstract class cannot be instantiated (even if it does not contain any abstract methods).

Finally, a virtual method that you override can now be marked final, preventing derived classes from overriding that method.

  TAbstractClass = class abstract
public
procedure Bar; virtual;
end;

TSealedClass = class sealed(TAbstractClass)
public
procedure Bar; override;
end;

TFinalMethodClass = class(TAbstractClass)
public
procedure Bar; override; final;
end;




[1] There is also the obsolete automated section from Delphi 2 – but that is not supported in .NET.

[2] One native Delphi trick is to declare a local descendant of a class in the current unit. Then you hard-cast an object instance into this local class. Now you have access to all the protected members of the object. This hack is so common that the .NET compiler has special logic to handle it too. It does work as long as the original class code is in the same assembly as the hacking code. This is because Delphi’s protected access maps to the .NET family or assembly access level. Similarly, Delphi’s private access maps to the .NET assembly access level, but the Delphi compiler itself enforces the cross-unit privateness."

[Note: This text differs slightly from the final printed version]

Thursday, August 31, 2006

DN4DP#1: Getting classy

As most of you will know by now, I was the tech editor of Jon Shemitz’ great .NET 2.0 for Delphi Programmers book and I wrote chapter 10 on The Delphi Language – covering what’s new in the language since Delphi 7. Jon has made one chapter available for download.

To give you some extra teasers from the book, I will in the coming months post a few excerpts of The Delphi Language chapter. Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Getting classy

The object model has been extended with static members; class static methods, class constructors, class var fields and class properties. For example, the AppendixDfn\ClassStatic project shows this

type
TFoo = class
strict private
class constructor Create;
private
class var FYank: integer;
class procedure SetYank(const Value: integer); static;
protected
class procedure OldVirtualClassProcedure; virtual;
public
class procedure OldClassProcedure;
class function ClassStaticMethod: integer; static;
class property Yank: integer read FYank write SetYank;
end;


Note that C#-style static methods must be declared as class procedure or class function with a static directive. The reason for this is historic; Delphi also supports class procedures that receive an extra implicit TClass parameter: a reference to the actual class type the call is made on. This can be used to invoke virtual class methods polymorphically, something that is not supported in C# or with the new class static methods in Delphi.

A class var declaration introduces a block of global-lifetime, class-scoped fields. Traditionally, Delphi programmers have used global variables in the implementation section of the unit for this purpose, but declaring class var fields directly in the class is undeniably much cleaner and clearer. Note that to be consistent, normal instance fields can now also optionally be declared in a var block.


The availability of class fields also opened the path for class properties[1]. These are declared like normal instance properties, but use a class property prefix. The read and write accessors can be class fields or class static methods (but not the older class methods).


Finally, a class’ single class constructor is guaranteed to run exactly once before any members of the class are referenced. It should be declared strict private[2] and cannot be referenced directly from user code[3] – it is always invoked by the CLR when it deems it necessary. Often, code in the initialization section of the unit would benefit from being moved to a class constructor – then you would not incur the overhead unless you actually use the class.


Tip Note that class constructors are not supported in Win32. To emulate a class constructor in Win32, put the code in a strict private class static method and call the method from the initialization section of the unit.





[1] In Delphi 7 and earlier, you could actually declare instance properties that referenced class methods as the read and write accessors, but this was a compiler quirk and didn’t actually work correctly at run-time (using the implicit Self: TClass parameter such as calling a virtual class method would crash, for instance). Also while Delphi 8 allowed you to declare class properties, there was no intuitive way of accessing them (you had to access them via an instance reference, not a class reference). This issue has been fixed in Delphi 2006.

[2] While Delphi 2006 does allow declaring non-private class constructors, you can’t call them and the generated IL declares them private anyway, so it is cleaner to declare them as such in the Delphi code as well.

[3] Hackers and compilers can ensure it has been called by using the RuntimeHelpers.RunClassConstructor method from the System.Runtime.CompilerServices namespace."

[Note: This text differs slightly from the final printed version]

Tuesday, August 29, 2006

Nordic Delphi meetup groups

Last week I had a very nice meeting and dinner with DevCo’s Nordic representatives Fredrik Haglund (Developer Relations and Evangelist) and Dan Nygren (Account Manager). We had some interesting discussions about everything from nuclear power plants to Delphi and DevCo. These guys are very dedicated and on a mission to make DevCo and Delphi succeed even better than under the Borland umbrella.

One of their goals is to encourage the formation of more technical (and less marketing-speak driven) meetings with and between local Delphi developers in cities throughout the Nordic area. Fredrik has already started a Delphi meetup.com group for Stockholm – they have their next meeting September 14th. In Gothenburg, Magnus Flysjö is running another Delphi group that will have their next meeting September 1st.

As far as I know there are no active Delphi meeting groups in Oslo (or Norway?) for the time being. There has been the (give or take) annual official Delphi launch meetings, but no user-driven events. Once upon a long time ago, there was a smartly named group – NoBUG (for Norwegian Borland User Group) – with their own Usenet group (no.org.nobug.diverse). It was pioneered by our own CTO, Morten Lindeman. To quote from the bottom of this page:

“NoBUG
The Norwegian Borland User Group recently announced their formation on several newsgroups. Their aim is to promote and support the Norwegian community of Borland product users. In addition to Borland C++, the group covers Pascal, Delphi, OWL etc. For more information, send an email request to: nobug__at__falcon.no
Alas, it ceased to exist several years ago. If there is enough interest, I’ll be glad to contribute to a new Delphi meetup group in Oslo. You can state your interest in a comment here, or even better by indicating your interest at meetup.com. Now with the upcoming free and inexpensive Turbo Delphi Explorer and Pro versions, it should be easier to get hobbyists and students interested, too.

A quick search reveals that there is some activity to try and start a group in Bergen (by Tom Reiertsen). He has registered the domain ndug.no (Norwegian Delphi User Group). Great initiative, Tom!

Tuesday, August 15, 2006

Extended Interface RTTI

To support the most basic mechanisms of its SOAP architecture, Delphi has supported extended interface RTTI since version 7. As we saw in the previous article, all interfaces support basic RTTI information such as the name of the interface, its GUID, unit, parent interface and number of methods.

To enable extended RTTI on an interface, compile it with {$M+}, {$TYPINFO ON} or {$METHODINFO ON} defined. Alternatively, you can have your interface inherit from IInvokable (defined in System with $M+ enabled). This will extend the generated RTTI for the interface with information about each method’s signature.

Both the client and server side support code for SOAP in Delphi uses the extended interface RTTI structures. Some of the most basic routines can be found in the IntfInfo unit (source is shipped with D2005 and D2006, but not in D7, it seems), for instance take a look at the FillMethodArray and GetIntfMetaData routines.

There is also code to generate WSDL (Web Service Description Language) from the list of registered interfaces in a web service (see WSDLPub.pas) and to dynamically generate an interface method table (IVT) with pointers to dynamically generated thunks that will eventually call TRIO.Generic. This method is responsible for packing up the client-side method call including parameters into an XML formatted SOAP message, execute it by sending it to the server and waiting for the return information, decoding the XML formatted SOAP reply message and updating out and var parameters (in OPToSOAPDomConv’s TOPToSoapDomConvert.ProcessSuccess method) and the result value. Pretty advanced stuff! Note that TRIO does not support the register calling convention for remoted interface calls – the recommendation is to use stdcall.

That was the background information about how the extended interface information is used and where you can find the implementation code that utilizes it. While there are low-level access routines in the IntfInfo unit (exported when DEVELOPERS is defined), we want to get our hands dirty and implement this ourselves from first principles.

As usual the RTTI structures for interface methods contain numerous packed shortstrings – meaning it is impossible to write correct Pascal declarations for them. By digging around in Borland’s extended interface RTTI consuming SOAP code, stepping through the code in the debugger and dumping out raw RTTI memory contents as an array of characters and deducing the (dynamic) field lengths I was able to reverse engineer and write some pseudo Pascal structures to map into the RTTI information. For example here is an ASCII dump from a single interface method’s RTTI that I manually tagged with probable field declarations:

{ MethodCount:1; HasMethodRTTI:1; 
Test:(
Name: #3, 'F', 'o', 'o',
Kind: #0,
CallConv: #0,
ParamCount: #3,
Flags: #8,
ParamName: #4, 'S', 'e', 'l', 'f',
TypeName: #14, 'I', 'M', 'y', 'M', 'P', 'I', 'n', 't', 'e', 'r', 'f', 'a', 'c', 'e',
TypeInfo: #24, 'T', 'O', #0,
Flags: #0,
Name: #1, 'A',
TypeName: #7, 'I', 'n', 't', 'e', 'g', 'e', 'r', }

At the outermost level we start with the record that follows the IntfUnit field of the tkInterface part of the TTypeData variant record from TypInfo.

  PExtraInterfaceData = ^TExtraInterfaceData;
TExtraInterfaceData = packed record
MethodCount: Word; // #methods
HasMethodRTTI: Word; // $FFFF if no method RTTI,
// #methods again if has RTTI
Methods: packed array[0..High(Word)-1] of
TInterfaceMethodRTTI;
end;

For all interfaces, the MethodCount field contains the number of methods in the interface. For “normal” interfaces (compiled with $METHODINFO OFF) the HasMethodRTTI field will be $FFFF indicating that there are no more RTTI for this interface. Extended RTTI interfaces (compiled with $METHODINFO ON) the HasMethodRTTI field will equal the MethodCount field and there will be a packed array of information about each method following it.

  PInterfaceMethodRTTI = ^TInterfaceMethodRTTI;
TInterfaceMethodRTTI = packed record
Name: TPackedShortString;
Kind: TMethodKind; // mkProcedure or mkFunction
CallConv: TCallConv;
ParamCount: byte; // including Self
Parameters: packed array[0..High(byte)-1] of TInterfaceParameterRTTI;
case TMethodKind of
mkFunction:
(Result: TInterfaceResultRTTI);
end;

The RTTI for a single interface method contains the name of the method, the kind of method (procedure or function), the calling convention, the number of parameters (including the implicit Self parameter), and a packed array of details about each parameter. If the method is a function, the parameter array is followed about information about the result type.

  PInterfaceParameterRTTI = ^TInterfaceParameterRTTI;
TInterfaceParameterRTTI = packed record
Flags: TParamFlags;
ParamName: TPackedShortString;
TypeName: TPackedShortString;
TypeInfo: PPTypeInfo;
end;

The record definition for a parameter contains flags (indicating var, const, out or value parameter, array parameters and references) , the parameter name, the string name of the parameter type and a pointer to a PTypeInfo with the RTTI for the parameter type (if it has RTTI).

  PInterfaceResultRTTI = ^TInterfaceResultRTTI;
TInterfaceResultRTTI = packed record
Name: TPackedShortString;
TypeInfo: PPTypeInfo;
end;

Finally, we have the record definition for a function result.

  PInterfaceResultRTTI = ^TInterfaceResultRTTI;
TInterfaceResultRTTI = packed record
Name: TPackedShortString;
TypeInfo: PPTypeInfo;
end;

Again we have the string name of the result type and a pointer to a PTypeInfo for the RTTI of the return type.

The TExtraInterfaceData structures above approximate the physical layout of the raw RTTI information generated by the compiler. For external code we want to translate this into some preprocessed and easier to use structures. Note that this is very similar to what we did for published methods. Both interface and published methods have signatures with parameter and return type information. So I decided to refactor out the signature related definitions and helper routines from the HVPublishedMethodParams unit into a separate HVMethodSignature unit.

unit HVMethodSignature;

interface

uses Classes, SysUtils, TypInfo, HVVMT;

type
TCallConv = (ccReg, ccCdecl, ccPascal, ccStdCall, ccSafeCall);
PMethodParam = ^TMethodParam;
TMethodParam = record
Flags: TParamFlags;
ParamName: PShortString;
TypeName: PShortString;
TypeInfo: PTypeInfo;
end;
TMethodParamList = array of TMethodParam;
PMethodSignature = ^TMethodSignature;
TMethodSignature = record
Name: PShortString;
MethodKind: TMethodKind;
CallConv: TCallConv;
ParamCount: Byte;
Parameters: TMethodParamList;
ResultTypeName: PShortString;
ResultTypeInfo: PTypeInfo;
end;

function MethodKindString(MethodKind: TMethodKind): string;

function MethodParamString(const MethodParam: TMethodParam;
ExcoticFlags: boolean = False): string;

function MethodParametesString(const MethodSignature: TMethodSignature;
SkipSelf: boolean = True): string;

function MethodSignatureToString(const Name: string;
const MethodSignature: TMethodSignature): string; overload;

function MethodSignatureToString(
const MethodSignature: TMethodSignature): string; overload;

implementation

function MethodKindString(MethodKind: TMethodKind): string;
begin
case MethodKind of
mkSafeProcedure,
mkProcedure : Result := 'procedure';
mkSafeFunction,
mkFunction : Result := 'function';
mkConstructor : Result := 'constructor';
mkDestructor : Result := 'destructor';
mkClassProcedure: Result := 'class procedure';
mkClassFunction : Result := 'class function';
end;
end;

function MethodParamString(const MethodParam: TMethodParam;
ExcoticFlags: boolean = False): string;
begin
if pfVar in MethodParam.Flags then Result := 'var '
else if pfConst in MethodParam.Flags then Result := 'const '
else if pfOut in MethodParam.Flags then Result := 'out '
else Result := '';
if ExcoticFlags then
begin
if pfAddress in MethodParam.Flags then
Result := '{addr} ' + Result;
if pfReference in MethodParam.Flags then
Result := '{ref} ' + Result;
end;

Result := Result + MethodParam.ParamName^ + ': ';
if pfArray in MethodParam.Flags then
Result := Result + 'array of ';
Result := Result + MethodParam.TypeName^;
if Assigned(MethodParam.TypeInfo) then
Result := Result + ' {' + MethodParam.TypeInfo.Name + '} ';
end;

function MethodParametesString(
const MethodSignature: TMethodSignature;
SkipSelf: boolean = True): string;
var
i: integer;
MethodParam: PMethodParam;
begin
Result := '';
for i := 0 to MethodSignature.ParamCount-1 do
begin
MethodParam := @MethodSignature.Parameters[i];
// Skip the implicit Self parameter for class and interface methods
// Note that Self is not included in event types
if SkipSelf and
(i = 0) and
(MethodParam.Flags = [pfAddress]) and
(MethodParam.ParamName^ = 'Self') and
(MethodParam.TypeInfo.Kind in [tkInterface, tkClass]) then
Continue;
Result := Result + MethodParamString(MethodParam^);
if i < MethodSignature.ParamCount-1 then
Result := Result + '; ';
end;
end;

function CallingConventionToString(CallConv: TCallConv): string;
begin
case CallConv of
ccReg : Result := 'register';
ccCdecl : Result := 'cdecl';
ccPascal : Result := 'pascal';
ccStdCall : Result := 'stdcall';
ccSafeCall: Result := 'safecall';
else Result := 'TCallConv('+IntToStr(Ord(CallConv))+')';
end;
end;

function MethodSignatureToString(const Name: string;
const MethodSignature: TMethodSignature): string; overload;
begin
Result := Format('%s %s(%s)',
[MethodKindString(MethodSignature.MethodKind),
Name,
MethodParametesString(MethodSignature)]);
if MethodSignature.MethodKind = mkFunction then
begin
Result := Result + ': ' + MethodSignature.ResultTypeName^;
if Assigned(MethodSignature.ResultTypeInfo) then
Result := Result + ' {' + MethodSignature.ResultTypeInfo.Name + '} ';
end;
Result := Result + ';' ;
if MethodSignature.CallConv <> ccReg then
Result := Result + ' ' +
CallingConventionToString(MethodSignature.CallConv) + ';';
end;

function MethodSignatureToString(
const MethodSignature: TMethodSignature): string; overload;
begin
Result := MethodSignatureToString(MethodSignature.Name^,
MethodSignature);
end;

end.

This code is a simple extension of the code we saw back in the article about the hack of getting published method parameters via an event. Extended interface method RTTI contains more detailed information than an event, so we have extended the structures with fields for PTypeInfo of the parameter and return types, calling convention and method name. The routines are straightforward conversions of the logical method information into their corresponding pseudo-code string representations. We covered these in the previous article.

The only piece we are missing now is the code in the middle to translate from the internal raw RTTI structure to the externally useful structures. Here is the additional code from the HVInterfaceMethods unit that also contains the RTTI structures.

unit HVInterfaceMethods;

interface

uses TypInfo, HVMethodSignature;

type
// Easy-to-use fixed size structure
PInterfaceInfo = ^TInterfaceInfo;
TInterfaceInfo = record
UnitName: string;
Name: string;
Flags: TIntfFlags;
ParentInterface: PTypeInfo;
Guid: TGUID;
MethodCount: Word;
HasMethodRTTI: boolean;
Methods: array of TMethodSignature;
end;

procedure GetInterfaceInfo(InterfaceTypeInfo: PTypeInfo;
var InterfaceInfo: TInterfaceInfo);

implementation

type
// … TExtraInterfaceData type definitions goes here …

function Skip(Value: PShortstring): pointer; overload;
begin
Result := Value;
Inc(PChar(Result), SizeOf(Value^[0]) + Length(Value^));
end;

function Skip(Value: PPackedShortString;
var NextField{: Pointer}): PShortString; overload;
begin
Result := PShortString(Value);
Inc(PChar(NextField), SizeOf(Char) + Length(Result^)
- SizeOf(TPackedShortString));
end;

function Skip(CurrField: pointer; FieldSize: integer)
: pointer; overload;
begin
Result := PChar(Currfield) + FieldSize;
end;

function Dereference(P: PPTypeInfo): PTypeInfo;
begin
if Assigned(P)
then Result := P^
else Result := nil;
end;

procedure GetInterfaceInfo(InterfaceTypeInfo: PTypeInfo;
var InterfaceInfo: TInterfaceInfo);
// Converts from raw RTTI structures to user-friendly Info structures
var
TypeData: PTypeData;
ExtraData: PExtraInterfaceData;
i, j: integer;
MethodInfo: PMethodSignature;
MethodRTTI: PInterfaceMethodRTTI;
ParameterInfo: PMethodParam;
ParameterRTTI: PInterfaceParameterRTTI;
InterfaceResultRTTI: PInterfaceResultRTTI;
begin
Assert(Assigned(InterfaceTypeInfo));
Assert(InterfaceTypeInfo.Kind = tkInterface);
TypeData := GetTypeData(InterfaceTypeInfo);
ExtraData := Skip(@TypeData.IntfUnit);

// Interface
InterfaceInfo.UnitName := TypeData.IntfUnit;
InterfaceInfo.Name := InterfaceTypeInfo.Name;
InterfaceInfo.Flags := TypeData.IntfFlags;
InterfaceInfo.ParentInterface := Dereference(TypeData.IntfParent);
InterfaceInfo.Guid := TypeData.Guid;
InterfaceInfo.MethodCount := ExtraData.MethodCount;
InterfaceInfo.HasMethodRTTI :=
(ExtraData.HasMethodRTTI = ExtraData.MethodCount);
if InterfaceInfo.HasMethodRTTI
then SetLength(InterfaceInfo.Methods, InterfaceInfo.MethodCount)
else SetLength(InterfaceInfo.Methods, 0);

// Methods
MethodRTTI := @ExtraData.Methods[0];
for i := Low(InterfaceInfo.Methods) to
High(InterfaceInfo.Methods) do
begin
MethodInfo := @InterfaceInfo.Methods[i];
MethodInfo.Name := Skip(@MethodRTTI.Name, MethodRTTI);
MethodInfo.MethodKind := MethodRTTI.Kind;
MethodInfo.CallConv := MethodRTTI.CallConv;
MethodInfo.ParamCount := MethodRTTI.ParamCount;
SetLength(MethodInfo.Parameters, MethodInfo.ParamCount);

// Parameters
ParameterRTTI := @MethodRTTI.Parameters;
for j := Low(MethodInfo.Parameters) to
High(MethodInfo.Parameters) do
begin
ParameterInfo := @MethodInfo.Parameters[j];
ParameterInfo.Flags := ParameterRTTI.Flags;
ParameterInfo.ParamName :=
Skip(@ParameterRTTI.ParamName, ParameterRTTI);
ParameterInfo.TypeName :=
Skip(@ParameterRTTI.TypeName, ParameterRTTI);
ParameterInfo.TypeInfo :=
Dereference(ParameterRTTI.TypeInfo);
ParameterRTTI := Skip(@ParameterRTTI.TypeInfo, SizeOf(ParameterRTTI.TypeInfo));
end;

// Function result
if MethodInfo.MethodKind = mkFunction then
begin
InterfaceResultRTTI := Pointer(ParameterRTTI);
MethodInfo.ResultTypeName :=
Skip(@InterfaceResultRTTI.Name, InterfaceResultRTTI);
MethodInfo.ResultTypeInfo :=
Dereference(InterfaceResultRTTI.TypeInfo);
MethodRTTI := Skip(@InterfaceResultRTTI.TypeInfo,
SizeOf(InterfaceResultRTTI.TypeInfo));
end
else
MethodRTTI := Pointer(ParameterRTTI);
end;
end;

end.

The code is a little tricky and hard to follow because of the need to skip over the variable length shortstring fields. The low-level code in IntfInfo is different, using a ReadString, ReadByte, ReadWord , ReadLong model. I like the self-documenting aspects of pseudo-record definitions and wanted to use them in the access code as well. Note that at some points, only one field in a record structure is aligned correctly and can be read.

With all the foundations in place, now we can write a little dumping routine that will print out a pseudo source code representation of an interface.

procedure DumpInterface(InterfaceTypeInfo: PTypeInfo);
var
InterfaceInfo: TInterfaceInfo;
i: integer;
begin
GetInterfaceInfo(InterfaceTypeInfo, InterfaceInfo);

writeln('unit ', InterfaceInfo.UnitName, ';');
writeln('type');
write(' ', InterfaceInfo.Name, ' = ');
if not (ifDispInterface in InterfaceInfo.Flags) then
begin
write('interface');
if Assigned(InterfaceInfo.ParentInterface) then
write(' (', InterfaceInfo.ParentInterface.Name, ')');
writeln;
end
else
writeln('dispinterface');
if ifHasGuid in InterfaceInfo.Flags then
writeln(' [''', GuidToString(InterfaceInfo.Guid), ''']');
if InterfaceInfo.HasMethodRTTI then
for i := Low(InterfaceInfo.Methods) to
High(InterfaceInfo.Methods) do
writeln(' ', MethodSignatureToString(
InterfaceInfo.Methods[i]))
else
for i := 1 to InterfaceInfo.MethodCount do
writeln(' procedure UnknownName',i,';');
writeln(' end;');
writeln;
end;

And finally we can write some test code to try it all out.

type
TNumber = integer;
TNewNumber = type integer;
TIntegerArray = array of integer;
TNormalClass = class
end;
TPersistentClass = class(TPersistent)
end;
TSetOfByte = set of byte;
TEnum = (enOne, enTwo, enThree);
type
{.$M+} {.$TYPEINFO ON}
// With regards to interface RTTI, METHODINFO
// has the same effect as $M and $TYPEINFO
{$METHODINFO ON}
IMyMPInterface = interface
['{AA503475-0187-4108-8E27-41475F4EF818}']
procedure TestRegister(A: integer; var B: string); register;
procedure TestStdCall(LongParaName: TObject;
const B: string; var C: integer; out D: byte); stdcall;
procedure TestSafeCall(out R: integer); safecall;
function Number: TNumber; cdecl;
function NewNumber: TNewNumber; cdecl;
function AsString: string; pascal;
function AsString2: string; safecall;
// Return types that are supported
procedure A2(const A: TIntegerArray);
procedure OkParam1(Value: TSetOfByte);
procedure OkParam2(Value: TSetOfByte);
procedure OkParam3(Value: Variant);
procedure OkParam4(Value: TNormalClass);
function OkReturn1: shortstring;
function OkReturn2: TObject;
function OkReturn3: IInterface;
function OkReturn4: TSetOfByte;
function OkReturn5: TNormalClass;
function OkReturn6: TEnum;
function OkReturn7: TClass;
function OkReturn8: Pointer;
function OkReturn9: PChar;
function OkReturn10: TIntegerArray;
end;
{$M-}

{$WARN SYMBOL_PLATFORM OFF}
procedure Test;
begin
DumpInterface(TypeInfo(IMyMPInterface));
end;

begin
try
Test;
except
on E:Exception do
writeln(E.Message);
end;
readln;
end.

And the output is:

unit TestExtendedInterfaceRTTI;
type
IMyMPInterface = interface (IInterface)
['{AA503475-0187-4108-8E27-41475F4EF818}']
procedure TestRegister(A: Integer {Integer} ; var B: String {String} );
procedure TestStdCall(LongParaName: TObject {TObject} ;
const B: String {String} ; var C: Integer {Integer} ;
out D: Byte {Byte} ); stdcall;
procedure TestSafeCall(out R: Integer {Integer} ); safecall;
function Number(): Integer {Integer} ; cdecl;
function NewNumber(): TNewNumber {TNewNumber} ; cdecl;
function AsString(): String {String} ; pascal;
function AsString2(): String {String} ; safecall;
procedure A2(const A: TIntegerArray {TIntegerArray} );
procedure OkParam1(Value: TSetOfByte {TSetOfByte} );
procedure OkParam2(Value: TSetOfByte {TSetOfByte} );
procedure OkParam3(Value: Variant {Variant} );
procedure OkParam4(Value: TNormalClass {TNormalClass} );
function OkReturn1(): ShortString {ShortString} ;
function OkReturn2(): TObject {TObject} ;
function OkReturn3(): IInterface {IInterface} ;
function OkReturn4(): TSetOfByte {TSetOfByte} ;
function OkReturn5(): TNormalClass {TNormalClass} ;
function OkReturn6(): TEnum {TEnum} ;
function OkReturn7(): TClass;
function OkReturn8(): Pointer;
function OkReturn9(): PAnsiChar;
function OkReturn10(): TIntegerArray {TIntegerArray} ;
end;

The code including the utility units and the test code for this and most of my other RTTI articles can be found in CodeCentral here. The test code also contains some conditional code to test the support of additional parameter types. My experiments indicates that the following parameter types are not supported in interface methods with extra RTTI:



  • All pointer types

  • open array parameters (array of Type), named dynamic array is ok

  • class references (such as TClass)

  • record types (such as TRect)

  • untyped var and out parameters

If the compiler encounters one of these parameter types in a $METHODINFO ON interface method it generates a compiler time error like “[Error] : Type '%s' has no type info” at the end statement of the interface (i.e. it does not indicate what method is the culprit).

Due to my summer holidays and general laxness, this article was brought to you a litter later than originally planned. Hopefully I will be able to follow up with the next article on extended $METHODINFO ON RTTI for public and published methods of a class within a reasonable time. This technology is what the Websnap scripting is based on.


Updated (27. Oct 2007): $METHODINFO was first available in Delphi 7, not Delphi 6.

Sunday, June 04, 2006

Simple Interface RTTI

Delphi supports getting RTTI for all interfaces, but it does not include method information for “normal” interfaces.

type
{$M-}
IMyMMInterface = interface
procedure Foo;
end;

By using the built-in TypeInfo function on the interface type, we get a pointer to the RTTI structure the compiler has generated for it, a pointer to a TTypeInfo record. This record is defined in the TypInfo unit and looks like this:

  PPTypeInfo = ^PTypeInfo;
PTypeInfo = ^TTypeInfo;
TTypeInfo = record
Kind: TTypeKind;
Name: ShortString;
{TypeData: TTypeData}
end;

Just following the TTypeInfo record in memory is another variable-length variant record that contains different fields and information depending on the value of the Kind field above. The supported type kinds are defined like this:


type
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);

For interfaces, we’re only interested in the tkInterface kind. The part of the TTypeData record that encodes the RTTI for normal interfaces look like this:

type
TIntfFlag = (ifHasGuid, ifDispInterface, ifDispatch);
TIntfFlagsBase = set of TIntfFlag;
// …
PTypeData = ^TTypeData;
TTypeData = packed record
case TTypeKind of
// …
tkInterface: (
IntfParent : PPTypeInfo; { ancestor }
IntfFlags : TIntfFlagsBase;
Guid : TGUID;
IntfUnit : ShortStringBase;
{PropData: TPropData});
// …
end;

From this we can se that we have access to the following type information for any interface:



  • A pointer to the parent interface (IntfParent)

  • Flags indicating if this interface has a GUID, if it is a dispintf interface and if it is a IDispatch interface (IntfFlags)

  • The GUID of the interface (if it has one) (Guid)

  • The unit the interface was declared in (IntfUnit)

  • The number of methods in the interface (PropData.Count)
We can write a simple function that dumps this information for a given interface.
type  
PExtraInterfaceData = ^TExtraInterfaceData;
TExtraInterfaceData = packed record
MethodCount: Word; { # methods }
end;

function SkipPackedShortString(Value: PShortstring): pointer;
begin
Result := Value;
Inc(PChar(Result), SizeOf(Value^[0]) + Length(Value^));
end;

procedure DumpSimpleInterface(InterfaceTypeInfo: PTypeInfo);
var
TypeData: PTypeData;
ExtraData: PExtraInterfaceData;
i: integer;
begin
Assert(Assigned(InterfaceTypeInfo));
Assert(InterfaceTypeInfo.Kind = tkInterface);
TypeData := GetTypeData(InterfaceTypeInfo);
ExtraData := SkipPackedShortString(@TypeData.IntfUnit);
writeln('unit ', TypeData.IntfUnit, ';');
writeln('type');
write(' ', InterfaceTypeInfo.Name, ' = ');
if not (ifDispInterface in TypeData.IntfFlags) then
begin
write('interface');
if Assigned(TypeData.IntfParent) then
write(' (', TypeData.IntfParent^.Name, ')');
writeln;
end
else
writeln('dispinterface');
if ifHasGuid in TypeData.IntfFlags then
writeln(' [''', GuidToString(TypeData.Guid), ''']');
for i := 1 to ExtraData.MethodCount do
writeln(' procedure UnknownName',i,';');
writeln(' end;');
writeln;
end;

The function expects a pointer to the type information of an interface. It digs out the generated interface RTTI and tries to write a pseudo interface type declaration inside a unit using the information available. It only knows the number of methods in the interface, so it just outputs some dummy names for them.

To test this we can define some plain vanilla ($M-} interfaces and then use the TypeInfo intrinsic function to get a RTTI pointer for each interface to send to the dumping function above.

program TestSimpleInterfaceRTTI;

{$APPTYPE CONSOLE}

uses
SysUtils,
TypInfo;

// ... Insert the code above here

type
{$M-}
IMyInterface = interface
procedure Foo(A: integer);
procedure Bar(const B: string);
procedure Nada(const C: array of integer; D: TObject);
end;
IMyDispatchInterface = interface(IDispatch)
['{9BC5459B-6C31-4F5B-B733-DCA8FC8C1345}']
procedure Foo; dispid 0;
end;
IMyDispInterface = dispinterface
['{8574E276-4671-49AC-B775-B299E6EF01C5}']
procedure Bar;
end;

begin
DumpSimpleInterface(TypeInfo(IMyInterface));
DumpSimpleInterface(TypeInfo(IMyDispatchInterface));
DumpSimpleInterface(TypeInfo(IMyDispInterface));
readln;
end.

Running this project produces the following output:

unit TestSimpleInterfaceRTTI;
type
IMyInterface = interface (IInterface)
procedure UnknownName1;
procedure UnknownName2;
procedure UnknownName3;
end;

unit TestSimpleInterfaceRTTI;
type
IMyDispatchInterface = interface (IDispatch)
['{9BC5459B-6C31-4F5B-B733-DCA8FC8C1345}']
procedure UnknownName1;
end;

unit TestSimpleInterfaceRTTI;
type
IMyDispInterface = dispinterface
['{8574E276-4671-49AC-B775-B299E6EF01C5}']
procedure UnknownName1;
end;

We are able to pick up the name of the unit (or program as it is in this case) the interface is declared in, the name of the interface and its parent interface and the GUID if it has one. We can also distinguish dispinterfaces that is used in the implementation of Automation servers (for dual interfaces that inherit from IDispatch).

As you can see we are missing the proper names of the interface methods and we don’t have any information about the parameters, return types or calling conventions. Compiling interfaces in the $M+ mode (or inheriting from IInvokable) changes all that – as we shall see in an upcoming article.



Copyright © 2004-2007 by Hallvard Vassbotn