String Arrays FROM FORTRAN

String Arrays FROM FORTRAN

Community Admin's picture

One of our apps has a C++ layer on top of FORTRAN. Lots of our C++ functions call the FORTRAN and have string arrays returned (in arguments). Currently this is done by allocating one large char variable in C++ and calling the FORTRAN, which actually returns an array of strings. We then have to take the C++ char variable and chop it back up into a 'proper' array. This takes an inordinate amount of string manipulation. Can you return an array of strings from FORTRAN as an array of strings in C++? (Can you even have arrays of character data in C++?). It would be nice, and a lot quicker and less 'dangerous' if I could avoid lots of space stripping and memcpy's....?

12 posts / 0 new
Last post
For more complete information about compiler optimizations, see our Optimization Notice.
Jugoslav Dujic's picture

Sorry for late reply, been quite busy last week; hope it would be helpful even now.

From C++'s viewpoint, CHARACTER(n):: string, CHARACTER:: string(n) and CHARACTER(n/6):: string(6) are the same thing; OTOH, from Fortran viewpoint, char string[n], char string[6][n/6] are the same thing; these all represent the same contiguous chunk of memory. So, it's possible to declare a (array of) string in C++ in one way and declare it in Fortran in another way; as long as they map to the same piece of memory you're OK. It's up to you to determine which treatment is easier for you in both languages. In both languages, (array of) string is treated as a starting address; length of string is relevant for fortran only (and array dimensions, if any, are not relevant). Of course, take care how to handle hidden length argument to Fortran routine and not to exceed buffer dimenstions. For example:

 
extern "C" void __stdcall FSUB(char* string, int nLen); 
... 
char string[7][20]; // Or maybe [20][7] -- I always mix these 
FSUB(string, 7); 
---------------
SUBROUTINE FSUB(string) 
CHARACTER(*):: string(*)  !Array of 20 strings 7 chars long 
!Or CHARACTER(140) string - one long string 
!Or CHARACTER(20) string(7) - aray of 7 strings 20 chars long 
DO i=1,20 
  WRITE(string(i), "(f7.3)") something 
END DO 

Now, it's not clearcut to me what exactly you're trying to achieve. Usually, it's better that the caller allocates the memory (see my recent post about programming precautions in this sense).

HTH
Jugoslav

Jugoslav www.xeffort.com
Community Admin's picture

Thanks for the reply Jugoslav. I don't mind doing the allocation and deletion in C++ (that is how its done currently), its the 'shape' of the string I get back that is the 'problem'. The C++ is an OLE automation layer that is typically used to return a VARIANT array of data to the outside world. So what generally happens is the OLE Automation method calls the FORTRAN which retrieves say, an array of 1500 item names of length 10 characters. Since this 'appears' in C++ as a long string, the code then memcpy's 10 char chunks from the string and puts them into a COleSafeArray using PutElement. The memory pointer is then stepped by 10 bytes. This would be repeated 1500 times to construct the variant array, which would be returned using a SafeArray.Detach(). Instead of all that messing around it would be nice to be able to do something like:

for (long i = 1; i <= 1500; i++)
{
SafeArray.PutElement(i, MyCPPStringArrayFromFORTRAN(i));
}

I try a little test app but couldn't seem to get it to work. I returned an array of 10 strings to C++, but the first element appeared to 'contain' all 10, the second 2-10, the third 3-10 etc.

Strings are far too complicated in C++!!!

Jugoslav Dujic's picture

You almost get it right -- should be

 These are the right HTML tags 
for (long i = 1; i <= 1500; i+=10)  
{ SafeArray.PutElement[i, MyCPPStringArrayFromFORTRAN(i)); }  

i.e. C++ has a concept of "matrix of characters" rather than "array of strings".

Btw, did you know you could avoid all that C++/copy string mess -- SafeArrays are accessible from Fortran too. You should USE DFCOM, pass CSafeArray by reference, and put elements directly from Fortran using kinda:

 
SUBROUTINE FSUB(lpSafeArray) 
!DEC$ATTRIBUTES VALUE:: lpSafeArray  !Not positive if this is required 
INTEGER(SELECTED_PTR_KIND):: lpSafeArray 
CHARACTER(10):: sArray(1500) 
 
DO i=1,1500 
    j = SafeArrayPutElement(lpSafeArray, i, LOC(sArray(i))) 
END DO 

Frankly, I've never worked with this but having taken a glance to docs plus DFCOM.f90 I think it should be simple enough.

Jugoslav

Jugoslav www.xeffort.com
Community Admin's picture

D'oh! I never thought of that, but now you come to mention it - it's obvious isn't it! Thanks!!!

Community Admin's picture

