4

I used to think that type safety in Delphi with regard to Interfaces is maintained by setting a unique (optional, but unique if filled in) GUID to it.

Then there came that question: Unspecified error when calling Word CentimetersToPoints via OLE
Little follow-up on it: http://pastebin.ca/2369858

And i started looking in stock Delphi TWordApplication component (namely Word200.pas unit). And there i see:

// *********************************************************************//
// Interface: _Application
// Flags:     (4560) Hidden Dual NonExtensible OleAutomation Dispatchable
// GUID:      {00020970-0000-0000-C000-000000000046}
// *********************************************************************//
  _Application = interface(IDispatch)
    ['{00020970-0000-0000-C000-000000000046}']
...
    function CentimetersToPoints(Centimeters: Single): Single; safecall;



// *********************************************************************//
// DispIntf:  _ApplicationDisp
// Flags:     (4560) Hidden Dual NonExtensible OleAutomation Dispatchable
// GUID:      {00020970-0000-0000-C000-000000000046}
// *********************************************************************//
  _ApplicationDisp = dispinterface
    ['{00020970-0000-0000-C000-000000000046}']
...
    function CentimetersToPoints(Centimeters: Single): Single; dispid 371;

or similar:

// *********************************************************************//
// Interface: _Global
// Flags:     (4560) Hidden Dual NonExtensible OleAutomation Dispatchable
// GUID:      {000209B9-0000-0000-C000-000000000046}
// *********************************************************************//
  _Global = interface(IDispatch)
    ['{000209B9-0000-0000-C000-000000000046}']
...
    function CentimetersToPoints(Centimeters: Single): Single; safecall;

// *********************************************************************//
// DispIntf:  _GlobalDisp
// Flags:     (4560) Hidden Dual NonExtensible OleAutomation Dispatchable
// GUID:      {000209B9-0000-0000-C000-000000000046}
// *********************************************************************//
  _GlobalDisp = dispinterface
    ['{000209B9-0000-0000-C000-000000000046}']
...
    function CentimetersToPoints(Centimeters: Single): Single; dispid 371;

And i feel totally lost here.

I used to think that dispinterface is "subclass" of interface like TPersistent to TObject is ? If yes, then how can be two interfaces with same GUID in same project ?

Or are they from different unrelated frameworks, like Delphi new class types to inherited TurboPascal object types ? Neither _GlobalDisp nor _ApplicationDisp seems to be used in Word200.pas so are they just like appendix, auto-imported but never actually used ?

I made the project, using both _Application and _ApplicationDisp and it compiles. But then i only wonder how does Delphi typcast it, if they have the SAME GUID ?

procedure TForm4.Button1Click(Sender: TObject);
 procedure show(const s: Single);
 begin
   ShowMessage(FloatToStr(s));
 end;
begin
  show( WordApplication1.CentimetersToPoints(1.0) );
  show( WordApplication1.Application.CentimetersToPoints(2.0) );
  show( WordApplication1.DefaultInterface.CentimetersToPoints(3.0) );
  show( _ApplicationDisp(WordApplication1.Application).CentimetersToPoints(4.0) );
  show( (WordApplication1.DefaultInterface as _ApplicationDisp).CentimetersToPoints(5.0) );
end;
Community
  • 1
  • 1
Arioch 'The
  • 15,799
  • 35
  • 62

1 Answers1

3

The dispinterface is really just a convenient way to use IDispatch for an automation interface. That's why they have the same GUID – they are exactly the same thing behind the scenes.

When you use IDispatch to invoke a method you typically have to call GetIdsOfNames to obtain the dispatch ID for your method. But since these are static, you can save time by skipping that step, if you know the dispatch ID. And that's what a dispinterface allows you to do.

When you call a method on a dispinterface you still end up calling Invoke on the IDispatch, but you skip the call to GetIdsOfNames.

When you use QueryInterface with an interface, you'll get the IDispatch. You can then cast it to its corresponding dispinterface. It's still the same interface, but when you invoke methods on the dispinterface you'll save that call to GetIdsOfNames.

So, if you have an IDispatch for the Word application object, say, you can write code like this:

var
  WordApp: Variant;
  WordDisp: _ApplicationDisp;
....
WordApp := CreateOleObject('Word.Application');
WordDisp := _ApplicationDisp(IDispatch(WordApp));

The _ApplicationDisp() cast is nothing more than a call to IntfCopy. Which in turn is nothing more than a call to _AddRef. And you can then write:

Writeln(WordApp.ProductCode);
Writeln(WordDisp.ProductCode);

