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 = -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. 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 Chapter | Contents | Next Chapter