Rapid-Q Documentation by William Yu (c)1999-2000, appended by John Kelly Chapter 9

 

09. SUBI, FUNCTIONI, and DLLs

You'll learn all there needs to know about creating your own SUB/FUNCTIONs with variable number of parameters. If you know a little about C, you'll probably know about the most popular command printf. Well, it's actually possible to implement your own printf in Rapid-Q!

9.1 SUB/FUNCTIONs with variable parameters
Traditional SUBs or FUNCTIONs have a fixed number of parameters we can pass, but SUBI and FUNCTIONI can accept an infinite number (255) of variable parameters. You're probably wondering how we can retrieve these parameters, well, they are actually stored in an internal stack. So what you'll have to do is probe this stack for the correct parameter. Here are the keywords used to do that:
 ParamStr$() - Array of string parameters
 ParamVal() - Array of numeric parameters
 ParamValCount - Number of numeric parameters passed
 ParamStrCount - Number of string parameters passed
As a simple example, we'll just test to see that it works:
 SUBI TestSUBI (...)
 PRINT "String Parameters: "; ParamStrCount
 FOR I = 1 TO ParamStrCount
 PRINT I;" "; ParamStr$(I)
 NEXT I
 PRINT "Numeric Parameters: "; ParamValCount
 FOR I = 1 TO ParamValCount
 PRINT I;" "; ParamVal(I)
 NEXT I
 END SUBI

 TestSUBI "Hello", 1234, "Hmmm", "Yeah...", 9876, 1*2*3*4, UCASE$("Last one")
You'll probably notice that the ParamStr$() and ParamVal() arrays aren't zero-based (ie. their first value starts at 1). This may seem strange, since everything in Rapid-Q is zero-based. Anyway, besides that, something equally strange is the use of (...) in replace of parameter names. It doesn't really matter what you put in the brackets, as long as it's a valid token (one word). Try creating a FUNCTIONI, it's no different from creating a standard FUNCTION except you replace your formal parameter names with (...).

9.2 More on FUNCTIONI
To expand our knowledge, let's do more examples, here's one that will find the maximum number from the parameters you provide it.
 FUNCTIONI FindMax (...) AS DOUBLE

 DIM Largest AS DOUBLE
 DIM I AS BYTE

 Largest = -999999999999

 FOR I = 1 TO ParamValCount
 IF ParamVal(I) > Largest THEN
 Largest = ParamVal(I)
 END IF
 NEXT

 FindMax = Largest ' or Result = Largest
 END FUNCTIONI
The function goes through the entire list of numeric parameters and checks which one is the largest. Any string parameters are properly ignored. Now let's test it:
 PRINT "Largest number is: "; FindMax(523, 12.4, 602, 45, -1200)
 PRINT "Largest number is: "; FindMax(523, 12.4, FindMax(602, 45, -1200))
You can comfortably embed your FUNCTIONI if you like. There's really nothing to it. Just remember the keywords you need to access the internal stack, and away you go!


9.3 Introduction to DLLs
What is a DLL? It's a Dynamic Link Library which contain exported functions which can be used by any programming language that support DLLs. Your RapidQ program has SUBs and FUNCTIONs, why not use SUBs and FUNCTIONs that are already written? You get access to them by calling them inside the DLL. You could even write your DLL in  FreeBasic, BCX, Delphi, or C++ and use those functions in Rapid-Q. Why use a DLL? Mainly so you can access  the windows operating system for your program. Another reason to use a DLL is to use existing libraries run  faster code. Because RapidQ is an interpreter program execution for large calculations using large number of loops can be done much faster with a DLL that uses true machine code. DLLs work by loading in the same process space as your program, so you have access to their exported functions. DLLs in Rapid-Q can only be dynamically linked at run-time (ie., when the program runs and not at compile time).

Not all DLLs are the same. Some DLLs are written especially for C++ and use 'COM interface' and some are written for Visual Basic to use 'ActiveX' technology (using an extension .OCX).  Rapid-Q can only use DLLs following standard calling conventions such as in C or PASCAL languages. In other words, be sure you can get the documentation on a DLL and don't always assume RapidQ can use all DLLs. See the FreeBasic DLL tutorial for examples of making your own DLL for RapidQ.

