Search code examples
cwindowswinapiconsole

How to properly and safely print to stdout/stderr using the Win32 API?


You may say.

//#DEFINE UNICODE
#include <Windows.h>

int main()
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    LPTSTR msg = TEXT("Hello, World!\n");
    WriteConsole(hOut, msg, lstrlen(msg), NULL, NULL);
}

This works, as long as STDOUT is not redirected, as in main > tmp.txt. Then, WriteConsole() errors, with the format message "The handle is invalid.". Then, WriteFile() should work, so you may say.

WriteFile(hOut, msg, lstrlen(msg), NULL, NULL);

This is great, except if 1) STDOUT is not redirected, and 2) UNICODE is set. Then it would print H e l l o , , because apparently WriteFile() does not "translate" the characters, it just sends the bytes. Maybe this can be solved by changing the DOS codepage, but it would need to be UTF-16, since Windows calls UTF-16 "UNICODE". On my computer, the codepages for UTF-16 are not available, so I would not rely on it.


The solution I found is to identify if the "file type" of the handle.

if (GetFileType(hOut) == FILE_TYPE_CHAR)
    WriteConsole(hOut, msg, lstrlen(msg), NULL, NULL);
else
    WriteFile(hOut, msg, lstrlen(msg) * sizeof(TCHAR), NULL, NULL);

Still, is this the proper solution? And if possible, what is the proper way of reading STDIN, since it suffers from a similar problem?


