Skip to content

Commit

Permalink
Unify int to hexadecimal char conversions (#1273)
Browse files Browse the repository at this point in the history
* Unify int to hexadecimal char conversions

This replaces about 7 different implementations with a single version
  • Loading branch information
marek-safar authored Feb 12, 2020
1 parent 6736356 commit 91c1d7c
Show file tree
Hide file tree
Showing 70 changed files with 413 additions and 457 deletions.
153 changes: 153 additions & 0 deletions src/libraries/Common/src/System/HexConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.CompilerServices;

namespace System
{
internal static class HexConverter
{
public enum Casing : uint
{
// Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
Upper = 0,

// Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
// This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ])
// already have the 0x20 bit set, so ORing them with 0x20 is a no-op,
// while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ])
// don't have the 0x20 bit set, so ORing them maps to
// [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want.
Lower = 0x2020U,
}

// We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ],
// where HHHH and LLLL are the high and low nibbles of the incoming byte. Then
// subtract this integer from a constant minuend as shown below.
//
// [ 1000 1001 1000 1001 ]
// - [ 0000 HHHH 0000 LLLL ]
// =========================
// [ *YYY **** *ZZZ **** ]
//
// The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10.
// Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10.
// (We don't care about the value of asterisked bits.)
//
// To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0').
// To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A').
// => hex := nibble + 55.
// The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 10.
// Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction.

// The commented out code below is code that directly implements the logic described above.

// uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU);
// uint difference = 0x8989U - packedOriginalValues;
// uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values
// uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */;

// The code below is equivalent to the commented out code above but has been tweaked
// to allow codegen to make some extra optimizations.

// The low byte of the packed result contains the hex representation of the incoming byte's low nibble.
// The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble.

// Finally, write to the output buffer starting with the *highest* index so that codegen can
// elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.)

// The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is
// writing to a span of known length (or the caller has already checked the bounds of the
// furthest access).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToBytesBuffer(byte value, Span<byte> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
{
uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;

buffer[startingIndex + 1] = (byte)packedResult;
buffer[startingIndex] = (byte)(packedResult >> 8);
}

#if ALLOW_PARTIALLY_TRUSTED_CALLERS
[System.Security.SecuritySafeCriticalAttribute]
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
{
uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;

buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
buffer[startingIndex] = (char)(packedResult >> 8);
}

#if ALLOW_PARTIALLY_TRUSTED_CALLERS
[System.Security.SecuritySafeCriticalAttribute]
#endif
public static unsafe string ToString(ReadOnlySpan<byte> bytes, Casing casing = Casing.Upper)
{
#if NET45 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NETSTANDARD1_0 || NETSTANDARD1_3 || NETSTANDARD2_0
Span<char> result = stackalloc char[0];
if (bytes.Length > 16)
{
var array = new char[bytes.Length * 2];
result = array.AsSpan();
}
else
{
result = stackalloc char[bytes.Length * 2];
}

int pos = 0;
foreach (byte b in bytes)
{
ToCharsBuffer(b, result, pos, casing);
pos += 2;
}
return result.ToString();
#else
fixed (byte* bytesPtr = bytes)
{
return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length, casing), (chars, args) =>
{
var ros = new ReadOnlySpan<byte>((byte*)args.Ptr, args.Length);
for (int pos = 0; pos < ros.Length; ++pos)
{
ToCharsBuffer(ros[pos], chars, pos * 2, args.casing);
}
});
}
#endif
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static char ToCharUpper(int value)
{
value &= 0xF;
value += '0';

if (value > '9')
{
value += ('A' - ('9' + 1));
}

return (char)value;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static char ToCharLower(int value)
{
value &= 0xF;
value += '0';

if (value > '9')
{
value += ('a' - ('9' + 1));
}

return (char)value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
<None Include="DiagnosticSourceUsersGuide.md" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.1'">
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
<Compile Include="System\Diagnostics\Activity.cs" />
<Compile Include="System\Diagnostics\DiagnosticSourceActivity.cs" />
<Reference Include="System.Memory" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public string? Id
{
// Convert flags to binary.
Span<char> flagsChars = stackalloc char[2];
ActivityTraceId.ByteToHexDigits(flagsChars, (byte)((~ActivityTraceFlagsIsSet) & _w3CIdFlags));
HexConverter.ToCharsBuffer((byte)((~ActivityTraceFlagsIsSet) & _w3CIdFlags), flagsChars, 0, HexConverter.Casing.Lower);
string id = "00-" + _traceId + "-" + _spanId + "-" + flagsChars.ToString();

Interlocked.CompareExchange(ref _id, id, null);
Expand Down Expand Up @@ -1018,7 +1018,7 @@ public static ActivityTraceId CreateFromBytes(ReadOnlySpan<byte> idData)
if (idData.Length != 16)
throw new ArgumentOutOfRangeException(nameof(idData));

return new ActivityTraceId(SpanToHexString(idData));
return new ActivityTraceId(HexConverter.ToString(idData, HexConverter.Casing.Lower));
}
public static ActivityTraceId CreateFromUtf8String(ReadOnlySpan<byte> idData) => new ActivityTraceId(idData);

Expand Down Expand Up @@ -1097,7 +1097,7 @@ private ActivityTraceId(ReadOnlySpan<byte> idData)
span[1] = BinaryPrimitives.ReverseEndianness(span[1]);
}

_hexString = ActivityTraceId.SpanToHexString(MemoryMarshal.AsBytes(span));
_hexString = HexConverter.ToString(MemoryMarshal.AsBytes(span), HexConverter.Casing.Lower);
}

/// <summary>
Expand All @@ -1120,26 +1120,6 @@ internal static unsafe void SetToRandomBytes(Span<byte> outBytes)
guidBytes.Slice(0, outBytes.Length).CopyTo(outBytes);
}

// CONVERSION binary spans to hex spans, and hex spans to binary spans
/* It would be nice to use generic Hex number conversion routines, but there
* is nothing that is exposed publicly and efficient */
/// <summary>
/// Converts each byte in 'bytes' to hex (thus two characters) and concatenates them
/// and returns the resulting string.
/// </summary>
internal static string SpanToHexString(ReadOnlySpan<byte> bytes)
{
Debug.Assert(bytes.Length <= 16); // We want it to not be very big
Span<char> result = stackalloc char[bytes.Length * 2];
int pos = 0;
foreach (byte b in bytes)
{
result[pos++] = BinaryToHexDigit(b >> 4);
result[pos++] = BinaryToHexDigit(b);
}
return result.ToString();
}

/// <summary>
/// Converts 'idData' which is assumed to be HEX Unicode characters to binary
/// puts it in 'outBytes'
Expand All @@ -1162,20 +1142,6 @@ private static byte HexDigitToBinary(char c)
return (byte)(c - ('a' - 10));
throw new ArgumentOutOfRangeException("idData");
}
private static char BinaryToHexDigit(int val)
{
val &= 0xF;
if (val <= 9)
return (char)('0' + val);
return (char)(('a' - 10) + val);
}

