jws
jws

Reputation: 2774

Why is C# PInvoke with serial port overlapped I/O sometimes giving me all zeros

I am making a simple RS-232 test app in C#. After having problems with .NET SerialPort, I decided to call Win32 APIs directly.

I am using overlapped I/O. It is working 70% of the time, and the other time the inbound data isn't written to the buffer.

My test app opens the COM port this way:

_handle = CreateFile(
    @"\\.\COM1",
    FileAccess.ReadWrite,
    FileShare.None,
    IntPtr.Zero,
    FileMode.Open,
    EFileAttributes.Overlapped,
    IntPtr.Zero
    );

Note the overlapped flag.

Then the app sets COM parameters. GetCommState, set baud, 0 stop bit, no parity, disable all flow control and RTS and DTR (all of this except baud is default anyway), then SetCommState.

Next the app sets timeout via SetCommTimeouts, and ReadIntervalTimeout is 1ms, all others are zero.

And then the app cycles DTR via EscapeCommFunction to CLRDTR, and a 200ms sleep, and EscapeCommFunction to SETDTR.

All of this sets up the COM port well. When connected to a shell, I/O works perfectly, because messages are short. When messages have any significant length, say 30 bytes at once, I am encountering problems with overlapped I/O response on ReadFile.

My rx code is below.

Problem The data is coming in properly, the overlapped operation completes as expected, the length from the overlapped operation is always correct, but ioBuffer 30% of the time isn't filled and remains all zeros.

Seems to be pinvoke. While I had problems using .NET SerialPort, losing data like this wasn't an issue.

Anyone spot something wrong?

DataReceivedArgs args = new DataReceivedArgs();
ManualResetEvent completionEvent = new ManualResetEvent(false);
NativeOverlapped nol = new NativeOverlapped();
nol.EventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle();
int dontCare = 0;

try
{
    for (; ; )
    {
        completionEvent.Reset();
        uint bytesRead = 0;

        nol.InternalHigh = IntPtr.Zero;
        nol.InternalLow = IntPtr.Zero;
        nol.OffsetHigh = 0;
        nol.OffsetLow = 0;

        byte[] ioBuffer = new byte[1024];

        if (!ReadFile(_handle.DangerousGetHandle(), ioBuffer, ioBuffer.Length, out dontCare, ref nol))
        {
            int lastError = Marshal.GetLastWin32Error();
            if (lastError != OperationInProgress)
            {
                if (lastError != ErrorInvalidHandle)
                {
                    Win32Exception ex = new Win32Exception(lastError);
                    MessageBox.Show("ReadFile failed. Error: " + ex.Message);
                }
                break;
            }

            completionEvent.WaitOne();
            // Have tried sleeping here to see if there is timing involved, no luck

            if (!GetOverlappedResult(_handle.DangerousGetHandle(), ref nol, out bytesRead, true))
            {
                bytesRead = 0;
            }
        }
        else
        {
            throw new IOException();
        }

        if (bytesRead > 0)
        {
            byte[] sizedBuffer = new byte[bytesRead];
            Array.Copy(ioBuffer, 0, sizedBuffer, 0, bytesRead);
            args.Data = sizedBuffer;
            DataReceived?.Invoke(this, args);
        }
    }
}
catch (ThreadAbortException)
{
}

And pinvoke signature

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadFile(
    IntPtr hFile, 
    [Out] byte[] lpBuffer, 
    int nNumberOfBytesToRead, 
    [Out] out int lpNumberOfBytesRead,
    ref NativeOverlapped lpOverlapped
    );

Upvotes: 1

Views: 1072

Answers (1)

jws
jws

Reputation: 2774

The outbound data of ReadFile is possibly getting moved by the GC.

PInvoke requires OUT parameters used by overlapped I/O to be pinned.

Fixed code:

DataReceivedArgs args = new DataReceivedArgs();
ManualResetEvent completionEvent = new ManualResetEvent(false);
byte[] ioBuffer = new byte[1024];
NativeOverlapped nol = new NativeOverlapped
{
    EventHandle = completionEvent.SafeWaitHandle.DangerousGetHandle()
};

try
{
    for (; ; )
    {
        completionEvent.Reset();
        uint bytesRead = 0;

        GCHandle pin = GCHandle.Alloc(ioBuffer, GCHandleType.Pinned);

        try
        {
            if (!ReadFile(_handle.DangerousGetHandle(), ioBuffer, ioBuffer.Length, out int dontCare, ref nol))
            {
                int lastError = Marshal.GetLastWin32Error();
                if (lastError != OperationInProgress)
                {
                    if (lastError != ErrorInvalidHandle)
                    {
                        Win32Exception ex = new Win32Exception(lastError);
                        MessageBox.Show("ReadFile failed. Error: " + ex.Message);
                    }
                    break;
                }

                completionEvent.WaitOne();

                if (!GetOverlappedResult(_handle.DangerousGetHandle(), ref nol, out bytesRead, true))
                {
                    bytesRead = 0;
                }
            }
            else
            {
                throw new IOException();
            }

            if (bytesRead > 0)
            {
                byte[] sizedBuffer = new byte[bytesRead];
                Array.Copy(ioBuffer, 0, sizedBuffer, 0, bytesRead);
                args.Data = sizedBuffer;
                DataReceived?.Invoke(this, args);
            }

        }
        finally
        {
            pin.Free();
        }
    }
}
catch (ThreadAbortException)
{
}

completionEvent.Dispose();

Upvotes: 1

Related Questions