Application Development with C++Builder and Delphi
Written by Charlie Calvert
Note: The views and information expressed in this document represent those of its author(s) who is solely responsible for its content. Borland does not make or give any representation or warranty with respect such content.
Using COM to link Delphi Code into C++Builder
The last few sections have shown that it is trivial
to link Delphi units and components directly into C++Builder.
Another great way to share code between the two tools is via COM
and OLE. You might want to use COM and OLE if you want to share
your code not only between Delphi and C++Builder, but also between
Delphi and Visual Basic, or other OLE aware tools such as Word,
Excel, Visual C++, etc.
There are three key ways to link Delphi COM objects
into C++Builder:
- Use OLE Automation or Dual Interfaces
- Use Delphi 97 to create ActiveX controls
- Write raw COM or OLE code to link in Delphi objects
The first two methods shown above are supported automatically
by C++Builder. The third method requires you to dig into the details
of OLE.
One simple way to link in Delphi objects via COM
is to use OLE Automation. C++Builder fully supports OLE automation,
and it provides a simple technique for accessing the methods of
an Automation Object.
Automation Objects can be accessed in one of two
fashions: You can access them via a COM IDispatch interface, or
via dual interfaces. C++Builder has built in support for IDispatch,
via the CreateOleObject VCL function and the Variant template
class. This is the simplest method for accessing Automation Objects,
and it is the one you should use unless speed is an extremely
important issue for you.
For instance, suppose you have a Delphi IDispatch
interface that exports the following methods:
ITest = interface(IDispatch)
['{1746E520-E2D4-11CF-BD2F-0020AF0E5B81}']
function Get_Value: Integer; safecall;
procedure Set_Value(Value: Integer); safecall;
function Get_Name: WideString; safecall;
procedure Set_Name(const Value: WideString);
safecall;
procedure Prompt(const text: WideString); safecall;
procedure VarTest(var v1, v2, v3: Variant); safecall;
property Value: Integer read Get_Value write
Set_Value;
property Name: WideString read Get_Name write
Set_Name;
end;
Assume further that the DLL that contains this interface
is referenced in the registry in association with CLSID that has
a ProgID called "TestLib.Test".
Here is how you can retrieve the interface and call
the Prompt method from inside C++Builder:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Variant V = CreateOleObject("TestLib.Test");
V.OleProcedure("Prompt", "Sammy");
}
This code first retrieves an instance of IDispatch
inside a Variant. It then uses a method of the Variant class to
call the Prompt method from the Delphi interface.
Here is how you would call the VarTest method:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Variant V1, V2, V3;
Variant V = CreateOleObject("TestLib.Test");
V1 = 5;
V2 = "Sam";
V3 = V;
V.OleProcedure("VarTest", V1, V2, V3);
}
Delphi users looking at this code should note that
Variant is a C++ class declared in SYSDEFS.H. It is not a simple
type as it is when used inside Delphi.
If you want to use dual interfaces in C++Builder,
then you can get very fast access to Automation Objects that reside
in a DLL. You can also access local servers, that is Automation
Objects in an executable, more quickly via dual interfaces, but
the built in overhead with local servers is so great that the
improvement in speed given by dual interfaces is less noticeable.
There are no built in tools for accessing dual interfaces
in C++Builder. Obviously it will be possible to use them (after
all, you can do anything in C++), but you will have to lay the
ground work yourself. (The basic technique is very similar to
the one outlined below in the section titled "Mapping Virtual
Method Tables".)
Using Delphi ActiveX Controls in C++Builder
C++Builder has built in support for ActiveX controls.
You can load them into the environment and use them just as if
they were native components. To get started:
- Choose "Component | Install"
from the menu.
- In the Install Components dialog select
the OCX button to bring up the Import OLE Control dialog.
- All of the registered components on your system
will appear in the Import OLE Control dialog. If you want
to register a new component, select the Register button in the
Import OLE Control dialog.
- After selecting the control you want to import,
press the OK button.
- Your new control will now be listed at the bottom
of the Installed Components dialog, and a interface source
file will be placed in the LIB directory.
- Press OK to recompile CMPLIB32 and add your control
the Component Palette. By default, the new ActiveX control will
appear in the OCX page of the Component Palette.
If you don't want to use the automated method shown
above, you can also use the C++ language to build your own OLE
containers. This is, of course, a difficult process, and the technique
outlined above is usually infinitely preferable. Don't forget
that you can use the tools in Borland C++ to create OLE containers,
and then link that code into your C++Builder project either directly,
or through libraries, or through DLLs.
Using the Windows API to Link in Delphi COM and OLE Objects
Delphi 97 is an extremely powerful tool for creating
COM and OLE objects. You can use it to build Automation objects,
dual interfaces, ActiveX controls, and all manner of simple (or
complex) COM objects.
C++Builder does not yet have the built in support
for COM that you find in Delphi 97. However, it does have access
to all the OLE code you find in Borland C++ 5.0, the Windows SDK,
the MSDN, or in commercially available books and libraries.
The following is a definition for a simple Delphi
COM object:
const
CLSID_ITable: TGUID = ( D1:$58BDE140;D2:$88B9;D3:$11CF;D4:($BA,$F3,$00,$80,$C7,$51,$52,$8B));
type
ITable = class(IUnknown)
private
FRefCount: LongInt;
FObjectDestroyed: TObjectDestroyed;
Table: TTable;
public
constructor Create(ObjectDestroyed: TObjectDestroyed);
destructor Destroy; override;
function QueryInterface(const iid: TIID; var
obj):
HResult; override; stdcall;
function AddRef: Longint; override; stdcall;
function Release: Longint; override; stdcall;
{ interface }
procedure Open; virtual; stdcall;
procedure Close; virtual; stdcall;
procedure SetDatabaseName(const Name: PChar);
virtual; stdcall;
procedure SetTableName(const Name: PChar); virtual;
stdcall;
procedure GetStrField(FieldName: PChar; Value:
PChar);
virtual; stdcall;
procedure GetIntField(FieldName: PChar; var Value:
Integer);
virtual; stdcall;
procedure GetClassName(Value: PChar); virtual;
stdcall;
procedure Next; virtual; stdcall;
procedure Prior; virtual; stdcall;
procedure First; virtual; stdcall;
procedure Last; virtual; stdcall;
function EOF: Bool; virtual; stdcall;
end;
Here is the same interface as it would be declared
inside a C++ project:
class IDBClass : public IUnknown
{
public:
STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*)
PURE;
STDMETHOD_(ULONG,AddRef) (THIS) PURE;
STDMETHOD_(ULONG,Release) (THIS) PURE;
// Interface
STDMETHOD_(VOID, Open) (THIS) PURE;
STDMETHOD_(VOID, Close) (THIS) PURE;
STDMETHOD_(VOID, SetDatabaseName) (THIS_ LPSTR
Name) PURE;
STDMETHOD_(VOID, SetTableName) (THIS_ LPSTR Name)
PURE;
STDMETHOD_(VOID, GetStrField) (THIS_ LPSTR FieldName,
LPSTR Value)
PURE;
STDMETHOD_(VOID, GetIntField) (THIS_ LPSTR FieldName,
int * Value)
PURE;
STDMETHOD_(VOID, GetClassName) (THIS_ LPSTR Name)
PURE;
STDMETHOD_(VOID, Next) (THIS) PURE;
STDMETHOD_(VOID, Prior) (THIS) PURE;
STDMETHOD_(VOID, First) (THIS) PURE;
STDMETHOD_(VOID, Last) (THIS) PURE;
STDMETHOD_(BOOL, EOF) (THIS) PURE;
};
Given the above definition, you could write the following
C++ code to use the Delphi object in C++:
#pragma argsused
void Window1_OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
int x, int y, UINT keyFlags)
{
char CR[2] ="r";
HRESULT hr;
PIDBClass P;
LPSTR S, Temp;
CoInitialize(NULL);
hr = CoCreateInstance(CLSID_IDBClass, NULL, CLSCTX_INPROC_SERVER,
IID_IUnknown, (VOID**) &P);
if (SUCCEEDED(hr))
{
S = (char *)malloc(1000);
Temp = (LPSTR)malloc(100);
P->SetDatabaseName("DBDEMOS");
P->SetTableName("COUNTRY");
P->Open();
strcpy(S, "Countries and their capitals:
");
strcat(S, CR);
strcat(S, CR);
while (!P->EOF())
{
P->GetStrField("NAME", Temp);
strcat(S, Temp);
strcat(S, ": ");
P->GetStrField("CAPITAL", Temp);
strcat(S, Temp);
strcat(S, CR);
P->Next();
}
MessageBox(hwnd, S, "COUNTRY TABLE",
MB_OK);
P->Release();
free(S);
free(Temp);
}
}
Clearly this technique takes a bit of work, but it
is not overly difficult. If you understand something about COM
objects, it provides a viable method for sharing code between
Delphi and C++.
Using DLLs to link Delphi Code into C++Builder
C ++Builder can easily access a Delphi DLL containing
functions and procedures. If you want to access a Delphi object
implemented inside a DLL from C++Builder then you should read
the section below called Mapping Virtual Method tables.
If you have a Delphi DLL that you can call from inside
a Delphi project then you do not need to change it at all to call
it from inside C++ Builder. To get started, take the Delphi unit
that lists the methods in your DLL and add it to your C++ project.
C++Builder will automatically compile the unit and generate a
C++ header file. In particular, you should link the unit in using
the techniques described above in the section called "Ground
Rules for Linking Delphi Code into C++Builder Projects."
You will not be able to access data declared in your
DLL without first calling a function or procedure. This same limitation
applies to all DLLs, regardless of the language used to implement
them.
DLLs are useful when you have a large project that
you want to divide into several modules. By placing code in DLLs,
you can divide your projects into several binary files that can
be loaded in and out of memory at will.
Mapping Virtual Method Tables
Delphi objects stored in a DLL are normally out of
reach of Ebony projects. However, there is a way to get at them
by matching the VMT of the Delphi object to the VMT of a virtual
abstract, or "PURE", C++ object.
Consider the following Delphi declaration:
TMyObject = class
function AddOne(i: Integer): Integer; virtual;
stdcall;
function AddTwo(i: Integer): Integer; virtual;
stdcall;
end;
A virtual abstract version of the same object could
be declared in C++ like this:
class __declspec(pascalimplementation) TMyObject:
public TObject
{
public:
virtual _stdcall int AddOne(int i) = 0;
virtual _stdcall int AddTwo(int i) = 0;
};
To match up the VMT of the C++ object to the VMT
of the Pascal object, all you need to do is retrieve a pointer
to the object from the DLL. One way to do this is to export a
function from the DLL which is designed for this explicit purpose:
function CreateObject(ID: Integer; var Obj): Boolean;
var
M: TMyObject;
begin
if ID = ID_MyObject then begin
M := TMyObject.Create;
Result := True
end else begin
M := nil;
Result := False
end;
Pointer(Obj) := M;
end;
exports
CreateObject name 'CreateObject';
You can call this method from the Ebony project with
the following code:
typedef Boolean (_stdcall *LPCreateObject)(int ID,
void *obj);
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TMyObject *MyObject;
LPCreateObject CreateObject;
HANDLE hlib = LoadLibrary("OBJECTLIB.DLL");
CreateObject = (LPCreateObject)GetProcAddress(hlib,
"CreateObject");
if (CreateObject(1, &MyObject))
{
int i = MyObject->AddOne(1);
ShowMessage((AnsiString)i);
}
FreeLibrary(hlib);
}
The code shown here first declares a pointer to a
function with the same signature as the CreateObject routine in
the DLL. It then calls the standard Windows API functions LoadLibrary
and GetProcAddress in order to retrieve a pointer to CreateObject.
If the call succeeds, you can call CreateObject, return the address
of the object you want to call, and then call one of it's methods.
Notice that all the methods in the Pascal object
are declared virtual. This is necessary since you want to match
up the VMTs, or Virtual Method Tables of the two declarations.
If the methods weren't declared virtual, then there would be no
virtual method table, and the ploy would not work. You can declare
non-virtual methods in your object if you wish, but you will not
be able to call these methods from C++.
Note also that the order of the methods you declare
is very important. The names of the methods you want to call are
not important to the C++ implementation; all that matters is the
order in which the methods are declared.
You can use this same technique, or one like it,
to export any object from a Delphi DLL into a C++Builder or Borland
C++ project. You do not have to use GetProcAddress and LoadLibrary,
but could instead import a Delphi unit that exports CreateObject
directly into your C++Builder project. However, I use GetProcAddress
in this example since it forces you to imagine the exact steps
necessary to make this process work. In other words, it forces
you to think about the addresses that are being shared between
the DLL and the Ebony project.
Summary
In this paper you have learned that C++Builder has
an unprecedented ability to access the code from it's sister product,
Delphi. As a rule, you can simply link Delphi code directly into
you C++Builder projects.
The types of code that can be shared between Delphi
and C++Builder include forms, components, ActiveX controls, and
simple methods and functions. You can simply link this code directly
into your projects without any extra work.
This paper also examined using OLE or DLLs to share
code between C++Builder and Delphi. OLE can be useful if you want
to share code with not only C++ Builder, but also with other,
non-Borland, environments, such as Word, Visual Basic, or Excel.
DLLs are a great way to share code if memory management issues
are significant. In particular, you can load and unload DLLs from
memory during the life of your project.
|
Connect with Us