internal static void ByteToHexDigits(Span<char> outChars, byte val)
{
Debug.Assert(outChars.Length == 2);
outChars[0] = BinaryToHexDigit((val >> 4) & 0xF);
outChars[1] = BinaryToHexDigit(val & 0xF);
}

internal static bool IsLowerCaseHexAndNotAllZeros(ReadOnlySpan<char> idData)
{
Expand Down Expand Up @@ -1232,14 +1198,14 @@ public static unsafe ActivitySpanId CreateRandom()
{
ulong id;
ActivityTraceId.SetToRandomBytes(new Span<byte>(&id, sizeof(ulong)));
return new ActivitySpanId(ActivityTraceId.SpanToHexString(new ReadOnlySpan<byte>(&id, sizeof(ulong))));
return new ActivitySpanId(HexConverter.ToString(new ReadOnlySpan<byte>(&id, sizeof(ulong)), HexConverter.Casing.Lower));
}
public static ActivitySpanId CreateFromBytes(ReadOnlySpan<byte> idData)
{
if (idData.Length != 8)
throw new ArgumentOutOfRangeException(nameof(idData));

return new ActivitySpanId(ActivityTraceId.SpanToHexString(idData));
return new ActivitySpanId(HexConverter.ToString(idData, HexConverter.Casing.Lower));
}
public static ActivitySpanId CreateFromUtf8String(ReadOnlySpan<byte> idData) => new ActivitySpanId(idData);

Expand Down Expand Up @@ -1307,7 +1273,7 @@ private unsafe ActivitySpanId(ReadOnlySpan<byte> idData)
id = BinaryPrimitives.ReverseEndianness(id);
}

_hexString = ActivityTraceId.SpanToHexString(new ReadOnlySpan<byte>(&id, sizeof(ulong)));
_hexString = HexConverter.ToString(new ReadOnlySpan<byte>(&id, sizeof(ulong)), HexConverter.Casing.Lower);
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@
<Compile Include="$(CommonPath)System\Text\SimpleRegex.cs">
<Link>Common\System\Text\SimpleRegex.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs">
<Link>Common\System\Net\ArrayBuffer.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ internal static class HeaderUtilities
// Validator
internal static readonly Action<HttpHeaderValueCollection<string>, string> TokenValidator = ValidateToken;

