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 (and some
C++) libraries. 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, GTK,
OpenGl, DirectX, Newton, and many more
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 :
'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.
Note the keyword 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. See Variable types and sizes below. 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.
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.