To make an existing managed application with unmanaged 'appendages' 64-bit capable, I decided to rewrite a 32 bit unmanaged VC++ dll as a managed VB.Net class library.
The application must be able to run on any framework from 2.0 up, and the VC++ code I ported was using named pipes, which are supported only from framework 3.5 up. So I had to resort to reflection, calling windows API functions for everything related to the pipes.
The result works perfectly in 32 bit mode, but in 64 bit, I get all kinds of trouble. The actual behavior may change dramatically with even the least change in source code, just swapping some variable declarations around or moving a simple assignment statement to another location in the code may cause any effect from
to
If you ask me, the quick change in symptoms, but especially the latter, points towards something somewhere, probably a pointer or handle, that is still being stored or passed to/from Win32 API at 32 bit sizes when running at 64 bit. But I don't succeed in figuring out what.
The most likely location is in the API declarations. The rest is all managed code, and compiled with options 'strict' and 'explicit' both on. Can someone have a look at the snippets below and see if he can spot something I keep overlooking?
Operation is asynchonous. The actual waits are performed at managed level on an array containing managed as well as unmanaged handles (through SafeWaitHandle wrapping), so events in managed together with unmanaged stuff can be dealt with by a single WaitHandle.WaitAny(...array...) call.
<DllImport("kernel32", SetLastError:=True)> Private Shared Function CreateNamedPipe(
lpName As String,
dwOpenMode As Int32,
dwPipeMode As Int32,
nMaxInstances As Int32,
nOutBufferSize As Int32,
nInBufferSize As Int32,
nDefaultTimeOut As Int32,
lpSecurityAttributes As IntPtr ' when declared as SECURITY_ATTRIBUTES runtime won't accept passing Nothing, even when marked <[Optional]>
) As Microsoft.Win32.SafeHandles.SafeFileHandle : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function ConnectNamedPipe(
hNamedPipe As SafeHandle,
ByRef lpOverlapped As System.Threading.NativeOverlapped
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function DisconnectNamedPipe(
ByVal hNamedPipe As SafeHandle
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function ReadFile(
<[In]> hFile As SafeHandle,
<Out> lpBuffer As IntPtr,
<[In]> nNumberOfBytesToRead As Int32,
<Out, [Optional]> ByRef lpNumberOfBytesRead As Int32,
<[In], Out, [Optional]> ByRef lpOverlapped As System.Threading.NativeOverlapped
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function WriteFile(
<[In]> hFile As SafeHandle,
<[In]> lpBuffer As IntPtr,
<[In]> nNumberOfBytesToWrite As Int32,
<[Out], [Optional]> ByRef lpNumberOfBytesWritten As Int32,
<[In], [Out], [Optional]> ByRef lpOverlapped As NativeOverlapped
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function CloseHandle(hHandle As SafeHandle) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function CloseHandle(hHandle As IntPtr) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function GetOverlappedResult(
ByVal hFile As SafeHandle,
ByRef lpOverlapped As System.Threading.NativeOverlapped,
ByRef lpNumberOfBytesTransferred As Int32,
ByVal bWait As Boolean
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function CancelIo(
<[In]> hFile As SafeHandle
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function PeekNamedPipe(
<[In]> hNamedPipe As SafeHandle,
<Out, [Optional]> ByRef lpBuffer As Byte(),
<[In]> nBufferSize As Integer,
<Out, [Optional]> ByRef lpBytesRead As Integer,
<Out, [Optional]> ByRef lpTotalBytesAvail As Integer,
<Out, [Optional]> ByRef lpBytesLeftThisMessage As Integer
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function GetProcessHeap() As IntPtr : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function HeapAlloc(
<[In]> hHeap As IntPtr,
<[In]> dwFlags As Int32,
<[In]> dwBytes As IntPtr
) As IntPtr : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function HeapFree(
<[In]> hHeap As IntPtr,
<[In]> dwFlags As Int32,
<[In]> lpMem As IntPtr
) As Boolean : End Function
<DllImport("kernel32", SetLastError:=True)> Private Shared Function CreateEvent(
<[In], [Optional]> lpEventAttributes As IntPtr,
<[In]> bManualReset As Boolean,
<[In]> bInitialState As Boolean,
<[In], [Optional]> lpName As String
) As IntPtr : End Function
This is how CreateEvent is used to create a handle managed code can wait for:
Private Shared Function AllocateEventHandle() As Microsoft.Win32.SafeHandles.SafeWaitHandle
Return New Microsoft.Win32.SafeHandles.SafeWaitHandle(CreateEvent(Nothing, False, False, Nothing), True)
End Function
Buffers to be used in calls to unmanaged code are created like this:
Private Shared Function AllocateBuffer(nBytes As Integer) As IntPtr
Return HeapAlloc(GetProcessHeap(), 0, New IntPtr(nBytes))
End Function
I don't know why it would make any difference between 32 and 64 bit, but this seems to be what was haunting me:
All over the .Net documentation, you read that this or that object type is thread-safe as long as it is static (VB shared) - so much that I silently started to believe it was true for all object types. I especially expected it to be true for things that are commonly, almost typically used in multi-threaded processing, such as event handles.
It appears that this is not always true for 64 bit code.
To recap: 32 bit builds of my project always ran as they should, AnyCpu builds on 64 bit systems did not.
For the named pipe handling (creation, connection, and I/O), I allocated win32 event handles and buffers through API calls. The event handles were wrapped in SafeWaitHandles for managed use, buffers were just being pointed to by IntPtr on the managed side.
Because these handles and buffers were created only once at application startup and never needed to be released or replaced during execution, I allocated them as soon as I could, from within the application's main startup thread. They would later be used by another thread, that may be started and stopped multiple times over the process' lifetime.
What I did now, more by way of experiment than out of wisdom, was move the creation of those handles and buffers into the thread that would use them (and release them at thread exit).
The problem went away as if it never existed, 32 and 64 bit runs are both perfectly stable now.
User contributions licensed under CC BY-SA 3.0