Also available at

Also available at my website http://tosh.me/ and on Twitter @toshafanasiev

Tuesday, 12 October 2010

Overflow checking in VB.NET

I am currently porting a VB6 application to VB.NET and having run the project through the VS2008 conversion wizard, I am now trying to fix the as yet unknowable number of errors that it could not resolve. One of these relates to truncation of integers to yield hi- or lo- words.
The hi-word was ok - VB.NET gives you shift operators so the following is possible.

Dim i As Int32
'...
Dim s As Short
s = i >> 16

Ordinarily ( i.e. in C# ) the lo-word would not pose a problem either as an unchecked ( the default ) conversion from int to short would yield the correct result, simply discarding the upper two bytes

static short LoWord( int val ) {
  return ( short ) val;
}

This can be confirmed by disassembling the compiled IL code ( using ILDasm.exe )

.method private hidebysig static int16  LoWord(int32 val) cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init (int16 V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  conv.i2
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method prog::LoWord

The conv.i2 instruction labelled IL_0002 does not perform overflow checking. However, a similarly written function in VB.NET does not behave the same way:

Function LoWordVb( ByVal i As Integer ) As Short
  Return CShort( i )
End Function

This disassembles to:

.method public static int16  LoWordVb(int32 i) cil managed
{
  // Code size       7 (0x7)
  .maxstack  1
  .locals init (int16 V_0)
  IL_0000:  ldarg.0
  IL_0001:  conv.ovf.i2
  IL_0002:  stloc.0
  IL_0003:  br.s       IL_0005
  IL_0005:  ldloc.0
  IL_0006:  ret
} // end of method prog::LoWordVb

Which uses the same overflow checking conv.ovf.i2 instruction as the following C# function:

  static short LoWordChecked( int val ) {
    checked {
      return ( short ) val;
    }
  }

Which disassembles to:

.method private hidebysig static int16  LoWordChecked(int32 val) cil managed
{
  // Code size       9 (0x9)
  .maxstack  1
  .locals init (int16 V_0)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  ldarg.0
  IL_0003:  conv.ovf.i2
  IL_0004:  stloc.0
  IL_0005:  br.s       IL_0007
  IL_0007:  ldloc.0
  IL_0008:  ret
} // end of method prog::LoWordChecked

( There are a few details that you need to be aware of when using overflow checking in C# which I won't go into - there's a very comprehensive and well written post on the subject here: http://www.codeproject.com/KB/cs/overflow_checking.aspx )
So, while C# offers extremely granular control of overflow checking by means of the checked construct, VB.NET does not ( that I could find out about ), so short of controlling this behaviour globally for the entire application ( something which I am a little wary of doing ), there's no immediately obvious way to perform this apparently simple task.
My solution was to copy, memory wise, the lower bytes of the Int32 to the Int16; this may not be the most elegant or efficient solution, but it does work.
VB.NET does not offer any of the syntax-level pointer manipulation that C# does ( once you use the terrifyingly named 'unsafe' construct and compiler switch ), however the .NET BCL makes everything you need available via the System.Runtime.InteropServices namespace in Mscorlib.dll - once you get to know these tools you can use them from any CLR targeting language.
First you need an instance of the GCHandle structure for the managed object you are copying from ( in this case it will have to be a boxed copy of the Int32 ), you obtain this as follows:

Dim i As Int32
' ....
Dim h As GCHandle = GCHandle.Alloc( i, GCHandleType.Pinned )

And the GCHandle must be cleaned up responsibly to prevent resource leaks ( yes, in .NET ) - I suggest a try/finally block immediately after allocation that calls h.Free() in the finally clause - ensuring that it's freed. 
One thing that may occur to you is that passing a value type instance such as Int32 as a parameter of type Object will result in a box operation, generating a reference that is never explicitly held onto by the calling function and that this could cause problems - this is not the case as both the Pinned and Normal members of the GCHandleType enumeration result in the GCHandle instance preventing the reference passed in from being collected until the handle is itself freed. Using Pinned rather than Normal gives our code the opportunity to use the address of the instance without worrying about it being moved by the garbage collector.
The next step is using the Marshal class to copy from memory to a managed instance

Dim s As Short
Dim o As Object
o = Marshal.PtrToStructure( h.AddrOfPinnedObject(), GetType( Short ) )
' simple unbox
s = CShort( o )

That's it! These elements can be put together in a function as follows ( a complete program is available at the end of the post ):

Imports System.Runtime.InteropServices
'....
Function LoWordVbUnchecked( ByVal i As Integer ) As Short
  Dim s As Short
  Dim h As GCHandle = GCHandle.Alloc( i, GCHandleType.Pinned )
  Try
    Dim o As Object
    o = Marshal.PtrToStructure( h.AddrOfPinnedObject(), GetType( Short ) )
    s = CShort( o )
  Finally
    h.Free()
  End Try
  Return s
End Function

A lot of words, you might think, but I think that this example serves to cover many interesting aspects of developing code for the .NET CLR.

Complete program:

'author: Tosh Afanasiev ( http://tosh.me/ )

Imports System
Imports System.Runtime.InteropServices

Module prog

  Public Sub Main()
    Dim i As Int32 = &Haaaabbbb
    Dim s As Int16 = i >> 16
    Console.WriteLine( "i: {0:x}", i )
    Console.WriteLine( "hi: {0:x}", s )
    Console.WriteLine( "using LoWordVb causes an OverflowException for this value" )
    Try
      s = LoWordVb( i )
    Catch ex As OverflowException
      Console.WriteLine( "told you: {0}", ex )
    End Try
    Console.WriteLine( "LoWordVbUnchecked is ok though" )
    s = LoWordVbUnchecked( i )
    Console.WriteLine( "lo: {0:x}", s )
  End Sub

  Function LoWordVb( ByVal i As Integer ) As Short
    Return CShort( i )
  End Function

  Function LoWordVbUnchecked( ByVal i As Integer ) As Short
    Dim s As Short
    Dim h As GCHandle = GCHandle.Alloc( i, GCHandleType.Pinned )
    Try
      Dim o As Object
      o = Marshal.PtrToStructure( h.AddrOfPinnedObject(), GetType( Short ) )
      s = CShort( o )
    Finally
      h.Free()
    End Try
    Return s
  End Function

End Module

No comments:

Post a Comment