Problem with SafeArrays from vba in fortran DLL

Problem with SafeArrays from vba in fortran DLL

I am trying to get Excel VBA code to interface with a fortran DLL, and am having a vexing problem. I started with the 'VB.NET-SafeArrays' example, but modified slightly to work in VBA:

VBA code:

Private Declare Sub ForCall Lib "<path to dll omitted>" (ByRef array1() As String)
Private myarray(2, 3) As String
Private Sub CommandButton1_Click()
    myarray(0, 0) = "One"
    myarray(0, 1) = "Two"
    myarray(0, 2) = "Three"
    myarray(0, 3) = "Four"
    myarray(1, 0) = "Five"
    myarray(1, 1) = "Six"
    myarray(1, 2) = "Seven"
    myarray(1, 3) = "Eight"
    myarray(2, 0) = "Nine"
    myarray(2, 1) = "Ten"
    myarray(2, 2) = "Eleven"
    myarray(2, 3) = "Twelve"
    Call ForCall(myarray)
End Sub

Fortran code (pretty much identical to the example):

subroutine ForCall (VBArray)
  ! Set the attributes needed for compatibility with VB.NET.  VB always uses STDCALL.
  !
  !dec$ attributes dllexport, stdcall, reference, alias : "ForCall" :: ForCall
  !dec$ attributes reference :: VBArray
  use ifcom ! Declare SafeArray and BSTR interfaces
  implicit none
  
  integer(int_ptr_kind()), intent(inout) :: VBArray  !Pointer to a SafeArray structure  
  integer, parameter :: LONG_ENOUGH_BUFFER = 2048 ! Assume we won't get a string longer than this
  character(LEN=LONG_ENOUGH_BUFFER)  mystring  ! Fortran string converted to/from BSTR
  integer(int_ptr_kind()) :: BSTRptr  ! Receives a pointer to a BSTR
    
  ! Array in which we will keep track of array bounds
  type bounds_type
    integer lb  ! Lower Bound
    integer ub  ! Upper Bound
    end type bounds_type
  integer nbounds  ! Number of bounds
  type(bounds_type), allocatable :: bounds(:)
  integer, allocatable :: indexes(:)  ! Array to hold current element indexes
  
  integer :: i, iRes, length
  ! First, we'll get the bounds of the array. This code makes no assumptions about the number of
  ! dimensions.
  !
  nbounds = SafeArrayGetDim (VBArray)
  allocate (bounds(nbounds), indexes(nbounds))
  
  do i=1,nbounds
    ires = SafeArrayGetLbound (VBArray, i, bounds(i)%lb)
    ires = SafeArrayGetUbound (VBArray, i, bounds(i)%ub)
    end do
    
  ! Example 1 - write to a text file (since we don't have a console) the
  ! bounds of the array.  You'll find this file in the BIN subfolder of
  ! the VB.NET project folder.
  open (2, file="C:testout.txt", status="unknown")
  write (2, *) "Shape of the array passed by VB:"  
  write (2,'("  (")',advance='no')
  do i=1,nbounds
    write (2,'(I0,":",I0)',advance='no') bounds(i)
    if (i < nbounds) write(2,'(",")',advance='no')
    end do
  write (2,'(")")')
  ! Example 2 - Write the values of the string elements to the file. This code
  ! also makes no assumptions about the number of dimensions. 
  !
  ! For each element we:
  !  1) Call SafeArrayGetElement to return a pointer to a BSTR element
  !  2) Convert the BSTR to a Fortran string (which we then write to the file)
  !  3) Free the string which we retrieved
  !
  ! Note that the current interface to SafeArrayGetElement has the second "indices"
  ! argument defined as a scalar - we work around that by passing the first element
  ! by reference.
  !
  write (2, *) "Strings from the array:"
  
  indexes = bounds%lb  ! Initialize to all lower bounds
  readloop: do
    ires = SafeArrayGetElement (VBArray, indexes(1), loc(BSTRPtr))
    length = ConvertBSTRToString (BSTRPtr, mystring)
    call SysFreeString(BSTRPtr)
    write (2, '("  A(")', advance='no')
    write (2, '(100(I0,:,","))', advance='no') (indexes(i),i=1,nbounds)
    write (2, '(") = ", A)') mystring(1:length)
    
    ! Determine what the next element is.  We increment the last index,
    ! and if it is greater than the upper bound, reset it to the lower bound and
    ! repeat for the next lower index.  If we run out of indexes, we're done.
    do i = nbounds, 1, -1
      indexes(i) = indexes(i) + 1
      if (indexes(i) <= bounds(i)%ub) exit
      indexes(i) = bounds(i)%lb
      if (i == 1) exit readloop
      end do
    end do readloop
        
  close (2) ! We're done with the file
  ! Example 3 - Modifying BSTR elements in a SafeArray.  Here we append
  ! " potato" to each of the elements.
  !
  indexes = bounds%lb  ! Initialize to all lower bounds
  writeloop: do
    ires = SafeArrayGetElement (VBArray, indexes(1), loc(BSTRPtr))
    length = ConvertBSTRToString (BSTRPtr, mystring)
    call SysFreeString (BSTRPtr)
    ! Append "potato" to the element
    mystring = trim(mystring) // " potato"
    
    ! Convert it back to a BSTR    
    BSTRptr = ConvertStringToBSTR (trim(mystring))
    
    ! Write it back to the array
    ires = SafeArrayPutElement (VBArray, indexes(1), BSTRptr)
    call SysFreeString (BSTRPtr)  ! Free our copy
        
    ! Determine what the next element is.  We increment the last index,
    ! and if it is greater than the upper bound, reset it to the lower bound and
    ! repeat for the next lower index.  If we run out of indexes, we're done.
    do i = nbounds, 1, -1
      indexes(i) = indexes(i) + 1
      if (indexes(i) <= bounds(i)%ub) exit
      indexes(i) = bounds(i)%lb
      if (i == 1) exit writeloop
      end do
    end do writeloop
  ! Deallocate our local arrays, though this would be done implicitly anyway
  !
  deallocate (bounds)
  deallocate (indexes)
  
  return  
