Pascal Header Translation Specification
Version 1.01
This article is designed to explain the Borland specification for the
generation of Pascal translations of C/C++ Header files. I've tailored
the article for Project JEDI
engineers who are hoping to create header translations that can be used by
Borland on its web site or on the Delphi product CD itself.
This article is divided into two sections:
- The first part describes why Delphi R&D developed this particular
specification.
- The second part lists a set of 8 rules of the road that must be
followed if you want to conform to our specification.
The Pascal files discussed in this article are properly known as
Interface units, but to avoid confusion with the COM interface type, I
will usually refer to them simply as "header translations".
A further document will be presented later to outline the legal issues
and obligations for all participants.
Understanding the Specification
When reading this article, you need to keep five things in mind:
- You are aiming to perform a task commonly taken on by Delphi R&D. In
particular, you are creating files that at least theoretically can be
shipped as part of Delphi itself. As a result, you have to follow the
protocols and conventions followed by the Delphi team. You may believe,
or perhaps even in your heart of hearts be certain, that there are better
rules or guidelines than the ones defined in this article. That is an
interesting point of view, but it is not relevant to the current
situation. In this case you simply must follow the conventions that all
Borland engineers follow because you are producing files that will, at
least potentially, be a small part of the Borland product line. To follow
any other course of action will be to abandon any hope of having your
header translations become a standard accepted by Borland.
-
The header translations you create must be compatible with C/C++
Builder. In particular, C++ Builder will potentially be used to convert
your header translations back into C/C++, producing an .hpp file for use in
C++Builder projects. It turns out that it is a non-trivial task to
convert a Pascal file into a C/C++ header file, particular if types already
defined in C/C++ header files are redefined in a Pascal header translation.
As a result, some very stringent guidelines have to be followed. One of
the primary purposes of this article is to explain and define those
guidelines.
-
Two tools provided by Borland, called wpar.exe
and pp.exe, can aid in the creation of header translations. These
tools will be discussed in more depth later in this article. When creating
header translation files, you may decide to use a header translation
utility. These tools are a great start, but you will have to go through
your files manually in order to bring them into conformity with Borland's
standards. Here is a link to one such tool called HeaderConvert. If you would like other
tools of this sort to be listed here, send mail to Borland JEDI support.
- Submitters are responsible for verifying that what they submit is
valid and correct. We may not have the resources to check up on
everything that is submitted. We'll do code examinations and spot checks
to determine accept/reject status, but that's all we can commit to. It's
in a submitter's best interest to make sure their submissions are clean.
A rash of bug reports against a particular submitter's works will
radically lower the likelihood of our accepting further works from that
submitter.
- If your aim is to appear on the Delphi Product CD, you should
concentrate on Microsoft header files and avoid obscure header files that
may not be of interest to the whole community. Be particularly on the
look out for files that may represent some kind of copyright protection.
Subjects of particular interest to the Delphi team include MAPI, TAPI, and
BackOffice related files. We still want to encourage work outside the
main stream, but it is likely to appear on the Borland or JEDI web sites,
and not on the product CD. Finally, when creating your files, work
through Project JEDI to ensure that you are not duplicating work already
undertaken by someone else.
In the next two sub sections of this article I'm going to give you some
background on key tools and conventions used by Borland engineers when
creating header translations. In the process, I will address two main
topics:
- How Borland engineers save time by creating an intermediate file
called a PAR file, that can help you save work when wrestling with the
ANSI and Unicode versions of a function or type declaration.
- How Borland engineers create header translations that are compatible
with C++Builder.
Some of these matters will be covered in more depth in the third
section of this article. At this time, I only want to give you enough
information so that you will understand the relevance and reasoning behind
some of the rules laid out in the second section of this article.
PAR Files
Header translators should be creating not .PAS files but .PAR files.
PAR files contain most of what you see in a PAS file, but they omit the
ANSI and WideChar versions of a declaration. There is a utility called
wpar.exe that will translate PAR files into PAS files, and in the process
automatically generate the correct WideChar and ANSI declarations.
For instance, in the PAR file, you would write the following:
{#BEGIN}
... // Code omitted here for the sake clarity
function GetPrintProcessorDirectory{$}(pName, pEnvironment: LPTSTR;
Level: DWORD; pPrintProcessorInfo: Pointer; cbBuf: DWORD;
var pcbNeeded: DWORD): BOOL; stdcall;
{#END}
Take special note of the #BEGIN, #END and {$} symbols. After you ran
the file through wpar.exe, you would get the following declarations:
function GetPrintProcessorDirectoryA(pName,
pEnvironment: PAnsiChar;
Level: DWORD; pPrintProcessorInfo: Pointer; cbBuf: DWORD;
var pcbNeeded: DWORD): BOOL; stdcall;
function GetPrintProcessorDirectoryW(pName, pEnvironment: PWideChar;
Level: DWORD; pPrintProcessorInfo: Pointer; cbBuf: DWORD;
var pcbNeeded: DWORD): BOOL; stdcall;
function GetPrintProcessorDirectory(pName, pEnvironment: PChar;
Level: DWORD; pPrintProcessorInfo: Pointer; cbBuf: DWORD;
var pcbNeeded: DWORD): BOOL; stdcall;
Clearly this is a tool that can save you considerable time and effort.
It will also make it very easy to convert our header translation files
over to Unicode if Microsoft releases versions of Windows that are based
on Unicode.
EXTERNALSYM, HPPEMIT and NODEFINE
The header translation files that you create will need to be compatible
with C++Builder. In particular, C++Builder can read in Pascal source
files, and can automatically generate a header file with an .hpp
extension. This can be a delicate process, and some rules need to be
defined about how to proceed in certain cases.
It is wonderful that C++Builder can read in Pascal source files.
However, it is perhaps a bit ironic that whatever translations you create
are going to be duplicates of files that already exist in C++Builder. For
instance, if you create a translation of WinSpool.h called WinSpool.pas,
then the file you created will be mostly filled with duplications of
information already available to C++Builder. In fact, if C++Builder
simply converted your file back into C/C++, then there will now be two
versions of each symbol you translated, one in the WinSpool.hpp and one in
WinSpool.h. This can cause maddening confusion for C/C++ programmers who
are trying to compile and link their programs. The EXTERNALSYM directive
was designed to clear up this problem.
I say that C++Builder converts Pascal source files to .hpp
files. In fact, it is the Pascal compiler, dcc32.exe, that is called on
to make the conversion. Dcc32.exe ships with C++Builder.
When you use EXTERNALSYM in a Pascal source file, you are telling
C++Builder that it should not paste that particular symbol back into the
HPP file that it is generating. That way, there will not be two versions
of a particular symbol, one in the WinSpool.hpp and one in WinSpool.h.
Consider the following excerpt from a Pascal header translation:
PRINTER_CONTROL_PAUSE = 1;
{$EXTERNALSYM PRINTER_CONTROL_PAUSE}
The EXTERNALSYM statement shown here tells the C++Builder compiler not
to insert the PRINTER_CONTROL_PAUSE identifier back into the HPP file it
is generating. There is no need for this declaration since it is already
included in the original header file from which the Pascal translation was
made. So all the HPP file need do is include the original header file.
All of which brings us around to HPPEMIT, which is the symbol used to
tell the C++Builder compiler that it needs to include a symbol which is
not active in the Pascal source file. For example, the following text is
found at the top of WinSpool.pas:
(*$HPPEMIT '' *)
(*$HPPEMIT '#include ' *)
(*$HPPEMIT '' *)
The Pascal compiler will ignore this information. The C++Builder
compiler, on the other hand, will explicitly insert #include
into the top of the WinSpool.hpp file that it generates from WinSpool.pas.
Though you are unlikely to have reason to use it, I will also mention
the NODEFINE directive. It instructs the compiler to create a private
symbol. No definition will be output to the HPP file, but some
information may be output to the OBJ file. This might be helpful when
generating RTTI or debugging information, for example. It is the
responsibility of the programmer to define this type, if it is required,
using $HPPEMIT.
This directive is meant to be used when a type definition uses Pascal
idioms that cannot be easily translated into C/C++. You might use this for
Pascal code that has been translated from C/C++ and given a different name
than the C/C++ code. The following example is from the WinSock unit.
type
{$NODEFINE PFDSet}
PFDSet = ^TFDSet;
{$NODEFINE TFDSet}
TFDSet = record
fd_count: u_int;
fd_array: array[0..FD_SETSIZE-1] of TSocket;
end;
Another issue that comes up involves name mangling incompatibilities.
That subject, however, is fairly complex, and will be treated in a
second document, and perhaps also in the Delphi 5 and C++Builder 5
compilers themselves.
When you combine the effect of the EXTERNALSYM, NODEFINE and HPPEMIT
statements, you end up with a system that allows the C++Builder compiler
to properly process Pascal Header translations. All of this means extra
work for people who are creating the header translations, but the end
result is a file that is truly useful to the entire Delphi community, the
C++Builder community, and the Delphi development team itself.
Basic Rules of the Road
Now you know a little something about the special issues involved in
creating a Pascal translation of a C/C++ header file. With this information
in mind, it is possible for you to better understand the basic rules of
the road you need to follow if you want to create a file that conforms to
the Delphi team's standards.
I am going to present two versions of the rules of the road. The first
version is very short and concise, and can be used as a reference.
Immediately following my listing of the rules you will find a series of
sub-sections which explicate these rules so that you can better understand
how they work.
Here are the basic rules that must be followed:
- Maintain the original header's symbol name and casing.
- Mark all symbols that are actually defined in the original header as {$EXTERNALSYM }. This applies to types, records, objects, functions and procedures. Use HPPEMIT as necessary.
- Introduce T and P type aliases version for use in Delphi.
- Some functions take a parameter that is a pointer to a type. If the type can never be passed as nil, then it should be changed to a var parameter that is usually not a pointer.
- Macros should be converted to functions or procedures.
- There should be no IFDEFS in your Pascal translations.
- All COM interfaces should be native Delphi interfaces.
- Source-code formatting is important. No tabs should appear in a source file. Indent only two spaces for each indent level. Details for formatting will be laid out in a separate article.
Some of these rules are fairly easy to understand, but others require
some explanation. In the next few subsections I will go through these
rules one by one, adding a few paranthetical comments to help make their
meaning as clear as possible.
Rule 1: Maintain Original Case and Spacing
You must maintain original header's symbol name and casing. For
instance, if a C/C++ header defines a type as a WORD, then the Pascal
header should list the type as a WORD, not as Word. The emphasis on case
applies to both types and function declarations. Be careful not to define
a type in your header that is already defined somewhere else. For
instance, DWORD is declared in Windows.pas, so you should not redeclare it
in your file unless Microsoft has redeclared it there with a value
different than the one in Windows.h.
Remember: The Pascal compiler will output exactly what the programmer
typed, each and every time the symbol is used. If you declare a symbol in
a Pascal header translation, and get the case wrong, it won't matter much
in Pascal programs, but it probably will cause big problems when processed
by C++Builder. This is particularly true when it comes to the EXTERNALSYM
issue, described in the next section.
There are also some useful cases where casing allows conflicts to be
avoided. consider if the Pascal source is being translated and it uses a
reserved word such as 'this' for a parameter name. It can be cased as
'This' and then the translation goes without a problem.
Rule 2: Mark Defined Symbols with EXTERNALSYM
Mark all symbols that are actually defined in the original header as
{$EXTERNALSYM <Ident>}. This applies to types, records, objects,
functions and procedures. As mentioned in the previous section, be
particularly careful about case!
Consider this definition from Winspool.h:
#define PORT_TYPE_WRITE 0x0001
It should appear in the Pascal file as follows:
PORT_TYPE_WRITE = $0001;
{$EXTERNALSYM PORT_TYPE_WRITE}
This ensures that the C++Builder process that translates the file back
into C/C++ knows that this symbol is already defined in a header file and
does not need to be re-declared. In fact, the process of re-declaring a
symbol in two places can cause considerable confusion for C/C++ programmers,
so it is important that we be both correct and polite in this regard.
It is best if you insert the EXTERNALSYM statement immediately after
your declaration for the code. It is not an error to place it immediately
before the Pascal declaration, or even in some remote corner of the file
such as the end. However, the file will be processed faster if you place
the EXTERMANSYM statement immediately after the Pascal declaration.
Because of the need to maintain unified standards, you could risk having
your header rejected by Borland if you failed to follow conventions on
this matter.
EXTERNALSYM also helps with the translation of particular types that
receive special treatment in Delphi. Consider the LPCTSTR type in this
record from the Windows.par file:
tagWNDCLASSEX{$} = packed record
cbSize: UINT;
style: UINT;
lpfnWndProc: TFNWndProc;
cbClsExtra: Integer;
cbWndExtra: Integer;
hInstance: HINST;
hIcon: HICON;
hCursor: HCURSOR;
hbrBackground: HBRUSH;
lpszMenuName: LPCTSTR;
lpszClassName: LPCTSTR;
hIconSm: HICON;
end;
Here is what the same record looks like in Windows.pas:
tagWNDCLASSEXA = packed record
cbSize: UINT;
style: UINT;
lpfnWndProc: TFNWndProc;
cbClsExtra: Integer;
cbWndExtra: Integer;
hInstance: HINST;
hIcon: HICON;
hCursor: HCURSOR;
hbrBackground: HBRUSH;
lpszMenuName: PAnsiChar;
lpszClassName: PAnsiChar;
hIconSm: HICON;
end;
As you can see, lpszMenuName and lpszClassName are declared as
PAnsiChar in Windows.pas. The conversion from LPCTSTR to PAnsiChar was
taken care of automatically by wpar.exe. As a result, you need not
concern yourself with the translation directly, but you should understand
that it is happening.
If you look back a WNDCLASSEXA, you will see that it is declared as
packed record. This directive effects the way the compiler aligns the
data in the record when it is displayed in memory. The subject of
properly aligning your data is complex enough that it will be treated in
separate document, which is currently still in draft form.
Rule 3: Declare Both Static and Pointer Versions of a Type
You should introduce both T<Ident> and P<Ident> type alias versions for
use in Delphi. Consider the following C/C++ struct:
typedef struct _PORT_INFO_3A {
DWORD dwStatus;
LPSTR pszStatus;
DWORD dwSeverity;
} PORT_INFO_3A, *PPORT_INFO_3A, *LPPORT_INFO_3A;
In Pascal, you will need to come up with both a PPortInfo3 and a
TPortInfo3 type alias.
This can be a bit more complicated than it might at first appear, in
part because you need to take into account by ANSI and WideChar versions
of a type, as discussed later in this article.
You should not, of course, create EXTERNALSYM statements for these
T<Ident> and P<Ident> types. EXTERNAL SYM is only for types that are
already declared in existing C/C++ headers.
Rule 4: var Parameters and Pointers
Some functions take a parameter that is a pointer to a type. If the
type can never be passed as nil, then it should be changed to a var
parameter and the type changed to the appropriate non-pointer type.
Consider the following C/C++ declaration from Winspool.h:
BOOL
WINAPI
EnumPrintersA(
DWORD Flags,
LPSTR Name,
DWORD Level,
LPBYTE pPrinterEnum,
DWORD cbBuf,
LPDWORD pcbNeeded,
LPDWORD pcReturned
);
Here is the Pascal translation of this type:
function EnumPrintersA(Flags: DWORD; Name: PAnsiChar;
Level: DWORD; pPrinterEnum: Pointer; cbBuf: DWORD;
var pcbNeeded, pcReturned: DWORD): BOOL; stdcall;
Notice that the last two parameters are passed as var parameters of
type DWORD, even though they are declared as pointers to a DWORD in the
original C/C++ header. If it were legal to call EnumPrintersA as follows,
then the last two parameters would have to be declared in Pascal as
pointers:
EnumPrintersA( 0, "SomeName", 0, nil, 0, nil, nil);
The problem is that in this evocation of the function, nil is being
passed in the last two parameters. The compiler will obviously give a
type mismatch if it is expecting a DWORD and gets a pointer value (nil)
instead. Now, in this particular case, Microsoft does not allow you to
pass nil in either of the last two parameters of this function. As a
result, it is okay to declare the function as a DWORD passed by reference.
Doing so will make the function easier for all Pascal programmers to call.
Needless to say, the act of deciding which parameters can be passed as
nil and which can't is a very tedious job. There is simply no way for it
to be done mechanically. It must be done by hand, probably with a copy of
the Windows Help file open next to you. If the help file explicitly says
that you can pass nil in a particular parameter, then you are not going to
be able to declare it as a var parameter.
A related subject involves the possible use of function overloading in
header translations. Though this subject shows considerable promise, for
now this practice will be discouraged until additional experience with the
subject leads to a either a complete rejection of the practice, or a
series of clearly defined rules on how the technology can best be used.
We will not reject the use of overloading out of hand, but if problems are
found with your use of overloading, then the entire header could be
rejected.
Rule 5: Translate Macros into Functions or Procedures
Delphi does not support macros. As a result, macros should be
converted to functions or procedures. If necessary, they should be
appropriately marked {$EXTERNALSYM}. Do not use the stdcall calling
convention for macros.
Rule 6: No IFDEFs in a Pascal Header Translation.
There should be no IFDEFs in a Pascal header translation. There are no
exceptions to this rule.
Since header translation files ship with Delphi, there is no need for
alternate definitions to support previous products. Borland doesn't
support using files from the current version of the product with previous
versions of the product. If these files were distributed independently of
the product, then yes, IFDEFs might be justifiable, but not for files that
ship with the product.
IFDEFs are discouraged because they are only useful if you intend to
rebuild the source. That defeats all of the productivity and performance
advantages of precompiled DCUs and DCPs and makes for some nasty
versioning headaches.
A simple Pascal pre-processor, called pp.exe, can be used to strip
IFDEFs out of your header translation files. This way you can maintain a
header file which is IFDEFed for different versions of the compiler.
However, when you submit the header translation to Borland, you should run
pp.exe on it so that it strips out all the IFDEFs, keeping only the code
relevant to the current version of Delphi.
Rule 7: COM Interfaces Should be Native Delphi Types
All COM interfaces should be native Delphi interfaces. Delphi supports
a native type called Interface, and you should use this type when creating
Pascal header translations. For instance, here is the Delphi declaration
for the IUnknown interface:
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID;
out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
As you can see, this type is declared not as a class, but as an
interface.
As per rule 6, there can be no IFDEFs for pre D3 versions. Our Delphi
header translations cannot support D1 and D2 versions or else they will no
longer be compatible with C++Builder.
Rule 8: Formating your Code Source-code formatting is very
important to the Delphi team. Do not include any tabs in any of your
source files. Indent only two spaces for each indent level.
You now know the key rules which must be followed when creating Pascal
header translations. Computer programming by its nature must be goverened
by very strict rules, and we have laid out a number of fairly stringent
ones in this document. Nevertheless, the Delphi R&D team and others at
Borland are excited about the possibilities inherent in this project. We
are proud of our development community and enjoy working with them.
Everyone believes that this will turn out to be a rewarding project for
all involved, and we extend our sincerest thanks to all who decide to
participate in this project.
Addendum
When translating SDK integer constants to Delphi syntax, maintain
the hexidecimal or decimal notation used in the original SDK.
When declaring 4-byte hexidecimal constants which have the high bit set,
you should use a typecast to prevent the Delphi compiler from promoting
the unsigned hexidecimal value to Int64.
Example (from windows.pas)
const
GENERIC_WRITE = $40000000;
GENERIC_READ = DWORD($80000000);
HRESULT constants: Any constant that will be used as an OLE / COM
result code should be cast as an HRESULT.
Interface declarations: Always put the IID (GUID) of an interface in the
Delphi interface declaration. This may require some digging, since the
SDK C headers usually do not include the interface IIDs. Look for a
corresponding .IDL files in the SDK which will contain the complete
interface definition.
IID constants: Since the Delphi interface types already contain their IIDs
and the compiler automatically extracts the IID when you pass an interface
to a GUID parameter, the SDK IID_ constant identifiers only need to exist
for SDK documentation compatibility. Instead of declaring the GUID data
twice (once in the interface, again in the IID_ constant), just declare
the IID_ constant as an alias to the actual interface. This has less
maintenance risk and makes DCUs and EXEs a little smaller. It's also a
lot easier to type.
Example:
const
IID_IPersistMoniker = IPersistMoniker;
|