private static readonly char[] s_hexUpperChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

internal static void SetQuality(ObjectCollection<NameValueHeaderValue> parameters, double? value)
{
Debug.Assert(parameters != null);
Expand Down Expand Up @@ -125,8 +123,8 @@ private static void AddHexEscaped(byte c, StringBuilder destination)
Debug.Assert(destination != null);

destination.Append('%');
destination.Append(s_hexUpperChars[(c & 0xf0) >> 4]);
destination.Append(s_hexUpperChars[c & 0xf]);
destination.Append(HexConverter.ToCharUpper(c >> 4));
destination.Append(HexConverter.ToCharUpper(c));
}

internal static double? GetQuality(ObjectCollection<NameValueHeaderValue> parameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<DefaultReferenceExclusions Include="System.Net.Mail" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\NotImplemented.cs">
<Link>ProductionCode\Common\System\NotImplemented.cs</Link>
</Compile>
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Mail/src/System.Net.Mail.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@
<Compile Include="$(CommonPath)System\Net\Security\NetEventSource.Security.cs">
<Link>Common\System\Net\Security\NetEventSource.Security.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
</ItemGroup>
<!-- Unix specific files -->
<ItemGroup Condition="'$(TargetsUnix)'=='true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ internal class QEncodedStream : DelegatedStream, IEncodableStream
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // F
};

//bytes that correspond to the hex char representations in ASCII (0-9, A-F)
private static readonly byte[] s_hexEncodeMap = new byte[] { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };

private ReadStateInfo _readState;
private readonly WriteStateInfoBase _writeState;

Expand Down Expand Up @@ -248,9 +245,9 @@ public int EncodeBytes(byte[] buffer, int offset, int count)
//append an = to indicate an encoded character
WriteState.Append((byte)'=');
//shift 4 to get the first four bytes only and look up the hex digit
WriteState.Append(s_hexEncodeMap[buffer[cur] >> 4]);
WriteState.Append((byte)HexConverter.ToCharUpper(buffer[cur] >> 4));
//clear the first four bytes to get the last four and look up the hex digit
WriteState.Append(s_hexEncodeMap[buffer[cur] & 0xF]);
WriteState.Append((byte)HexConverter.ToCharUpper(buffer[cur]));
}
}
WriteState.AppendFooter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@
<Compile Include="$(CommonPath)System\Net\Security\NetEventSource.Security.cs">
<Link>Common\System\Net\Security\NetEventSource.Security.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
</ItemGroup>
<!-- Unix specific files -->
<ItemGroup Condition="'$(TargetsUnix)'=='true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
<Compile Include="$(CommonPath)System\Net\NetworkInformation\NetworkInformationException.cs">
<Link>Common\System\Net\NetworkInformation\NetworkInformationException.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<!-- Logging -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,7 @@ public override bool Equals(object comparand)

public override string ToString()
{
return string.Create(_address.Length * 2, _address, (span, addr) =>
{
int p = 0;
foreach (byte value in addr)
{
byte upper = (byte)(value >> 4), lower = (byte)(value & 0xF);
span[p++] = (char)(upper + (upper < 10 ? '0' : 'A' - 10));
span[p++] = (char)(lower + (lower < 10 ? '0' : 'A' - 10));
}
});
return HexConverter.ToString(_address.AsSpan(), HexConverter.Casing.Upper);
}

public byte[] GetAddressBytes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ public void TryParseSpan_Invalid_ReturnsFalse(string address)
}

[Fact]
public void ToString_NullAddress_NullReferenceException()
public void ToString_NullAddress_EmptyString()
{
var pa = new PhysicalAddress(null);
Assert.Throws<NullReferenceException>(() => pa.ToString());
Assert.Equal("", pa.ToString());
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
<Compile Include="$(CommonPath)System\Runtime\CompilerServices\PreserveDependencyAttribute.cs">
<Link>Common\System\Runtime\CompilerServices\PreserveDependencyAttribute.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\HexConverter.cs">
<Link>Common\System\HexConverter.cs</Link>
</Compile>
<!-- Logging -->
<Compile Include="$(CommonPath)System\Net\Logging\NetEventSource.Common.cs">
<Link>Common\System\Net\Logging\NetEventSource.Common.cs</Link>
Expand Down
Loading

0 comments on commit 91c1d7c

Please sign in to comment.