What is wrong with this? the HRESULT from the SafeArrayPutElement comes back with 'Catastrophic failure'

  
[C++]  
void CSATESTDlg::OnOK()  
{  
	CDialog::OnOK();  
  
	LPSTR cData;  
	SAFEARRAY * saResult;  
	SAFEARRAYBOUND rgsabounds[] = {{10, 0}};  
	saResult = SafeArrayCreate(VT_VARIANT, 1, rgsabounds);  
  
	cData = new char[128];  
	memset(&cData, ' ', 128);  
  
	//----------------  
	// Get the strings  
	//----------------  
  
	long lCount = GET_STRINGS (&saResult);  
  
	for (long i = 0; i < 10; i++)  
	{  
		HRESULT hr = SafeArrayGetElement(saResult, &i, &cData);  
		OutputDebugString(cData);  
		memset(&cData, ' ', 128);  
	}  
  
}  
  
[FORTRAN]  
*------------------------------------------------------------------------------  
  
	INTEGER*4 FUNCTION GET_STRINGS (lpSafeArray)  
  
*------------------------------------------------------------------------------  
	!DEC$ATTRIBUTES VALUE:: lpSafeArray  
  
	USE DFCOM  
	IMPLICIT NONE  
  
	CHARACTER(10):: sArray(0:9)  
  
	INTEGER*4	i, j  
	INTEGER*4	lpSafeArray  
	INTEGER*4	location  
  
*------------------------------------------------------------------------------  
  
	sArray(0) = "STRING_001"  
	sArray(1) = "STRING_002"  
	sArray(2) = "STRING_003"  
	sArray(3) = "STRING_004"  
	sArray(4) = "STRING_005"  
	sArray(5) = "STRING_006"  
	sArray(6) = "STRING_007"  
	sArray(7) = "STRING_008"  
	sArray(8) = "STRING_009"  
	sArray(9) = "STRING_010"  
  

	DO i = 0, 9  
		location = LOC(sArray(i))  
		j = SafeArrayPutElement(lpSafeArray, i, location)  
	END DO  
  
	GET_STRINGS = 10  
  
	RETURN  
	END  
Jugoslav Dujic's picture

Re-looking at the docs, it seems that 2nd argument should be LOC(i); I thought that it's an ordinary index, but docs say it's a pointer to an array of indices. IIRC, SafeArrays are indexed from zero so (maybe) you should pay attention to that also.

Jugoslav www.xeffort.com
Jugoslav Dujic's picture

...also, not being familiar with OLE stuff, I don't know what kind of string should be put into a Variant; if it's C-like, you should terminate your strings with CHAR(0). (And make sure their length fits).

Jugoslav www.xeffort.com
Community Admin's picture

I've progressed a little - I'm now getting 'Invalid index' as my error. vis a vis the kind of string I would expect it would have to be a BSTR - so I might need to use ConvertStringtoBSTR. But it seems it's the index giving me problems...

Community Admin's picture

I've now got my test app to report 'Bad variable type', which leads me to believe I need to use BSTRs, but if I try to use ConvertStringToBSTR I get link errors because the things I'm USEing are already defined (this being a mixed language app) - what do I do about that?

Jugoslav Dujic's picture

[sigh] I recall problems with doubly defined symbols with DFNLS and DFCOM... Which ones, btw? I workarounded them by manually adding needed interfaces in my own module.

Try copying only necessary stuff from DFCOM.f90 to your file. I guess you'd need at least MultiByteToWideChar (ANSI to UNICODE conversion), (which is what MBConvertMBToUnicode) does and SysAllocString:

 
INTERFACE 
      INTEGER FUNCTION MultiByteToWideChar(CodePage, dwFlags, szPath, cchPath, widePath, cchWide) 
      !DEC$ATTRIBUTES STDCALL, ALIAS: "_MultiByteToWideChar@24":: MultiByteToWideChar 
      INTEGER Codepage, dwFlags, cchPath, cchWide 
      !DEC$ATTRIBUTES REFERENCE:: szPath 
      CHARACTER(*) szPath 
      INTEGER(2) widePath(*) 
      END FUNCTION 
END INTERFACE 
INTERFACE  
   INTEGER*4 FUNCTION SysAllocString(unistr) 
   !DEC$ ATTRIBUTES STDCALL, ALIAS : '_SysAllocString@' :: SysAllocString 
   INTEGER*2,     INTENT(IN):: unistr(*) 
   END FUNCTION SysAllocString 
END INTERFACE 
!Convert szPath to UNICODE 
CHARACTER(30)::         szPath="ANSI string to convert"C 
INTEGER(2)::         olePath(30) 
iSt = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, olePath, 2*SIZE(olePath)) 
bstr = SysAllocString(olePath) 

(The above is untested). That could be a thorny road though; I'm on a slippery ground about OLE, Variants, BSTRs & co. Anyone has a better idea? Leo?

Jugoslav www.xeffort.com
Community Admin's picture

I'm actually a chemical engineer - so I don't know much more about OLE, varaints and BSTRs than you! Thanks for your help so far anyway. I'm now wondering if I'm trying to be too complicated. The current code uses COleSafeArrays for construction purposes, but detaches the underlying VARIANT for return. Can I construct that VARIANT directly in FORTRAN if I manually null-terminate my strings and set the pointer element of the variant stucture to the (address of?) the string array?

Login to leave a comment.