Library Calling Conventions in BCX


C_DECLARE statement

Purpose:

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:

  • Data type: Arguments
    Paramlist follows the same rules as parameters in a standard SUB or FUNCTION declaration. The parameters used in the C_DECLARE Paramlist are dummies but must communicate the data type, either by using a data type sigil ( $ % ! # ) or by using the AS DataType clause.

Remarks:

_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:

  1. a standalone function that will occupy its own .obj file.
  2. a test program that C_DECLARE's and later calls the imported (linked) function
  3. a batch file to build the example.

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!

DECLARE statement

Purpose:

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)

Remarks:

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.

Example 1:

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

LIB statement

Purpose:

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:

  • RetVal [OPTIONAL] return value from function being called.
  • DLLFunction Name of the function being called. The name should appear as it is exported from the DLL according to its specifications. Explicit ALIASes as used with DECLARE constructs are not permitted.
  • DLLName Name of the DLL containing the function being called(the extension part is optional). Contrary to DECLARE constructs which load DLLs into memory the moment DLLFunction is declared, this method loads them into memory only if the function is actually called.
  • Parameters [OPTIONAL] parameter list for function being called. Nested calls to other DLL functions may also be used directly as parameters (see Example 2 below).

Remarks:

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.

Example 1:

MessageBox(LIB "user32", NULL, "Your Msg", "Your Title", MB_OK)

Example 2:

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

Result:

This is an image of the output produced by the above code.

Example 3:

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

Result:

Add: 1236.5678
Sub: 1232.5678
Mul: 2469.1356
Div: 617.2839

Press any key to continue . . .

$LIBERROR directive

Purpose:

$LIBERROR specifies the file to which errors regarding library and function failures are to be logged.

Syntax:

$LIBERROR "C:\dev\BCX\LibError.log"

Parameters:

  • Data type: STRING
    LibError.log is a string literal containing the path to, and the name of, the file to which errors regarding library and function failures are to be logged.

Example 1:

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 procedure

Purpose:

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:

  • RetVal [OPTIONAL] return value from function being called.
  • Data type: STRING
    DLL_Name Name of the DLL containing the function being called (the extension part is optional). Contrary to the DECLARE constructs which load DLLs into memory at the moment that the DLLFunction is declared, this method loads them into memory only if the function is actually called.
  • Data type: STRING
    DLLFunction Name of the function being called. The name should appear as it is exported from the DLL according to its specifications. Explicit ALIASes as used with DECLARE constructs are not permitted.
  • Data type: INTEGER
    NumberOFParams Number of DLLFunction parameters.
  • Data type: Identifier
    ArgArray Array to hold the arguments of the DLLFunction parameters. This array can be defined as equal or greater than the maximum number of arguments that a function may use.

Remarks:

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.

Example:

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 statement

Purpose:

STDCALL is a reserved BCX keyword used in two different contexts.

Remarks:

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