jamjury's blog

By jamjury, history, 12 months ago, In English

In my last blog post I wrote about mapping stdin into memory. This time we're going to be mapping stdout.

Firstly, we need to get a handle to stdout. That's easy enough with Win32 API GetStdHandle:

HANDLE Stdout = GetStdHandle(STD_OUTPUT_HANDLE);

Alternatively we could directly use NT API NtCurrentTeb, which GetStdHandle uses under the hood, but it's a little more lengthy:

HANDLE Stdout = NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->StdOutputHandle;

Next we need to map our file into memory. Windows (or any other platform) doesn't support write-only pages, so we have to map into read-write pages. Let's try to follow what we did for stdin (CreateFileMapping):

HANDLE FileMapping = CreateFileMapping(
    Stdout,          // [in] File
    NULL,            // [in] FileMappingAttributes [optional]
    PAGE_READWRITE,  // [in] Protect
    0,    /* one  */ // [in] MaximumSizeHigh
    4096, /* page */ // [in] MaximumSizeLow
    NULL             // [in] Name [optional]
);

Unfortunately, we get Access is denied, because stdout is not readable. What can we do about it?.. We can reopen the file with read access!

First, let's get the name of the output file. We can do it with Win32 API GetFinalPathNameByHandleW. We can't get VOLUME_NAME_DOS or VOLUME_NAME_GUID, so let's use VOLUME_NAME_NT:

WCHAR FileName[MAX_PATH];
DWORD ret = GetFinalPathNameByHandleW(Stdout, FileName, MAX_PATH, VOLUME_NAME_NT);

Again we could instead directly call NT API that GetFinalPathNameByHandleW uses under the hood — NtQueryObject:

struct { UNICODE_STRING Name; WCHAR Buffer[MAX_PATH]; } FileNameBuffer;
NtQueryObject(Stdout, ObjectNameInformation, &FileNameBuffer, sizeof(FileNameBuffer), NULL);

As a result we get something like this: \Device\ImDisk1\54345f194c63c344b90cbfcd5944cfd3\run-a6c9a90c30e45e273a5ca250be6b2efb\run\output.fd0138e687.txt

This means that CodeForces uses ImDisk and creates with it ramdisk to store output files to speed up writing. Nice!

Now we need to open this file. Win32 API function for this is CreateFile, which, despite the name, also opens files:

HANDLE File = CreateFileW(
    FileName,                           // [in] lpFileName
    GENERIC_READ | GENERIC_WRITE,       // [in] dwDesiredAccess
    FILE_SHARE_READ | FILE_SHARE_WRITE, // [in] dwShareMode
    NULL,                               // [in] lpSecurityAttribute [optional]
    OPEN_EXISTING,                      // [in] dwCreationDisposition
    0,                                  // [in] dwFlagsAndAttributes
    NULL                                // [in] hTemplateFile [optional]
);

But if we do it just like that, we get: The system cannot find the path specified. What we have to do instead is replace "\Device" in the beginning of FileName with "\\?" (in C string you'd use escapes: "\\\\?"). Alternatively we can use the underlying NT API NtOpenFile, which accepts NT object names directly:

HANDLE File;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK IoStatusBlock;

InitializeObjectAttributes(&ObjectAttributes, &FileNameBuffer.Name, 0, NULL, NULL);
NtOpenFile(
   &File,                              // [out] FileHandle
   FILE_READ_DATA | FILE_WRITE_DATA,   // [in]  DesiredAccess
   &ObjectAttributes,                  // [in]  ObjectAttributes
   &IoStatusBlock,                     // [out] IoStatusBlock
   FILE_SHARE_READ | FILE_SHARE_WRITE, // [in]  ShareAccess
   0                                   // [in]  OpenOptions
);

If we open file without FILE_SHARE_READ | FILE_SHARE_WRITE we get STATUS_SHARING_VIOLATION.

Now that we've opened the file for both reading and writing we can map it and write to it without any issues.

const char* msg = "Hello from memory-mapped stdout\n";
size_t len = strlen(msg);
HANDLE map = CreateFileMapping(File, NULL, PAGE_READWRITE, 0, len, NULL);
void *view = MapViewOfFile(map, FILE_MAP_WRITE, 0, 0, 0);
memcpy(view, msg, len);

Except we get a funny bug from CodeForces runner.

Stack trace

After some digging around, we find that CodeForces runner really doesn't like string "PAGE_READWRITE" present anywhere in the code. So we just substitute this macro by hand with value 4.

A-a-and we finally get...

Hello from memory-mapped stdout

That's it!

Next time we'll use some other NT APIs to not just map statically-sized output file, but also extend it while it's mapped — for this we'll need MEM_RESERVE, which is only present in Windows. We'll also pack it all into a neat class, like we did last time with input. See you there!

P.S.
  • Vote: I like it
  • +4
  • Vote: I do not like it