Calling Visual Basic from Forth

How do we call functions in DLLs compiled with Visual Basic? The trick here is having a word equivalent to iForth's PASCAL-CALLBACK . This kind of callback implements the calling convention of Visual Basic. See Neural-Net demo for an example of use. Visual Basic assumes the callee cleans up the stack, but the order of the arguments is the same as for "C" DLLs (I think FORTRAN uses the same convention.)

VB strings and matrices can be a major source of pitfalls. See the SAFEARRAY and BSTR words in nn51.frt for ideas on how to handle some of the problems. Carefully check the argument passing conventions. VB passes pointers to pointers in non-intuitive ways.

Here is a typical Visual BASIC declaration that I used to infer how a VB DLL has to be called:

   Public Declare Function Train Lib "NN50.DLL" Alias "TRAIN" _
        (ByRef dwArrayPointer() As Double, lpEpochs As Long, nResetWeights As Integer, _
        nLearningRate As Double, nMomentum As Double, nMaxNeurons As Integer, _
        ByVal lpCallBackOfEpoch As Long, ByVal lpCallBackOfRMSError As Long) As Integer

The second parameter "lpEpochs As Long" is the address of a variable Epochs . The parameters nResetWeights, nLearningRate, nMomentum and nMaxNeurons are also passed by reference (address). A Long and an Integer are simply 32-bit cells. A double is equivalent to a Forth DFLOAT and its address must be passed. In the Forth case, "ByVal lpCallBackOfEpoch As Long" is the address of the (pascal-)callback mentioned above. (What the "ByVal" and "As Long" are good for is anybody's guess.)

   0   VALUE epoch
   0   VALUE nMaxNeurons
   0   VALUE nResetWeights
   0   VALUE #epochs
   0e FVALUE LRate	-- Learning rate (0.1 to 10)
   0e FVALUE nMomentum	-- Neural network momentum (0.1 to 10)

#120 4 DOUBLE MATRIX MUL{{

CREATE OLE_SAFEARRAY1
	2   C, 0 C,	-- cDims ( 16-bit word), a matrix has 2 dimensions
	$10 C, 0 C,	-- Flags (16-bit word), do not resize or reallocate.
	1 DFLOATS ,	-- cbElements (size in bytes of DFLOAT)
	0  ,		-- lock count
	MUL{{ DADDR ,	-- pointer to data
			-- rgsabound[2] Describe the dimensions claimed above
	#120 , 1 ,	-- dimension 0: #120 cElements lLbound = 0?
	   4 , 1 ,	-- dimension 1:    4 cElements lLbound = 0?

: INIT-EVERYTHING ( -- )
	OLE_SAFEARRAY1 LOCAL 'array
	'OF 'array	'OF #epochs	'OF nResetWeights
	'OF LRate	'OF nMomentum	'OF nMaxNeurons
	CallBackEpoch	CallBackRMSError
	8 NN50-LIB: TRAIN FOREIGN 1 = IF ." successful"
	 			    ELSE ." unsuccessful"
			           ENDIF ;

As you can see in INIT-EVERYTHING , the first parameter is something special. "ByRef dwArrayPointer() As Double" is VB speak for a pointer to a pointer that points to a SAFEARRAY structure that describes (in this case) a 2 dimensional matrix of DFLOATS. The first indirection is handled by putting OLE_SAFEARRAY1 in a local variable and passing its address (accessing the address of a LOCAL is not allowed in some Forths). On building an OLE SAFEARRAY you're advised to check the VBASIC pages at the MS site, or consult the documentation of any (Windows) C compiler.

The layout of the rgsabound[2] field, lLbound, is strange. It didn't work when I put the expected 0 there.

Another pitfall is the BASIC string variable. BASIC may reallocate strings at will, so manipulating direct string addresses is not safe. It is also unsafe to pass a pointer to a string to BASIC, because it may want to do very weird things to it. However, what seems to work well enough is passing BASIC a pointer to a BSTR. A BSTR is a zero-delimited string prefixed with a 32-bit count. The address of the first character is passed (not the address of the count). Although I could not find any reference saying it directly, a BSTR seems to be the default BASIC string format. (Note that general BSTRs contain UNICODE, not ASCII. However, the BSTRs VBASIC uses to interface with DLLs are special in that they contain 8-bit ASCII.)

CREATE $buff 0 , #256 CHARS ALLOT

-- OLE BSTR
: >BSTR ( "text" -- c-addr )
	0 <WORD> $FF AND
	DUP $buff !  >S $buff CELL+ S MOVE  $buff CELL+ S> + C0!
	$buff CELL+ ;

Sometimes passing a Zstring (without prefixed count) works, but I encountered a situation where a BASIC DLL allocated 256 MBytes and then hang itself.

Comments appreciated:

c/o Marcel Hendrix - mhx@iae.nl
Last modified: May 25, 2000 free counter Valid HTML 3.0