WinBatch Tech Support Home

Database Search

If you can't find the information using the categories below, post a question over in our WinBatch Tech Support Forum.

TechHome

Tutorials
plus

Can't find the information you are looking for here? Then leave a message over on our WinBatch Tech Support Forum.

How To Call Windows API Functions


This article was written by: David Gray President, P6 Consulting

The WinBatch documentation for the extremely versatile and powerful, however the DLLCall function is documentation is limited. The documentation warns 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 supplysome of the missing detail and to serve as a supplemental reference for WinBatch programmers.

How Data Is Passed To Functions

There are two, and only two, ways to pass data to a function.

  1. By Value - The value of the variable is passed to the function. The function cannot change it.
  2. By Reference - A reference, or pointer, to the variable that holds the value is passed to the function. The function may be able to alter the value of the variable.

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.

How Data Is Returned From Functions

There are two, and only two, ways to return data from a function.

  1. By Value - The value is returned as the value of the function. Simple scalars, except for strings, are usually returned in this way.
  2. By Reference - A reference, or pointer, to the memory location that holds the value is returned by the function as its value. Strings, arrays, and other complex data structures are always returned in this way.

Why Is This So?

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.

Argument Types

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.

Notes

1

Windows functions that return a BOOL can be used as true/false expressions in if statements. For example:

DWORD_SIZE = 4 ; Size, in bytes, of a DWORD value.
If DllCall ( KernelFQFN , long:'GetComputerNameA' , lpbinary:hBinBuff , lpbinary:hDWordBuff )
    Dummy = BinaryEodSet ( hDWordBuff , DWORD_SIZE ) ; Probably overkill.
    NameLength = BinaryPeek4 ( hDWordBuff , 0 ) ; Read returned string length, use it
    Dummy = BinaryEodSet ( hBinBuff , NameLength ) ; to set End of Data for WIL run-time,
    rComputerName = BinaryPeekStr ( hBinBuff , 0 , NameLength ) ; and read name from buffer.
Else
    LastAPIError = DllCall ( KernelFQFN , word:'GetLastError' )
    MsgBoxText = StrCat ( 'Windows kernel function GetComputerNameA failed, erroc code = ' , LastAPIError )
    Dummy = Message ( FileRoot ( IntControl (1004 , 0 , 0 , 0 , 0 ) ) , MsgBoxText )
EndIf

The above example takes one action (collects and returns the computer name from the buffer) if the call to GetComputerNameA returns @true and reports the error code set by the function if it fails.

Another way to code this same call is like this, using the ! (not) operator to reverse the sense of the test.

DWORD_SIZE = 4 ; Size, in bytes, of a DWORD value.
If DllCall ( KernelFQFN , long:'GetComputerNameA' , lpbinary:hBinBuff , lpbinary:hDWordBuff )
    LastAPIError = DllCall ( KernelFQFN , word:'GetLastError' )
    MsgBoxText = StrCat ( 'Windows kernel function GetComputerNameA failed, erroc code = ' , LastAPIError )
    Dummy = Message ( FileRoot ( IntControl (1004 , 0 , 0 , 0 , 0 ) ) , MsgBoxText )
    rComputerName = ''
EndIf
Dummy = BinaryEodSet ( hDWordBuff , DWORD_SIZE ) ; Probably overkill.
NameLength = BinaryPeek4 ( hDWordBuff , 0 ) ; Read returned string length, use it
Dummy = BinaryEodSet ( hBinBuff , NameLength ) ; to set End of Data for WIL run-time,
rComputerName = BinaryPeekStr ( hBinBuff , 0 , NameLength ) ; and read name from buffer.

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 endif.

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 lpstr, as in the following example, taken from my library function GetDriveType_P6C.

DriveTypeCode = DllCall ( KernelFQFN , word:"GetDriveTypeA" , lpstr:WorkDriveRoot )

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.

If DllCall ( KernelFQFN , long:'GetComputerNameA' , lpbinary:hBinBuff , lpbinary:hDWordBuff )

The above function uses two buffers, hBinBuff and hDWordBuff. I shall defer a discussion of hDWordBuff for now, as it is addressed in Note 6 on arguments of type LPDWOR.

The first buffer, hBinBuff, is an input/output string buffer. The function prototype in the Windows SDK defines the argument thus:

LPTSTR lpBuffer

The following code from function GetComputerName_P6C creates the buffer.

