Home

Calling Third-Party DLLs from an XBasic Program

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

  Using Third-Party DLLs in XBasic
  The DEC file
  Initializing Strings and Arrays
  Exported data
  The LIB file
  The Blowback() function
  Troubleshooting
    1. Problem: the program won't compile in the PDE.
    2. Problem: the program compiles in the PDE, but runtime errors occur.
    3. Problem: the program compiles and runs in the PDE, but linker errors occur when trying to create a standalone EXE.
    4. Problem: the program compiles and runs in the PDE; it also works as a standalone EXE, but behaves differently.
  Notes
    1: Indirect function calls
    2: Calling protocol and the stack pointer
    3: FUNCTION or CFUNCTION?
    4: How XBasic passes and receives arguments
 

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