Writing DLLs for CTD using Delphi

convoyafternoonDéveloppement de logiciels

13 nov. 2013 (il y a 7 années et 11 mois)

303 vue(s)

Writing DLLs for CTD using Delphi
Joe Meyer, Ice Tea Group, LLC.
Centura Team Developer is a great tool that comes with lots of ready made functions and almost
everything you need is already present. Well, almost…
From time to time the functionality of CTD has to be expanded. Maybe because something is needed
that can't be done easily using Sal language or maybe because you need the speed of native compiled
code. Of course, you could start writing a COM object, import the interface using CTD's ActiveX
explorer and then go through all that variant programming hell that's required when using COM in
Mmh, COM isn't exactly what you've been trained for? Heard of it but don't know how to develop COM
objects? Ok, then you'll probably want to use Visual C++ to write a DLL. If this puts a smile on your
face and you feel your fingers itching to start Visual Studio, you'll probably don't want to read this
If you also know how to deal with Delphi, Borland's (or whatever their name is this year) famous
Pascal development tool, then you might ask yourself whether it's possible to write DLLs using Delphi
and then interface this DLL in CTD.
Well, there's good news and there's bad news. Good news is that Delphi is actually very good in writing
DLLs that integrate into CTD just fine. Bad news is, that there's a few pitfalls and you have to know a
few things to make your DLL project successful.
I’ll start my article with a comparison of string types and then move on to other types as well.
One of these things is not like the other
Delphi strings
Unlike almost every other programming language, Pascal has a special string type (sigh).
Back in the old times when Pascal was invented until the time before Borland launched the first 32-bit
Delphi for Windows, a Pascal string was limited to a length of 255 characters. Even worse, the first
byte of the string held the length of the string.
The new string type was introduced with the first Delphi 32-bit version which made the 255 character
limitation obsolete. Today's current version (Delphi 5) doesn't limit the length of strings (well, Windows
does, though). But fortunately there's also another string type in Delphi: PChar. The PChar is a pointer
to a character array, just like the C style char*. Delphi also provides routines to manipulate this string
C strings
C doesn't have a built-in data type such as String. Strings are just arrays of characters instead. The
length of the string is specified by a binary zero following the last character in the array. C's libraries
provide quite a few functions to handle these special character arrays.
CTD strings
CTD has 2 types of strings: String and Long String.
The Long String is actually a String and behaves exactly like a String so what is it good for anyway? To
answer this question we have to take a little excursion into database programming, to be exact: how
to get data in and out through the programming interface of a database.
As you certainly know, SQL databases usually have different data types, such as char and blob.
The char data type is used to store strings of certain length and it can be used in a WHERE clause and
to join tables. With a blob field you can't do that. But what to do if you want to store strings of more
than the maximum length of 255 characters (length depends on the type of SQL database)? Well,
simply use a blob field and store any amount of characters in it. But, since almost nothing comes for
free, you loose the possibility to use this data as a selection criteria, nor can you use it as an index.
Why did I tell you that? It's to make you realize that char fields are different from blob fields. Even
worse, they're handled completely different in the communication between client and server. Blob
fields are retrieved and sent using a special procedure internally.
In order to tell the SQL server what type you're actually accessing, you must use an appropriate type
to let the SQL server know whether the string is a char type or a blob. Simple as that, you only have to
use CTD's String type for char and varchar of any length up to the maximum length supported and use
Long String for those strings that are stored in a blob field. And this is only relevant in SQL
statements. In plain Sal language you can use one or the other, it makes no difference. You can even
assign a Long String to a String.
But there's another issue with CTD: it uses opaque handles to store the strings internally. These
handles aren't compatible to any other compiler. But that’s not really a problem because when
declaring an external function that expects a string as a parameter, you can simply pass a standard
CTD string and it will be converted to LPSTR on the fly.
In order to enable CTD to access DLLs and pass and receive strings, you can declare a DLL's function
parameter like this:
This advises CTD to convert the internal handle to a standard C style character array and pass it to the
DLL's function.
It gets a bit more complicated when the DLL’s function returns a string and that is explained in the
next chapter.
Handle or Pointer, that's the question
There are two ways in general to declare a string parameter to be compatible with CTD: PChar and
Handle. The PChar way is the easiest. CTD will handle the conversion from Handle to C style string and
pass a char* to the DLL. Since Delphi has a compatible type, you can declare it as PChar in Delphi and
it will work just fine.
Here's a small example to outline the way of coding:
Procedure DoNothing (AString:PChar); stdcall;
ShowMessage (AString);
CTD's External declaration:
Function: DoNothing
String: LPSTR
No big deal, right? But what if you want the parameter to return a value?
You might come up with the following code:
Procedure DoNothing (AString:PChar); stdcall;
StrCopy (AString, 'Return value');
CTD's External declaration:
Function: DoNothing
Receive String: LPSTR
Not much different, isn't it? Well, not on first sight
When calling DoNothing in Sal language, you need to initialize the string that's passed to the DLL.
Typically you'll use SalStrSetBufferLength() to allocate some memory that's large enough to hold the
entire content that's being returned from the DLL.
While this will work in general, I find it a bit ugly to call SalStrSetBufferLength() each time I call the
DLL's function. Wouldn't it be nice if Delphi would accept a native CTD string handle and modify it
directly? Without having to call SalStrSetBufferLength() first? I bet your answer is yes!
Now lean back or get yourself a glass of wine because you don't have to find the solution yourself. As
you already know, CTD can call functions residing in an external DLL. But the other way around,
external functions are able to call CTD functions within the DLL. Cool, isn't it?
The string type within CTD is just a 4-byte handle and we can declare it in Delphi like this:
THString = DWORD;
The DLL that exports CTD's functions is called CDLLI20.DLL. The number in the DLLs name simply
reflects the version of CTD. For version 1.5 the DLL would be called CDLLI15.DLL.
This DLL exports a few functions which are used to handle CTD’s internal string type:
From the perspective of a Delphi programmer this is ugly code so let's see if we can do better when
declaring the interface to the DLL in Pascal language:
DLLName = 'cdlli20.dll'; // cdlli15.dll when using CTD1.5
function SWinInitLPHSTRINGParam (var StringHandle:THString; Len:DWORD) : BOOL; stdcall
external DLLName;
function SWinStringGetBuffer (StringHandle:THString; var Len:DWORD) : PChar; stdcall
external DLLName;
Much nicer, isn't it? Ok, ok, we’ve made it beautiful but how do we deal with it?
SWinInitLPHSTRINGParam is the key to create CTD strings. It takes 2 parameters, a handle and a
length. The handle will be created by the CTD runtime if you initialize it with 0 prior to calling this
function. The second parameter tells the runtime how many bytes to allocate for the actual string and
this is how to call the function:
Hdl : THString;
Hdl := 0;
SWinInitLPHSTRINGParam (Hdl, 200);
The above sample creates a CTD string handle that points to a 200 byte string buffer. Fine, but how to
stuff the text into that buffer? Nothing easier than that.
Look at the SWinStringGetBuffer function. It expects 2 parameters. The first one is the handle that we
just created. The second one returns the length of the buffer where this handle is pointing to. The
result of the function is what we need: a PChar pointer that points directly to the buffer. This pointer
can be used to manipulate the string’s content.
Now we have all we need to create a CTD native string and fill it with text but what if the Delphi DLL
receives a handle from the calling CTD application? The answer: simply forget about the
SWinInitLPHSTRINGParam function because the handle is already created and most probably will
already contain text. All you need is to call SWinStringGetBuffer and you get the length of the string
plus a pointer to it’s content.
Though the above already worked fine, I was still unsatisfied. I like my Pascal functions to be most
flexible and easy to handle so I wrote a few wrapper functions that are more intuitive:
function SWinCreateString (StringValue:PChar) : THString; overload;
function SWinCreateString (StringValue:String) : THString; overload;
function SWinCreateString (StringLength:DWORD) : THString; overload;
procedure SWinSetString (StringHandle:THString; Value:PChar); overload;
procedure SWinSetString (StringHandle:THString; const Value:string); overload;
function SWinGetString (StringHandle:THString) : string; overload;
procedure SWinGetString (StringHandle:THString; var Value:PChar); overload;
function SWinCreateString (StringValue:PChar) : THString;
Len : DWORD;
result := 0;
if SWinInitLPHSTRINGParam (result, StrLen (StringValue) + 1) then
StrCopy (SWinStringGetBuffer (result, Len), StringValue);
function SWinCreateString (StringValue:String) : THString;
result := SWinCreateString (PChar (StringValue));
function SWinCreateString (StringLength:DWORD) : THString;
SWinInitLPHSTRINGParam (result, StringLength);
procedure SWinSetString (StringHandle:THString; Value:PChar);
SWinSetString (StringHandle, string (Value));
procedure SWinSetString (StringHandle:THString; const Value:string);
Len : DWORD;
Len := Length (Value);
if SWinInitLPHSTRINGParam (StringHandle, Len + 1) then
StrPCopy (SWinStringGetBuffer (StringHandle, Len), Value);
function SWinGetString (StringHandle:THString) : string;
Len : DWORD;
result := StrPas (SWinStringGetBuffer (StringHandle, Len));
procedure SWinGetString (StringHandle:THString; var Value:PChar);
Len : DWORD;
Value := SWinStringGetBuffer (StringHandle, Len);
The above functions provide means for creating, setting and getting strings in many ways. It is
possible to create strings by passing either a PChar or a Delphi native string type. Setting and getting
a string’s content can also be done by using either PChar or the native string type.
You might have noticed already that there are some functions with identical names but different
parameters. They are marked as overloaded so the compiler will decide by the list of specified
parameters which function has to be called.
The functions themselves don’t do any spectacular things, they just use SWinInitLPHSTRINGParam and
SWinStringGetBuffer to access the strings.
Armed with the above functions it should be a snap to handle CTD strings inside Delphi so go ahead
and use them. I don’t want to see any SalStrSetBufferLength in your CTD applications anymore.
A Number is a Number is a Number
CTD knows just 1 single type of numbers. It doesn’t care whether the number has decimals or not,
everything is a number. By the way, even a Boolean is a number internally.
Delphi knows various types of numbers:
Type Range Size
Integer -2147483648..2147483647 32 Bit signed
Cardinal 0..4294967295 32 Bit unsigned
Shortint -128..127 8 Bit, signed
Smallint -32768..32767 16 Bit, signed
Longint -2147483648..2147483647 32 Bit, signed
Int64 -263..263 –1 64 Bit, signed
Byte 0..255 8 Bit, unsigned
Word 0..65535 16 Bit, unsigned
Longword 0..4294967295 32 Bit, unsigned
Again CTD provides functions to convert to and from the internal Number type:
BOOL CBEXPAPI SWinCvtDoubleToNumber(double, LPNUMBER);
BOOL CBEXPAPI SWinCvtNumberToDouble(LPNUMBER, double FAR *);
Because the Pascal programmer hates C code he’ll probably want to translate the functions to
something like this:
function SWinCvtIntToNumber(Value:integer; var Num:TNumber) : BOOL; stdcall external
function SWinCvtWordToNumber(Value:word; var Num:TNumber) : BOOL; stdcall external
function SWinCvtLongToNumber(Value:integer; var Num:TNumber) : BOOL; stdcall external
function SWinCvtULongToNumber(Value:DWORD; var Num:TNumber) : BOOL; stdcall external
function SWinCvtDoubleToNumber(Value:double; var Num:TNumber) : BOOL; stdcall external
function SWinCvtNumberToInt(Num:PNumber; var Value:integer) : BOOL; stdcall external
function SWinCvtNumberToWord(Num:PNumber; var Value:word) : BOOL; stdcall external
function SWinCvtNumberToLong(Num:PNumber; var Value:integer) : BOOL; stdcall external
function SWinCvtNumberToULong(Num:PNumber; var Value:DWORD) : BOOL; stdcall external
function SWinCvtNumberToDouble(Num:PNumber; var Value:double) : BOOL; stdcall external
As you might have noticed, the above functions make use of a type TNumber. This type is used for
CTD's internal Number format and is declared as follows:
TNumber = packed record
numLength : byte;
numValue : array [0..23] of byte;
PNumber = ^TNumber;
You see, TNumber is a record of 24 bytes, preceeded by a length byte. PNumber is just a pointer to a
TNumber type. The record has to be declared using the packed directive. This makes sure that the
compiler will not expand the size of the numLength byte to align to even boundaries, in other words it
prevents the compiler from filling in 3 bytes between numLength and numValue.
The length byte is very important because it can tell us if a Number is NULL. A Number is NULL when
the length is 0. In order to have a convenient way to check this, I wrote the following highly
sophisticated function:
function NumberIsNull (const Num:TNumber) : boolean;
result := Num.numLength = 0;
The function returns TRUE when a Number is NULL by simply checking the numLength part for 0.
Actually the number conversion routines are pretty straightforward but for the sake of clarification I
wrote 2 small functions that make use of the routines and demonstrate how to use them:
function ShiftLeft (Value:integer) : TNumber; stdcall;
num : TNumber;
SWinCvtIntToNumber (Value shl 1, Num);
result := num;
function ShiftRight (Value:integer) : TNumber; stdcall;
num : TNumber;
SWinCvtIntToNumber (Value shr 1, Num);
result := num;
The functions take an integer datatype, shift the value bitwise and return the result as a native
Number type.
So that’s it for today. You’ve learned how to avoid the Visual C++ beast and use cute Delphi instead. If
you’re already experienced in programming Pascal, all the above shouldn’t be a problem for you at all.
I hope you enjoyed it.