Using COFF C object files with Delphi X2 and above
(Originally published in codeproject.com)
This article applies to Delphi XE2 and above and Visual Studio 2010 and above.
People who develop software with Delphi and want to link extraneous object files are happy with Embarcadero for two reasons: the first one is because Embarcadero did not come up with any OMF (Object File Format) extension to use with the new 64-bit compiler, it just uses COFF. The second one is because the Delphi XE2 can also link with COFF object files when compiling for 32-bit (in addition to OMF).
Background
There are literally millions of lines of ‘C’ code waiting to be compiled and linked with Delphi in order to receive a nice GUI. I know that Embarcadero has a C++ Builder, but for pure (no GUI) C or C++ development, it stays behind Visual Studio in every respect.
How can we use “C” code compiled with Visual Studio inside Delphi? Till now, it was nearly impossible because Visual Studio compiles to COFF, Delphi understood only OMF, and converters from COFF to OMF were not reliable because the OMF used by Delphi has many proprietary undocumented portions.
I mentioned earlier that the latest Delphi release can read COFF object files, but I have not said that it is a piece of cake to actually link those object files with Delphi.
So, is it easy or not to link with Delphi code compiled with Visual Studio?
The answer is: “It is not difficult, just pay attention to a few details. It is even easier for 64-bit code.”
Using the Code
To show how it is done, let’s open Visual Studio, select New Project in the File menu, and go to the Visual C++ section. You can select the Empty Project on the General node, but is preferable to select either Win32 Console Application or Win32 Project on the Win32 Project node because this allows you to test the whole project in VS. After doing this, add a C++ file and a Header file from the Project menu. These two files are the ones where we are going to put the code that will compile and link with Delphi. In Solution Explorer, right-click the new C++ file you just added, select Properties, and in the sub-node Advanced of the C/C++ node, select Compile as C Code (/TC), and in the Precompiled Headers sub-node, select Not Using Precompiled Headers. Now, in the Project properties, General node, select Use Unicode Character Set. Disable all settings of the project that deal with exception handling to avoid complications (i.e., Enable C++ Exceptions=”No” and Buffer Security Check = “No”). Finally, set Whole Program Optimization to “No” in the Optimization sub-node.
You are set and can start coding away your C functions. When done, right-click on the source file and select Compile. Do compilations for 32-bit and 64-bit, then copy the .obj files to the folder where you have the Delphi project.
The Delphi linker has neither information about the parameters of the functions you defined in Visual Studio nor information about all the externals that need to be resolved at link time by the Delphi Linker. So, you need to declare all that in a Delphi unit.
To link with 32-bit Delphi programs, the golden rules are:
- All the function declarations will be proceeded by an underscore and the calling convention should be cdecl (all right, this is the default).
- All the externals should have the cdecl calling spec; if it is a call to a Windows API, you must use in VS an intermediate function with a cdecl calling spec (see our demo program to see how we solved the call to the
MessageBoxW
API). - Genuine externals with the cdecl calling spec are probably exports from msvcrt.dll and you can use this DLL from your Delphi program without the need to figure out replacement functions (see our demo program to see how).
To link Visual Studio object files with 64-bit Delphi programs, it is easier because:
- There is only one calling convention, fastcall.
- Function names are not underscored.
- Externals to the Windows API are directly resolved without our action.
- Using msvcrt.dll functions is just a question of declaring them in Delphi.
I provide a demo with full source code which you are recommended to download and study if you are not experienced in these matters.
The Delphi source file is shown below. Notice all the points I mentioned earlier. For 32-bit: underscored functions, cdecl calling convention, direct use of msvcrt.dll exported functions, and use of an intermediate function in Visual Studio with the cdecl extension when a call to Windows API (stdcall) is to be made from Delphi.
As you can see below, for 64-bit, it is easier mostly because there is only one calling convention.
interface
{$IF CompilerVersion < 23}
-> Requiires Delphi XE2 or later
{$IFEND}
uses
Winapi.Windows, Winapi.Messages, System.SysUtils,
System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm2 = class(TForm)
GroupBox1: TGroupBox;
lblFirstValue: TLabel;
edFirstValue: TEdit;
lblSecondValue: TLabel;
edSecondValue: TEdit;
btAddValues: TButton;
GroupBox2: TGroupBox;
lblCaption: TLabel;
lblMessage: TLabel;
edCaption: TEdit;
edMessage: TEdit;
btShowMessage: TButton;
lblDispStrings: TLabel;
Label5: TLabel;
GroupBox3: TGroupBox;
lblPubIntVar: TLabel;
edPublicIntVal: TEdit;
btGeetCVars: TButton;
btGetString: TButton;
lblPublicStrVar: TLabel;
edPublicStrVal: TEdit;
procedure btAddValuesClick(Sender: TObject);
procedure btShowMessageClick(Sender: TObject);
procedure btGeetCVarsClick(Sender: TObject);
procedure btGetStringClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
type
bigarray = array[0..127] of char;
{$IFDEF CPUX86}
{$L CtoDelphi32.obj}
function _addNumbers(value1 : integer; value2: integer):integer;cdecl;external;
function _wcscpy_s(S1:PChar; count: size_t; S2: PChar): Integer; cdecl;
external 'msvcrt.dll' name 'wcscpy_s';
function _wcscat_s(S1:PChar; count: size_t; S2: PChar): Integer;
external 'msvcrt.dll' name 'wcscat_s';
procedure _cShowGetMessage(incaption: string; intext:string;
size : integer; var retVal: bigArray);cdecl;external;
function _MessageBoxW2(theHwnd:HWND; lpText : PWideCHAR;
lpCaption : PWideCHAR; uType:UINT):integer; cdecl;
// Actually, these are not procedures but pointers to the VS variables:
procedure _publicCInteger;external;
procedure _publicCArray;external;
// Public variable to be accessed from C
var
_myDelphiPublicIntVariable : integer;
_myDelphiPublicStrVariable : string;
{$ELSE}
{$IFDEF CPUX64}
{$L CtoDelphi64.obj}
function addNumbers(value1 : integer; value2: integer):integer;external;
procedure cShowGetMessage(incaption:string; intext:string;
size : integer; var retVal: bigArray);external;
function wcscpy_s(S1:PChar; count: size_t; S2: PChar): Integer;
external 'msvcrt.dll' name 'wcscpy_s';
function wcscat_s(S1:PChar; count: size_t; S2: PChar): Integer;
external 'msvcrt.dll' name 'wcscat_s';
// Actually, these are not procedures but pointers to the VS variables:
procedure publicCInteger;external;
procedure publicCArray;external;
// Public variable to be accessed from C
var
myDelphiPublicIntVariable : integer;
myDelphiPublicStrVariable : string;
{$ENDIF}
{$ENDIF}
implementation
{$R *.dfm}
procedure TForm2.btGeetCVarsClick(Sender: TObject);
var
myCInt : integer;
begin
{$IFDEF CPUX86}
myCInt := integer((@_publicCInteger)^);
showMessage(inttostr(myCInt));
{$ELSE}
{$IFDEF CPUX64}
myCInt := integer((@publicCInteger)^);
showMessage(inttostr(myCInt));
{$ENDIF}
{$ENDIF}
end;
procedure TForm2.btGetStringClick(Sender: TObject);
var
myCArray : pchar;
begin
{$IFDEF CPUX86}
myCArray := pchar((@_publicCArray)^);
showMessage(myCArray);
{$ELSE}
{$IFDEF CPUX64}
myCArray := pchar((@publicCArray)^);
showMessage(myCArray);
{$ENDIF}
{$ENDIF}
end;
procedure TForm2.btAddValuesClick(Sender: TObject);
var
retValue : integer;
value1, value2 : integer;
begin
value1 := strToInt(edFirstValue.Text);
value2 := strToInt(edSecondValue.Text);
{$IFDEF CPUX86}
_myDelphiPublicIntVariable := strToInt(edPublicIntVal.Text);
retValue := _addNumbers(value1, value2);
{$ELSE}
{$IFDEF CPUX64}
myDelphiPublicIntVariable := strToInt(edPublicIntVal.Text);
retValue := addNumbers(value1, value2);
{$ENDIF}
{$ENDIF}
showMessage('Sum is '+inttoStr(retValue));
end;
procedure TForm2.btShowMessageClick(Sender: TObject);
var
retVal : bigArray;
arrayLength : integer;
begin
arrayLength := length(retVal);
{$IFDEF CPUX86}
_myDelphiPublicStrVariable := edPublicStrVal.Text;
_cShowGetMessage(edCaption.Text, edMessage.Text, arrayLength, retVal);
{$ELSE}
{$IFDEF CPUX64}
myDelphiPublicStrVariable := edPublicStrVal.Text;
cShowGetMessage(edCaption.Text, edMessage.Text, arrayLength, retVal);
{$ENDIF}
{$ENDIF}
showMessage(retVal);
end;
function _MessageBoxW2(theHwnd:HWND; lpText : PWideCHAR;
lpCaption : PWideCHAR; uType:UINT):integer; cdecl;
begin
result := MessageBoxW(theHwnd, lpText, lpCaption, uType);
end;
end.
All right, that’s all about it. I won’t show here the ‘C’ source files, they are pretty basic anyway and the most important task do be done in Visual Studio is its configuration (as I referred above). Have a careful look at the.vcxproj file.
A final note, in VS stdafx.h file there is a #define DEBUGGING
. You enable it to test the routines in VS, you disable it when you compile just the CtoDelphi.cpp file.
Download the VS2010 and Delphi XE2 32-bit/64-bit demo program source code Now!