Title: Objects in DLLs
Question: Store one or more classes you have written in a DLL, and enable them to be instantiated in any version of Delphi (4 upwards) or from a C++ program.
Answer:
Many articles I've read about COM seem to give the impression that you need to write a COM Object, or an ActiveX control, or OLE server to make use of COM.
However it is possible to derive benefit by designing COM interfaces in your own programs without needing to register any COM object in the Windows registry. By providing COM interfaces for the classes you write, you can make the class available to other programs in a language-independent way, without the need to provide implementation code OR any registerable COM object.
The idea for this demonstraton came from recently re-reading a 1997 article written by Danny Thorpe in the now-defunct Delphi Informant magazine. The article was not about COM. It was about how virtual methods are resolved at run-time through the Virtual Method Table. One of the ideas he mentioned though was that, provided you had the class definition of an ancestor class (even if it was completely abstract ie. had no implementation code) it would be possible to call the methods of a descendant class just by using its pointer (eg. one provided from a DLL) and by casting the pointer as its ancestor. The point being made was that the object instance of the descendant contains a pointer to its own VMT, not to the ancestor's VMT, and therefore the correct descendant methods would be called.
I don't think he was suggesting that returning a pointer to an object from a DLL was a "good idea", but the idea intrigued me. However a little googling soon confirmed the flaw. There were Delphi programmers asking "how do I access an object in a DLL written in C++", with discussions about whether the VMTs are constructed the same way in C++ as in Delphi, etc. All in all, it's a risky business trying to bridge between objects written in different languages.
The sad thing is that Microsoft "cracked that nut" years ago! Wikipedia correctly introduce the Component Object Model as follows:
"The essence of COM is a language-neutral way of implementing objects that can be used in environments different from the one they were created in".
Now, I don't expect C++ programmers would ever warm to the concept of language independence. They live in their own little world and want to keep it that way!
But it became apparent to me that COM interfaces provided the answer: An established and safe way to access objects written in DLLs, not only across different languages but also for Delphi-to-Delphi since, by using COM interfaces we don't need to make assumptions about how the VMT is implemented or how it is accessed via the pointer, in case they change from one version to another.
I won't cover any theory about interfaces or COM because others have already done that elsewhere on the web. A good article to read is at:
http://delphi.about.com/od/comoleactivex/a/comdelphi.htm
The key thing to notice is that the DLL provides a function (in this case "GetISumIntegers") that constructs the implementation class, yet returns it as the interface type, not as the class type that implements the interface.
In the test code a simple object that accepts 2 different integers, and provides a function to get their sum is shown. The test code demonstates that the fields in the implementation class (ie. integers X and Y) persist such that their sum can then be obtained by calling the SumXY function after X and Y have previously been set.
Below is the code that interfaces the class in a DLL, and also oode for a test program that calls the DLL.
unit SumIntegersUnit;
{ This unit is part of (used by) the DLL code }
interface
type
{ Interface definition.
The calling program needs a copy only of the ISumIntegers definition,
and does not need the TSumIntegers class definition. }
ISumIntegers = interface
procedure SetX(x: integer);
procedure SetY(y: integer);
function SumXY: integer;
property X: integer write SetX;
property Y: integer write SetY;
end;
TSumIntegers = class(TInterfacedObject, ISumIntegers)
private
FX, FY: integer;
public
procedure SetX(x: integer);
procedure SetY(y: integer);
function SumXY: integer;
end;
implementation
{ TSumInteger }
procedure TSumIntegers.SetX(x: integer);
begin
Self.FX := x;
end;
procedure TSumIntegers.SetY(y: integer);
begin
Self.FY := y;
end;
function TSumIntegers.SumXY: integer;
begin
Result := FX + FY;
end;
end.
----------------------------------------
The DLL code, compile as "SumIntegers.dll"
library SumIntegers;
uses
SumIntegersUnit in 'SumIntegersUnit.pas';
{$R *.res}
// This function returns the ISumIntegers interface
function GetISumIntegers: ISumIntegers; stdcall;
begin
Result := SumIntegersUnit.TSumIntegers.Create;
end;
exports
GetISumIntegers;
begin
end.
==========================================
The test program's form unit.
The demo shows that, despite the external class being
instaniated 200 times, in an overlapping way, with random
timeouts before the SumXY is called, there is no clashing
or overwriting of any other object fields.
-----------------------------
unit TestSumUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
const
UM_THREADDONE = WM_USER + 1;
type
TTestThread = class(TThread)
protected
procedure Execute; override;
public
failed: boolean;
end;
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
successful: integer;
procedure ThreadTerminate(Sender: TObject);
procedure ThreadDone(var msg: TMessage); message UM_THREADDONE;
public
{ Public declarations }
end;
ISumIntegers = interface
procedure SetX(x: integer);
procedure SetY(y: integer);
function SumXY: integer;
property X: integer write SetX;
property Y: integer write SetY;
end;
function GetISumIntegers: ISumIntegers; stdcall external 'SumIntegers.dll';
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TTestThread }
procedure TTestThread.Execute;
var
sum_ints: ISumIntegers;
sumxy: integer;
i: integer;
x, y: integer;
begin
sum_ints := GetISumIntegers;
for i := 0 to Random(15) do
begin
x := Random(1000);
y := Random(1000);
sum_ints.X := x;
sum_ints.Y := y;
Sleep(Random(20));
sumxy := sum_ints.SumXY;
if sumxy (x + y) then
Self.failed := True;
end;
end;
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
thrd: TTestThread;
begin
Randomize;
for i := 1 to 200 do
begin
thrd := TTestThread.Create(True);
thrd.OnTerminate := Self.ThreadTerminate;
thrd.FreeOnTerminate := True;
thrd.Resume;
Sleep(10);
end;
end;
procedure TForm1.ThreadDone(var msg: TMessage);
begin
if msg.WParam = 1 then
begin
Inc(successful);
Label1.Caption := IntToStr(successful);
end;
end;
procedure TForm1.ThreadTerminate(Sender: TObject);
begin
if not (Sender as TTestThread).failed then
PostMessage(Handle, UM_THREADDONE, 1, 0);
end;
end.