The BCX statement C_DECLARE is used to expose a SUB or FUNCTION procedure that has been exported from an external Dynamic Link Library (DLL) or to expose a procedure that has been compiled and placed in an .obj file or library.
Syntax 1:C_DECLARE [ SUB | FUNCTION ] ProcName(Paramlist) Parameters: |
_cdecl is the default C calling convention. The stack is cleaned up by the calling function, so vararg functions can be used. Arguments are pushed on the stack from right to left.
C_DECLARE adds the prototypes to the C code, plus it allows you to declare the function data type and its arguments using BASIC syntax, instead of C.
C_DECLARE is not compatible with LOADLIBRARY. If LOADLIBRARY is to be used then DECLARE must be used as the declaration specification for the functions in the DLL.
For example ...
C_DECLARE FUNCTION FOO (A AS UINT) AS INTEGER
translates to C code
int __cdecl FOO(UINT);
which will not work with LOADLIBRARY.
Instead, you would need to inline:
int(*FOO)(UINT);
Optional arguments are allowed, although not all compilers support optional arguments for syntax 2. The OPTIONAL keyword is not needed and should not be used.
C_DECLARE supports variable arguments (...).
The C_DECLARE statement can be used in the development of .obj files, however, that can be difficult to implement. Every time a different compiler is used, there is a whole new puzzle to sort out. And linking .obj files created with disparate compiler systems is usually problematic. Here is an example using the Microsoft compiler for implementation. It has three parts:
Copy and save as Add_Two_Ints.bas, the code for the standalone function.
$NOMAIN FUNCTION Add_Two_Ints (A, B) AS INTEGER EXPORT FUNCTION = A + B END FUNCTION
Copy and save the Test.bas snippet
C_DECLARE FUNCTION Add_Two_Ints (A, B) AS INTEGER PRINT Add_Two_Ints(3, 5) PAUSE
And finally, copy and save as Build.bat, the following batch file to compile the project with a Microsoft VS140 version compiler.
@CLS
:***********************************************
: Create two C files using BCX
:***********************************************
@BC Test
@BC Add_Two_Ints
:***********************************************
: Adjust the following for your compiler system
:***********************************************
@CALL "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat" X86
:***********************************************
: Compile steps
: NOTE: /Gd use __cdecl calling convention
:***********************************************
@cl.exe /c /Gd Add_Two_Ints.c
@cl.exe /c /Gd Test.c
:***********************************************
: Link step
:***********************************************
@link.exe Test.obj Add_Two_Ints.obj /OUT:Test.exe /RELEASE /MACHINE:X86 /SUBSYSTEM:CONSOLE
@ECHO All Done!
The BCX statement DECLARE provides a procedure declaration specification that allows the function to be used with the _stdcall calling convention.
Syntax 1:DECLARE [ SUB | FUNCTION ] ProcName(Paramlist) |
Syntax 2:DECLARE [ SUB | FUNCTION ] ProcName AS STRING _ LIB "DllName.Dll" _ ALIAS "ProcName" _ (Paramlist) |
Optional arguments are allowed, although not all compilers support optional arguments for syntax 2. The OPTIONAL keyword is not needed and should not be used.
Once you C_DECLARE or DECLARE a SUB or FUNCTION inside your program, you can call it from any other SUB or FUNCTION. In a CONSOLE app, you can place DECLARE statements anywhere in your source program.
In a GUI program, you must place it a SUB or FUNCTION so that the initialization code has some place valid to run. The smart place to put your DECLARE in a GUI is inside your WinMain FUNCTION, or if you are using the simplified BCX_XXXXX GUI commands, inside the SUB FORMLOAD .
In a DLL app, you should place the DECLARE inside DllMain.
Here is a complete example which calls a DLL function which returns a string. The example is in three sections, the DLL, a program to call the function from the DLL and a batch file to compile the programs.
The first section is the BCX DLL code. Cut and save this as RESPOND.BAS
$DLL STDCALL FUNCTION Respond (Buf$) EXPORT Buf$ = Buf$ & " The Eagle has landed" FUNCTION = LEN(Buf$) END FUNCTION
The second section is the program which will call the function in the DLL. Cut and save this as TESTDLL.BAS
DIM a$, l DECLARE FUNCTION Respond LIB "respond.dll" DECLARE "Respond"(foo$) AS INTEGER a$ = "Boy Howdy" l = LEN(a$) ? l;" ";a$ l = Respond(a$) ? l;" ";a$ PAUSE
Here, in the third section, is a batch file to translate, compile and link the RESPOND.BAS and TESTDLL.bas.
If Pelles C is used as the compiler, cut and save the following as BUILD.BAT
BC Respond POCC /Ze /Gn /Zx Respond.c POLINK /dll Respond.obj BC TestDLL POCC /Gd /Ze /Zx TestDLL.c POLINK TestDLL.obj
BCX provides a universal method for calling both _stdcall and _cdecl 32 bit DLL functions irrespective of the differences between the two calling conventions.
Syntax:[RetVal =]DLLFunction(LIB "DLLName[.dll]" [, Parameters, ...]) Parameters:
|
Unlike the conventional DECLARE and C_DECLARE statements, the dynamic DLL function calling engine provides the following advantages:
To be able to ensure this non-language-specific flexibility, the DLLs being called should meet the following reasonable requirements:
The overwhelming majority of commonly used Windows API, VisualBasic, Delphi, and third-party DLLs are based on these simple rules.
👉 While the DECLARE and C_DECLARE convention is still supported by BCX for backward compatibility reasons, both conventions should not be used simultaneously in the same code because this will render the dynacall engine's memory management facilities ineffective.
MessageBox(LIB "user32", NULL, "Your Msg", "Your Title", MB_OK)
BCryptGenRandom is part of the Cryptography API: Next Generation (CNG). In the example below, the RND_S() wrapper function uses LIB to call BCryptGenRandom dynamically.
DIM AS ULONG Random DIM AS INTEGER i, j PUSHCOLORS CLS COLOR 2, 0 : LOCATE 12, 25, 0 INPUT "Hit [Enter] to see a sample", i : COLOR 7, 0 CLS FOR i = 1 TO 23 FOR j = 1 TO 70 STEP 20 Random = RND_S() + 9 LOCATE i, j, 0 COLOR MOD(i, 4.0) + 10, 0 PRINT Random NEXT NEXT LOCATE i, j COLOR 7, j POPCOLORS PAUSE FUNCTION RND_S() AS ULONG DIM AS LONG Status DIM AS ULONG dwFlags, ulRand dwFlags = &H00000002 ' BCRYPT_USE_SYSTEM_PREFERRED_RNG Status = BCryptGenRandom(LIB "bcrypt.dll", 0, CAST (PUCHAR, &ulRand), SIZEOF(ulRand), dwFlags) IF Status <> 0 THEN ' If failed, return fallback value FUNCTION = GetTickCount() END IF FUNCTION = ulRand END FUNCTION
This example uses the BCX keyword LIB to dynamically load, from OleAut32.dll, three CY data type functions, VarCyAdd, VarCySub, and VarCyMul. The CY data type is a numeric data type, with a range from -922,337,203,685,477.5808 to 922,337,203,685,477.5807. CY data type functions are used for fixed-point calculations where accuracy is particularly important. CY calculations are commonly applied in currency analytics.
OleAut32.dll does not contain a VarCyDiv. Included in the example is a surrogate version in the FUNCTION CyDiv.
There are more CY data type functions in the OleAut32.dll header, that you can investigate and possibly implement in BCX using the techniques shown in functions below to guide you. Microsoft documentation for the OleAut32.dll header is at the Microsoft Win32 API oleauto.h webpage.
DIM x AS CY, y AS CY, z AS CY x.int64 = 12345678 ' Represents 1234.5678 y.int64 = 20000 ' Represents 2.0000 z = CyAdd(x, y) PRINT "Add: "; FormatCy$(z) ' Expect 1236.5678 z = CySub(x, y) PRINT "Sub: "; FormatCy$(z) ' Expect 1232.5678 z = CyMul(x, y) PRINT "Mul: "; FormatCy$(z) ' Expect 2469.1356 z = CyDiv(x, y) PRINT "Div: "; FormatCy$(z) ' Expect 617.2839 PAUSE FUNCTION CyAdd (a AS CY, b AS CY) AS CY '--------------------------------------------------------------------------- ' Performs addition of two CY (CURRENCY) values using OleAut32.dll. ' @param a First CY value (64-bit integer scaled by 10000). ' @param b Second CY value (64-bit integer scaled by 10000). ' @return CY value representing the sum of a and b. ' @note Returns zeroed CY on error. Windows-specific due to OleAut32.dll '--------------------------------------------------------------------------- DIM result AS CY IF VarCyAdd(LIB "OleAut32.dll", a, b, &result) = 0 THEN FUNCTION = result ELSE PRINT "Error in CyAdd" DIM zero AS CY FUNCTION = zero END IF END FUNCTION FUNCTION CySub (a AS CY, b AS CY) AS CY '--------------------------------------------------------------------------- ' Performs subtraction of two CY (CURRENCY) values using OleAut32.dll. ' @param a First CY value (minuend, scaled by 10000). ' @param b Second CY value (subtrahend, scaled by 10000). ' @return CY value representing a minus b. ' @note Returns zeroed CY on error. Windows-specific due to OleAut32.dll. '--------------------------------------------------------------------------- DIM result AS CY IF VarCySub(LIB "OleAut32.dll", a, b, &result) = 0 THEN FUNCTION = result ELSE PRINT "Error in CySub" DIM zero AS CY FUNCTION = zero END IF END FUNCTION FUNCTION CyMul (a AS CY, b AS CY) AS CY '--------------------------------------------------------------------------- ' Performs multiplication of two CY (CURRENCY) values using OleAut32.dll. ' @param a First CY value (scaled by 10000). ' @param b Second CY value (scaled by 10000). ' @return CY value representing the product of a and b (scaled by 10000). ' @note Returns zeroed CY on error. Windows-specific due to OleAut32.dll. '--------------------------------------------------------------------------- DIM result AS CY IF VarCyMul(LIB "OleAut32.dll", a, b, &result) = 0 THEN FUNCTION = result ELSE PRINT "Error in CyMul" DIM zero AS CY FUNCTION = zero END IF END FUNCTION FUNCTION CyDiv (a AS CY, b AS CY) AS CY '--------------------------------------------------------------------------- ' Performs division of two CY (CURRENCY) values using DOUBLE arithmetic. ' @param a First CY value (dividend, scaled by 10000). ' @param b Second CY value (divisor, scaled by 10000). ' @return CY value representing a divided by b (scaled by 10000). ' @note Uses DOUBLE for division due to absence of VarCyDiv in OleAut32.dll. ' Returns zeroed CY if divisor is zero or conversion fails. '--------------------------------------------------------------------------- DIM result AS CY DIM aDouble AS DOUBLE, bDouble AS DOUBLE, divDouble AS DOUBLE aDouble = CyToDouble(a) bDouble = CyToDouble(b) IF bDouble <> 0 THEN divDouble = aDouble / bDouble result = CyFromDouble(divDouble) FUNCTION = result ELSE PRINT "Error: Division by zero" DIM zero AS CY FUNCTION = zero END IF END FUNCTION FUNCTION CyToDouble (cy AS CY) AS DOUBLE '--------------------------------------------------------------------------- ' Converts a CY (CURRENCY) value to a DOUBLE. ' @param cy CY value (64-bit integer scaled by 10000). ' @return DOUBLE value representing cy / 10000. ' @note Returns 0.0 on error. Windows-specific due to OleAut32.dll. '--------------------------------------------------------------------------- DIM d AS DOUBLE IF VarR8FromCy(cy, &d) = 0 THEN FUNCTION = d ELSE PRINT "Error in CyToDouble" FUNCTION = 0.0 END IF END FUNCTION FUNCTION CyFromDouble (d AS DOUBLE) AS CY '--------------------------------------------------------------------------- ' Converts a DOUBLE to a CY (CURRENCY) value. ' @param d DOUBLE value to convert. ' @return CY value representing d * 10000 (64-bit integer). ' @note Returns zeroed CY on error. Windows-specific due to OleAut32.dll. '--------------------------------------------------------------------------- DIM result AS CY IF VarCyFromR8(d, &result) = 0 THEN FUNCTION = result ELSE PRINT "Error in CyFromDouble" DIM zero AS CY FUNCTION = zero END IF END FUNCTION FUNCTION FormatCy (cy AS CY) AS STRING '--------------------------------------------------------------------------- ' Formats a CY (CURRENCY) value as a string with 4 decimal places. ' @param cy CY value (64-bit integer scaled by 10000). ' @return String representing cy / 10000, formatted to 4 decimal places. ' @note Relies on CyToDouble for conversion. '--------------------------------------------------------------------------- DIM s$ DIM d AS DOUBLE d = CyToDouble(cy) sprintf(s$, "%.4f", d) FUNCTION = s$ END FUNCTION
Add: 1236.5678 Sub: 1232.5678 Mul: 2469.1356 Div: 617.2839 Press any key to continue . . .
$LIBERROR specifies the file to which errors regarding library and function failures are to be logged.
Syntax:$LIBERROR "C:\dev\BCX\LibError.log" Parameters:
|
This example is similar to the one above with an addition of the $LIBERROR directive and a change in TESTLIBERROR.bas from the name respond.dll to a nonexistent noresponse.dll. When compiled and run this example will write the errors to the LibError.log. The example is in three sections, the DLL, a program to call the function from the DLL and a batch file to compile the programs.
The first section is the BCX DLL code. Cut and save this as RESPONDELIBERROR.BAS
$DLL STDCALL FUNCTION Respond (Buf$) EXPORT Buf$ = Buf$ & " The Eagle has landed" FUNCTION = LEN(Buf$) END FUNCTION
The second section is the program which will call the function in the DLL. Cut and save this as TESTLIBERROR.BAS. Be sure to modify the path following the $LIBERROR directive to suit your setup.
$LIBERROR "C:\t\LibError.log" DIM a$, l DECLARE FUNCTION Respond LIB "noresponse.dll" DECLARE "Respond"(foo$) AS INTEGER a$ = "Boy Howdy" l = LEN(a$) ? l;" ";a$ l = Respond(a$) ? l;" ";a$
Here, in the third section, is a batch file to translate, compile and link the RESPONDE.bas and TESTLIBERROR.bas.
If Pelles C is used as the compiler, cut and save the following as BUILDLIBERROR.BAT. Be sure to modify the path to the Povars32.bat file to suit your setup.
CALL Povars32.bat BC RESPONDELIBERROR POCC /Ze /Gn /Zx RESPONDELIBERROR.c POLINK /dll RESPONDELIBERROR.obj BC TESTLIBERROR POCC /Gd /Ze /Zx TESTLIBERROR.c POLINK TESTLIBERROR.obj
Run BUILDLIBERROR.BAT. When compiled, run TESTLIBERROR. The output should be similar to the following
[03/31/15 11:04:14] Failed to load responde.dll [03/31/15 11:04:14] Failed to find process Respond
BCX_DYNACALL is a user accessible runtime variant of the of the BCX translator internally used BCX_DynaCall function. This user accessible variant provides, for the user, an easier runtime execution of functions not known at compile time.
Syntax:[RetVal =] BCX_DYNACALL(DLL_Name AS STRING, _ DLLFunction AS STRING, _ NumberOFArgs AS INTEGER, _ ArgArray) Parameters:
|
This variation is mainly to allow a user's program to have the ability to setup functions at runtime. For instance, the functions could be read from a configuration file. The easiest way is just to define an array that is equal or greater than the maximum number of arguments that a function may use. The arguments are then, and must be, cast as integer.
DIM AnyType[4] AS INT_PTR ' INT_PTR works for 32-bit and 64-bit executables AnyType[0] = (INT_PTR) NULL AnyType[1] = (INT_PTR) "Your Title" AnyType[2] = (INT_PTR) "Your Msg" AnyType[3] = (INT_PTR) MB_OK BCX_DYNACALL("user32", "MessageBox", 4, AnyType)
STDCALL is a reserved BCX keyword used in two different contexts.
Win32 API functions and user-defined callbacks use the _stdcall calling convention. _stdcall is used as well by Visual BASIC, and Delphi. When _stdcall is used, the stack is cleaned up by called function, so compiler makes vararg functions cdecl, and a function prototype is required. Arguments pushed on stack right to left.
Syntax 1:$DLL STDCALL |
Syntax 2:SUB Foo (Bloof AS DOUBLE) STDCALL |