Tuesday, July 04, 2006

ANSI BSTR in VB.NET

Assume you have an C DLL working fine with your VB6 program and now you are hitting the road to migrate towards VB.NET. Suddenly, you see, that the strings in structures tunneled to DLL from VB.NET code don't come properly. That's because VB.NET marshals strings as BSTR (double byte counted strings) and your DLL was trained to accept ANSI strings (same as BSTR but single byte). Try to use predefined MarshalAs(UnmanagedType.AnsiBStr) - maybe you'll be lucky. Didn't work for me.
There are two ways out:
- rewrite your DLL (if sources and resources are still available etc.)
- adjust you VB.NET code with AnsiBStr class (as follows) to get strings transferred to and from unmanaged world.

Enjoy!

Imports System.Runtime.InteropServices
'
' Class AnsiBString realizes marshaling of ANSI strings to
' unmanaged DLLs tuned for VB6 clients

'

Public Class AnsiBString
Implements IDisposable

' Used by IDisposable
Protected disposed As Boolean = False

' String buffer size
Dim Size As Integer
' String buffer
Dim Buffer As String
' Private pointer to unmanaged memory block contaning bytes
Private _ptr As IntPtr
'
' Public property - pointer to unmanaged memory containing single byte BSTR
'
ReadOnly Property Ptr() As IntPtr
Get
' String buffer starts after buffer size integer
Ptr = _ptr.ToInt32() + IntPtr.Size
End Get
End Property
'
' Constructor
'
Sub New(ByVal s As String)
'
' ONLY FOR DEBUGGING PURPOSES
' REMOVE FOR RELEASE VERSION
' System.Diagnostics.Trace.WriteLine("ANSIBSTR: Ctor for " + s + " " + Me.GetHashCode().ToString())

' Set buffer
Buffer = s
' Calculate size
Size = 0
If Not String.IsNullOrEmpty(Buffer) Then
Size = Buffer.Length
End If
' Allocate unmanaged memory block to store size and string buffer
' plus one byte for trailing zero to be safe
_ptr = Marshal.AllocHGlobal(Size + IntPtr.Size + 1)
' Store size in unmanaged memory
Marshal.WriteIntPtr(_ptr, 0, CType(Size, IntPtr))
If Size > 0 Then
' Get string buffer as ANSI bytes
Dim bArray() As Byte = System.Text.ASCIIEncoding.Default.GetBytes(Buffer)
' Write ANSI bytes to unmanaged memory
Marshal.Copy(bArray, 0, CType(_ptr, Integer) + IntPtr.Size, bArray.Length)
' Write zero byte at end for safety
Marshal.WriteByte(_ptr, IntPtr.Size + bArray.Length, 0)
End If
End Sub
'
' Counter part - get ANSI BSTR from pointer
'
Shared Function FromPointer(ByVal aPtr As IntPtr, Optional ByVal bFreePointer As Boolean = False) As AnsiBString
Dim size As Integer
If aPtr = IntPtr.Zero Then
FromPointer = New AnsiBString("")
Else
' Calculate string buffer size
size = CType(Marshal.ReadIntPtr(aPtr, -(IntPtr.Size)), Integer)
Dim bytes() As Byte
ReDim bytes(size)
' Read string bytes
Marshal.Copy(aPtr, bytes, 0, size)
' Create AnsiBString instance
Dim ansiBstring As New AnsiBString(System.Text.ASCIIEncoding.Default.GetString(bytes))
FromPointer = ansiBstring
If bFreePointer Then
Try
Marshal.FreeHGlobal(aPtr)
Catch ex As Exception
System.Diagnostics.Trace.WriteLine(System.Reflection.Assembly.GetExecutingAssembly().FullName + ":" + _
System.Reflection.Assembly.GetCallingAssembly().FullName + ": " + _
"free ANSI string pointer (" + aPtr.ToString() + "): " + ex.Message + ": " + ex.StackTrace)
End Try
End If
End If
End Function

Public Overloads Function ToString() As String
ToString = Buffer
End Function

Protected Overridable Overloads Sub Dispose( _
ByVal disposing As Boolean)
If Not Me.disposed Then
If disposing Then
' free managed resources
End If
'
' ONLY FOR DEBUGGING PURPOSES
' REMOVE FOR RELEASE VERSION
' System.Diagnostics.Trace.WriteLine("ANSIBSTR: free unmanaged buffer for " + Me.GetHashCode().ToString())

Try
Marshal.FreeHGlobal(_ptr)
Catch ex As Exception
'System.Diagnostics.Trace.WriteLine("ANSIBSTR: exception by release unmanaged memory " + _ptr.ToString() + " " + Me.GetHashCode().ToString())
End Try
' Note that this is not thread safe.
End If
Me.disposed = True
End Sub

#Region " IDisposable Support "
' Do not change or add Overridable to these methods.
' Put cleanup code in Dispose(ByVal disposing As Boolean).
Public Overloads Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overrides Sub Finalize()
'
' ONLY FOR DEBUGGING PURPOSES
' REMOVE FOR RELEASE VERSION
' System.Diagnostics.Trace.WriteLine("ANSIBSTR: Finalizer for " + Buffer)

Dispose(False)
MyBase.Finalize()
End Sub
#End Region
End Class