Can't find the information you are looking for here? Then leave a message over on our WinBatch Tech Support Forum.
The WinBatch documentation for the extremely versatile and powerful DLLCall
function is
pretty sketchy. To their credit, they warn you that you need a lot of knowledge beyond what is in
their documentation to get the most from this function. The purpose of this article is to supply
some of the missing detail and to serve as a supplemental reference for WinBatch programmers.
There are two, and only two, ways to pass data to a function.
WIL passes most variables by value. The exception is arrays, which are always passed by reference if you pass the entire array. Individual array elements are passed by value, however.
Conversely, both Visual Basic and VBA pass values by reference, except for whole arrays,
which are always passed by reference. Except in the case of arrays, however, you can
override this behavior by preceding the argument name by the ByVal
keyword when you
declare the function.
Because Windows API functions, and other functions that follow the WINAPI
calling
convention pass data by value, you will see this keyword in documentation for calling Windows API
functions from within Visual Basic and VBA programs.
There are two, and only two, ways to return data from a function.
To understand why things are the way they are, you must understand a bit about how function calls work, in general, and how they work on Intel processors, in particular.
Traditionally, information is passed from main programs to functions and subroutines by assembling a list of the things the function or subroutine needs to know, then passing that list to the function in a specific way, so that the function knows where to find the external data that it needs. There are various ways to accomplish this, and the method used is at least partially processor dependent.
Intel processors call programs by constructing a stack frame, containing the list of arguments or their values and the address of the next instruction in the calling program to execute when the called function returns. This is known as the return address, and is always required when a function or subroutine is called, no matter what processor is doing the calling, even if the function has no arguments, or external data requirements. The stack frame is pushed onto a stack, a standard type of data structure for which the Intel processors provide nice built-in support. The called function removes the arguments from the stack, in the inverse of the order in which the caller added them. The last thing that gets put onto the stack, and the first that comes off, is the return address. The called function saves the return address and uses it to reset the Instruction Pointer, an internal register used by all processors, to effect the return to the caller, which resumes where it left off.
Though stacks are a very convenient way to pass data from caller to called function, they suffer from the severe limitation that all stacks are a fixed size. To lessen the impact of this limitation, most things bigger than a machine word (or a memory address) are passed by reference, rather than by value. Integers and long integers (DWORD values) are usually passed by value because it takes the same amount of stack space to pass the value as it does to pass a reference to it, and the called function has less work to do to obtain the value.
Though these matters are not a concern in day to day programming, it is essential for you to understand these concepts if you intend to delve into any but the most trivial Windows API functions. As you shall see in the example presented in this article, it may be shocking how complex it can be to do seemingly simple things, like get the NetBIOS machine name of your computer. I hope that the foregoing explanation will help you understand why this is so.
Though the WinBatch documentation covers the basic argument, or parameter, types that you will
encounter in calling into the Windows API and other DLLs that follow the WINAPI
calling
convention, some of the more obscure types are omitted, and there is hardly a word about
structures, which are required by many of the more powerful functions. The table below lists
all the argument and return types of which I am aware. For the sake of completeness, I include the
types covered in the WinBatch documentation. The notes following the table explain and illustrate
how to use each of these types. These notes comprise the majority of this article.
C/C++ Type | WinBatch Type | Size (Bytes) | Comments |
BOOL | @true/@false |
4 | This is usually a return value. This value is returned as a Long Integer with a value of -1 for True and 0 for False. See Note 1. |
WORD | word |
2 | On current 32 bit platforms, this is a 16-bit integer. WinBatch and Visual Basic programmers must keep in mind that any function that expects a word value is implicitly expecting a 16 bit signed integer. In plain English, this means a whole number between -32,767 and +32,767. See Note 2. |
INT or INTEGER | word |
2 | On current 32 bit platforms, this is a 16-bit integer. WinBatch and Visual Basic programmers must keep in mind that any function that expects a word value is implicitly expecting a 16 bit signed integer. In plain English, this means a whole number between -32,767 and +32,767. See Note 2. |
UINT | word |
2 | On current 32 bit platforms, this is a 16-bit integer. WinBatch and Visual Basic programmers must keep in mind that any function that expects a word value is implicitly expecting a 16 bit unsigned integer. In plain English, this means a whole number between 0 and +65,535. See Note 3. |
DWORD | long |
4 | This is a signed integer of 32 bits, the size of a machine word on Intel i386, i486, and Pentium processors. In plain English, this is a whole number between -2,147,483,647 and +2,147,483,647. See Note 4. |
LONG | long |
4 | This is a signed integer of 32 bits, the size of a machine word on Intel i386, i486, and Pentium processors. In plain English, this is a whole number between -2,147,483,647 and +2,147,483,647. See Note 4. |
LPSTR | lpstr or lpbinary |
4 | This is a long pointer (32 bit memory address)
to a null-terminated string. A string can contain anything except a null
(Num2Char ( 0 ) ) character. If you are passing a string to a function
and the function will not modify its value, you can pass any WinBatch variable
using a type of lpstr . However, if the called function will modify the
string, you must allocate and pass a binary buffer using an argument of type
lpbinary and passing the handle returned by the WIL BinaryAlloc ( )
function. See Note 5.
|
LPDWORD | lpbinary |
4 | This is a long pointer (32 bit memory address)
to a DWORD, or Long Integer. The reason it's a pointer instead of the integer itself
is that the called function needs to write something into it. The function is requesting a
memory location big enough to hold a long integer. Unlike the LPSTR discussed above,
you must always allocate and pass a binary buffer using an argument of type
lpbinary and passing the handle returned by the WIL BinaryAlloc ( )
function. See Note 6.
|
HANDLE | long |
4 | This is a long pointer (32 bit memory address)
to a memory location that contains structured data that is maintained by Windows.
Handles are always returned by some Windows function, such as GetFocus ( ) ,
which returns a window handle. Some WinBatch functions, such as FileOpen ( )
and BinaryAlloc ( ), return handles, too. You can treat a handle as a long
unsigned integer.
|
Structures | lpbinary |
4 | This is a long pointer (32 bit memory address)
to a memory location that contains structured data, defined by a TypeDef Struct
block in a C/C++ header file. Structures consist of multiple data elements, possibly of
several types, aligned in a specific order, which are manipulated as a unit. See
Note 7.
|
1 | Windows functions that return a
The above example takes one action (collects and returns the computer name from the buffer)
if the call to Another way to code this same call is like this, using the
The above example does something (reports an error and returns a blank string) if the
call fails. Otherwise, execution continues with the statement just after the
|
||||||||||||||||||||
2 | Calling this a WORD or an INTEGER is a holdover from 16-bit Windows, which was designed for machines (the Intel 8086 and 80286) that had a machine word size of 16 bits. |
||||||||||||||||||||
3 | The distinction between signed and unsigned integers is rather arcane and can be ignored most of the time because WinBatch, Visual Basic, and most other modern languages take care of it for you and hide the details from you. Both short and long integers are stored internally as a string of zeros and ones. Strings of 16 bits are called Integers and strings of 32 bits are called Longs, for historical reasons discussed in Notes 2 and 4. Strings of bits are numbered from right to left, starting at zero, just as you would number strings of decimal digits as Units, Tens, Hundreds, Thousands, and so forth. Consequently, the leftmost bit is called the MSB, or Most Significant Bit. Conversely, the rightmost bit is called the LSB, or Least Significant Bit. The MSB of a signed integer is reserved for the sign. A zero signifies a plus sign (and a positive value) and a one signifies a minus sign (and a negative value). Conversely, an unsigned integer treats the MSB as a significant digit, allowing it to hold a positive number twice as large as the largest possible value of a signed integer of the number of bits. Consistent with established mathematical conventions, the unsigned value is interpreted as a positive number. Note: An important side effect of this design is that an unsigned integer can never hold a negative value. If you store a negative value into an unsigned integer, or pass it to a function that expects an unsigned integer, the number will be interpreted as a very large positive number. The above discussion ignores the peculiar way that numbers are stored by Intel processors, which is beyond the scope of this paper. |
||||||||||||||||||||
4 | Calling this a DWORD or a LONG is a holdover from 16-bit Windows, which was designed for machines (the Intel 8086 and 80286) that had a machine word size of 16 bits. Consequently, a 32 bit integer required two machine words for storage. |
||||||||||||||||||||
5 | How you handle a string depends upon how the called function will use it. If the function only reads the string, the function definition will denote the
argument as [in] for input. In this case, you may pass any scalar
variable (anything except an array, a binary buffer, or a handle), using type
If the function modifies or returns the string, the function definition will denote the argument as [in/out] for input/output or [out] for output. In this case, you must pass a binary buffer, as shown in the following example, taken from my tutorial example function GetComputerName_P6C.
The above function uses two buffers, The first buffer,
The following code from function
The following code retrieves the computer name from the buffer.
|
||||||||||||||||||||
6 | Arguments of type LBDWORD are handled almost the same way as are
This function uses the DWORD buffer for both input and output. Therefore, before the
DLL call is made, the following line writes a number into the buffer, using the WIL
Following the call, the actual length of the ComputerName string is left in the same
buffer, overwriting the maximum length that was passed into the function. The following
line of code retrieves it for use with the
|
||||||||||||||||||||
7 | Data structures (structures, for short) are composed of multiple data items aligned
in a specific order, defined in a structure definition such as the following definition
of the
When you call a function that expects such a structure, you must allocate a binary
buffer, using the
The minimum number of bytes that you must allocate for a
Note that you can usually deduce the type of an element from the first letter or two of its name - the part preceding the first upper case letter in the name. |
Example function GetComputerName_P6C demonstrates most of the concepts discussed in this article. This deceptively simple Windows function has two inputs and three outputs.
Consequently, this function demonstrates using input/output buffers of two types - string and DWORD.
Since it returns a BOOL, it lets us demonstrate a way to call such a function without wasting a user
defined variable on a return value by embedding the call in the body of an if
statement.
Published 26 May 2004
©2004, P6 Consulting, Irving, Texas, USA
All rights reserved world wide
Article ID: W16466
File Created: 2005:02:18:12:20:52
Last Updated: 2005:02:18:12:20:52