none
Marshaling data from an unmanaged buffer variable. RRS feed

  • Question

  • I'm trying to use http://msdn.microsoft.com/en-us/library/aa382711(VS.85).aspx function in .NET. 

    p/invoke data type conversion: http://msdn.microsoft.com/en-us/library/ac7ay120.aspx 

    BOOL WINAPI EcGetSubscriptionRunTimeStatus(  
      __in   LPCWSTR SubscriptionName,  
      __in   EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID StatusInfoId,  
      __in   LPCWSTR EventSourceName,  
      __in   DWORD Flags,  
      __in   DWORD StatusValueBufferSize,  
      __in   PEC_VARIANT StatusValueBuffer,  
      __out  PDWORD StatusValueBufferUsed  
    );  
     

    This is my prototype in VB.NET

    Private Declare Unicode Function EcGetSubscriptionRunTimeStatus Lib "wecapi.dll" _  
    (ByVal SubscriptionName As String, _  
    ByVal StatusInfoId As EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID, _  
    ByVal EventSourceName As String, _  
    ByVal Flags As UInt32, _  
    ByVal StatusValueBufferSize As UInt32, _  
    ByVal StatusValueBuffer As IntPtr, _
    ByRef StatusValueBufferUsed As UInt32) _  
    As Boolean 

    I do not know how to get the data from the StatusValueBuffer.  What value do I pass to it?  Is it even the right data-type?  Can someone shed some light on this interop problem?  Would tlbimp.exe be helpful in this case?

    Here is a usage example in C++: http://msdn.microsoft.com/en-us/library/bb513655(VS.85).aspx

    Thanks!

    EDIT...

     I got it to work somewhat... (I wish I understood what is going on here!) 

    The second call to EcGetSubRTS returns code other than 0... so I assume that it fails.  However, changing the StatusValuebuffer to Byte() returns (1000000020000000).  And I have confirmed that position 0 is the actual status. 
    • Edited by fixitchris Sunday, July 20, 2008 3:06 AM edit
    Friday, July 18, 2008 11:18 PM

Answers

  • oy, it was just one of those things.  Thanks for all that helped.  I learned a lot!

    For solution see: C:\Program Files\Microsoft SDKs\Windows\v6.1\Samples\SysMgmt\Events
    • Marked as answer by fixitchris Saturday, July 26, 2008 4:03 AM
    Saturday, July 26, 2008 4:03 AM

