Search code examples
c++windowswin32-processoverlapped-io

Why is WriteFile not advancing the file offset for subsequent writes in this code?


I created an overlapped WriteFile scheme, where one thread fills a ring-buffer and advances the head-pointer and calls WriteFile, and another thread watches events from OVERLAPPED writes to advance the tail pointer. The logic is behaving as expected, but the file size is not increasing, it remains the same and just over-writes from position 0. I tested this by writing incremental value into the memory written to the file, and the index data increases, but it keeps writing at what is effectively fseek(0).

Here is the code.

First I open the file as GENERIC_WRITE and FILE_FLAG_OVERLAPPED, and I create 8 events, one for each page I am going to write. Each page is 256KBytes. I was originally using just one event, but I wanted to verify that the # of events wasn't the problem.

void
FileWriter::open(string fn)
{
    cout << "Opening file " << fn << endl;
    m_file_handle = CreateFileA(
        fn.c_str(),
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_FLAG_OVERLAPPED,
        NULL);
    if (m_file_handle == INVALID_HANDLE_VALUE)
    {
        throw runtime_error("Unable to create FileWriter file handle");
    }
    for (size_t i = 0; i < MAX_OVERLAPPED_WRITES; ++i)
    {
        m_events[i] = CreateEvent(NULL, TRUE, FALSE, NULL);
    }
}

Now in another thread I call this queue_page function whenever a buffer fills (8 x 256K pages). m_ov is an array of 8 OVERLAPPED structures. Prior to this call, the m_pages[][] acts as a ring buffer, and the m_pages[m_head][] is advanced, sending down the m_pages[old page][] to write to disk.

void
FileWriter::queue_page(unsigned page, unsigned len)
{
    ZeroMemory(&m_ov[page], sizeof(OVERLAPPED));
    m_ov[page].hEvent = m_events[page];
    if (!WriteFile(
        m_file_handle,
        (LPCVOID)m_pages[page],
        len * sizeof(float),
        NULL,
        (LPOVERLAPPED)&m_ov[page]))
    {
        DWORD err = GetLastError();
        if (err != ERROR_IO_PENDING)
        {
            cout << "GetLastError() = " << err << endl;
            throw runtime_error("Failed to write overlapped");
        }
    }
}

The thread that follows the async writes and moves the circular-buffer tail pointer is simple:

void
FileWriter::wait(DWORD msec)
{
    DWORD ret = WaitForMultipleObjects(MAX_OVERLAPPED_WRITES, m_events, FALSE, msec);
    if (ret < MAXIMUM_WAIT_OBJECTS)
    {
        unsigned page =  ret - WAIT_OBJECT_0;
        if (page < MAX_OVERLAPPED_WRITES) {
            ResetEvent(m_events[page]);
        }
        cout << "Page write completed " << ret << " page=" << page << endl;
        m_tail = (m_tail + 1) & 0x7;
    }
}

Upon inspection the ring buffer is working fine and not overflowing, but the output file is always 256KBytes.

Not sure how to debug what is going on, or what I missed.


Solution

  • Answer from @RbMm, byte-access files require the caller to set Offset/OffsetHigh in the overlapped structure, according to this: https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-overlapped

    Fix looks like this:

    void
    FileWriter::queue_page(unsigned page, unsigned len)
    {
        ZeroMemory(&m_ov[page], sizeof(OVERLAPPED));
        m_ov[page].hEvent = m_events[page];
        m_ov[page].OffsetHigh = (m_file_offset >> 32) & 0xFFFF'FFFF;
        m_ov[page].Offset = m_file_offset & 0xFFFF'FFFF;
        if (!WriteFile(
            m_file_handle,
            (LPCVOID)m_pages[page],
            len * sizeof(float),
            NULL,
            (LPOVERLAPPED)&m_ov[page]))
        {
            DWORD err = GetLastError();
            if (err != ERROR_IO_PENDING)
            {
                cout << "GetLastError() = " << err << endl;
                throw runtime_error("Failed to write overlapped");
            }
        }
        m_file_offset += len * sizeof(float);
    }