9.4 How to call a DLL
There is an example called DLL.BAS included in your Rapid-Q distribution. Included is also a DLL called PASCAL.DLL. You may want to take a look at it. To call DLLs in Rapid-Q is almost identical to the way you call DLLs in PowerBASIC or VisualBASIC. It may look nasty, but I've decided to conform to that standard.  One thing to note is that CASE MATTERS! Yes, that's right, if you miscapitalized your function, ka-boom! Oops, not to confuse you even more, but there's actually 2 functions. One which you will call in your program, and the other is the one contained in the DLL. These two functions/subs must have matching parameters, or ka-boom!
 DECLARE SUB Test LIB "TEST.DLL" ALIAS "MyFunc" (S AS STRING)
So you must be wondering, which is case sensitive, Test or MyFunc. Well, Test is the one you'll be calling in your own program, so you know that can't be it, MyFunc is the one contained in the DLL, so make sure you typed that in correct case! The above SUB will point to the memory address of your DLL (when loaded), and execute the code within. As you can see, there's only one parameter for our MyFunc routine. There are only 2 extra keywords introduced here, LIB and ALIAS. After LIB should be a string corresponding to the file name (and path if needed) of your DLL. If your DLL resides in your $PATH setting, you don't need to specify any path (ie. if your TEST.DLL resides in the folder as your application or in  C:\WINDOWS\SYSTEM). The second keyword ALIAS is to specify the function/subroutine you can execute in that DLL. Make sure you check your case, this is very important. If you're not sure of the case, you can use a DLL viewer (such as Quick View, more on this later). What are the valid datatypes to pass?
SHORT/WORD, INTEGER/LONG/DWORD, DOUBLE, STRING, QRECT, QNOTIFYICONDATA, and any UDT you create.
Unlike some implementations which force you to pass your STRING as a pointer, Rapid-Q will do all this for you, so you need not worry. Whenever you see LPZSTR as a parameter, that just means Long Pointer Zero-Terminated String. You can safely pass any string variable. You may notice that some BASIC languages (like VB) will ignore ALIAS whenever the function matches the same function you're declaring. For example:
 DECLARE SUB MyFunc LIB "TEST.DLL"
However, this DOES NOT apply to Rapid-Q, you can't do this, so don't even try.
Calling a DLL function or calling your own function is exactly the same, there's nothing more you need to know:
 MyFunc("Hello")


9.5 Using Quick View
As mentioned before, if you're not sure of the exact case of your function, it's a good idea to take a look using Quick View.
Quick View might be included with Windows. . To invoke Quick View, load up Windows Explorer (or click on "My Computer"), then find a .DLL file, right click on it, and select Quick View from the menu. Look for an Export Table, that's where you'll find all the available functions. If Quick View does not appear then download a DLL viewer such as ADllExports.exe  (you can search for it on the Web).

9.6 Writing your own DLLs
It's not possible to write DLLs in Rapid-Q. However there are a few BASIC compilers that let you write DLLs that use similar BASIC code as Rapid-Q. Some of the easiest to use are FreeBasic or BCX, which are free for downloading. Other alternatives are PowerBasic, HotBasic, or RealBasic. There are special procedures to follow such as knowing whether Rapid-Q passes a value or a pointer.  Rapid-Q passes all variable tyes (i.e., BYTE, INTEGER, LONG, etc.) by its value. However, User Defined Types and STRINGs are passed by reference (as a ponter). This is important to remember for STRINGs, since Rapid-Q  only passes the pointer (location in memory of the string, not the string itself). VisualBasic or PowerBasic may not always use string pointers. Please use pointers to strings in your parameters to avoid using OLE to allocate the string itself. Rapid-Q won't use OLE to allocate the string, since I don't anticipate many DLLs that don't use pointers to strings. To really understand how to work with DLLs you should review the FreeBasic DLL tutorial.

9.7 Using User Defined Types (UDTs) or unsupported types in a DLL call
 Rapid-Q user defined type is passed as a pointer (i.e., always as ByRef). Rapid-Q stores the data in a packed format so that all information in the UDT is placed end-on-end depending on the size of the variable declared in the UDT.  What is important is make sure you have your DECLARE SUB or DECLARE FUNCTION statements correct when you pass a pointer to the DLL. Commonly the variable name is prefixed with an "lp" meaning long pointer). :