MAX_COMPUTERNAME_LENGTH = 31 ; Per WinBase.H from Win32 SDK
BuffSize = MAX_COMPUTERNAME_LENGTH + 1 ; Must be MAX_COMPUTERNAME_LENGTH + 1
hBinBuff = BinaryAlloc ( BuffSize ) ; Buffer to hold the name.
If !hBinBuff
    MsgBoxText = StrCat ( 'Error allocating binary buffer of ' , BuffSize , ' bytes' )
    Dummy = Message ( FileRoot ( IntControl (1004 , 0 , 0 , 0 , 0 ) ) , MsgBoxText )
    Goto GetFileStats_P6C_End
EndIf

The following code retrieves the computer name from the buffer.

DWORD_SIZE = 4 ; Size, in bytes, of a DWORD value.
Dummy = BinaryEodSet ( hDWordBuff , DWORD_SIZE ) ; Probably overkill.
NameLength = BinaryPeek4 ( hDWordBuff , 0 ) ; Read returned string length, use it
Dummy = BinaryEodSet ( hBinBuff , NameLength ) ; to set End of Data for WIL run-time,
rComputerName = BinaryPeekStr ( hBinBuff , 0 , NameLength ) ; and read name from buffer.
6

Arguments of type LBDWORD are handled almost the same way as are LPSTR arguments that are marked input/output or [out] for output. You must pass a binary buffer, as shown in the following example, taken from my tutorial example function GetComputerName_P6C.

DWORD_SIZE = 4 ; Size, in bytes, of a DWORD value.
hDWordBuff = BinaryAlloc ( DWORD_SIZE ) ; Read/write buffer for length of name.
If !hBinBuff
    MsgBoxText = StrCat ( 'Error allocating binary buffer of ' , DWORD_SIZE , ' bytes' )
    Dummy = Message ( FileRoot ( IntControl (1004 , 0 , 0 , 0 , 0 ) ) , MsgBoxText )
    Goto GetFileStats_P6C_End
EndIf
Dummy = BinaryEodSet ( hDWordBuff , DWORD_SIZE ) ; Allocate buffer big enough for DWORD

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 BinaryPoke4 function, which writes a numeric value into a DWORD.

Dummy = BinaryPoke4 ( hDWordBuff , 0 , BuffSize ) ; insert size of hBinBuff.

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 BinaryPeekStr ( ) function to read the string itself from the other buffer, like so.

NameLength = BinaryPeek4 ( hDWordBuff , 0 ) ; Read returned string length
Dummy = BinaryEodSet ( hBinBuff , NameLength ) ; to set End of Data for WIL run-time,
rComputerName = BinaryPeekStr ( hBinBuff , 0 , NameLength ) ; and read name from buffer.
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 _PROCESS_INFORMATION structure, taken from WinBase.H, which is part of the Windows SDK.

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

When you call a function that expects such a structure, you must allocate a binary buffer, using the BinaryAlloc ( ) function, then use a succession of BinaryPeek ( ) functions to read it or BinaryPoke ( ) functions to fill it. You must calculate the offset to each element of the structure, then give that offset to the BinaryPeek ( ) or BinaryPoke ( ) function for the correct size entity. The following table shows the calculations for the _PROCESS_INFORMATION structure shown above.

Element (Field) Name Element Type Element Width Element Offset
hProcess HANDLE 4 0
hThread HANDLE 4 4
dwProcessId DWORD 4 8
dwThreadId DWORD 4 12

The minimum number of bytes that you must allocate for a _PROCESS_INFORMATION structure is, therefore, 16 (12 + 4) bytes - the offset of the last element plus its width.

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.

Putting It All Together

Example function GetComputerName_P6C demonstrates most of the concepts discussed in this article. This deceptively simple Windows function has two inputs and three outputs.

  1. The function returns True if it succeeds or False if it fails.
  2. The function returns the NetBIOS Computer Name of the machine as a null terminated string in the buffer to which the first argument points.
  3. The function uses the small buffer to which the second argument points to return the length of the string.

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.

References

  1. WBHowTo_Call_Windows_API_Functions_Examples.zip contains both of the functions discussed in this article, along with a small WinBatch script that demonstrates the function for getting the NetBIOS machine name that is the basis for most of the examples.
  2. Data Type Conversion Chart for VB, VBA, and WinBatch is a reference chart of variable types encountered in calling Windows API functions and other DLLs. This chart covers WinBatch, Visual Basic and VBA, Assembly, and C/C++.



Article ID:   W16292
File Created: 2017:07:28:12:58:09
Last Updated: 2014:07:18:12:25:44