end subroutine ForCall

This code almost works. The fortran program can determine the correct dimensions of the array, but cannot retrieve the strings. Looking at the output file:

Shape of the array passed by VB:
  (0:2,0:3)
 Strings from the array:
  A(0,0) = ?
  A(0,1) = ?
  A(0,2) = ??
  A(0,3) = ??
  A(1,0) = ??
  A(1,1) = ?
  A(1,2) = ??
  A(1,3) = ??
  A(2,0) = ??
  A(2,1) = ?
  A(2,2) = ???
  A(2,3) = ???

The attempt to append ' potato' to the strings also almost works. The attached file is a screenshot of what the matrix looks like after the fortran functions returns. I am not very experienced with this sort of thing, but it looks like a unicode to ascii conversion problem. If the fortran code is expecting each character in the array to be two bytes long and the characters are actually a single byte, the conversion will not work. Does this sound like a reasonable explanation? Is there some way of telling the fortran code not to do the character conversion?

Thank you for looking

Fichier attachéTaille
Télécharger strings-after.png15.22 Ko
6 posts / 0 nouveau(x)
Dernière contribution
Reportez-vous à notre Notice d'optimisation pour plus d'informations sur les choix et l'optimisation des performances dans les produits logiciels Intel.

If you are calling this from Excel, then Excel does not use UTF16 BSTR's in calls to VBA procedures - they are "ANSI" BSTR's.  The conversion routine in the Intel modules is for the UTF16 variant.

Looking at my own routines, the BSTR handle is equivalent to a C-pointer to a character array, i.e. you can do something like:

