Aliasing DLL functions using the AS keyword (Functions/Subroutines/Programs)
Created at 09 MAY 1996 03:20PM
Aliasing DLL functions using the AS keyword
( and other tips for calling DLL functions from Basic+)
In Basic+, all variables are passed by reference and all expressions are passed by value. In other words, passing a variable as an argument to a procedure always allows the called procedure to modify that variable, but passing an expression does not. (As a result, you can pass a variable so that it can t be modified by passing it as an expression; to do so, append null to the variable if it is a string or add 0 to it if it is a number.) Also, Basic+ does not expose data typing, although variables are stored internally in a structure that contains one of several strong types, depending on the variable s value (numeric or text, integer or floating point, etc.) and depending on the last operation that affected the variable (numeric vs. string, etc.). Basic+ variables are referred to as variants , because their storage can vary based on the source of the variable s value and the operations being applied to the variable.
The C/C++ languages, which are used to create most DLL s, implement variables and parameters quite differently. Each variable has a strong (ie. specific) type, and only values of that type can be stored in that variable. Unlike Basic+, which handles conversions when a procedure is being executed, the C/C++ compiler accounts for conversions when the source is being compiled, and stops the compilation if the types can not be converted. What this means is that data must be passed to the DLL in the expected format or an error, possibly a GPF, can occur.
The second difference is that variables in C/C++ are always passed by value. To pass by reference, you must pass a pointer to the value instead of the value itself; in this case, the pointer is being passed by value but the called function can modify the data which the pointer is pointing to.
The third difference is that string values in C/C++ are always passed by pointer and the string values almost always end with a null terminator, char(0). In Basic+, all variables can contain binary data, including char(0), but in C/C++, char(0) is used to signify the end of a string. This means that string values passed to DLL functions should have a char(0) appended; for example:
Some_DLL_Function( This is an example of passing a null-terminated string : \00\)
Fourth, arrays and structures in C/C++ are always passed by pointer. Arrays and structures have to be constructed in a very specific manner in Basic+, and passing an array or structure to a DLL function requires that a pointer be passed or an error, possibly a GPF, can occur. Arrays are very easy to build using the powerful string manipulation operators that are built into Basic+. In version 3.1, a set of functions has been added to the language to support the creation and disection of complex C structures. Using these methods, arrays of structures, structures of arrays, and even arrays of structures of pointers to pointers to structures of arrays of pointers to double-precision floating point numbers are straight-forward to build.
Lastly, memory allocation is handled automatically in Basic+. When you concatenate two strings, the result is placed in a buffer large enough to contain it. In C/C++, the same operation could cause a GPF if the buffer is either not sized adequately large in the first place or not re-sized before concatenating the second string. Where this affects the Basic+ programmer is when a DLL function has to return a string, an array, a structure, or some other complex value. (Simple types are char, signed and unsigned integers, floating point values, and pointers; complex types are made of multiple simple types. A string, for example, is an array of type char.) When a DLL function returns a complex value, the caller (in this case a Basic+ procedure) must typically supply a location for the value to be placed in. Quite often the DLL function must know the size of the buffer (the location for the value to be placed in) as well. These two operation are most easily carried out using the built-in Basic+ functions len() and str(); for example:
* this is an example of a DLL function that returns an ASCIIZ
* (ie. null-terminated) string
Buf = str(\00\, 256) ;* pre-size a buffer for the return value
Some_DLL_Function(Buf, len(Buf)) ;* pass the buffer and its size
Value = Buf [1,\00\] ;* extract null-terminated return value
This finally brings us to the issue of the AS keyword. There are functions that can take different numbers of parameters (CDECL functions like wsprintf in the Windows API), functions that can take different types of parameters (the Windows API function GetProcAddress can take a number or a pointer to a string), and functions that either take a pointer to a string or a pointer to nothing (like the entry parameter in GetPrivateProfileString). While using functions that take a variable number of parameters is usually unnecessary from OpenInsight, (since most of them are used to format strings and it is easier to do that in Basic+ than to call DLL functions), the other examples above are quite common. Start with the declaration of GetPrivateProfileString (which is located in the DLL_KERNEL record in the SYSPROCS table):
SHORT PASCAL GetPrivateProfileString(LPCHAR,LPCHAR,LPCHAR,LPCHAR,SHORT,LPCHAR)
The LPCHAR parameter type tells OpenInsight to pass a pointer to the string value in the Basic+ variable. This is how strings and binary data (including C structures and arrays) are most easily passed to DLL functions. Calling this function from a Basic+ procedure is as easy as:
declare subroutine GetPrivateProfileString
Buf = str(\00\, 256)
GetPrivateProfileString( Window Designer : \00\, "lockcontrols : \00\, |
"0": \00\, Buf, len(Buf), oinsight.ini : \00\)
Controls_Locked = Buf [1,\00\]
(Note: the expression \00\ is the same as the expression char(0) .) The problem is that, to get a list of keys in a section, you must pass NULL (ie. a pointer whose value is 0) for the entry parameter. Because the GetPrivateProfileString function is declared as taking an LPCHAR for the entry parameter (the second parameter), you cannot pass a NULL pointer. If, for example, you pass the value 0, as below, OpenInsight will convert the number 0 to the string 0 and pass a pointer to that string, so the pointer itself will not be NULL:
GetPrivateProfileString( Window Designer : \00\, 0, \00\, Buf,
The same problem occurs if you pass (an empty string), since the pointer to that empty string will be passed instead of a NULL pointer. To reiterate, a NULL pointer has the value of 0, and a pointer that is pointing to anything will never have the value of 0.
The solution is to create an alias for the DLL function GetPrivateProfileString. To explain how this mechanism works, it is meaningful to understand how DLL functions are callable in Basic+ in the first place, and in order to understand that, it helps to understand how DLL functions are called in C programs. In a C program, calling a function looks a lot like calling a function in Basic+. For example, the above call to GetPrivateProfileString would be written:
char achBuf [256];
BOOL bLocked;
GetPrivateProfileString( Window Designer , "lockcontrols , "0",
achBuf, sizeof(achBuf), oinsight.ini );
bLocked = atoi(achBuf);
In C/C++, variables must be declared and are declared as a specific type, literal values automatically have a null terminator added by the compiler, the sizeof() operator returns the size of a variable, the atoi() function converts a string to an integer, and statements are terminated with semicolons. Other than that, the above code is basically identical to earlier Basic+ example. When the C program is loaded to be run, Windows analyzes a section of the executable code that contains a list of used DLL s and functions from those DLL s; each DLL that is used by the program is loaded using the API function LoadLibrary and a pointer to each function is obtained using the API function GetProcAddress. When the program calls a DLL function, like GetPrivateProfileString above, it first pushes each parameter onto the stack (the memory area referenced by the SS register in Intel processors) and then it calls the address of the the function as supplied by GetProcAddress. All of this processing is built into the program by the C/C++ compiler and linker or is handled by the Windows loader (which is part of the operating system).
In Basic+, the compiler has no knowledge of DLL functions or Basic+ functions, and there is no linking step. All of the processing is performed when the Basic+ procedure is executed; the Basic+ interpreter understands several different types of callable functions, including Basic+ and DLL functions. If the called function is a DLL function, then the interpreter pushes the parameters onto the stack, loads the DLL using LoadLibrary, gets the function address using GetProcAddress, and calls that function address. The information that the interpreter needs, then, is:
1. The name of the DLL
2. The name of the function
3. The number of parameters
4. The order in which the parameters are pushed onto the stack (CDECL or PASCAL)
5. The type of each parameter
6. The type of the return value
This information is all contained in the DLL declaration records, like DLL_KERNEL in the earlier example. When the Declare_FCNS routine is run for a DLL declaration record, it creates a compiled version of each DLL function declaration and stores each compiled declaration in the SYSOBJ table (system table for object code) using the name of the function. For example, the compiled declaration for GetPrivateProfileString is stored as the $GETPRIVATEPROFILESTRING record in the SYSOBJ table (since object code is prefixed with $ ).
Similarly, if a Basic+ function were named GetPrivateProfileString, then its compiled version would be stored in SYSOBJ as $GETPRIVATEPROFILESTRING. This is what allows Basic+ functions and DLL functions to be called in an identical manner from Basic+ procedures. The difference is that the compiled declarations for DLL functions include information that tells the interpreter to call a function in a DLL instead of executing the function as a set of Basic+ instructions.
What would happen, though, if someone changed the GetPrivateProfileString declaration to take an LPVOID for the entry name instead of an LPCHAR:
SHORT PASCAL GetPrivateProfileString(LPCHAR,LPVOID,LPCHAR,LPCHAR,SHORT,LPCHAR)
After running Declare_FCNS, a list of entries in a specific section of an INI file could be obtained using the following code, since the second parameter is now passed without modification as a pointer, and the value being passed is 0 (or NULL, as it is referred to in C/C++):
declare subroutine GetPrivateProfileString
List = str(\00\, 1024)
GetPrivateProfileString( Window Designer : \00\, 0, \00\, List, |
len(List), oinsight.ini : \00\)
FormDes_Entries = List [1,\0000\]
convert \00\ to @fm in FormDes_Entries
The side-effect of changing the function declaration from LPCHAR to LPVOID is that the original definition for GetPrivateProfileString would have been over-written. But because the name of the compiled declaration ($GETPRIVATEPROFILESTRING) does not affect which function in the DLL is called (since the $GETPRIVATEPROFILESTRING record contains the name of the DLL function to call, in this case GETPRIVATEPROFILESTRING), you can alias the DLL function to a new name using the AS keyword without breaking any code that uses the previous declaration of GetPrivateProfileString:
SHORT PASCAL GetPrivateProfileString(LPCHAR,LPCHAR,LPCHAR,LPCHAR,SHORT,LPCHAR)
SHORT PASCAL GetPrivateProfileString(LPCHAR,LPVOID,LPCHAR,LPCHAR,SHORT,LPCHAR) →
AS GetPrivateProfileSection
Now, two callable functions are created in SYSOBJ, $GETPRIVATEPROFILESTRING and $GETPRIVATEPROFILESECTION. They both call the GetPrivateProfileString function in the KERNEL DLL, but GetPrivateProfileSection passes the second parameter straight to the DLL as if it were a pointer, while GetPrivateProfileString obtains a pointer to the second parameter and passes that pointer to the DLL. This makes more sense if you consider how you would use GetPrivateProfileSection to accomplish what GetPrivateProfileString does; the following two calls are functionally equivalent:
GetPrivateProfileString(Section, Entry, Default, Buf, len(Buf), File)
GetPrivateProfileSection(Section, GetPointer(Entry), Default, Buf, len(Buf), File)
In other words, when you declare a DLL function as taking an LPCHAR, the DLL is called with a pointer to the parameter and not the parameter itself, and when you declare a DLL function as taking an LPVOID, the parameter itself is passed directly to the DLL function.
The LPCHAR declaration implies that the parameter is to be in string format, but other types are supported as well. For example, if a DLL function takes a pointer to a long integer, you can declare the function as taking an LPLONG, but just as with LPCHAR, you can not pass a NULL pointer unless you declare the function as taking LPVOID.
Lastly, passing arrays and structures to a DLL requires only a small amount of work. Following is an example of how to pass an array of structures, each containing an integer, a char array, and a pointer to a string. Assume that the C/C++ definition for the structure and the function are as follows:
typedef struct AcctTran_TAG {
int nTranID; // a transaction id number
char achTranType [8]; // either CREDIT or DEBIT
LPSTR lpszDescription; // an extended description of the transaction
} AcctTran, FAR * LPACCTTRAN;
// this function takes a pointer to an array of transaction records and the
// number of records in the array, and returns TRUE for successful processing
BOOL WINAPI ProcessAcctTran(LPACCTTRAN, UINT);
The DLL function ProcessAcctTran is declared in OpenInsight as:
USHORT PASCAL ProcessAcctTran(LPCHAR, USHORT)
The ACCTTRAN structure is defined using the Define_Struct utility as having three elements:
1. signed short
2. char[8]
3. signed long
The reason that the signed long type (the third element) is used instead of the pointer type is that the return value from the GetPointer Basic+ function is a number equivalent to the address of the variable, not an actual 4-byte binary pointer; the pointer type in the structure definition tool is only intended to be used when a pointer is manipulated as a 4-byte binary structure.
Assume that the data to pass to the DLL function is contained in a Basic+ record with three multi-valued fields: transaction ID, transaction type, and transaction description. The following Basic+ code builds the equivalent array of C structures and calls the DLL function, passing that array:
function DoProcessing(Trans)
declare function Build_Struct, ProcessAcctTran
$insert Logical
* count the number of transactions
NumTrans = count(Trans<1>, @vm) + (Trans<1> # "")
* dimension an array to hold the strings pointed to by each transaction
dim Desc(NumTrans)
* build the array of transaction records
Array = ""
Locked = ""
for i = 1 to NumTrans
* the structure contains a NULL pointer if there is no description or
* a pointer to the description if there is one
if len(Trans<3,i>) then
Desc(i) = Trans<3,i>
LockVariable Desc(i) as CHAR ;* ensure the pointer will not change
Pointer = GetPointer(Desc(i))
Locked<i> = TRUE$
end else
Pointer = 0 ;* NULL in C/C++ is defined as 0
end
* to build an array of structures, just build the structures and
* concatenate them
Array := Build_Struct("ACCTTRAN", Trans<1,i>, Trans<2,i>, Pointer)
next i
* call the DLL function
Success = ProcessAcctTran(Array, NumTrans)
* since we locked the decriptions so they would not move around in memory,
* we must now unlock them
for i = 1 to NumTrans
if Locked<i> then
UnlockVariable Desc(i)
end
next i
return Success
________
See Also: Programmer s Reference chapter 7: Calling DLL Functions from Basic+, Blank_Struct(), Build_Struct(), Declare_FCNS routine, Define_Struct routine, GetPointer(), GetValue(), LockVariable statement, Parse_Struct subroutine, Struct_Flush subroutine, Struct_Len(), Struct_To_Var(), UnlockVariable statement, and Var_To_Struct(), and Programmer s Reference Appendix B: Pointers and Structures in Basic+ (page 723).
Source code: