1

I'm currently writing a small utility library to help improve performance when writing to the console, and I'm facing an issue where it fails to actually output any text. Below is my code:

public static class QuickDraw
{
    private static short Width => (short)Console.WindowWidth;
    private static short Height => (short)Console.WindowHeight;

    private static SafeFileHandle Handle =>
        Kernel32.CreateFile("$CONOUT", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

    private static Kernel32.Coord Cursor =>
        new Kernel32.Coord((short)Console.CursorLeft, (short)Console.CursorTop);

    private static Kernel32.SmallRect WriteRegion =>
        new Kernel32.SmallRect() { Left = 0, Top = 0, Right = Width, Bottom = Height };
    private static Kernel32.Coord BufferSize =>
        new Kernel32.Coord(Width, Height);
    private static Kernel32.Coord BufferCoord =>
        new Kernel32.Coord(0, 0);

    public static void Write(char[] text, ConsoleColor fg, ConsoleColor bg)
    {
        Kernel32.CharInfo[] buffer = new Kernel32.CharInfo[Width * Height];
        Kernel32.Coord cursor = Cursor;

        for (int i = 0; i < text.Length; i++)
        {
            if (text[i] == '\n')
            {
                cursor.X = 0;
                cursor.Y++;
            }
            else
            {
                int index = (cursor.Y * Width) + cursor.X;

                // Set character
                buffer[index].Char.AsciiChar = (byte)text[i];

                // Set color
                // (Crazy heckin bitwise crap, don't touch.)
                buffer[index].Attributes = (short)((int)fg | ((int)bg | (2 << 4)));

                // Increment cursor
                cursor.X++;
            }

            // Make sure that cursor does not exceed bounds of window
            if (cursor.X >= Width)
            {
                cursor.X = 0;
                cursor.Y++;
            }
            if (cursor.Y >= Height)
            {
                cursor.Y = 0;
            }
        }

        var writeRegion = WriteRegion;
        Kernel32.WriteConsoleOutput(Handle, buffer, BufferSize, BufferCoord, ref writeRegion);

        Console.SetCursorPosition(cursor.X, cursor.Y);
    }
}

// Taken from https://stackoverflow.com/a/2754674/7937949
internal static class Kernel32
{
    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern SafeFileHandle CreateFile(
        string fileName,
        [MarshalAs(UnmanagedType.U4)] uint fileAccess,
        [MarshalAs(UnmanagedType.U4)] uint fileShare,
        IntPtr securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        [MarshalAs(UnmanagedType.U4)] int flags,
        IntPtr template);

    [DllImport("Kernel32.dll", SetLastError = true)]
    public static extern bool WriteConsoleOutput(
        SafeFileHandle hConsoleOutput,
        CharInfo[] lpBuffer,
        Coord dwBufferSize,
        Coord dwBufferCoord,
        ref SmallRect lpWriteRegion);

    [StructLayout(LayoutKind.Sequential)]
    public struct Coord
    {
        public short X;
        public short Y;

        public Coord(short x, short y)
        {
            X = x;
            Y = y;
        }
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct CharUnion
    {
        [FieldOffset(0)] public char UnicodeChar;
        [FieldOffset(0)] public byte AsciiChar;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct CharInfo
    {
        [FieldOffset(2)] public CharUnion Char;
        [FieldOffset(2)] public short Attributes;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SmallRect
    {
        public short Left;
        public short Top;
        public short Right;
        public short Bottom;
    }
}

I've mostly taken the implementation from this post, adding a new class to simplify its use. I'm not entirely sure where my issue is, as my debugger is not working right, but I'm pretty sure it's in my implementation in QuickDraw.

When I try to use QuickDraw.Write(), the cursor moves to the end of whatever string it was trying to print, but nothing actually shows up. What am I doing wrong?

  • Did you try to first set the Attributes and then the AsciiChar? – slow Apr 17 '18 at 14:41
  • By far the most important detail to take care of with pinvoke is to ensure that you can always diagnose failure. You don't have the friendly framework anymore to throw exceptions for you, you have to do this yourself now. These functions have return values that tell you that they failed, but you don't check them. So no way to discover that WriteConsoleOutput() failed because the Handle value is junk. Consider GetStdHandle(). And consider that this code isn't going to help you, console output is not slow because there is a bug in the Console class. – Hans Passant Apr 17 '18 at 14:50
  • @HansPassant After getting this to work, my program writes to the console much faster. The Console class has quite a bit of overhead when calling `Write()` or `WriteLine()`, and this was my attempt to solve that issue. –  Apr 17 '18 at 16:21

1 Answers1

1

First of all, you didn't copy the code right. There are some mistakes.

  1. in CharInfo change

    [FieldOffset(2)] public CharUnion Char;

    to

    [FieldOffset(0)] public CharUnion Char;

  2. Change $CONOUT to this CONOUT$

  3. First set the Attribute and then the AsciiChar

And of course, if you want the correct foreground-/background color representation then you have to delete the 2 << 4. I don't even know why you put it there.

slow
  • 566
  • 4
  • 17
  • The `2 << 4` was included in a comment on the original post, I thought it might help :P –  Apr 17 '18 at 15:49
  • I've switched to using `GetStdHandle()`, and this now works. However, the background and foreground do not change with what I input. –  Apr 17 '18 at 15:53
  • I finally figured it out! The example I saw was using `2` as an example, and I should've interpreted it as `(int)ConsoleColor.Background << 4`. Thanks! –  Apr 17 '18 at 21:31