FreeBASIC is a free open source 32-bit BASIC compiler for Windows, DOS, and Linux. It is
a powerful development tool and has support for many C libraries including Allegro,
SDL, and OpenGL. It is
nearly 100% compatible with QuickBASIC, and shares many keywords with RapidQ. FreeBASIC
is not case sensitive and has full support for pointers (functions, virtual tables, etc), unsigned data
types and nested UDT structures.
FreeBasic is more powerful
than RapidQ:
1) it is optimized compiled machine code, not interpreted, and allows inline Assembly
code.
2) it can
link with C-libraries including iDispatch COM and DirectX9
3) has advanced support for pointers and variable types and much more.
Where do you download FreeBasic? ; www.freeBasic.net,
and check the forum at www.freeBasic.net/forum.
'Make a program called Test.bas that will be
compiled as RapidQ code:
DECLARE SUB TestInteger lib "DLL_test"
ALIAS "TestInteger@4" (byval myData AS Integer)
TestInteger(12345)
Now make a FreeBasic file called DLL_test.bas :
'option explicit 'this keyword may not be needed, check FB docs
'this declare statement sets the case sensitive name
for the sub in the DLL
'without it the name would be all UPPERCASE. Be sure to use ByVal, ByRef, etc in
FreeBasic
DECLARE SUB TestInteger lib "DLL_test" alias "TestInteger" (byval myData AS Integer)
SUB TestInteger (byval myData AS Integer) EXPORT
print "In FreeBasic your integer = " , myData
END SUB
Now compile DLL_test.bas as a FreeBasic DLL file;
i.e., at the DOS prompt (or in an IDE)
*** the -dll command instructs it to
generate a DLL file. ***
fbc -dll DLL_test.bas
That's it! You can run Test.exe (the RapidQ program) at the DOS prompt and you
should see "12345" printed. You will notice a few important
details. The DECLARE statement in RapidQ has a funny @4 name added to the sub in
the DLL name:
DECLARE SUB TestInteger lib "DLL_test" ALIAS "TestInteger@4"
(byval myData AS Integer)
This is because the compiler code that generates the DLL automatically
tags on this "mangled name" so that programs using the DLL know to use
the STDCALL convention. The @4 means that 4 bytes will be put on the stack for
so the calling program knows how many parameters need to be read. If instead you
were going to pass two integers to the DLL then the compiler will add @8 to the
end of the name, and for a three integer-parameter SUB it will add @12, and so on.
Remember case is important for the Sub or Function in
the DLL.
Other things to note here, the FreeBasic option explicit command is like $TYPECHECK ON in
RapidQ, it may not be needed. The other is EXPORT,
which informs FreeBasic that this SUB will be the one available to external programs
calling the DLL. All other SUB / FUNCTION, and variables will be global only to
the FreeBasic program, not the RapidQ program. The other thing you will
notice is that FreeBasic makes an DLL_test.dll.a. The .a files are for import
libraries, you can delete them because you won't need them for RapidQ.
OK so now I know how to call a simple DLL with an integer.
What about other
types? What about pointers?
'Let's try one with a DOUBLE
and a pointer to an INTEGER:
DECLARE SUB TestVars lib "DLL_test"
ALIAS "TestVars@12" (byval myData AS Double, ByRef myInt AS INTEGER)
DIM a AS INTEGER
a = 12
TestVars(12345.12345, a)
ShowMessage STR$(a) 'let's see what
returns
The FreeBasic code looks very familiar:
option explicit
DECLARE SUB TestVars lib "DLL_test" alias "TestVars" (byval myData AS
Double, ByRef myInt AS INTEGER)
SUB TestVars(byval myData AS Double, ByRef myInt AS INTEGER) EXPORT
print "In FreeBasic your Double = " , myData
print "In FreeBasic your integer value is = " ,
myInt
print "now it is 100"
myInt = 100
END SUB
As you can see it works pretty much like in RapidQ. If you want the SUB or
FUNCTION to change the variable, then pass it ByRef. Notice
the
"TestVars@12" because the
Double is 8 bytes, and the pointer (32-bit Windows) is 4 bytes. If you
find it too difficult to figure out the name mangling? Download a DLL viewer like
ADllExports.exe, and the program will show you the actual name.
For RapidQ users! The RapidQ BYTE, SHORT, SINGLE and
DOUBLE has a bug when
passing these data types to a DLL.
BYTE, SHORT, WORD, SINGLE, and DOUBLE must be stored in a 32-bit long if
passing ByVal to a DLL.
Basically always try to pass a 32-bit variable such as INTEGER, DWORD, or LONG.
If your DLL needs less than 2 SINGLE or more than 2 DOUBLE variables, these will
not be passed correctly! The safest way to pass these variables ByRef, if
possible. If you must pass them ByVal then you should just state them as
INTEGERS or LONG, in the DECLARE statement.
'here is the actual API
call
SUB gluPerspective(fovy as Double, aspect as Double, zNear as Double, zFar as
Double)
'here is how to implement it
DECLARE SUB gluPerspective_API LIB "glu32.dll" ALIAS "gluPerspective"
_
(fovy1 as long,fovy2 as long, aspect1 as long, aspect2 as long, _
zNear1 as long, zNear2 as long, zFar1 as long, zFar2 as long)
DEFDBL d: DEFINT i(7)
d = fovy: MemCpy (VarPtr(i(0)), VarPtr(d), 8)
d = aspect: MemCpy (VarPtr(i(2)), VarPtr(d), 8)
d = zNear: MemCpy (VarPtr(i(4)), VarPtr(d), 8)
d = zFar: MemCpy (VarPtr(i(6)), VarPtr(d), 8)
gluPerspective_API(i(0), i(1), i(2), i(3), i(4), i(5), i(6), i(7))
END SUB
'here is another example of BYTE
Declare Sub keybd_event Lib "user32" Alias "keybd_event" _
(bVk As BYTE, bScan As Long, dwFlags As Long, dwExtraInfo As Long)
'all you have to do is change BYTE to LONG, and it will work fine
Declare Sub keybd_event Lib "user32" Alias "keybd_event"
_
(bVk As Long, bScan As Long, dwFlags As Long, dwExtraInfo As Long)
Arrays and looping
Obviously a simple programs are not of real use. You do not want to
call a DLL for a single simple calculation because it takes time to just run
the DLL code. What you need a DLL
for is complicated calculations requiring multiple loops, access to high level
libraries, and so forth. The most common use for RapidQ is speeding up
calculations.
Take an example where you would like to
find the average of a large data set:
SUB CalcAverage(Datas() AS SINGLE, NumPts AS LONG, ByRef Average AS SINGLE)
DIM Summ AS SINGLE, i AS INTEGER
FOR i = 0 TO NumPts
Summ = Summ + Datas(i)
NEXT i
Average = Summ / NumPts 'this will pass back the new average
END SUB
This calculation will take about 50 times longer in RapidQ relative to FreeBasic.
So how do I pass my array to the DLL?
RapidQ handles arrays by reference, you just have to pass the reference to the
first element in the array AND make sure FreeBasic knows it is
getting an array of a specific type and how many elements are in it. There are actually two ways to do this:
FreeBasic has special keyword PTR to indicate the SUB gets a pointer to an
array of SINGLE:
SUB CalcAverage (Datas AS SINGLE PTR, BYVAL NumPts as LONG, ByRef Average AS SINGLE) EXPORT DIM Summ AS SINGLE Dim fp As Single Ptr fp = @Datas 'asign the address local DIM f(NumPts) AS SINGLE 'allocate a local array memcpy(@f(0), fp, NumPts*SIZEOF(SINGLE)) 'copy it FOR i = 0 TO NumPts 'now the code is just like RQ Summ = Summ + Datas(i) NEXT i Average = Summ / NumPts 'this will pass back the new average memcpy(fp, @f(0), NumPts*SIZEOF(SINGLE)) 'be sure to copy back! END SUB
It is important to know RapidQ and FreeBasic use different descriptors of arrays (i.e., definition of the length of the array and the number of dimensions). But each program knows the address of the data. That is why the code is complicated with the assignment of the array address (fp =@Datas) and the memcpy command to create a new array that FreeBasic. There is another simpler way to write the code using indexing with brackets [ ] and using BYVAL Datas AS SINGLE PTR:
SUB CalcAverage (BYVAL Datas AS SINGLE PTR, BYVAL NumPts as LONG, ByRef Average AS SINGLE) EXPORT DIM Summ AS SINGLE FOR i = 0 TO NumPts 'now the code is just like RQ Summ = Summ + Datas[i] 'note brackets not parens! NEXT i Average = Summ / NumPts 'this will pass back the new average END SUB
Passing strings and functions
Strings are complicated due to differences between compilers on
how to describe all the parameters of a string. Like arrays, the complier needs
to know how long the string is, and so forth. You can freely pass strings to
FreeBasic, but it will just treat it as an array of bytes. An easy way to work
with strings is to copy the passed string to a temporary FreeBasic string.
RapidQ always passes strings by reference.
Compile this with fbc -dll mydll.bas
declare function teststr lib "mydll" alias "teststr" ( byval tstr as byte ptr) as long function teststr (byval tstr AS BYTE PTR) as long EXPORT dim sss as string 'this is local to sub only, not RapidQ PRINT "the first char = "; CHR$(tstr[0]) 'note reference 0 based sss = *tstr 'dereference value held in pointer sss=sss+"bzzzz" 'now treat it like any string print "in dll the new string =" ;sss teststr = INSTR(sss, "bz") 'return back function result end function Test with this RapidQ code: declare function teststr lib "mydll" alias "teststr@4" (tstr1 as string) as long DEFSTR aa$="abcdefgh" DEFLNG zz zz=teststr ( (aa$)) print "teststr=" ; zz
There are several important things to notice. First FreeBasic uses ByVal tstr
AS BYTE PTR to treat the string as an array that is passed by reference. Second,
the temporary string, sss, is assigned the string with the asterisk (*)
character which is used in the C language to dereference a pointer (that
is get the value the memory that pointer is pointing to). The memory is
assigned to sss until a CHR$(0) is reached, indicating a zero-terminated string.
Third, you can also access individual characters with the index tstr[0] like
that in arrays discussed above.
The last thing to point out is the FUNCTION works, by passing back to RapidQ a
LONG value to the variable zz. At the time of this writing,
ONLY LONG, and INTEGERS can be passed back to RapidQ from a
FreeBasic FUNCTION.
User defined types and more complicated
calls
Since FreeBasic uses a syntax very similar to RapidQ, you can
also pass UDTs (User define types) with ease. The following example shows just
how complicated the structure can be and you will have total access to the
RapidQ data inside the FreeBasic DLL:
Let's define the structure in RapidQ that uses most variable types:
DECLARE SUB TestUDT lib "DLL_test" alias "TestUDT@4" (lpUDT AS LONG) '@4 is 4 bytes for UDT address TYPE myUDT a AS BYTE b AS SHORT c As LONG d As STRING * 7 'must specify size e(0 to 20) As DOUBLE 'just the pointer is passed to the DLL! END TYPE DIM TheUDT as myUDT TheUDT.a = &HF0 TheUDT.b = &HBBBB TheUDT.c = &HFF000001 TheUDT.d = "hello!" +CHR$(0) 'must end buffer with zero char TheUDT.e(3) = 5.2076 TestUDT(TheUDT) ShowMessage str$(TheUDT.e(3)) 'returns back new value
Again the FreeBasic code looks very familiar except for some variable types:
TYPE myUDT FIELD=1 'sets this is a PACKED structure a AS BYTE 'that is default in RapidQ, not FB b As SHORT c As LONG d As ZSTRING * 7 'ZSTRING means zero-terminated string e As DOUBLE PTR 'RapidQ passes the array pointer! END TYPE DECLARE SUB TestUDT lib "DLL_test" alias "TestUDT" (BYREF lpUDT AS myUDT) SUB TestUDT (BYREF lpUDT AS myUDT) EXPORT Print "Byte = "; HEX$(lpUDT.a) Print "Short = "; HEX$(lpUDT.b) Print "Long = "; HEX$(lpUDT.c) Print "print string is "; lpUDT.d 'FB knows it is a string Print "third char = "; CHR$(lpUDT.d[3]) 'print out just one character lpUDT.e[3] = -20.567 'change its value by pointer reference ... END SUB
There are some important things to note. The ByRef statement for the UDT because
RapidQ always passes UDTs by reference. Also, the FIELD=1
statement must be set in FreeBasic to handle the RapidQ UDT, which is commonly
called a "packed structure." That just means all variables in
the UDT are packed right next to each other without padding or aligning the data
on specific boundaries. The other is the conversion of STRING to ZSTRING, and an
array of DOUBLE to DOUBLE PTR. Again these are needed in FreeBasic to work with
the way RapidQ actually passes the data to any DLL, not just FreeBasic.
From this one example you should be able to see how to pass different variable
types. Here is a table of other variable types. Note that Freebasic has more
types than RapidQ, including unsigned versions of all RapidQ types. FreeBasic
assumes that variables are passed ByRef unless specified ByVal.
| RAPID-Q declaration | FreeBasic declaration | Range |
| var AS BYTE | ByVal var AS UBYTE | unsigned 0 to 255 |
| var AS BYTE | ByVal var AS BYTE | signed -128 to 127 |
| var AS SHORT | ByVal var AS SHORT | signed -32768 to 32767 |
| var AS WORD | ByVal var AS USHORT | unsigned 0 to 65365 |
| var AS LONG | ByVal var AS LONG | signed 4 bytes 32-bits |
| var AS INTEGER | ByVal var AS INTEGER | signed 4 bytes 32-bits |
| var AS DWORD (actually a LONG in Windows) | ByVal var AS UINTEGER | unsigned 4 bytes 32-bits |
| var AS STRING | var AS ZSTRING * size | fixed length or find last 0 char. |
| var AS SINGLE | ByVal var AS SINGLE | signed 4 bytes 32-bits ( 6 decimal digits) |
| var AS DOUBLE | ByVal var AS DOUBLE | signed 8 bytes 64-bits ( 15 decimal digits ) |
| Arry(index) AS BYTE, var AS STRING | ByVal Arry AS BYTE PTR | Array of bytes, can be strings |
| Arry(index) AS INTEGER, etc. | ByVal Arry
AS INTEGER PTR access by Arry[i] |
Array of 32-bit integers |
| var AS UDTtype | ByRef var AS UDTtype | packed structure |
Advanced Topics, passing data between
the FreeBasic DLL and your RapidQ program
In this example you will pass the address of a RapidQ sub to the FreeBasic DLL. FreeBasic will then call your RapidQ sub by passing
an integer to it. You will find this important if a RapidQ component needs to be
updated while the FreeBasic DLL is doing a long loop.
'**** FreeBasic DLL CODE filename: FB_CallBackFromDll.Bas
'compile this as fbc -dll FB_CallBackFromDll.Bas
' -----------------------------------------------------------------
' TEST SET INTEGER IN CALLING APP FROM A FreeBasic DLL
' by Jacques March 7th, 2006
' -----------------------------------------------------------------
' -----------------------------------------------------------------------------
' first set function declares
Declare function CallBackInDll lib "FB_CallBackFromDll" _
alias "FB_CallBackFromDll@4"_
(ByVal iCallBack AS FUNCTION(ByVal i AS INTEGER) AS INTEGER) as integer
' -----------------------------------------------------------------------------
function CallBackInDll (ByVal iCallBack AS FUNCTION(ByVal i AS INTEGER) AS INTEGER) as integer export
Dim iProgress As Integer
While iProgress < 200
iCallBack (iProgress)
Sleep (200)
iProgress = iProgress + 1
Wend
end function
' -----------------------------------------------------------------------------
'Now this will be your RapidQ program
'**** RAPIDQ CODE FileName: WhatEverYouwant.Bas
' -----------------------------------------------------------------------------
$ESCAPECHARS ON
$TYPECHECK ON
$INCLUDE "RAPIDQ.INC"
'
Declare function CallBackInDll lib "FB_CallBackFromDll" _
alias "FB_CallBackFromDll@4"(ByVal iCallBack as Integer) as integer
'
Create frmGauge As QForm
Center
ClientWidth = 300
ClientHeight = 10
CREATE Gauge AS QGauge
Align = alClient
Max = 200
Color = &HFF00
position = 10
Kind = gkHorizontalBar ' gkPie
END CREATE
End Create
'-------------
Sub GaugeCallBack (iProgress As Integer)
Print "In RQ_CallBack: iProgress = ";iProgress ' or change your Gauge position
Gauge.Position = iProgress
DoEvents
End Sub
'
frmGauge.Show
'
CallBackInDll(CodePtr(GaugeCallBack))
'
' -------------------------------------------------------------
DefStr sExit
Input " CR to QUIT ", sExit
frmGauge.Close
Application.Terminate
' -------------------------------------------------------------
'
Other details:
FreeBasic will automatically use UPPERCASE for the Sub/Function in the DLL
unless you use the ALIAS keyword in the FreeBasic program.
RapidQ uses the STDCALL convention and this is the default calling
convention for FreeBasic and Windows API calls. STDCALL pushes the arguments
from right to left onto the stack so that the last argument can be accessed by
ebp+8. With stdcall convention the callee ( the called function ) has to
completely clean up the stack, that is local variables/temps plus the arguments.
as a last convention with stdcall function names are appeneded a @<num>
where num is the number of bytes taken on the stack by the arguments of the
function. CDECL convention hand pushes the argument list from right to left onto
the stack so that the first element of the argumentlist is at ebp+8. no suffix
is appended. this is most often used for variable argument lists and the like as
the first element can tell how many arguments have been passed to the function.