The problem
Sunday, April 27, 2003
When calling APIs, you often need to pass structures of a certain size. These will mostly hold char arrays. In classic VB, you could use fixed-length strings to create a structure of the right size. This is now longer possible in VB.NET. You can't use Char or Byte arrays of a pre-declared length in structures, either.
So you need to fill the structure in some way. It doesn't work if you declare a string member, and then fill it by assigning a new string of a certain length. A string field increases the structure's size by not more than four bytes - String is a reference type now. The same applies to arrays (it doesn't help if you resize them using ReDim). You could also fill the structure with lots of integers, and work with MoveMemory, but that is a matter of taste.
The way to do this in VB.NET is to apply a marshalling attribute to the respective fields in the structure. Here, I'm taking a slightly different approach that gives you more control over memory - whether that's always preferable is a different matter.
Pointers to blobs
Structures are almost always passed by reference, so what you really need to do is allocate memory, and pass a pointer to it. You can declare an API in two ways:
Declare Sub CreatePageFault Lib "kernel32.dll"(ByRef struct As TPageFaultInfo) Declare Sub CreatePageFault Lib "kernel32.dll"(ByVal pStruct As Integer)
To the API, it's both the same - it receives a pointer. In the second version, you need to get the address of the struct yourself, whereas otherwise Visual Basic will handle this for you. By the way, the Marshal class has methods to help you out with structures and pointers; of course, this doesn't solve our problem.
So if we can't have structures of the right size (and, btw, we'd need marshalling attributes as well, because UDTs aren't whole memory blocks anymore by default), why not use something else? How do we hack about in unmanaged memory? The pointer hacker's tools are called GetProcessHeap, HeapAlloc, MoveMemory, and HeapFree - not!
Fortunately, the Marshal class has Shared (static) methods that allow us to do what we want. In VB.NET, you can manage your own memory, but you do it the functional way. So let's encapsulate memory blobs, so they can be used safely. The idea is to have immutable blob objects, with methods that read out the buffer.
Using a blob object
You instantiate a CBlob object by telling the constructor the size of the memory block you want to allocate (there is only one constructor). If something goes wrong, be prepared to handle an exception. Once created, the instance is immutable. You cannot resize the buffer, move it, or write to it. You pass it on to the API, using the Address property. The you read, using various properties or methods, specifying the byte offset; invalid offsets cause an exception (but not a page fault!). When you're done, call Dispose to free the blob:
' size of the SHFILEINFO structure (Ansi) Dim blob As New CBlob(352) ' pass address and size SHGetFileInfo(pszPath, 0, blob.Address, blob.Size, SHGFI_TYPENAME) ' offset, length (inclusive) Dim sTypeName As String = blob.GetTextAnsi(272, 80) ' free the buffer blob.Dispose
Remember to declare the API (SHGetFileInfo, in this example) to take an Integer (Int32) by value (not a structure by reference). You don't pass the blob object either, rather, you use its Address property. The blob object encapsulates a pointer to a buffer in the unmanaged heap, so the buffer is not reclaimed by the garbage collector; need to free it by calling Dispose.
Under the hood, CBlob uses Marshal's Shared heap methods:
The CBlob class
Here is the complete source code to CBlob. Remember that clients need to free the memory by calling Dispose.
Imports System.Runtime.InteropServices Public Class CBlob Private Const ERR_MSG As String = "This is raw memory territory, so watch out" Private m_Address As Integer Private m_Size As Integer Private m_IsDisposed As Boolean Public Sub New(ByVal cb As Integer) Try m_Size = cb m_Address = Marshal.AllocHGlobal(cb) If m_Address = 0 Then Throw New Exception("Couldn't obtain pointer") End If Catch e As Exception Throw e End Try End Sub Private Sub Destruct() Me.Dispose End Sub Public Sub Dispose() ' you need to call this! If m_Address <> 0 Then Marshal.FreeHGlobal(m_Address) End If m_Address = 0 m_Size = 0 m_IsDisposed = True End Sub Public ReadOnly Property IsDisposed As Boolean Get Return m_IsDisposed End Get End Property Public ReadOnly Property Address As Integer Get Return m_Address End Get End Property Public ReadOnly Property Size As Integer Get Return m_Size End Get End Property Public ReadOnly Property LowerBound As Integer Get Return 0 End Get End Property Public ReadOnly Property UpperBound As Integer Get Return m_Size - 1 End Get End Property Public Default ReadOnly Property Bytes(ByVal index As Integer) As Byte Get If index < 0 Or index > m_Size - 1 Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.ReadByte(m_Address, index) End Get End Property Public ReadOnly Property Int16s(ByVal startByte As Integer) As Short Get If startByte < 0 Or startByte > m_Size - 3 Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.ReadInt16(m_Address, startByte) End Get End Property Public ReadOnly Property Int32s(ByVal startByte As Integer) As Integer Get If startByte < 0 Or startByte > m_Size - 5 Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.ReadInt32(m_Address, startByte) End Get End Property Public ReadOnly Property Int64s(ByVal startByte As Integer) As Long Get If startByte < 0 Or startByte > m_Size - 9 Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.ReadInt64(m_Address, startByte) End Get End Property Public Function GetTextAnsi(ByVal startByte As Integer, ByVal cAsciiChars As Integer) As String If startByte < 0 Or startByte + cAsciiChars > m_Size Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.PtrToStringAnsi(m_Address + startByte, cAsciiChars) End Function Public Function GetTextUni(ByVal startByte As Integer, ByVal cUniChars As Integer) As String If startByte < 0 Or startByte + cUniChars * 2 > m_Size Then Throw New IndexOutOfRangeException(ERR_MSG) End If Return Marshal.PtrToStringUni(m_Address + startByte, cUniChars) End Function Public Overrides Function ToString() As String Return Me.GetTextAnsi(0, m_Size) End Function End Class
Extending CBlob
You can extend CBlob, and add properties that access special byte offsets. This way it's easier for clients. For the relevant API structures, just inherit from CBlob, calculate the offsets, and add properties that call the base class's methods or properties in order to return the values.