Refactor ReadConsole P/Invoke in ConsoleHost (#9165)

Replace `StringBuilder` with stack allocated `Span<char>` to avoid extra allocation/copies during P/Invoke
This commit is contained in:
Ilya 2019-03-22 21:51:13 +05:00 committed by Dongbo Wang
parent 43b84519a7
commit bc95a8e088
2 changed files with 66 additions and 25 deletions

View file

@ -716,35 +716,48 @@ namespace Microsoft.PowerShell
/// </summary>
/// <param name="consoleHandle"></param>
/// Handle to the console device returned by GetInputHandle
/// <param name="initialContent">
/// Initial contents of the edit buffer, if any. charactersToRead should be at least as large as the length of this string.
/// <param name="initialContentLength">
/// Length of initial content of the edit buffer. Zero if no initial content exists.
/// Must be less than editBuffer length.
/// </param>
/// <param name="editBuffer">
/// Edit buffer with optional initial content.
/// Caution! Last position in the edit buffer is for a null in native code.
/// </param>
/// <param name="charactersToRead">
/// Number of characters to read from the device.
/// Must be less than editBuffer length.
/// </param>
/// <param name="endOnTab">
/// true to allow the user to terminate input by hitting the tab or shift-tab key, in addition to the enter key
/// True to allow the user to terminate input by hitting the tab or shift-tab key, in addition to the enter key
/// </param>
/// <param name="keyState">
/// bit mask indicating the state of the control/shift keys at the point input was terminated.
/// Bit mask indicating the state of the control/shift keys at the point input was terminated.
/// </param>
/// </param>
/// <returns></returns>
/// <exception cref="HostException">
/// If Win32's ReadConsole fails
/// </exception>
internal static string ReadConsole(ConsoleHandle consoleHandle, string initialContent,
int charactersToRead, bool endOnTab, out uint keyState)
internal static string ReadConsole(
ConsoleHandle consoleHandle,
int initialContentLength,
Span<char> editBuffer,
int charactersToRead,
bool endOnTab,
out uint keyState)
{
Dbg.Assert(!consoleHandle.IsInvalid, "ConsoleHandle is not valid");
Dbg.Assert(!consoleHandle.IsClosed, "ConsoleHandle is closed");
Dbg.Assert(initialContent != null, "if no initial content is desired, pass string.Empty");
Dbg.Assert(initialContentLength < editBuffer.Length, "initialContentLength must be less than editBuffer.Length");
Dbg.Assert(charactersToRead < editBuffer.Length, "charactersToRead must be less than editBuffer.Length");
keyState = 0;
CONSOLE_READCONSOLE_CONTROL control = new CONSOLE_READCONSOLE_CONTROL();
control.nLength = (ULONG)Marshal.SizeOf(control);
control.nInitialChars = (ULONG)initialContent.Length;
control.nInitialChars = (ULONG)initialContentLength;
control.dwControlKeyState = 0;
if (endOnTab)
{
@ -753,28 +766,34 @@ namespace Microsoft.PowerShell
control.dwCtrlWakeupMask = (1 << TAB);
}
DWORD charsReadUnused = 0;
StringBuilder buffer = new StringBuilder(initialContent, charactersToRead);
DWORD charsReaded = 0;
bool result =
NativeMethods.ReadConsole(
consoleHandle.DangerousGetHandle(),
buffer,
editBuffer,
(DWORD)charactersToRead,
out charsReadUnused,
out charsReaded,
ref control);
keyState = control.dwControlKeyState;
if (result == false)
{
int err = Marshal.GetLastWin32Error();
HostException e = CreateHostException(err, "ReadConsole",
ErrorCategory.ReadError, ConsoleControlStrings.ReadConsoleExceptionTemplate);
HostException e = CreateHostException(
err,
"ReadConsole",
ErrorCategory.ReadError,
ConsoleControlStrings.ReadConsoleExceptionTemplate);
throw e;
}
if (charsReadUnused > (uint)buffer.Length)
charsReadUnused = (uint)buffer.Length;
return buffer.ToString(0, (int)charsReadUnused);
if (charsReaded > (uint)charactersToRead)
{
charsReaded = (uint)charactersToRead;
}
return editBuffer.Slice(0, (int)charsReaded).ToString();
}
/// <summary>
@ -3019,15 +3038,30 @@ namespace Microsoft.PowerShell
[DllImport(PinvokeDllNames.ReadConsoleDllName, SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ReadConsole
private static extern unsafe bool ReadConsole
(
NakedWin32Handle consoleInput,
StringBuilder buffer,
char* lpBuffer,
DWORD numberOfCharsToRead,
out DWORD numberOfCharsRead,
ref CONSOLE_READCONSOLE_CONTROL controlData
);
internal static unsafe bool ReadConsole
(
NakedWin32Handle consoleInput,
Span<char> buffer,
DWORD numberOfCharsToRead,
out DWORD numberOfCharsRead,
ref CONSOLE_READCONSOLE_CONTROL controlData
)
{
fixed (char* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
return ReadConsole(consoleInput, bufferPtr, numberOfCharsToRead, out numberOfCharsRead, ref controlData);
}
}
[DllImport(PinvokeDllNames.PeekConsoleInputDllName, SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PeekConsoleInput

View file

@ -307,8 +307,9 @@ namespace Microsoft.PowerShell
#if UNIX
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
#else
uint unused = 0;
string key = ConsoleControl.ReadConsole(handle, string.Empty, 1, false, out unused);
const int CharactersToRead = 1;
Span<char> inputBuffer = stackalloc char[CharactersToRead + 1];
string key = ConsoleControl.ReadConsole(handle, initialContentLength: 0, inputBuffer, charactersToRead: CharactersToRead, endOnTab: false, out _);
#endif
#if UNIX
@ -1287,7 +1288,7 @@ namespace Microsoft.PowerShell
endedOnBreak = 3
}
private const int maxInputLineLength = 8192;
private const int MaxInputLineLength = 1024;
/// <summary>
/// Reads a line of input from the console. Returns when the user hits enter, a break key, a break event occurs. In
@ -1468,13 +1469,19 @@ namespace Microsoft.PowerShell
_rawui.ClearKeyCache();
uint keyState = 0;
string s = string.Empty;
Span<char> inputBuffer = stackalloc char[MaxInputLineLength + 1];
if (initialContent.Length > 0)
{
initialContent.AsSpan().CopyTo(inputBuffer);
}
#endif
do
{
#if UNIX
keyInfo = Console.ReadKey(true);
#else
s += ConsoleControl.ReadConsole(handle, initialContent, maxInputLineLength, endOnTab, out keyState);
s += ConsoleControl.ReadConsole(handle, initialContent.Length, inputBuffer, MaxInputLineLength, endOnTab, out keyState);
Dbg.Assert(s != null, "s should never be null");
#endif
@ -1861,9 +1868,9 @@ namespace Microsoft.PowerShell
completedInput += restOfLine;
}
if (completedInput.Length > (maxInputLineLength - 2))
if (completedInput.Length > (MaxInputLineLength - 2))
{
completedInput = completedInput.Substring(0, maxInputLineLength - 2);
completedInput = completedInput.Substring(0, MaxInputLineLength - 2);
}
// Remove any nulls from the string...