'RECT is a structure of the form:
 TYPE RECT
   Left AS LONG
   Top AS LONG
   Right AS LONG
   Bottom AS LONG
 END TYPE

DECLARE FUNCTION GetWindowRect LIB "USER32" ALIAS "GetWindowRect" _
 (hWnd AS LONG, lpRect AS RECT) AS LONG
'note lpRect is really a LONG expecting a value to be an address in memory

Now you write your code like this:
DIM R AS RECT
 GetWindowRect(MainForm.Handle, R)
Sometimes in Windows programming you need to pass a NULL instead of the address of a UDT, for example to use a default. A NULL is just a long of value 0. To avoid getting a compile error, just change the type in the delaration:
DECLARE FUNCTION SpecialFunction LIB "Func.Dll" ALIAS "SpecialFunction"_
 (hWnd AS LONG, lpRect AS LONG) AS LONG 'tell the program to expect a long value

SpecialFunction(MainForm.Handle, 0&) 'now this works
SpecialFunction(MainForm.Handle, R) 'and this works too
We just changed the type of lpRect to LONG. This is because we want to pass a pointer (which is just a number). This will also work for passing the UDT normally as well


What if we want to pass complicated structures?
 
If you pass a  UDT to a DLL, RapidQ will automatically pass the address to the start of one contiguous block of memory holding the UDT data. The DLL will then read from this memory location. After returning from the DLL, we can retrieve the returned data (in the case above  GetWindowRect returns the RECT structure of the Window). This is rather simple, but what if there are nested TYPEs within a TYPE? Rapid-Q does not directly support nested TYPES but there is a work-around using QMEMORYSTREAM. 
TYPE Struct1
  A AS INTEGER
END TYPE

 TYPE Struct2
   S AS Struct1
   I AS INTEGER
 END TYPE
If a DLL call requires a TYPE, which may have nested TYPEs, then you can do the same kind of conversion:
TYPE Struct1
 A AS INTEGER
 END TYPE

 TYPE Struct2
 S AS LONG
 I AS INTEGER
 END TYPE

 DIM S1 AS Struct1
 DIM S2 AS Struct2

 DIM M1 AS QMEMORYSTREAM
 DIM M2 AS QMEMORYSTREAM

 M1.WriteUDT(S1)
 S2.S = M1.Pointer
 M2.WriteUDT(S2)

 '' Call DLL function...
Now the last case we have to consider is strings within TYPEs. We can't just declare a string anymore, we have to declare a pointer to a string, for example:
TYPE TStruct
 ST AS STRING    'actually sends a Long Pointer Zero-Terminated String
 Other AS INTEGER
 END TYPE
This would not be valid since ST would be passed by value (in which case it can consume any number of bytes in memory). However, most DLL calls want strings passed by reference (ie. just give it the pointer to the string). A conversion is necessary, using VARPTR:
 TYPE TStruct
 ST AS LONG
 Other AS INTEGER
 END TYPE

 DIM Struct AS TStruct
 DIM S AS STRING

 S = SPACE$(100) '' Allocate some space for string
Struct.ST = VARPTR(S)
'' To get back the string use VARPTR$
S = VARPTR$(Struct.ST)
Please note that this string conversion is only necessary for TYPEs and not necessary for normal DLL calls. For normal DLL calls which have STRING as parameters, just pass the string and not the pointer, since Rapid-Q will translate this automatically for you. You can, of course, do the conversion yourself, just remember to change the STRING parameter to something like LONG, and pass VARPTR(S$) instead of the actual string S$.

9.8 API conversion table
Most API programming in Rapid-Q is for working with the large and powerful the windows operating system.API. Documentation for Windows API can be found at www.msdn.microsoft.com by entering the name of the FUNCTION or SUB in the search box. However, almost all documentation is in C. This can be challenging to understand for a BASIC programmer. The easiest way to explain how to read it is an example. Here is how the Windows API function WriteFile is declared in C:

