locked
Marshal variable length array RRS feed

  • Question

  • I'm trying to call the GetInterfaceInfo Windows API - it accepts a structure as one of its parameters and this structure has a member that is an array. The API populates the contents of the array when it is called and there can be any number of items in the array. How can I define such an array in my structure?

    If I just use a normal .NET array like so then the program just crashes (no exception thrown)

    <StructLayoutAttribute(LayoutKind.Sequential)> _
    Public Structure IP_INTERFACE_INFO
            Public NumAdapters As Integer
            Public Adapter() As IP_ADAPTER_INDEX_MAP
    End Structure

    so I declare it like this instead:

    <StructLayoutAttribute(LayoutKind.Sequential)> _
    Public Structure IP_INTERFACE_INFO
            Public NumAdapters As Integer
            <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=64, ArraySubType:=UnmanagedType.Struct)> _
            Public Adapter() As IP_ADAPTER_INDEX_MAP
    End Structure

    and this works fine, but the problem is I have to define the SizeConst attribute as you can see to something large (64 in this example) so that the API doesnt fail because the array wasnt big enough. Firstly this is a waste of memory as in most cases the number of items the API sticks in the array will be less than 10, but secondly if there was ever a case where the API wanted to put more than 64 items in then it would fail.

    Using ReDim to set the length of the array at runtime seems to have no effect, and that SizeConst attribute has to be set to a constant value so there's no way I can dynamically set it.

    Any ideas?

    Cheers
    Chris


    My blog: http://cjwdev.wordpress.com
    Tuesday, June 8, 2010 8:00 PM

Answers

  • You have to do it manually, as you don't know the size. When this is the case you have to call the api once to determine how large the buffer has to be to store the structure, then call it again with a correctly sized buffer. You can allocate a blob of memory with Marshal.AllocHGlobal, and then pull the data out of that with a bit of pointer manipulation. Here I use IntPtr.Add which is .Net 4.0. If you aren't using it, then you can do:

    New IntPtr(ptr.ToInt64 + offset)

    ---

    (Edit: refactored it)

     

     

    Option Strict On
    Option Explicit On
    Option Infer On
    
    Imports System.Runtime.InteropServices
    Imports System.ComponentModel
    Imports System.Security
    
    Module Module1
    
     <SuppressUnmanagedCodeSecurity()> _
     Private Class NativeMethods
      <DllImport("iphlpapi", SetLastError:=True, CharSet:=CharSet.Unicode)> _
      Public Shared Function GetInterfaceInfo( _
      ByVal pIfTable As IntPtr, _
      ByRef dwOutBufLen As Integer) As Integer
      End Function
     End Class
    
     <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
     Private Structure IP_ADAPTER_INDEX_MAP
    
      Public Index As Integer
      <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)> _
      Public Name As String
    
      Public Overrides Function ToString() As String
       Return String.Format("Index:{0}, Name:{1}", Index, Name)
      End Function
    
     End Structure
    
     Sub Main()
      ' As we don't know the size beforehand, we can't make a blittable type.
      ' So we do it the long way...
      ' 1st ask to see how many bytes of memory are required:
      Dim piInfo As IntPtr = IntPtr.Zero
      Dim requiredSize As Integer
      Const ERROR_OK As Integer = 0 ' what a dumb name.
      Const ERROR_INSUFFICIENT_BUFFER As Integer = 122
      Const ERROR_NO_DATA As Integer = 232
      Dim result As Integer = NativeMethods.GetInterfaceInfo(piInfo, requiredSize)
      If result = ERROR_NO_DATA Then
       Console.WriteLine("No adapters are installed")
       Exit Sub
      End If
      If result <> ERROR_INSUFFICIENT_BUFFER Then Throw New Win32Exception ' we are expecting the buffer to be too small
      piInfo = Marshal.AllocHGlobal(requiredSize)
      Try
       ' Then call again with the correct amount of memory allocated.
       If NativeMethods.GetInterfaceInfo(piInfo, requiredSize) <> ERROR_OK Then Throw New Win32Exception
       ' Then pull the information out one field at a time, first is NumAdapters
       Dim offset As Integer = 0 ' Keep increasing this as we move through the memory
       Dim numAdapters As Integer = Marshal.ReadInt32(piInfo, offset)
       offset += 4 ' size of an integer   
       Dim adapters(numAdapters - 1) As IP_ADAPTER_INDEX_MAP
       ' Then follows the adapaters array.
       For i As Integer = 0 To numAdapters - 1
        adapters(i) = DirectCast(Marshal.PtrToStructure(IntPtr.Add(piInfo, offset), GetType(IP_ADAPTER_INDEX_MAP)), IP_ADAPTER_INDEX_MAP)
        offset += Marshal.SizeOf(GetType(IP_ADAPTER_INDEX_MAP))
        Console.WriteLine(adapters(i))
       Next
      Finally
       Marshal.FreeHGlobal(piInfo)
      End Try
    
     End Sub
    
    End Module
    

     

    Output:

    Index:11, Name:\DEVICE\TCPIP_{3E76A801-8B11-4763-9747-A04DE1418649}
    Index:14, Name:\DEVICE\TCPIP_{3CA0BA39-2D52-45D7-A59C-0870D3A78658}

     

    • Edited by jo0ls Monday, July 5, 2010 1:17 PM
    • Marked as answer by Chris128 Monday, July 5, 2010 8:30 PM
    Monday, July 5, 2010 11:12 AM

