1

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
    );
jws
  • 2,171
  • 19
  • 30
  • Why not solve the problem you have with the SerialPort class? https://learn.microsoft.com/en-us/dotnet/api/system.io.ports.serialport – Wim ten Brink Nov 15 '19 at 14:18
  • 1
    SerialPort has several design problems. You can search the web to find out what they are. I hit several well known problems with unreliable notifications and UI thread deadlocks with a very simple use case. – jws Nov 15 '19 at 14:30

1 Answers1

1

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();
jws
  • 2,171
  • 19
  • 30
  • Do you have a working example of the code? I am working on something similar, it would really help me to have something to refer to – Pratik Gaikwad Jan 23 '20 at 23:13
  • I don't have it handy. All that's missing is code that launches this as a worker thread after opening the port. A member variable, DataReceived, is a standard event. `delegate void DataReceivedDelegate(object sender, DataReceivedArgs args); event DataReceivedDelegate DataReceived;` and DataRecievedArgs is a simple wrapper of EventArgs (`class DataReceivedArgs : EventArgs`...). WriteFile for sending was straight forward. – jws Jan 28 '20 at 21:48
  • I don't understand why `ioBuffer` is being garbage collected when it's still being reference in the third line `byte[] ioBuffer = new byte[1024];`. – Rick Mar 19 '21 at 00:12
  • Rick - referenced allocations can move around in memory by the GC when they are unpinned. – jws Mar 22 '21 at 09:54