Rapid-Q Documentation by William Yu (c)1999-2000 Chapter 9


9. 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 = -99999

     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. So you could write your DLL in Delphi or C++ and use those functions in Rapid-Q. 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. executed at run-time).

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 correctly! 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 path 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 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"
As long as your function matches that of the DLL function, you can ignore using ALIAS. 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 is included with Windows (you don't need to download it). 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. There are probably better DLL viewers available, but I won't advertise them here. Most of the time, Quick View is all you'll need.

9.6 Writing your own DLLs
As of this writing, it's not possible to write DLLs in Rapid-Q. Most languages are capable of creating DLLs, the only problem I see is STRINGs, especially if you're using VisualBasic or PowerBasic. 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.

9.7 Using unsupported types in DLL call
THIS SECTION IS NULL AND VOID WITH THE INTRODUCTION OF STRUCT, BUT CAN STILL BE USED IF YOU LIKE. Rapid-Q has a different storage mechanism for user defined TYPEs which renders it useless for DLL calls that require certain TYPE parameters. However, all is not lost. There is a cheap workaround which works just fine, but does need a little getting used to. DLL functions that require TYPE parameters need to be passed a pointer to that TYPE, so how is this accomplished? Let's look at an example of a DLL function which requires a TYPE parameter:
    DECLARE FUNCTION GetWindowRect LIB "USER32" ALIAS "GetWindowRect" _
                     (hWnd AS LONG, lpRect AS RECT) AS LONG
RECT is a structure of the form:
    TYPE RECT
        Left AS LONG
        Top AS LONG
        Right AS LONG
        Bottom AS LONG
    END TYPE
Naturally, you would like this to work:
    DIM R AS RECT

    GetWindowRect(MainForm.Handle, R)
YES, THIS WORKS NOW. However, Rapid-Q stores user defined types noncontiguously, ie. not as one whole block as required by the DLL call, so this call is invalid. So what does work? A simple workaround is necessary, using the ever useful QMEMORYSTREAM component:
    '' Notice the change in lpRect
    DECLARE FUNCTION GetWindowRect LIB "USER32" ALIAS "GetWindowRect" _
                     (hWnd AS LONG, lpRect AS LONG) AS LONG

    DIM R AS RECT
    DIM M AS QMEMORYSTREAM

    M.WriteUDT(R)
    GetWindowRect(MainForm.Handle, M.Pointer)
    M.Position
    M.ReadUDT(R)
The first thing we do is change the parameters of the DLL Function. We just changed the type of lpRect to LONG. This is because we want to pass a pointer (which is just a number). Here we use QMEMORYSTREAM to store the user defined type (using M.WriteUDT) in one contiguous block for our DLL call. All we have to do is pass the pointer to this block of memory and the DLL will read from this memory location. After calling the function we should retrieve the returned data (if necessary, in this case GetWindowRect returns the rect structure of the Window). This is rather simple, but what if there are nested TYPEs within a TYPE?
     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
         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
A lot of people have requested an API guide to convert their WinAPI declarations in C or VB to use with Rapid-Q, so 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 VB Rapid-Q
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 Windows Handles byval as LONG LONG
INT, UINT INTEGER, DWORD byval as LONG INTEGER, DWORD
LONG LONGINT byval as LONG LONG
LPARAM LONGINT byval as LONG LONG
LPDWORD ^DWORD as LONG BYREF as DWORD
LPINT, LPUINT ^INTEGER as LONG BYREF as LONG
LPRECT ^TRECT as ANY QRECT
LPSTR, LPCSTR PCHAR byval as STRING BYREF 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 LONG
SHORT SHORT byval as INTEGER SHORT
WORD WORD byval as INTEGER WORD
WPARAM LONGINT byval as LONG LONG

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 ChapterContentsNext Chapter