BOOL WriteFile(

  HANDLE hFile,                   // handle to file to write to
  LPCVOID lpBuffer,               // pointer to data to write to file
  DWORD nNumberOfBytesToWrite,    // number of bytes to write
  LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
  LPOVERLAPPED lpOverlapped       // pointer to structure needed for overlapped I/O
);

and now the same version in RapidQ:

DECLARE FUNCTION WriteFile LIB "kernel32" ALIAS "WriteFile"_
   (hFile AS LONG, _                         'handle to file to write to
   lpBuffer AS STRING, _                     'pointer to data to write to file
   nNumberOfBytesToWrite AS DWORD, _         'number of bytes to write
   BYREF lpNumberOfBytesWritten AS DWORD, _  'pointer to number of bytes written
   lpOverlapped AS OVERLAPPED) AS LONG       'pointer to structure needed for overlapped I/O

Notice how variable types (BOOL, HANDLE, LPCVOID, etc.) are stated before the variable name (hFile, lpBuffer, etc.). Now Let's take the example of a more complictated example, creating an stream of data for an Audio-Visual-Interlaced file. The function is contained in a DLL called AVIFIL32.DLL in your Windows system Folder.

The AVIFileCreateStream function creates a new stream in an existing file and creates an interface to the new stream. The declaration in C is like this:

STDAPI AVIFileCreateStream(
  PAVIFILE pfile,      
  PAVISTREAM * ppavi,  
  AVISTREAMINFO * psi  
);   // semicolon ends the entire command.

