[Blobs]

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.