CALL C_F_POINTER(TRANSFER(bstr, C_NULL_PTR), char_array, [SysStringByteLen(bstr))

where char_array is a fortran pointer to a character array, and then you can copy (or pointer associate) the array elements into an actual character scalar.

Thanks for the response. I am trying to get that code to work, but I suspect I don't understand how the variables work. The DLL receives a pointer from VBA which points to a vba safestring. From there, the code

ires = SafeArrayGetElement (VBArray, indexes(1), loc(BSTRPtr))

makes the pointer BSTRPtr point to the string at indexes(1) of VBArray. I then try to convert the BSTRPtr to an fortran array with

Initalization:

character(LEN=LONG_ENOUGH_BUFFER)  char_array
integer :: N1(1)

Called after SafeArrayGetElement:

N1(1) = 1
CALL C_F_POINTER(TRANSFER(loc(BSTRPtr), C_NULL_PTR), char_array, N1)

However, the char_array variable ends up as unreadable giberish. Can you see any problems with the way I am doing this?

Citation :

Patrick S. a écrit :

N1(1) = 1 CALL C_F_POINTER(TRANSFER(loc(BSTRPtr), C_NULL_PTR), char_array, N1)

The BSTRPtr thing is the address of the string.  With the LOC in there you extract the address of the address of the string - one "address of" too many.  Get rid of the LOC reference.

Example cobbled together from bits and bobs of my code attached.

Fichiers joints: 

Fichier attachéTaille
Télécharger 2013-10-17-bstr.f909.52 Ko

Sorry for the delayed response. I am not able to get you attached code to run unmodified. However, I was able to glean the important bits from the functions you provided (I think). The code to get a string from the safestring seems to be

CHARACTER(:), ALLOCATABLE :: str
CHARACTER(C_CHAR), POINTER :: array(:)
 integer(C_INTPTR_T) :: BSTRptr

ires = SafeArrayGetElement (VBArray, indexes(1), loc(BSTRPtr))
    CALL C_F_POINTER( TRANSFER(BSTRPtr, C_NULL_PTR), array, [SysStringByteLen(BSTRPtr)] )
    ALLOCATE(CHARACTER(SIZE(array)) :: str)
    FORALL (i = 1:SIZE(array)) str(i:i) = array(i)

This successfully puts the first element of the matrix into the string 'str.' However, running the function you provided gives an error:

Function call:

ires = SafeArrayGetElement (VBArray, indexes(1), loc(BSTRPtr))
str = ExcelBSTRToChar(BSTRPtr)

Function:

  !*****************************************************************************
  !!
  !! FUNCTION ExcelBSTRToVS
  !> Returns a deferred length character string from a Excel BSTR.
  !!
  !! @param[in]     bstr              The integer point to the BSTR.  The BSTR 
  !! must contain ANSI characters.
  !!
  !! @return A varying string with the contents of the BSTR.
  !!
  !*****************************************************************************
  
  FUNCTION ExcelBSTRToChar(bstr) RESULT(str)
    
    USE IFCOM, ONLY: SysStringByteLen
    USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_CHAR, C_PTR, C_F_POINTER,  &
        C_INTPTR_T, C_NULL_PTR
    
    !---------------------------------------------------------------------------
    ! Arguments
    
    INTEGER(C_INTPTR_T), INTENT(IN) :: bstr
    
    ! Function result
    CHARACTER(:), ALLOCATABLE :: str
    
    !---------------------------------------------------------------------------
    
    ! Array aliased to the contents of the BSTR.
    CHARACTER(C_CHAR), POINTER :: array(:)
    
    INTEGER :: i              ! String index.
    
    !***************************************************************************
    
    ! Make the contents of the BSTR available as a CHARACTER array.
    CALL C_F_POINTER( TRANSFER(bstr, C_NULL_PTR), array,  &
        [SysStringByteLen(bstr)] )
    
    ALLOCATE(CHARACTER(SIZE(array)) :: str)
    FORALL (i = 1:SIZE(array)) str(i:i) = array(i)
    
  END FUNCTION ExcelBSTRToChar

The errors I get are

error #6404: This name does not have a type, and must have an explicit type.   [EXCELBSTRTOCHAR]
error #6054: A CHARACTER data type is required in this context.   [EXCELBSTRTOCHAR] 

Both of these point to the 'str = ExcelBSTRToChar(BSTRPtr)' line. I suspect this error is due to the fact that I havent used fortran in quite a few years, but do you have any suggestions on how to fix this?

Well, I can see one error - on line 30 of what you posted of the ExcelBSTRToChar function above (line 278 in the complete file) I've AGAIN left off the KIND= specifier before C_CHAR.  I will now spend the rest of the week sleeping in the chook house in contrition.  Fortunately I like eating eggs.

But that doesn't explain the error message you see.  Are you comping the exact file above?  If not, what specific source are you compiling?  Note that the ExcelBSTRToChar function in the original example was in a module, which provides an explicit interface for the function inside that module and whereever the module is USEd ("explicit interface" means the compiler explicitly knows what the function looks like).

What compiler version are you running?

Connectez-vous pour laisser un commentaire.