Solution

  • console handle is file handle (begin form win7 if i not mistake) and we always can use WriteFile for write to it, redirected it or not. however question here - in which format must be data for WriteFile. in case we write to console - output is handle in conhost.exe process. and he assume that data in multibyte format and use MultiByteToWideChar function for convert data to UTF-16 (wide character) string. in place CodePage is used value returned from GetConsoleOutputCP. we also can change this value by SetConsoleOutputCP. say set SetConsoleOutputCP(CP_UTF8); and pass utf-8 data to WriteFile. in case std handle (out/err) isconsole handle we also can use WriteConsoleW function. for output just in UTF-16 format. from server side (conhost.exe), when we call WriteFile or WriteConsole[A/W] - ApiDispatchers::ServerWriteConsole called, which call ApiRoutines::WriteConsoleAImpl (in case WriteFile or WriteConsoleA) or ApiRoutines::WriteConsoleWImpl -> WriteConsoleWImplHelper in case WriteConsoleW. and ApiRoutines::WriteConsoleAImpl use MultiByteToWideChar(GetConsoleOutputCP(), ..) for convert data to UTF-16 and then call WriteConsoleWImplHelper too of course. so in case console device - bit better use WriteConsoleW for avoid double conversion - from UTF-16 to multibyte in our process (optional, we just output in multibyte with SetConsoleOutputCP) and multibyte to UTF-16 in conhost (always). so code can be next:

    static HANDLE _G_hFile = 0;
    static BOOLEAN _G_bConsole = FALSE;
    static UINT _G_CodePage;
    
    inline void PutChars(PCWSTR pwz)
    {
        PutChars(pwz, (ULONG)wcslen(pwz));
    }
    
    void PutChars(PCWSTR pwz, ULONG cch)
    {
        if (!_G_hFile)
        {
            return ;
        }
    
        if (_G_bConsole)
        {
            WriteConsoleW(_G_hFile, pwz, cch, &cch, 0);
            return ;
        }
    
        PSTR buf = 0;
        ULONG len = 0;
        while (len = WideCharToMultiByte(_G_CodePage, 0, pwz, cch, buf, len, 0, 0))
        {
            if (buf)
            {
                WriteFile(_G_hFile, buf, len, &len, 0);
    
                break;
            }
    
            if (!(buf = (PSTR)_malloca_s(len)))
            {
                break;
            }
        }
    
        if (buf)
        {
            _freea_s(buf);
        }
    }
    
    #define _malloca_s(size) ((size) < _ALLOCA_S_THRESHOLD ? alloca(size) : new BYTE[size])
    
    inline void _freea_s(PVOID pv)
    {
        PNT_TIB tib = (PNT_TIB)NtCurrentTeb();
        if (pv < tib->StackLimit || tib->StackBase <= pv) delete [] pv;
    }
    

    now question is how detect are std ouput point to console device. say if look to cmd.exeimplementation, it have next internal function

    BOOL FileIsConsole(int fd ); 
    

    fd passed to function hFile = _get_osfhandle and then used GetFileType(hFile) == FILE_TYPE_CHAR for detect are console or not: assume that console if GetFileType returned FILE_TYPE_CHAR.

    then it used in next way

      int CmdPutChars(PCWSTR psz, int cch)
      {
         FileIsConsole(1) ? WriteConsoleW(..psz, cch) : MyWriteFile(..psz, cch..);
      }
    

    implementation of GetFileType is look like:

    DWORD WINAPI GetFileType(_In_ HANDLE hFile)
    {
        switch ((ULONG_PTR)hFile)
        {
        case 0:
            RtlNtStatusToDosError(STATUS_INVALID_HANDLE);
            return FILE_TYPE_UNKNOWN;
    
        case STD_ERROR_HANDLE:
        case STD_OUTPUT_HANDLE:
        case STD_INPUT_HANDLE:
            hFile = GetStdHandle((ULONG)(ULONG_PTR)hFile);
            break;
        }
    
        IO_STATUS_BLOCK iosb;
        FILE_FS_DEVICE_INFORMATION ffdi;
        NTSTATUS status = NtQueryVolumeInformationFile(hFile, &iosb, &ffdi, sizeof(ffdi), FileFsDeviceInformation);
        if (0 > status)
        {
            RtlNtStatusToDosError(status);
            return FILE_TYPE_UNKNOWN;
        }
    
        switch (ffdi.DeviceType)
        {
        case FILE_DEVICE_KEYBOARD:
        case FILE_DEVICE_MOUSE:
        case FILE_DEVICE_NULL:
        case FILE_DEVICE_PARALLEL_PORT:
        case FILE_DEVICE_PRINTER:
        case FILE_DEVICE_SERIAL_PORT:
        case FILE_DEVICE_SCREEN:
        case FILE_DEVICE_SOUND:
        case FILE_DEVICE_MODEM:
        case FILE_DEVICE_CONSOLE:
            return FILE_TYPE_CHAR;
    
        case FILE_DEVICE_NAMED_PIPE:
            return FILE_TYPE_PIPE;
    
        case FILE_DEVICE_CD_ROM:
        case FILE_DEVICE_CD_ROM_FILE_SYSTEM:
        case FILE_DEVICE_CONTROLLER:
        case FILE_DEVICE_DATALINK:
        case FILE_DEVICE_DFS:
        case FILE_DEVICE_DISK:
        case FILE_DEVICE_DISK_FILE_SYSTEM:
        case FILE_DEVICE_VIRTUAL_DISK:
            return FILE_TYPE_DISK;
        }
    
        SetLastError(NOERROR);
        return FILE_TYPE_UNKNOWN;
    }
    

    so FILE_TYPE_CHAR returned not only for FILE_DEVICE_CONSOLE but for several other devices too (like FILE_DEVICE_NULL etc). this of course not real problem - say if our output redirected to null device and we detect it as console - WriteConsole of course retirn error, but and WriteFile also no any effect will be, despite no error. in any case for be more exactly can use next code:

    void InitPrintf()
    {
        _G_CodePage = GetConsoleOutputCP();
    
        if (_G_hFile = GetStdHandle(STD_OUTPUT_HANDLE))
        {
            FILE_FS_DEVICE_INFORMATION ffdi;
            IO_STATUS_BLOCK iosb;
            if (0 <= NtQueryVolumeInformationFile(_G_hFile, &iosb, &ffdi, sizeof(ffdi), FileFsDeviceInformation))
            {
                switch (ffdi.DeviceType)
                {
                case FILE_DEVICE_CONSOLE:
                    _G_bConsole = TRUE;
                    break;
                }
            }
        }
    }
    

    also can use next util for easy formated print

    void PutChars_v(PCWSTR format, ...)
    {
        va_list ap;
        va_start(ap, format);
    
        PWSTR buf = 0;
        int len = 0;
        while (0 < (len = _vsnwprintf(buf, len, format, ap)))
        {
            if (buf)
            {
                PutChars(buf, len);
                break;
            }
    
            ++len;
            if (!(buf = (PWSTR)_malloca_s(len * sizeof(WCHAR))))
            {
                break;
            }
        }
    
        if (buf)
        {
            _freea_s(buf);
        }
    
        va_end(ap);
    }