views:

2459

answers:

4

I am trying to call out to a legacy dll compiled from FORTRAN code. I am new to Interop, but I've read some articles on it and it seems like my case should be fairly straightforward.

The method I really want to call has a complex method signature, but I can't even call this simple GetVersion method without getting a protected memory violation.

Here's my DllImport code:

[DllImport("GeoConvert.dll", 
            EntryPoint="_get_version@4", 
            CallingConvention=CallingConvention.StdCall)]
public static extern void GetGeoConvertVersion([MarshalAs(UnmanagedType.LPStr, SizeConst=8)]
                                                    ref string version);

Here's the FORTRAN code:

SUBROUTINE GetVer( VRSION )
C
!MS$DEFINE  MSDLL 
!MS$IF DEFINED (MSDLL)
        ENTRY Get_Version (VRSION) 
      !MS$ATTRIBUTES DLLEXPORT,STDCALL :: Get_Version
      !MS$ATTRIBUTES REFERENCE :: VRSION
!MS$ENDIF
!MS$UNDEFINE  MSDLL 
C
  CHARACTER*8  VRSION
C
  VRSION = '1.0a_FhC'                                        
C
  RETURN
  END

Here's my unit test that fails:

[Test]
public void TestGetVersion()
{
    string version = "";
    LatLonUtils.GetGeoConvertVersion(ref version);
    StringAssert.IsNonEmpty(version);
}

Here's the error message I get:

System.AccessViolationException
Message: Attempted to read or write protected memory. 
         This is often an indication that other memory is corrupt.

Other things I've tried:

  • Using the default marshalling
  • Passing a char[] instead of a string (get method signature errors instead)
A: 

Have you tried using a StringBuilder?

Create your String as a StringBuilder and pass that into the dll function.

Im unsure as to what Marashlling statement to use, perhapse the default might work.

Have a look at: Marshal C++ “string” class in C# P/Invoke

Heres a good article the might help as well: Interop Marshalling

TK
A: 

OK, I got it to work, the problem was passing by ref. I'm not sure why, but this works:

[DllImport("GeoConvert.dll", 
                EntryPoint="_get_version@4", 
                CallingConvention=CallingConvention.StdCall)]
    public static extern void GetGeoConvertVersion([MarshalAs(UnmanagedType.LPArray)]
                                                    byte[] version);

With this test:

[Test]
    public void TestGetVersion()
    {
        //string version = "";
        byte[] version = new byte[8];
        LatLonUtils.GetGeoConvertVersion(version);
        char[] versionChars = System.Text.Encoding.ASCII.GetChars(version);

        string versionString = new string(versionChars);
    }
brien
I just tried this and it worked beautifully :) One comment tho - you can use `System.Text.Encoding.ASCII.GetString()` if you want to replace your final two lines of code with a single line. A little cleaner, but works both ways. Thx for posting the update! It really helped me out.
Mike
A: 

I cannot try this solution since I do not have a FORTRAN compiler, but I think this would work for you:

    [DllImport("GeoConvert.dll", 
            EntryPoint="_get_version@4", 
            CallingConvention=CallingConvention.StdCall,
            CharSet=CharSet.Ansi)]
    public static extern void GetGeoConvertVersion(StringBuilder version);
Jeremy
+2  A: 

...snip... OK, I got it to work, the problem was passing by ref. I'm not sure why, but this works: ...snip...

You need to pass by reference because that is the semantic being used by the FORTRAN code. The client code is passing in a buffer that the FORTRAN code is going to write to in lieu of using a return value.

...snip... !MS$ATTRIBUTES REFERENCE :: VRSION ...snip...

This attribute in your FORTRAN code specifies that this parameter is passed by reference. That means the FORTRAN code is going to write to this address. If the DllImport doesn't declare it as a ref value also, you will get an access violation.

Todd
Why does the call-by-reference pragma need to be there to begin with? Does Microsoft Fortran not use call by reference by default, like other Fortran compilers do? (Or is call by value the default only for DLLs?)
crosstalk
tflanagan, not quite. Passing by reference made it fail, the default (by value) succeeded, even though FORTRAN is clearly asking for a reference. The issue is that a byte[] is already a pointer, so passing a byte[] by ref is a pointer to a pointer, so I got a memory access error.
brien