All replies

  • Use SizeParamIndex - it tells the marshaller which parameter contains the size of the array.
    Monday, July 5, 2010 8:16 AM
  • Thanks that looks like exactly what I need! The only problem is that I went back to this code yesterday and for some reason it wouldn't work at all no matter what size array I passed in to it... but I'll have another crack at it tonight with this new information :) Thanks again Chris
    My blog: http://cjwdev.wordpress.com
    Monday, July 5, 2010 8:23 AM
  • Sorry, made a mistake - not had my coffee yet. SizeParamIndex is used in api calls, to say which method parameter has the number of elements in an array that is another parameter. It's not appropriate here because it's in a structure - I wasn't paying attention.

    I'll take another look...

     

    Monday, July 5, 2010 9:34 AM
  • You have to do it manually, as you don't know the size. When this is the case you have to call the api once to determine how large the buffer has to be to store the structure, then call it again with a correctly sized buffer. You can allocate a blob of memory with Marshal.AllocHGlobal, and then pull the data out of that with a bit of pointer manipulation. Here I use IntPtr.Add which is .Net 4.0. If you aren't using it, then you can do:

    New IntPtr(ptr.ToInt64 + offset)

    ---

    (Edit: refactored it)

     

     

    Option Strict On
    Option Explicit On
    Option Infer On
    
    Imports System.Runtime.InteropServices
    Imports System.ComponentModel
    Imports System.Security
    
    Module Module1
    
     <SuppressUnmanagedCodeSecurity()> _
     Private Class NativeMethods
      <DllImport("iphlpapi", SetLastError:=True, CharSet:=CharSet.Unicode)> _
      Public Shared Function GetInterfaceInfo( _
      ByVal pIfTable As IntPtr, _
      ByRef dwOutBufLen As Integer) As Integer
      End Function
     End Class
    
     <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
     Private Structure IP_ADAPTER_INDEX_MAP
    
      Public Index As Integer
      <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)> _
      Public Name As String
    
      Public Overrides Function ToString() As String
       Return String.Format("Index:{0}, Name:{1}", Index, Name)
      End Function
    
     End Structure
    
     Sub Main()
      ' As we don't know the size beforehand, we can't make a blittable type.
      ' So we do it the long way...
      ' 1st ask to see how many bytes of memory are required:
      Dim piInfo As IntPtr = IntPtr.Zero
      Dim requiredSize As Integer
      Const ERROR_OK As Integer = 0 ' what a dumb name.
      Const ERROR_INSUFFICIENT_BUFFER As Integer = 122
      Const ERROR_NO_DATA As Integer = 232
      Dim result As Integer = NativeMethods.GetInterfaceInfo(piInfo, requiredSize)
      If result = ERROR_NO_DATA Then
       Console.WriteLine("No adapters are installed")
       Exit Sub
      End If
      If result <> ERROR_INSUFFICIENT_BUFFER Then Throw New Win32Exception ' we are expecting the buffer to be too small
      piInfo = Marshal.AllocHGlobal(requiredSize)
      Try
       ' Then call again with the correct amount of memory allocated.
       If NativeMethods.GetInterfaceInfo(piInfo, requiredSize) <> ERROR_OK Then Throw New Win32Exception
       ' Then pull the information out one field at a time, first is NumAdapters
       Dim offset As Integer = 0 ' Keep increasing this as we move through the memory
       Dim numAdapters As Integer = Marshal.ReadInt32(piInfo, offset)
       offset += 4 ' size of an integer   
       Dim adapters(numAdapters - 1) As IP_ADAPTER_INDEX_MAP
       ' Then follows the adapaters array.
       For i As Integer = 0 To numAdapters - 1
        adapters(i) = DirectCast(Marshal.PtrToStructure(IntPtr.Add(piInfo, offset), GetType(IP_ADAPTER_INDEX_MAP)), IP_ADAPTER_INDEX_MAP)
        offset += Marshal.SizeOf(GetType(IP_ADAPTER_INDEX_MAP))
        Console.WriteLine(adapters(i))
       Next
      Finally
       Marshal.FreeHGlobal(piInfo)
      End Try
    
     End Sub
    
    End Module
    

     

    Output:

    Index:11, Name:\DEVICE\TCPIP_{3E76A801-8B11-4763-9747-A04DE1418649}
    Index:14, Name:\DEVICE\TCPIP_{3CA0BA39-2D52-45D7-A59C-0870D3A78658}

     

    • Edited by jo0ls Monday, July 5, 2010 1:17 PM
    • Marked as answer by Chris128 Monday, July 5, 2010 8:30 PM
    Monday, July 5, 2010 11:12 AM
  • Thanks, that's pretty much what I ended up with yesterday but I just couldn't quite get it working properly. The thing that I think I got stuck on was that I did not realise the string member of the IP_ADAPTER_INDEX_MAP was a pointer to a string. I used Marshal.Copy to copy about 200 bytes from the start of the data that GetInterfaceInfo returned and I could see the index number of the first adapter followed by a load of bytes that were each separated by a 0 byte, so I assumed that was a unicode string... and actually I'm sure it was because I used System.Text.Encoding.Unicode.ToString on them and it did give me the adapter name. However I did just test your code out and it does work... so I'm a bit puzzled. The only difference is that I tested your code on XP and I was testing my code last night on Win7 x64, though I cant see why this API would be any different on each.

    I'll try your code out on the Win7 machine tonight and let you know how it goes :)

    Thanks again

    Chris


    My blog: http://cjwdev.wordpress.com
    Monday, July 5, 2010 12:14 PM
  • Hmm well I just tried using Marshal.Copy as I mentioned, but on the XP machine, and it shows me the same thing. I'm totally confused...

    For example, if I use Marshal.Copy to copy say 100 bytes from the start of the data that GetInterfaceInfo returns like so:

    Dim Bytes(99) As Byte
    Marshal.Copy(piInfo, Bytes, 0, Bytes.Length)
    For i As Integer = 0 To Bytes.Length - 1
      Debug.WriteLine(Bytes(i))
    Next

    then what I see is the byte array shown below. The first 4 bytes are obviously the numAdapters field (1 in my case), then the next 4 bytes are the index member of the first IP_ADAPTER_INDEX_MAP (2 in my case) but then according to your code the next 4 bytes should be a pointer to a string... but they are not, the next 4 bytes and a load more after are the actual string itself.

    1

    0

    0

    0

    2

    0

    0

    0

    92

    0

    68

    0

    69

    0

    86

    0

    73

    0

    67

    0

    69

    0

    92

    0

    84

    0

    67

    0

    80

    0

    73

    0

    80

    0

    95

    0

    123

    0

    50

    0

    70

    0

    50

    0

    56

    0

    50

    0

    69

    0

    54

    0

    54

    0

    45

    0

    48

    0

    67

    0

    53

    0

    52

    0

    45

    0

    52

    0

    57

    0

    52

    0

    68

    0

    45

    0

    65

    0

    66

    0

    65

    0

    55

    0

    45

    0

    56

    0

    69

    0

    54

    0

    67

    0

    55

    0

    48

    0

    53

    0

    68

    0

    54

    0

    51

    0

    48

    0

    66

    0

    125

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    0

    Any chance you can explain how this is possible?

    Thanks

    Chris


    My blog: http://cjwdev.wordpress.com
    Monday, July 5, 2010 12:43 PM
  • Ah, I changed my code since then, it now uses a ByValTStr and marshals the whole structure at once. This is assuming the layout is just as you show - there's no pointer.

    Originally I was going by the description in the help files for the Name field:

    Name A pointer to a Unicode string that contains the name of the adapter.

    I'm a C noob, so I'm not sure exactly what it is expecting. On the one hand it says there that it wants a pointer, but the signature just looks like an array of WCHARS - which you would expect to see directly following the first field. I suspect that with the code I originally posted (which worked with my 2 adapters) the marshaller was doing some tricks to get it to work.

     

    OOI I used the p/invoke signature toolkit to generate a VB.Net version of the structure, and it did:

    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.[Unicode])> _
    Public Structure IP_ADAPTER_INDEX_MAP
      
      '''ULONG->unsigned int
      Public Index As UInteger
      
      '''WCHAR[]
      <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=-1)> _
      Public Name As String
    End Structure
    It didn't know the value of that const, but agrees with my refactored version. The ByValTStr Strings appear directly in the structure, there's no pointer involved.

    Monday, July 5, 2010 1:13 PM
  • Well I continued to struggle with this when I got home, then realised I had made one stupid mistake - I had not specified the Unicode CharSet for my structure. As soon as I did that, it all started working perfectly :)

    So the code I ended up with looks extremely similar to yours, though I had actually written the majority of it last night already just made a few stupid mistakes.. ah well, learn from your mistakes eh.

    Tell you what else I learnt - its not a good idea to test code that releases your IP address when you are in the middle of downloading a large file! Not my smartest day today.

    Thanks for your help!

    Oh and thanks for answering a question I asked a while ago on another forum in your first post (http://www.vbforums.com/showthread.php?p=3822788)

     

    Cheers

    Chris


    My blog: http://cjwdev.wordpress.com My Windows API Library: http://cjwdev.wordpress.com/2010/07/04/cjwdev-windowsapi-api-pack-released/
    Monday, July 5, 2010 8:34 PM