STDAPI means this is a FUNCTION that exists externally, and returns a LONG value (Often  'HRESULT' is used instead, but it all really means a LONG value is returned).
AVIFileCreateSteam is the name of the function.
pfile is a variable of type PAVFILE, which actually a variable holding the value holding a pointer. The 'p' reminds you it is a pointer.
ppavi is passed by reference (the * symbol means 'give me the address of the variable)
psi is a UDT of type AVISTREAMINFO and is passed to the API by reference (* means pointer).


Here is how you make the same Declare function in RapidQ:
Declare Function AVIFileCreateStream Lib "avifil32.dll" ALIAS "AVIFileCreateStreamA"_
 (ByVal   pfile AS LONG, _
 Byref    ppavi AS LONG, _
 ByRef   psi As AVISTREAMINFO) AS LONG


Notice there are big differences, especially since the actual name in the DLL is AVIFileCreateStreamA, but you will always call this function in Rapid-Q by AVIFileCreateStream. 

Here is another example, showing how to pass a string to a Windows API:
STDAPI AVIFileOpen( PAVIFILE * ppfile, LPCTSTR szFile, UINT mode, CLSID pclsidHandler );


Here is how you make the same Declare function in RapidQ:

Declare Function AVIFileOpen Lib "avifil32.dll" ALIAS "AVIFileOpen"_
(ByRef ppfile AS LONG,_            'the * in the C code means ByRef
 ByVal szFile As String, _         'szFile = 'string with zero terminated character' of file name
 ByVal uMode AS LONG,_             'the API wants a UINT, or an unsigned integer, but LONG is ok 
_ByVal pclsidHandler AS LONG) AS LONG    'CLSID is a pointer, but we will pass a NULL, so use ByVAL


As you can see, translating C code can be tricky. Luckily, many API declarations have already been done for Visual Basic, and converting from Visual Basic to RapidQ is much easier than C to RapidQ. There is even a tool in FreeBasic called SWIG that converts C include files to FreeBasic code. To help understand how to convert between different variable types, here is a basic summary with some brief explanations. Special thanks to Mayuresh S. Kadu for this WIN32 API in VB guide.

C data type Pascal Visual Basic
ByRef is default
Rapid-Q
ByVal is default for most
ATOM SHORT byval as INTEGER SHORT
BOOL BOOLEAN byval as LONG LONG
BYTE BYTE byval as BYTE BYTE
CHAR CHAR byval as BYTE BYTE
CHAR[20] STRING[20] as STRING N/A in API Declarations
COLORREF LONGINT byval as LONG LONG
DWORD DWORD byval as LONG DWORD
Windows handles ie. HDC
HWD, HRESULT, HMENU,etc. 
Windows Handles byval as LONG LONG
INT, UINT INTEGER, DWORD byval as LONG INTEGER, DWORD
LONG LONGINT byval as LONG LONG
Pointers to a variable that are passed by value
LPVOID, LPDWORD, 
LPINT, LPUNIT, etc.
LONGINT byval as LONG BYVAL as LONG
Pointers to a long
LPVOID * , LPDWORD *, 
LPINT *, etc
^DWORD as LONG BYREF as LONG
LPARAM LONGINT byval as LONG LONG
LPINT, LPUINT ^INTEGER as LONG BYREF as LONG
LPRECT ^TRECT as ANY QRECT
LPSTR, LPCSTR,  LPCTSTR PCHAR byval as STRING as STRING or
BYVAL as STRING
LPVOID LONGINT as ANY LONG
LPWORD ^WORD as INTEGER BYREF as WORD
LRESULT LONGINT byval as LONG LONG
NULL NIL byval as LONG BYVAL as LONG (use 0& in call)
SHORT SHORT byval as INTEGER SHORT
WORD WORD byval as INTEGER WORD
WPARAM LONGINT byval as LONG LONG

Many other types exist- the general rule is that if C declares the variable with an “L” or “LP” as the fist characters then it is a pointer of type LONG. A variable with 'DW' means double word (LONG or DWORD in RQ), and lpWxxx means word (WORD or SHORT in RQ).  
RapidQ can accept but does not strictly support the unsigned data types such as DWORD, UINT8, UINT16, UINT32, etc. Usually this is not a problem if you are careful in handling the numbers in math statements. If you need 10 byte Reals, or 64 bit integers, then try FreeBasic, BCX, RealBasic, etc. instead. For those data types in red, they are pointers to the variable. In Rapid-Q, this requires using VARPTR in passing the address of the variable, and not the value of the variable. Here's some sample conversions:
 In VB:
 DECLARE SUB Test LIB "USER32" ALIAS "What" _
 (byval L AS LONG, byval S AS STRING)

 Test (1230, "Hello world!")

 In Rapid-Q:
 DECLARE SUB Test LIB "USER32" ALIAS "What" _
 (byval L AS LONG, byval S AS STRING)

 Test (1230, "Hello world!")
Nothing different there, note that Rapid-Q doesn't use BYVAL, so you can ignore putting them there. In the above example, the string is passed by reference, ie. the address of the string is passed, not the string itself. Here's an example which passes the whole string (not the address, so it involves OLE to allocate space for the string), this works fine in VB but not in Rapid-Q:
 In VB:
 DECLARE SUB Test LIB "USER32" ALIAS "What" _
 (byval L AS LONG, S AS STRING)

 Test (1230, "Hello world!")

 In Rapid-Q:
 Not possible, since it doesn't use OLE to allocate the string.
How about NULL strings? Here's another situation (please note that I don't use VB much so it may be possible to declare S as STRING and pass it a vbNullString, I'm not really sure so I'll play it safe):
 In VB:
 DECLARE SUB Test LIB "USER32" ALIAS "What" _
 (byval L AS LONG, byval S AS LONG)

 Test (1230, 0&)

 In Rapid-Q:
 DECLARE SUB Test LIB "USER32" ALIAS "What" _
 (byval L AS LONG, byval S AS LONG)

 Test (1230, 0&)
So what if we want to pass a string instead of a NULL string, then you'd need to use VARPTR:
 In VB or Rapid-Q:
 DIM S AS STRING
 S = "Hello world!"

 Test (1230, VARPTR(S))
A NULL string basically means an address of 0. VARPTR just returns the address of a variable. How about other pointers, like LPWORD? A WORD is just an unsigned 16-bit number, VB only supports signed 16-bit numbers called INTEGERs, but the general idea is to remove the byval keyword when passing variables by reference:
 In VB (doesn't support WORD, so use next best):
 DECLARE SUB Test LIB "USER32" ALIAS "Another" _
 (L AS INTEGER)

 DIM L AS INTEGER

 Test (L)

 In Rapid-Q:
 DECLARE SUB Test LIB "USER32" ALIAS "Another" _
 (BYREF L AS WORD)

 DIM L AS WORD

 Test (L)
In Rapid-Q, use BYREF when passing variables by reference, in VB you don't need to use BYREF, as the above example demonstrates.

Prev Chapter Up Contents Next Chapter