This primer is part of the documentation of
dllGuide.exe, a
program which simplifies the application of many of the steps in preparing a DLL
for use by an XBasic program. This program, with source code and other
documentation, can be downloaded
here).
Contents
Top
Using
Third-Party DLLs in XBasic
A "third-party" DLL is one that is neither an XBasic system DLL nor
a DLL created from a program written in XBasic. To call functions in a
third-party DLL you need at least the DLL itself and some kind of
documentation. DLL distributions also commonly include one or more C/C++
header files (.h), maybe a Visual Basic module file (.bas), a LIB file, and
sometimes the source code.
From the materials in the DLL distribution you need to create a DEC
file, which will tell XBasic how to locate and call the functions in the
DLL. If you want to create a standalone EXE, you will also need a LIB
file, which can be created if one is not provided.
Before making the DEC file, however, check that the function names,
as exported by the DLL, are compatible with XBasic - you can use
dllGuide to do this, or the Windows program
QuickView. The exported (or
"public") names may not be the same as the function names in the documentation
or source code. An XBasic name can contain letters, numerals, the
underscore "_", and "$". The name cannot start with a numeral, and "$" can
only be used at the end of the function name. If a function name in the
DLL does not follow these rules, then an XBasic program cannot directly call the
function. It will be necessary to use
indirect function calls
, for which the DEC and LIB files may not be needed.
Once the various files are collected or created, they should be
distributed as follows:
- the DEC file goes to the XBasic\include folder
- the LIB file goes to the XBasic\lib folder
- the DLL goes to \Windows, \Windows\system, XBasic\bin, or any folder listed in
the PATH environment variable in autoexec.bat.
The DEC and LIB, by the way, must have the same name as the DLL, with
different extensions. If the DLL is called "dllname.dll", then the other
files must be called "dllname.dec" and "dllname.lib".
Functions in the third-party DLL can be called from an XBasic program
just like XBasic functions; all that is necessary is to include an IMPORT
"dllname" statement in the PROLOG (do not include the .dll extension).
Top
The DEC
file
If the function names are XBasic-compatible, the next step is to
create the DEC file. This is often the most difficult step in the process,
especially if it is necessary to decipher a C++ header file. However, if
you really understand how to use a function, declaring it properly in the DEC
should not be a problem.
The DEC file can include any or all of the following: TYPE
declarations for any composite variables, EXTERNAL FUNCTION (or CFUNCTION)
declarations for each function you intend to use, and the definitions of any
global constants. An important point to remember is that the DEC does not need
to include any information you aren't actually going to use: only those TYPEs,
functions, and constants you are using need to be declared. This means
that you can start out with a very simple DEC file, perhaps declaring only one
or two functions, and build it up as your understanding of the DLL develops.
a. Declaring functions.
Several questions need to be answered in order to
properly declare a function.
1. You know the function is EXTERNAL, but is it a
FUNCTION, CFUNCTION, or SFUNCTION? These keywords define the
protocol that XBasic will use to call the function. You
can ignore SFUNCTION - it is identical to FUNCTION in Windows, to CFUNCTION
in Linux. Windows API functions are (almost) all FUNCTIONs. If a
Visual Basic module (.bas) file is available, look for the keyword CDECL in the
function declaration; if present, use CFUNCTION, otherwise use
FUNCTION. A function written in a C/C++ language will probably be a
CFUNCTION unless a keyword like _stdcall is used, but the complexity of
header files may make this easy to miss.
If all else fails, see
Note 3.
2. Are arguments passed by value, by reference, or by
address? Generally, an argument is passed by address ("&" prefix)
if its value is changed by the function, or passed by value if it is not
changed. Indications of pass-by-address are the pointer symbol ("*")
preceding the argument in a C header file, or the 'ByRef' keyword in a
Visual Basic module file. Note that ByRef is equivalent to
pass-by-address in XBasic, not pass-by-reference; XBasic's
pass-by-reference ("@" prefix) is never used when calling third-party
functions.
It is a syntax error to
use the "&" prefix in the argument list in a function declaration, so
when passing by address it is a good idea to use a variable name that makes
it clear that an argument is an address, not a value. Many
programmers follow the convention of beginning the name of any pointer
(address) with the letters 'p' or 'lp'; 'addr' is another possibility. If
the argument is a pointer to a string, 'lpstr' or 'lpsz' are often
used.
3. What is the data type of each argument? Any argument passed by
address is always XLONG; the following considerations apply only when passing by
value.
Basic C/C++ data types, their XBasic
equivalents, and other synonyms:
char
(signed) SBYTE
BYTE
char
(unsigned) UBYTE
BYTE
short
(signed) SSHORT
short
(unsigned) USHORT
WORD
int
(signed) SLONG
int
(unsigned) ULONG
DWORD
long
(signed) SLONG
long
(unsigned) ULONG
DWORD
long long
(signed) GIANT
float SINGLE
double DOUBLE
Strings are usually indicated in C/C++ by
the 'char *' notation, which indicates a pointer to an array of bytes - in
XBasic terms, an address of a string.
All integer types except GIANT are passed as
4 bytes, even 1- or 2-byte integers; they can all be declared as XLONGs,
and still be passed correctly. This point is useful when a function
requires an argument of an integer type that has no XBasic equivalent, like
BOOLEAN. GIANT, SINGLE, and DOUBLE variables, however, need to be
properly declared.
String arguments passed by value can be
declared using the STRING keyword, or the "$" suffix. Arguments of
composite data type can be declared using the name of the type. Both
types of arguments are almost always passed by address; but if the argument
is an input, whose value will not be changed by the function, it can be
passed by value regardless of what the documentation for the DLL may say.
The reason is that "pass-by-value", for these data types, really means
"make a copy and pass the address of the copy" (see
Note 4).
Arrays are never passed by value. The
argument in the EXTERNAL FUNCTION declaration should be an XLONG address,
like 'lpArray', never an array name like 'array[]'. Eg:
In DEC:
EXTERNAL FUNCTION
ExtFunc(lpArray)
In program:
ExtFunc
(&array[])
Occasionally,
a DLL function requires a function address as an argument - typically, the
address of a function in your program that allows you to customize certain
behavior of the DLL. This address is passed as an ordinary XLONG; do not
use the FUNCADDR data type for this purpose, it isn't
necessary:
In
DEC:
EXTERNAL FUNCTION ExtFunc(funcAddress)
In program:
ExtFunc (&MyFunction())
3. What is the RETURN type of the function?
Simple data types, including DOUBLE and
GIANT, are returned as 4- or 8-byte values. XBasic can assign
function return values to simple variables without difficulty.
Eg:
In DEC:
EXTERNAL FUNCTION DOUBLE
ExtDoubleFunc()
In program:
x# = ExtDoubleFunc()
STRING data types require caution. The
function returns the 4-byte address of a string, which XBasic will use to access
the string as if it were a normal XBasic string. However, the returned
string does not have the header that all XBasic strings must have. As a
result, an error will occur - sometimes a system-crashing error.
Functions that return strings should be declared as the normal XLONG
default; then use the CSTRING$() to copy the returned string into a proper
XBasic variable. Eg:
In DEC:
EXTERNAL FUNCTION
ExtStringFunc()
In program:
s$ =
CSTRING$(ExtStringFunc())
Composite data types are returned as a
4-byte address. XBasic automatically copies the data from that address into the
destination variable, so functions can return composite types correctly. In other words, the following approach is valid:
In DEC:
TYPE
USERTYPE
... 'member
definitions
...
END
TYPE
EXTERNAL FUNCTION USERTYPE
ExtCompositeFunc()
In program:
USERTYPE x
x = ExtCompositeFunc()
ExtCompositeFunc() is actually returning an address of data in its own memory
space, and sometimes this address is needed later to free the allocated
memory. In such a case, the above approach will not work -
&x points to a copy of the data, in XBasic's memory space, not to
the original data in the DLL's space. One approach to this situation
is:
In DEC:
EXTERNAL FUNCTION
ExtCompositeFunc() 'return type is XLONG
In program:
USERTYPE x
lpExtData = ExtCompositeFunc() 'returns address of data in DLL's
space
XstCopyMemory (lpExtData, &x, SIZE(x))
'copy data to XBasic variable
Later in the program the address lpExtData
is available to free memory using the appropriate function in the
DLL:
ExtFreeMemory
(lpExtData)
b. Declaring composite TYPEs.
A couple of points to consider when
translating C/C++ structures into XBasic composite TYPEs:
- if an array is part of a structure,
remember that the number of elements in an XBasic array is one more than the
array dimension. An array in a C structure, eg. array[10], contains 10
elements; a 10-element array in XBasic is array[9].
- XBasic sometimes inserts pad bytes in a
TYPE, in order to keep members properly aligned. These pads are invisible
to the user program; but if the DLL does not use pads, then it will misread
the members in the XBasic TYPE. This problem can be very difficult to
debug, and tricky to solve. The presence of pad bytes in the XBasic
type can be detected by using the SIZE() intrinsic; if SIZE(USERTYPE) is
greater than the sum of the bytes in all the members of the USERTYPE, then
USERTYPE contains pad bytes.
There is not really a good solution to this
problem. About all that can be done is to reserve a block of memory
of the appropriate size (as an array of UBYTES, or as a string), fill the
block with data in such a way as to simulate a "packed" TYPE (no pad
bytes), then send the DLL the address of this block instead of the address
of the XBasic composite TYPE. This inelegant approach requires writing
directly to memory, which can be hazardous.
Top
Initializing
Strings and Arrays
Most commonly, when a DLL function needs to return string or array,
the calling program is required to first allocate the necessary memory, then
pass the address of the argument. For example,
s$ =
NULL$(256) 'allocate 256 bytes
to s$
DIM
a[99]
'allocate an array
ExtFunc (&s$,
&a[]) 'pass addresses to the external
function
Documentation should make it clear how many bytes need to be
allocated. Failure to allocate sufficient space can cause big problems, because
the called function may then overwrite other data.
All XBasic strings have a null '\0' byte automatically appended, so
it is not necessary to add a null when sending a string to a C/C++
function.
A problem can occur when a DLL is intended to be used by a Visual
Basic program, and one or more functions require strings as input
arguments. Visual Basic strings are preceded by a ULONG header giving the
length of the string, and are (like C strings) terminated by a \0
character. XBasic strings have a different kind of header that is not
compatible with Visual Basic. To send a string from XBasic to a function
that is expecting a VB-style string, it is necessary to add a length header as
follows:
s$ = "a string"
len =
LEN(s$) 'length of the
string
s$ = NULL$(4) + s$ 'make space for a length
header
ULONGAT(&s$) = len 'header = length of original
string
lpStr = &s$ + 4 'send this
address to the function
ExternalFunc (lpStr)
Strings returned as output arguments are easier, since VB strings
also have a terminating \0. Thus the XBasic CSTRING$() intrinsic will convert
the returned string to XBasic-format:
ExternalFunc
(&lpStr) 'function returns a string address
s$ =
CSTRING$(lpStr) 'convert from C or VB string to XBasic
string
Top
Exported Data
In a few DLLs, some of the public symbols exported by the DLL refer not to functions, but to data. The data are stored within the DLL, and can be accessed from a program that has loaded the DLL. XBasic does not have built-in facilities to deal with exported data, however, so special procedures are necessary.
Exported data can be recognized in C/C++ header files by entries such as:
extern unsigned long int expData;
which means that the symbol expData refers to an external ULONG constant. If this is a symbol exported by the DLL, then the value of the constant can be accessed by loading the DLL:
hLib = LoadLibraryA (&dllname$)
addr = GetProcAddress (hLib, &"expData")
expData = ULONGAT(addr)
The functions LoadLibraryA() and GetProcAddress() are in the Windows API DLL kernel32.dll. The library must be loaded even if it has been IMPORTed in the PROLOG. The call to GetProcAddress() returns the address of the value of the constant expData; the value itself can be obtained using the ULONGAT() intrinsic function.
If exported constant is a string, array, or composite-type, then the procedure is slightly more complicated. A line in the header file like
extern const gsl_rng_type *gsl_rng_taus;
means that the exported symbol gsl_rng_taus is a pointer to a constant of type gsl_rng_type. GetProcAddress() will return the address of this pointer, which must extracted from the DLL and then be used to find the actual data:
gsl_rng_type T 'declare a composite-type variable
hLib = LoadLibraryA (&dllname$)
addr = GetProcAddress (hLib, &"gsl_rng_taus")
lpData = ULONGAT(addr)
XstCopyMemory (lpData, &T, SIZE(gsl_rng_type))
The final step copies the data from the DLL to the XBasic variable T, where the individual members of the variable can be accessed.
Top
The LIB
file
A LIB file is not needed if the user program is to be run only in
the PDE, or if it uses only
indirect function calls to the
DLL.
How a LIB file is used:
- the function is declared in the DEC file as, for example,
EXTERNAL FUNCTION ExtFunc(a,b,c).
- the user writes a program that calls the
function.
- Run, Assembly generates assembly code with a line
call _ExtFunc@12. The @n suffix
(not used for CFUNCTIONs) is the total number of bytes in the function's
argument list.
- when creating an EXE, the linker looks in the LIB
file for the name _ExtFunc@12.
- if found, the name points to an object member
within the LIB. The object member contains either an index (known as an
ordinal) which points to the function's address in the DLL, or another
name, which is the name of the function as exported by the DLL.
- using either the ordinal or the name, the linker
includes code in the EXE that will allow it to locate the function in the
DLL when necessary.
Importing by ordinal will allow the DLL to load slightly faster than
importing by name, but it may mean the EXE will not work with a new version of
the DLL. If the new version uses different ordinals, it will be necessary to
recompile the XBasic source to create a working EXE. Therefore, unless
there is a particular reason to import by ordinal, it is better to import by
name.
Errors in the LIB file, or improper format, are the
most common cause of linker errors. Starting with Version 6.0, Microsoft's
LIB.EXE and LINK.EXE create a LIB file whose format is not readable by the older
version of LINK.EXE used by XBasic. Also, non-Microsoft languages may
create LIB files that use a different format. The simplest solution to
such problems is to rebuild the LIB file. The XBasic distribution contains a
version of LIB.EXE which can be used for this purpose, but first it is necessary
to create a DEF file.
An accurate
DEF file is critical to the building of a proper LIB file. The DEF is a
ordinary text file, that gives the name of the DLL and the names of the exported
functions. The DEF must list each function that the XBasic program calls; it is
not necessary that it list every function in the DLL. A typical DEF file
might look like this:
LIBRARY
dllname.dll
EXPORTS
DLLFuncOne@0
@1
DLLFuncTwo@12
@2
DLLCFuncOne
@3
DLLCFuncTwo @4
dllname.dll is the file name of the DLL, without a path.
Each function exported by the DLL is listed. If the function
is declared EXTERNAL FUNCTION, it has an @n suffix giving the total number of
bytes in the argument list; EXTERNAL CFUNCTIONS do not have this suffix.
If imported by ordinal, both FUNCTIONs and CFUNCTIONs will be followed by an
ordinal (which also, confusingly, uses the "@" symbol). Note that these
names in the DEF file are the same as the names used in the LIB file (see 'How a
LIB file is used', above), except that they do not have a leading underscore.
Given a DEF file, the LIB can be created using the LIB.EXE that is
included with the XBasic distribution, in the XBasic\bin folder. The
syntax is
lib -machine:i386
-def:dllname.def -out:dllname.lib
This approach
creates a LIB that imports by ordinal. As noted above, it is usually better to
import by name, but there doesn't seem to be any simple way to do this. The
dllGuide.exe program mentioned at the beginning of this primer can be used to
create a LIB that imports by name.
Top
The Blowback() Function
If you are going to use third-party DLLs, you should be familiar with the use of the Blowback() function. Many DLLs require a certain amount of cleanup when they are no longer needed - handles need to be closed, memory may need to be de-allocated, etc. If your program ends abnormally due to an error, the code you have written to perform this cleanup may not be executed, and when you try to rerun the program it may fail. To remedy this situation, the XBasic PDE automatically looks for a function in your program called Blowback(). If found, the function is called when your program ends, whether it ends normally, by pressing the 'kill' button, or due to an error. By including the necessary cleanup code in a Blowback() function, you can ensure that it will always be executed, no matter how your program ends (as long as it doesn't crash the PDE itself, that is).
If you run your program as a standalone EXE, the Blowback() function will not be called unless your program calls it.
Blowback() has no arguments, so any variables it may require must be SHARED. This creates a bit of a problem if the required variables are strings, composites, or arrays, because SHARED variables of these kinds are apparently already de-allocated by the time the PDE calls Blowback(). The solution is to declare the variables EXTERNAL instead of SHARED. Within a single module, EXTERNAL variables behave like SHARED variables, but EXTERNAL strings, composites, and arrays are still accessible in Blowback().
Top
Troubleshooting
1. Problem: the program won't compile in the
PDE.
- an "Undeclared" error indicates that the function is
not in the DEC file, or that the DEC file has not been imported. Make sure
your program includes an IMPORT "dllname" statement in the PROLOG.
- argument-count and Type Mismatch errors indicate an
inconsistency between the function declaration in the DEC file and its usage in
the program. One or the other is in error.
- "Undefined" _ExtFunc@12 (or just _ExtFunc) may mean that
ExtFunc() is not in the DLL, at least under the name being used. Some
DLLs built from C/C++ source code use names in the DLL that are different
from the function names. The LIB file correlates function names and DLL
names, but XBasic doesn't use the LIB file when running a program in the
PDE. One indication of this kind of problem is that the Visual Basic
module file, if available, uses the ALIAS keyword in the function
declaration. Sometimes it is possible to create a standalone EXE from the
program, even though it won't compile in the PDE, because the linker does use
the LIB. To run the program in the PDE, however, will require an
indirect function call.
Another possibility is that the operating
system is not loading the DLL. This may be because the DLL is not in an
appropriate folder (see
Problem 4), or because the DLL imports functions
from another DLL that you don't have or that the system can't find. The Windows utility program
QuickView will usually list the imported functions (and their respective
DLLs). See also the "List Imports" selection in the "DLL" menu of
dllGuide.exe.
Finally, this error can occur if you have tried
to declare EXTERNAL FUNCTION ExtFunc(x,y,z) in the PROLOG of your program,
rather than in the DEC file. Functions in
third-party DLLs must be declared in the DEC.
2. Problem: the program compiles in the PDE, but runtime
errors occur.
- usually this indicates a misunderstanding of how the
function is used. Be careful to distinguish between arguments passed by
value and those passed by address. Never use the XBasic
"pass-by-reference" (@ prefix) method - if an argument is passed "ByRef",
use XBasic's "pass-by-address" (& prefix). Memory must usually be
allocated to strings (using, for example, s$ = NULL$(numberOfBytes)) and
arrays (using DIM) before calling the function, otherwise memory-access
errors will occur.
- if the
function uses a composite-TYPE argument, XBasic may be inserting
pad bytes.
- the function may use the C calling protocol, in which
case it should be declared EXTERNAL CFUNCTION ExtFunc (x, y, z) in the DEC
file. If a Visual Basic module-definition file is available, look for
the keyword CDECL in the function declaration; if present, then C protocol
is being used. C protocol is the default for functions written in
C/C++, unless STDCALL (or something like it) is specified.
3. Problem: the program compiles and runs in the PDE, but
linker errors occur when trying to create a standalone EXE.
- this usually indicates a problem with the
LIB file.
Make sure the LIB file is in the XBasic\lib folder. If no
LIB came with the DLL, you will need to make one.
- an "invalid file" error indicates that the LIB file is
in a format that XBasic's linker cannot use. Microsoft's LIB.EXE and
LINK.EXE, from version 6.0 on, create a short-format LIB file that is
incompatible with the older LINK.EXE distributed with XBasic. Some
LIB files are intended for use with particular compilers, and are
incompatible with either the new or old Microsoft formats. In any
case, a new LIB file will need to be created.
- sometimes a "conflicting subsystem" error will occur,
also apparently when the linker encounteres a V6.0 short-format LIB
file. Again, make a new LIB.
4. Problem: the program compiles and runs in the PDE; it
also works as a standalone EXE, but behaves differently.
Because of the way Windows searches for a DLL, it is
possible that the DLL loaded when a program is run in the PDE may not be
the same as the one loaded when the program runs as a standalone.
This may happen if different versions of the DLL exist in different
directories.
When run in the PDE, directories
are searched in the following order:
1. the directory containing
xb.exe
2. the current default
directory
3. the Windows system
directory
4. the Windows
directory
5. directories in the PATH
environment variable
When run as a standalone, directories are searched in
the following order:
1. the directory containing the standalone
EXE
2. the current default
directory
3. the Windows system
directory
4. the Windows
directory
5. directories in the PATH
environment variable (which should include
XBasic\bin)
If different versions of the DLL exist in, say,
XBasic\bin and \Windows, the XBasic\bin version will be found first and
used if the program is run in the PDE, but the \Windows version will be
found first when run as a standalone.
Top
Notes
Note 1: Indirect function calls
Indirect ("computed") function calls are useful when the DLL exports
a function using a name containing characters that cannot be used in
XBasic. It happens, for example, that a DLL will export functions under
the same name used in the import library (LIB); a function listed in the source
code as SomeFunction(a, b) may be exported as _SomeFunction@8. If you use SomeFunction as the function name, XBasic will not find the function in the DLL, at least when
running your program in the PDE. If you try _SomeFunction@8, the "@" character will cause
a syntax error.
The solution is an indirect function call. Your program takes
over the job of loading the DLL and finding the address of the function. The
following is one way to implement this method.
In the PROLOG, import the Windows DLL kernel32.dll. You will
need to have kernel32.dec and kernel32.lib, both of which are included in the
XBasic distribution.
IMPORT
"kernel32"
In the initialization section of your
program:
'declare a SHARED FUNCADDR variable for each function in the DLL that
you
' intend to call. The argument list for each function must use type
names, not
' variables.
SHARED
FUNCADDR addrDLLFunc1 (XLONG, XLONG)
SHARED FUNCADDR DOUBLE
addrDLLFunc2 (XLONG, DOUBLE)
'load the DLL and get a handle for it
dllName$ =
"NameOfDll.dll" 'include path if necessary
hLib = LoadLibraryA (&dllName$) 'a kernel32 function
IFZ hLib THEN GOTO
DLLNotFound
'get the address within the DLL of each function you intend to call,
using the name
' as exported by the DLL. The returned address will be zero
if the function is not found.
addrDLLFunc1 = GetProcAddress (hLib, &"_DLLFunc1@8") 'a kernel32 function
addrDLLFunc2 = GetProcAddress (hLib, &"_DLLFunc2@12")
Create an XBasic wrapper function for
each function:
FUNCTION Func1 (a,
b)
SHARED FUNCADDR addrDLLFunc1 (XLONG,
XLONG)
@addrDLLFunc1 (a, b) 'indirect function
call
END FUNCTION
FUNCTION DOUBLE Func2 (a,
b#)
SHARED FUNCADDR DOUBLE addrDLLFunc2 (XLONG,
DOUBLE)
RETURN (@addrDLLFunc2 (a, b#)) 'indirect function
call
END FUNCTION
Then anywhere in the program, Func1(a, b) can be called in the
usual manner to invoke the external function _DLLFunc1@8, or Func2(a, b#) to invoke _DLLFunc2@12.
When you no longer need to call functions in the DLL, you can unload
it by calling FreeLibrary(hLib). If this isn't done, the operating system will
unload the DLL automatically when the PDE or your standalone program is
terminated.
Reason for the wrapper function: If the external function
follows C protocol, it does not pop arguments from stack before returning (see
Note 2). But XBasic always assumes STDCALL protocol when calling a
function indirectly, so it too does not pop the arguments. Thus the stack
pointer is not properly restored. By wrapping the external function in an
XBasic function, the stack pointer is automatically corrected when the wrapper
function exits. Even if the external function uses STDCALL, the wrapper
makes it easier to call the function anywhere in the program, without including
the SHARED FUNCADDR and without using the @func() syntax. Also, this approach
can be used to create a wrapper DLL that XBasic can import in the normal
manner.
When a function is called indirectly it should not be included in
the DEC file; a DEC file may not even be necessary if all functions in the DLL
are called this way. However, it may be desirable to create and import a
DEC file in order to include type declarations and global constants, even if the
DEC contains no function declarations. This works OK in the PDE, but a
problem will occur when building a standalone. The ".mak" file that XBasic
generates to create the EXE will list a LIB file corresponding to the name of
the imported DEC file. If all functions are being called indirectly, no
LIB file is actually needed, and may not exist; therefore an error will occur
when the linker fails to locate the LIB. You will need to manually edit
the ".mak" file, removing the reference to the unneeded LIB.
Note 2: Calling protocol and the stack
pointer
When function is called, its arguments are
first pushed onto the stack, which is the area of memory pointed to by the
processor's stack pointer (esp) register. Pushing a value or address onto
the stack means copying it into the stack memory, then adjusting the stack
pointer to point to it. Since all programs have access to the stack pointer, the
called function can easily locate the argument by simply checking the stack
pointer. The stack thus provides a convenient method of communicating
between functions in a program.
When a function returns control to the calling program, the stack
pointer needs to be reset to the value it had before the arguments were pushed
onto the stack. A calling protocol defines, among other things, whether it
is the calling program or the called function that is responsible for resetting
the stack pointer. The DECLARE FUNCTION protocol, usually referred to as
STDCALL, requires the called function to reset the pointer; DECLARE CFUNCTION,
often called CDECL, requires the calling program to do it (XBasic handles this
automatically, your program doesn't have to do it).
It is
necessary that both the calling program and the called function agree on the
protocol, otherwise the stack pointer will be incorrect. Under some
circumstances this can cause a program to crash. In particular, if you
call the function from within a SUB, you get an error like "Memory Invalid
Access - $$ExceptionSegmentViolation".
Note 3: FUNCTION or CFUNCTION?
Look first for the _stdcall keyword in the C header file (indicating
a FUNCTION), or for the CDECL keyword in a Visual Basic module file (indicating
CFUNCTION). Otherwise, the following technique can be used to determine
which type of calling protocol you are dealing with:
- Create a DEC file, using EXTERNAL FUNCTION to declare
the function. It is important that the argument list in the
declaration is correct.
- Write a simple program that imports the DLL and
calls the function.
- Set a breakpoint at the point where the function
is called, then run the program.
- When the PDE pauses at the function, select
'registers' from the Debug menu, and scroll down to the esp register.
This is the stack pointer. Make a note of the value in the
register.
- Single step past the function call.
- The value in the stack pointer register should be
the same as it was before the function call. If the function is
actually a CFUNCTION, then the stack pointer will be decreased by the total
number of bytes in the argument list of the function. If the stack
pointer changes by any other amount, the function is a FUNCTION, but you've
made a mistake in the argument list.
Note 4: How XBasic passes and receives arguments.
An understanding of argument passing is not necessary to use
third-party DLLs, but it can be useful.
1. Simple data types, XLONG or shorter, non-STRING, non-array
SomeFunction(x) passes a copy of the value
of x. The number of bytes passed is always 4, regardless of the size of the
variable. Any change the function may make to the copy is lost when
the function returns.
SomeFunction(@x) passes a copy of the value
of x (4 bytes). On return, the (possibly changed) value is copied back to
the memory location assigned to x. Non-XBasic functions do not
necessarily return the copy-of-x in the location that XBasic expects to
find it, so this method should never be used for third-party DLLs.
SomeFunction(&x) passes the 4-byte
address of x. Any change the function makes to the contents of this
address are retained when the function returns.
2. DOUBLE and GIANT data types
These work the same way as the shorter
types, except that 8 bytes are copied and passed for the SomeFunction(x)
and SomeFunction(@x) methods. The SomeFunction(&x) method passes a
4-byte address.
3. STRING
SomeFunction(s$) clones (copies) the string,
then passes the 4-byte address of the clone. Any changes made to the clone
are lost when the function returns.
SomeFunction(@s$) passes the address of the
string, then copies the returned address to the string handle (a memory address XBasic
uses to keep track of the string's location). This is
necessary in case the string has moved. No clone is made. The method
assumes that the called function stores the changed address of the string
at the appropriate location, which is not generally true for third-party
DLLs.
SomeFunction(&s$) passes
the address of the string. The function can modify the string only if it
does not move it, because the new address is not copied to the handle. Any
function written in XBasic or other versions of BASIC may move strings at
any time; C/C++ does not move strings, so this is the usual method of passing
strings to third-party DLLs.
4. Composite (user-defined) data types
SomeFunction(x) passes the address of a copy
of the value of the composite variable. Changes made to the copy are
lost when the function returns.
SomeFunction(@x) passes the address of the
composite variable. No copy is made. Unlike strings, composites do not move
in memory, so it is not necessary to copy the returned address of the
composite. This method would probably work with a third-party DLL,
but for consistency it is better to use &x.
SomeFunction(&x) is identical to
SomeFunction(@x), in terms of the assembly code generated. The only
difference is the declaration of the argument in the DEC file, which is
'USERTYPE x' (or whatever the type name is) for the @x method, 'XLONG x' if
using &x.
5. Arrays
SomeFunction(a[]) is illegal syntax.
SomeFunction(@a[]) passes the address of the
array, then copies the returned address to the array handle. This is
necessary in case the array has been DIMed or REDIMed by the function. The
method assumes that the called function stores the changed address of the
array at the appropriate location, which is not generally true for
third-party DLLs.
SomeFunction(&a[]) passes the address of
the array, but does not copy the returned address. Changes made to
the array by the function are retained. This is the only method by which
arrays can be passed to third-party DLLs.
Home