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.