All replies

  • You'll need to declare the EC_VARIANT structure, find it in the EvColl.h header file of the Windows SDK.  Pass it with "ref", PEC_VARIANT is a pointer to EC_VARIANT.  Check this web page on advice for declaration structures that contain a union.  The Count member is at offset 8, Type at offset 12.

    Hans Passant.
    Saturday, July 19, 2008 7:14 PM
    Moderator
  • No bugz, that is most genius, thanks. However, the function is still returning -1 instead of 0 (ERROR_SUCCESS)... but at the same time GetLastWin32Error returns 0.  Is there something I should know about the data types in the function declaration or the structure?

    <StructLayout(LayoutKind.Explicit)> _  
        Private Structure EC_VARIANT  
            <FieldOffset(0)> Dim BooleanVal As Boolean 
            <FieldOffset(0)> Dim DateTimeVal As ULong  
            <FieldOffset(0)> Dim StringValue As IntPtr  
            <FieldOffset(0)> Dim BinaryVal As Byte 
            <FieldOffset(0)> Dim BooleanArr As IntPtr  
            <FieldOffset(0)> Dim Int32Arr As IntPtr  
            <FieldOffset(0)> Dim StringArr As IntPtr  
            <FieldOffset(8)> Dim Count As UInt32  
            <FieldOffset(12)> Dim Type As UInt32  
        End Structure 

     

    • Edited by fixitchris Sunday, July 20, 2008 12:23 AM edit
    Saturday, July 19, 2008 11:54 PM
  • It returns a BOOL, 0 or 1.  -1 is mysterious.  Use Marshal.GetLastWin32Error() to find out what went wrong.  Not quite sure if the VB.NET Declare keyword allows specifying DllImportAttribute.SetLastError, use <DllImport> in case of doubt.
    Hans Passant.
    Sunday, July 20, 2008 12:26 AM
    Moderator
  • -I am beginning to understand the reason for managed code... heh.

    Er... my fault.  I changed the returnValue variable to a boolean and the function returned true, so it works ok.  Thanks for pointing out that I have to specify the GetLastError attribute to get the Win32 error. That was very helpful.

    <DllImport("wecapi.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _ 



    I'm confused on Step 2 in the example code (http://msdn.microsoft.com/en-us/library/bb513655(VS.85).aspx) , maybe you can explain what is happening here... It is the same function, EcGetSubscriptionRunTimeStatus, and it still takes EC_VARIANT, except it will return a pointer based on the EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID.EcSubscriptionRunTimeStatusEventSources that I pass it.  Except what happens is the function returns FALSE and the StatusBufferUSED parameter returns 52.  So then I try to run the function again with 52 as the StatusbufferSIZE and the code crashes... So I'm not really sure how to get this array on the first call...

    FatalExecutionEngineError:
    The runtime has encountered a fatal error.  The address of the error was at ......... The error code is 0xc0000005.  This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code.  Common sources of this bug include user marshaling errors for COM-interop or PInvoke, which may corrupt the stack.

    Thanks for your time!!! -chris

    Here is the relevant C++ example:
        LPVOID lpwszBuffer;
        DWORD dwEventSourceCount, dwRetVal = ERROR_SUCCESS;
        std::vector<BYTE> buffer;
        std::vector<BYTE> eventSourceBuffer;
        std::vector<BYTE>::iterator sourceNameIterator;
        PEC_VARIANT vStatus, vEventSources;
        EC_HANDLE hSubscription;
        LPCWSTR lpSubname = L"TestSubscription";
        RUNTIME_STATUS runtimeStatus;
        std::wstring eventSource;

    // Step 2: Get the event sources array to query for event source status.  
        dwRetVal = GetStatus(lpSubname, NULL,   
            EcSubscriptionRunTimeStatusEventSources,   
            0,  
            eventSourceBuffer,   
            vEventSources);  
        if (ERROR_SUCCESS != dwRetVal){  
            goto Cleanup;  
        }  
     
        // Ensure that a handle to the event sources array has been obtained.  
        if (vEventSources->Type != EcVarTypeNull &&   
            vEventSources->Type != (EcVarTypeString | EC_VARIANT_TYPE_ARRAY) )  
        {  
            dwRetVal = ERROR_INVALID_DATA;  
            goto Cleanup;  
        }  
     
        dwEventSourceCount = vEventSources->Count; 

    Sunday, July 20, 2008 3:05 AM
  • You are probably going to have to initialize EC_VARIANT.StringArr.  It should be an IntPtr in your declaration, set it to Marshal.AllocHGlobal(52).  Then recover the string with Marshal.PtrToStringUni(), followed by FreeHGlobal().

    Hans Passant.
    Sunday, July 20, 2008 10:34 AM
    Moderator
  • It seems like the second call fails 50% of the time with the error mentioned above (FatalExecutionEngineError) regarding stack corruption or a CLR bug. (I'm running Server2008 x86 and the app is built for x86)
    I found this regarding the CLR error but I'm too bug eyed right now to make sense of code:  http://www.vbforums.com/showthread.php?t=530701
    UPDATE:  I believe changing <FieldOffset(0)> Dim BooleanVal As Boolean in EC_VARIANT to Uint32 solved the CLR crashing issue.  Is this safe?

    I can't seem to get around calling the EcGetSubscriptionRunTimeStatus function twice.  The first time to obtain the buffer size, and the second time to allocate the unmanaged EC_VARIANT.StringArr. 

    I feel like I am missing something though, because the EC_VARIANT.Type = 132, which does not match any constants available.  
    http://msdn.microsoft.com/en-us/library/aa382729(VS.85).aspx
    http://msdn.microsoft.com/en-us/library/aa385773(VS.85).aspx

    The unmanaged code requires it to be of type EC_VARIANT_TYPE_ARRAY(128) or EcVarTypeString(4).  So per the below code it looks like I am ERROR_INVALID_DATA...
    // Ensure that a handle to the event sources array has been obtained.  
        if (vEventSources->Type != EcVarTypeNull &&   
            vEventSources->Type != (EcVarTypeString | EC_VARIANT_TYPE_ARRAY) )  
        {  
            dwRetVal = ERROR_INVALID_DATA;  
            goto Cleanup;  
        }  
     

    Furthermore, the EC_VARIANT.StringArr is a pointer to an array...  Do I have to use Marshal.PtrToStrucutre instead of Marshal.PtrToStringUni to get this array into a String()/Char()?

    dwEventSourceCount = vEventSources->Count;  
          
        // Step 3: Get the status of each event source.  
        for (DWORD I = 0; I < dwEventSourceCount ; I++)  
        {  
            eventSource = vEventSources->StringArr[I];  
     
            // Get the status of the subscription event source.  
            dwRetVal = GetStatus(lpSubname,   
                eventSource.c_str(),  
                EcSubscriptionRunTimeStatusActive,   
                0,   
                buffer,   
                vStatus);  
            if (ERROR_SUCCESS != dwRetVal)  
     


    I tried this, but I get a "No parameterless constructor definded for this object" error:

     Dim ClientList(CType(vStatusBuffer.Count - 1, String)) As String  
                        Dim current As IntPtr = vStatusBuffer.StringArr  
     
                        For i As Int32 = 0 To vStatusBuffer.Count - 1  
                            ClientList(i) = CType(Marshal.PtrToStructure(current, GetType(String)), String)  
                            Marshal.DestroyStructure(current, GetType(String))  
                            current = IntPtr.op_Explicit(current.ToInt64() + Marshal.SizeOf(ClientList(i)))  
                        Next  
     


    My code:

    <StructLayout(LayoutKind.Explicit)> _  
        Private Structure EC_VARIANT  
            <FieldOffset(0)> Dim BooleanVal As Boolean 
            <FieldOffset(0)> Dim Uint32Val As UInt32  
            <FieldOffset(0)> Dim DateTimeVal As ULong  
            <FieldOffset(0)> Dim StringValue As IntPtr  
            <FieldOffset(0)> Dim BinaryVal As Byte 
            <FieldOffset(0)> Dim BooleanArr As IntPtr  
            <FieldOffset(0)> Dim Int32Arr As IntPtr  
            <FieldOffset(0)> Dim StringArr As IntPtr  
            <FieldOffset(8)> Dim Count As UInt32  
            <FieldOffset(12)> Dim Type As UInt32  
        End Structure 
     
     
    <DllImport("wecapi.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _  
        Private Shared Function EcGetSubscriptionRunTimeStatus( _  
            ByVal SubscriptionName As String, _  
            ByVal StatusInfoId As EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID, _  
            ByVal EventSourceName As String, _  
            ByVal Flags As UInt32, _  
            ByVal StatusValueBufferSize As UInt32, _  
            ByRef StatusValueBuffer As EC_VARIANT, _  
            ByRef StatusValueBufferUsed As UInt32 _  
                ) As Boolean 
        End Function 
     
     
    Private Sub GetSourceStatus(ByVal SubscriptionName As String)  
            Dim ec_handle As IntPtr = IntPtr.Zero  
            Dim dwRetVal As Boolean = False 
            Dim vStatusBuffer As EC_VARIANT = Nothing 
            Dim vStatusBuffer2 As EC_VARIANT = Nothing 
            Dim vStatusSize As UInt32 = 0  
            Dim vStatusUsed As UInt32 = 0  
            Dim er As Int16 = 0  
            Dim bRetry As Boolean = False 
     
            Try 
                ec_handle = EcOpenSubscription(SubscriptionName, 1, 2)  
     
                If ec_handle <> IntPtr.Zero Then 
                    dwRetVal = EcGetSubscriptionRunTimeStatus(SubscriptionName, _  
                        EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID.EcSubscriptionRunTimeStatusEventSources, _  
                        vbNullString, _  
                        Nothing, _  
                        vStatusSize, _  
                        vStatusBuffer, _  
                        vStatusUsed)  
     
                    er = Marshal.GetLastWin32Error  
                    If er = 122 Then 
                        'insufficient buffer size  
                        MsgBox("buffer too small : need " & vStatusUsed)  
                        vStatusSize = vStatusUsed  
                        vStatusBuffer = Nothing 
                        vStatusUsed = 0  
                        bRetry = True 
     
                    ElseIf er <> 0 Then 
                        MsgBox("win32 er1 : " & er.ToString)  
                    Else 
                        '0=success  
                    End If 
     
                    If bRetry Then 
                        MsgBox("retrying with larger buffer...")  
     
                        dwRetVal = EcGetSubscriptionRunTimeStatus(SubscriptionName, _  
                        EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID.EcSubscriptionRunTimeStatusEventSources, _  
                        vbNullString, _  
                        Nothing, _  
                        vStatusSize, _  
                        vStatusBuffer2, _  
                        vStatusUsed)  
     
                    End If 
     
                    If dwRetVal Then 
                        vStatusBuffer2.StringArr = Marshal.AllocHGlobal(CInt(vStatusUsed))  
                        MsgBox("    type: " & vStatusBuffer2.Type.ToString & "   count: " & vStatusBuffer2.Count.ToString & "   vstatusused: " & vStatusUsed)  
     
                        Dim strArr As String() = Nothing 
                        'Marshal.PtrToStructure(vStatusBuffer.StringArr, strArr)  
     
                        MsgBox(Marshal.PtrToStringUni(vStatusBuffer2.StringArr, CInt(vStatusUsed)))  
                        Marshal.FreeHGlobal(vStatusBuffer2.StringArr)  
                    End If 
     
     
                Else 
                    er = Marshal.GetLastWin32Error  
                    MsgBox("win32 er2: ec_handle = 0 : " & er.ToString)  
                End If 
     
     
            Catch ex As Exception  
                MsgBox("first catch " & ex.Message)  
            Finally 
     
                If ec_handle <> IntPtr.Zero Then 
                    EcClose(ec_handle)  
                End If 
            End Try 
        End Sub 
     

    Thanks!
    • Edited by fixitchris Monday, July 21, 2008 11:18 PM edit
    Monday, July 21, 2008 5:08 PM
  • "I can't seem to get around calling the EcGetSubscriptionRunTimeStatus function twice."

    That's how it is done, so don't worry about it. The C++ example calls it twice too.

    "I feel like I am missing something though, because the EC_VARIANT.Type = 132, which does not match any constants available."

    The EC_VARIANT is a 32 bit value. The second link you give shows that if a certain bit is set (10000000 binary = 128) then it indicates that the type is an array of whatever the other bits signify. So 132 in binary is 10000100, meaning it is an array of the type indicated by 100 in binary, which is 4 when represented in decimal, so it is an array of string.

    "
    The unmanaged code requires it to be of type EC_VARIANT_TYPE_ARRAY(128) or EcVarTypeString(4)."

    128 OR 4 = 132

    The rest will take some fiddling.
    Tuesday, July 22, 2008 12:06 AM
  • Thanks for your input.


    In regards to getting the string array from a pointer, I tried this but failed.... 
    Dim ClientList(CType(vStatusBuffer.Count - 1, String)) As String  
    Dim current As IntPtr = vStatusBuffer.StringArr  
     
    For i As Int32 = 0 To vStatusBuffer.Count - 1  
         ClientList(i) = CType(Marshal.PtrToStructure(current, GetType(String)), String)  
         Marshal.DestroyStructure(current, GetType(String))  
         current = IntPtr.op_Explicit(current.ToInt64() + Marshal.SizeOf(ClientList(i)))  
    Next 

    Nobugz,
    What is the purpose of using Marshal.AllocHGlobal() on vStatusBuffer.StringArr

    vStatusUsed = 52 'Buffer Size  
    vStatusBuffer.StringArr = Marshal.AllocHGlobal(CInt(vStatusUsed))     
    MsgBox(Marshal.PtrToStringUni(vStatusBuffer.StringArr, CInt(vStatusUsed)))     
    Marshal.FreeHGlobal(vStatusBuffer.StringArr)   
     

    I was looking at my code and I don't understand what vStatusBuffer.StringArr = Marshal.AllocHglobal(52) would accomplish... (It seems that statement would override vStatusBuffer.StringArr with a random 52-byte address).  I thought I would have to instantiate another IntPtr variable and assign to it Marshal.AllocHglobal(vStatusBuffer.StringArr)<-- This makes more sense to me, and as I understand it a part of memory accessible by the CLR will be allocated from unmanaged memory (ie. vStatusBuffer.StringArr).

    Thanks
    • Edited by fixitchris Tuesday, July 22, 2008 1:25 AM edit
    Tuesday, July 22, 2008 1:21 AM
  • That was only appropriate for a single string, you seem to be getting an array of strings.  I have no idea what the "vStatusUsed" return value is supposed to mean in the context of a string array.  Is it the number of strings?  The size of the string pointer array?  The total size of all returned strings?  You'd have to experiment.  Try a nice big buffer from AllocHGlobal, then look at its contents after the function returns with Debug + Windows + Memory1.
    Hans Passant.
    Tuesday, July 22, 2008 1:33 AM
    Moderator
  • I'm still confused about AllocHglobal...
    Lets say that vStatusBuffer.StringValue is a pointer to a single string that is 52 bytes long, returned from a function.  Can you clarify which statement is correct?

    vStatusBuffer.StringValue = Marshal.AllocHglobal(52)
    Dim RealString as String = Marshal.PtrToStringUni(vStatusBuffer.StringValue)     

    OR...

    Dim Pointer2 as IntPtr = Marshal.AllocHglobal(vStatusBuffer.StringValue)
    Dim RealString as String = Marshal.PtrToStringUni(Pointer2, 52


    _______________________________________________________________________________________________

    When the function returns a String Array type, the vStatusUsed is the size of the returned buffer so I am assuming the total size of the entire array (all bytes of all items)?  (I pass the vStatusUsed [received from the first call to the function along with Win32Err 122] as the StatusValueBufferSize on the second call to the function.)
    The EC_VARIANT.Count in this case is the number of array items.

    I have two client entries PC123.domain.com and PC124.domain.com and the EC_Variant.Count = 2,  EC_Variant.Type = 132 and the vBytesUsed = 128. 

    StatusValueBufferSize [in] 
            
    The size of the user-supplied buffer that will hold the run time status information.

    StatusValueBuffer
    [in] 
            
    The user-supplied buffer that will hold the run time status information. The buffer will hold the appropriate value depending on the EC_SUBSCRIPTION_RUNTIME_STATUS_INFO_ID value passed into the StatusInfoId parameter.

    StatusValueBufferUsed
    [out] 
            
    The size of the user supplied buffer that is used by the function on successful return, or the size that is necessary to store the property value when function fails with ERROR_INSUFFICIENT_BUFFER.


    <StructLayout(LayoutKind.Explicit)> _     
        Private Structure EC_VARIANT     
            <FieldOffset(0)> Dim BooleanVal As Boolean    
            <FieldOffset(0)> Dim DateTimeVal As ULong     
            <FieldOffset(0)> Dim StringValue As IntPtr     
            <FieldOffset(0)> Dim BinaryVal As Byte    
            <FieldOffset(0)> Dim BooleanArr As IntPtr     
            <FieldOffset(0)> Dim Int32Arr As IntPtr     
            <FieldOffset(0)> Dim StringArr As IntPtr     
            <FieldOffset(8)> Dim Count As UInt32     
            <FieldOffset(12)> Dim Type As UInt32     
        End Structure    
     




    Thanks.  I will try experimenting more. 
    • Edited by fixitchris Tuesday, July 22, 2008 2:26 AM edit
    Tuesday, July 22, 2008 2:25 AM
  • You'd make the Marshal.AllocHGlobal() call before the 2nd function call.  After it returns, you need to read the array of string pointers.  Call Marshal.ReadIntPtr() in a loop to get each pointer one by one, call Marshal.PtrToStringUni() passing that pointer to read the string.  I'd guess.
    Hans Passant.
    Tuesday, July 22, 2008 2:36 AM
    Moderator
  •   Nobugz,  I have been trying different things and I'm a little closer.

    Without calling AllocHGlobal at all I have been able to only retrieve the first 2 letters of the pointer contents, I'm not sure why the remainder of the string does not return???!!!:

    UPDATE:  In a one element array, ptr1 Memory1 shows "WI  4GM48W6Q4DN"(2 white spaces) but it should show "WIN-4GM48W6Q4DN" as the array element. 

    For idxcnt = 0 To vStatusBuffer.Count - 1  
        ptr1 = Marshal.ReadIntPtr(vStatusBuffer.StringArr, idxcnt * Marshal.SizeOf(GetType(IntPtr)))  
        MsgBox("|" & Marshal.PtrToStringUni(ptr1) & "|")  
    Next 

    Also in a 2 item array, the first iteration of the above loop returns nothing, but the second iteration returns the complete string for the second array element. 

    Regarding AllocHGlobal:
    I have tried calling

    vStatusBuffer.StringArr = Marshal.AllocHGlobal(CInt(vStatusUsed))


    before calling the function a second time, but after the 2nd call the vstatusBuffer.StringArr is overwritten with values from the second call. 

    I don't think I'm using the AllocHGlobal correctly.
    • Edited by fixitchris Tuesday, July 22, 2008 9:05 PM EDIT
    Tuesday, July 22, 2008 5:01 PM
  • Is my EC_VARIANT struct correct to receive an array of pointers in the StringArr field?  I can't do IntPtr() because CLR says that a field is overlapping another one...

    Thursday, July 24, 2008 2:04 PM
  • oy, it was just one of those things.  Thanks for all that helped.  I learned a lot!

    For solution see: C:\Program Files\Microsoft SDKs\Windows\v6.1\Samples\SysMgmt\Events
    • Marked as answer by fixitchris Saturday, July 26, 2008 4:03 AM
    Saturday, July 26, 2008 4:03 AM