Both produce the same output. The former first calls GetIdsOfNames before calling Invoke. The latter goes straight to Invoke.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • what it reads to me is that `dispinterface` is just a macro for `interface(IDispatch)`. This does not answer why two interfaces were declared and if any of declarations could be omitted. For example _Application is full of safecall methods. None of those have dispid. But on the run it is compiled as indirect call in procedure vectors table. So it looks like the table is filled with actual proc addresses, relative or absolute... when ? compile-time ? Word.exe load ? By whom ? Is it done by method name ? If no - by what id (declaration order is alphabet-sorted, hence random for dispid) ? – Arioch 'The Apr 30 '13 at 08:43
  • @Arioch'The The method declarations in `_Application` are early bound. A `dispinterface` is simply a convenient optimised way to call `IDispatch.Invoke`. So a `dispinterface` is still late bound. It is possible to have an interface that does not support early binding. In which case a `dispinterface` comes in handy. Whenever you have both early and late binding available, the `dispinterface` is pointless. – David Heffernan Apr 30 '13 at 08:48
  • But how early is early bound here ? Are vtable offsets filled with compiler or with RTL ? or vtable is not used and only vectors table is created ? Is it filled using methos names as ids? or does compiler actually find and use _ApplicationDisp interface to compile visibly unrelated _Application interface calls ? Should this compile and work if i remove _***Disp from units ? Will this compile on the computer having no MS Office installed ? What i don't have, is the picture of different pieces co-operating to give the end result. – Arioch 'The Apr 30 '13 at 08:56
  • @Arioch'The Early bound is early bound. `_Application` is an `interface` like any other, no dispids at all. The `vtable` is determined by the order of declarations, just like any other interface. This interface supports both early bound and late bound. Calling methods on `_Application` are early bound. Using the `dispinterface` is late bound. You can certainly remove the `dispinterface` from the code and everything will work. Well, you won't be able to use the `dispinterface`. Compilation never requires Office to be installed. – David Heffernan Apr 30 '13 at 09:02
  • Well, okay, coming from another direction. This is all the sequence call: http://pastebin.ca/2369880 The method `function CentimetersToPoints(Centimeters: Single): Single; safecall;` is 155th (0-based) in the intf declaration. However all the magic numbers are very different. Not even 155x4==620. Adding here 7 methods of IDispatch we come to 620+4x7=648 magic number there. Okay. But that v-table entry redirect to double jumpe table with quite different magic numbers! how is that table created and bound to Delphi-internal v-table? `OLE32.DLL` and `WinWord.exe` do not know Delphi internals. – Arioch 'The Apr 30 '13 at 09:23
  • (155+7)*4 == 648. What more are you looking for? – David Heffernan Apr 30 '13 at 09:34
  • @DavidHeffernans, yes, i always figured this out and updated the comment. But there are more magic numbers on the list - 162 and 34. Okay, 162 is 648 / 4 probably (if not co-incidence). But there still is 34. And... you know... OLE32 nor WinWord can know where Delphi is chosen for CentimetersToPoints to be. I would just move it to topmost position and v-table index would be 7 (offset 28) but that would not break compatibility, despite nothing in OLE32 or WinWord.exe changed. So the actual binding is something else... – Arioch 'The Apr 30 '13 at 09:41
  • 1
    @Arioch'The If it was moved in the declaration of `_Application`, then that would break everything. Because early binding vtable is ordered according to order of declarations of methods. So, `_Application` is an ordinary interface with early binding and a vtable. Nothing more, nothing less. It happens to derive from `IDispatch` so that it also supports late binding. That can happen through `GetIdsOfNames/Invoke` which is what you get when using a `Variant` wrapped `IDispatch`. Or it can happen with just `Invoke` which is what you get when calling via a `dispinterface`. End of story. – David Heffernan Apr 30 '13 at 09:45
  • So you mean that Office OLE/COM interop for each method publish BOTH id and declaration order ? Sounds both fragile and violating DRY principle... – Arioch 'The Apr 30 '13 at 09:48
  • Yes, you've got it. That's why you can do Office interop with either early binding or late binding. I expect that they have tools that allow them to adhere to DRY. – David Heffernan Apr 30 '13 at 09:50
  • Just made local copy of word2000.pas, swapped CeintimietersToPoints and MillimetersToPoint and... yes, it is really order-specific. Like walking over thin ice... I wish they better build indirection tables based on names or disp-id's rather than order of declarations for early binding. Seems way too fragile to cross-programs interfaces. Well, perhaps in 16-bit times they were squeezing eveyr byte (out of OLE, ha-ha)... – Arioch 'The Apr 30 '13 at 09:56