Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions src/Microsoft.Android.Build.BaseTasks/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,21 +595,9 @@ public static string ToHexString (byte[] hash)

public static string ToHexString (ReadOnlySpan<byte> hash)
{
const int MaxStackCharLength = 128;
int charLength = hash.Length * 2;
Span<char> chars = charLength <= MaxStackCharLength
? stackalloc char[charLength]
: new char[charLength];
for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) {
byte b = hash [i];
chars [j] = GetHexValue (b / 16);
chars [j + 1] = GetHexValue (b % 16);
}
return ((ReadOnlySpan<char>) chars).ToString ();
return HexUtilities.ToHexString (hash);
}

static char GetHexValue (int i) => (char) (i < 10 ? i + 48 : i - 10 + 65);

public static void DeleteFile (string filename, object log)
{
try {
Expand Down
80 changes: 80 additions & 0 deletions src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#nullable enable
using System;
using System.IO;

namespace Microsoft.Android.Build.Tasks
{
/// <summary>
/// Allocation-free helpers for rendering bytes as hexadecimal.
/// </summary>
/// <remarks>
/// This file is also linked into <c>Microsoft.Android.Sdk.TrimmableTypeMap</c>, which
/// deliberately does not reference <c>Microsoft.Android.Build.BaseTasks</c> (that would drag
/// Microsoft.Build.*, LibZipSharp, K4os.LZ4 and Mono.Unix into it). Only the copy compiled
/// into <c>Microsoft.Android.Build.BaseTasks</c> is <c>public</c>; the linked copy stays
/// <c>internal</c>, otherwise <c>Xamarin.Android.Build.Tasks</c> — which references both
/// assemblies — fails with <c>CS0433</c>.
/// </remarks>
#if MICROSOFT_ANDROID_BUILD_BASETASKS
public
#endif
static class HexUtilities
Comment thread
jonathanpeppers marked this conversation as resolved.
{
/// <summary>
/// Convert a value in the <c>0..15</c> range to its hexadecimal digit.
/// </summary>
public static char GetHexValue (int value, bool upperCase = true)
Comment thread
jonathanpeppers marked this conversation as resolved.
{
if (value < 10)
return (char) (value + '0');
return (char) (value - 10 + (upperCase ? 'A' : 'a'));
}

/// <summary>
/// Write <paramref name="value"/> into <paramref name="destination"/> as exactly two
/// hexadecimal digits.
/// </summary>
/// <exception cref="ArgumentException">
/// <paramref name="destination"/> is shorter than two characters.
/// </exception>
public static void WriteHex (Span<char> destination, byte value, bool upperCase = true)
{
if (destination.Length < 2)
throw new ArgumentException ("Destination must be at least 2 characters long.", nameof (destination));

destination [0] = GetHexValue (value >> 4, upperCase);
destination [1] = GetHexValue (value & 0x0f, upperCase);
}

/// <summary>
/// Write <paramref name="value"/> to <paramref name="writer"/> as exactly two hexadecimal
/// digits, without allocating.
/// </summary>
public static void WriteHex (TextWriter writer, byte value, bool upperCase = true)
{
if (writer == null)
throw new ArgumentNullException (nameof (writer));

writer.Write (GetHexValue (value >> 4, upperCase));
writer.Write (GetHexValue (value & 0x0f, upperCase));
}

/// <summary>
/// Convert <paramref name="bytes"/> to a hexadecimal string, without allocating
/// intermediate strings.
/// </summary>
public static string ToHexString (ReadOnlySpan<byte> bytes, bool upperCase = true)
{
const int MaxStackCharLength = 128;

int charLength = bytes.Length * 2;
Span<char> chars = charLength <= MaxStackCharLength
? stackalloc char [charLength]
: new char [charLength];
for (int i = 0, j = 0; i < bytes.Length; i += 1, j += 2) {
WriteHex (chars.Slice (j, 2), bytes [i], upperCase);
}
return ((ReadOnlySpan<char>) chars).ToString ();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<!-- Makes the shared HexUtilities.cs `public` here; it stays `internal` where it is linked in. -->
<DefineConstants>$(DefineConstants);MICROSOFT_ANDROID_BUILD_BASETASKS</DefineConstants>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<ItemGroup>
<Compile Include="..\..\src-ThirdParty\System.Runtime.CompilerServices\CompilerFeaturePolyfills.cs" Link="CompilerFeaturePolyfills.cs" />
<Compile Include="..\Microsoft.Android.Build.BaseTasks\HexUtilities.cs" Link="HexUtilities.cs" />
<Compile Include="..\..\external\Java.Interop\src\Java.Interop.Tools.JavaCallableWrappers\Java.Interop.Tools.JavaCallableWrappers\Crc64Helper.cs" Link="Crc64Helper.cs" />
<Compile Include="..\..\external\Java.Interop\src\Java.Interop.Tools.JavaCallableWrappers\Java.Interop.Tools.JavaCallableWrappers\Crc64.Table.cs" Link="Crc64.Table.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Buffers;
using System.Buffers.Binary;
using Java.Interop.Tools.JavaCallableWrappers;
using Microsoft.Android.Build.Tasks;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

Expand All @@ -18,7 +19,7 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName)
Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
Span<byte> hash = stackalloc byte [8];
BinaryPrimitives.WriteUInt64LittleEndian (hash, crc ^ length);
return ToHexString (hash);
return HexUtilities.ToHexString (hash, upperCase: false);
} finally {
ArrayPool<byte>.Shared.Return (rented);
}
Expand All @@ -37,7 +38,7 @@ internal static string ToCrc64 (string ns, string assemblyName)
System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash);
ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash);
BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten);
return ToHexString (hash);
return HexUtilities.ToHexString (hash, upperCase: false);
}

static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
Expand All @@ -62,23 +63,4 @@ static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName,

return bytesWritten;
}

static string ToHexString (ReadOnlySpan<byte> hash)
{
const int maxStackCharLength = 128;
int charLength = hash.Length * 2;
Span<char> chars = charLength <= maxStackCharLength
? stackalloc char [charLength]
: new char [charLength];

for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) {
byte b = hash [i];
chars [j] = GetHexValue (b / 16);
chars [j + 1] = GetHexValue (b % 16);
}

return ((ReadOnlySpan<char>) chars).ToString ();
}

static char GetHexValue (int value) => (char) (value < 10 ? value + '0' : value - 10 + 'a');
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Reflection;
using System.Text;

using Microsoft.Android.Build.Tasks;
using Xamarin.Android.Tools;

namespace Xamarin.Android.Tasks.LLVMIR
Expand Down Expand Up @@ -470,6 +471,23 @@ void WriteStructureType (GeneratorWriteContext context, StructureInstance si, ou

void WriteType (GeneratorWriteContext context, Type type, object? value, out LlvmTypeInfo typeInfo, LlvmIrGlobalVariable? globalVariable = null)
{
// Fast path: a basic scalar type (`byte`, `uint`, ...) can never be a structure instance,
// an array or a string blob, so skip the reflection-heavy probing below. Arrays and string
// blobs write one element at a time, so this runs millions of times for a typical
// application and `Type.IsPrimitive`/`Type.IsArray` dominate the generator otherwise.
if (basicTypeMap.ContainsKey (type)) {
string basicIRType = GetIRType (context, type, out ulong basicSize, out bool basicIsPointer);
typeInfo = new LlvmTypeInfo (
isPointer: basicIsPointer,
isAggregate: false,
isStructure: false,
size: basicSize,
maxFieldAlignment: basicSize
);
context.Output.Write (basicIRType);
return;
}
Comment thread
jonathanpeppers marked this conversation as resolved.

if (IsStructureInstance (type)) {
if (value == null) {
throw new ArgumentException ($"must not be null for structure instances ({type})", nameof (value));
Expand Down Expand Up @@ -947,10 +965,12 @@ void WriteCommaWithStride (ulong counter)

void WriteByteTypeAndValue (byte v)
{
WriteType (context, elementType, v, out _);

context.Output.Write (' ');
WriteValue (context, elementType, v);
// This is by far the hottest path in the generator: a hello world MAUI app writes
// ~3.6 million bytes here. WriteType()/WriteValue() would box the byte and allocate a
// handful of strings per element, so write the (always identical) type and the two hex
// digits directly.
context.Output.Write ("i8 u0x");
Comment thread
jonathanpeppers marked this conversation as resolved.
HexUtilities.WriteHex (context.Output, v, upperCase: false);
}
}

Expand Down
166 changes: 166 additions & 0 deletions tests/Microsoft.Android.Build.BaseTasks-Tests/HexUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.Android.Build.Tasks;
using NUnit.Framework;

namespace Microsoft.Android.Build.BaseTasks.Tests
{
[TestFixture]
public class HexUtilitiesTests
{
[Test]
public void GetHexValue_UpperCase ()
{
const string expected = "0123456789ABCDEF";
for (int i = 0; i < expected.Length; i++) {
Assert.AreEqual (expected [i], HexUtilities.GetHexValue (i), $"Mismatch at {i}.");
}
}

[Test]
public void GetHexValue_LowerCase ()
{
const string expected = "0123456789abcdef";
for (int i = 0; i < expected.Length; i++) {
Assert.AreEqual (expected [i], HexUtilities.GetHexValue (i, upperCase: false), $"Mismatch at {i}.");
}
}

[Test]
public void GetHexValue_DefaultsToUpperCase ()
{
Assert.AreEqual ('A', HexUtilities.GetHexValue (10));
}

[Test]
public void WriteHex_Span_UpperCase ()
{
Span<char> chars = stackalloc char [2];
HexUtilities.WriteHex (chars, 0xab);
Assert.AreEqual ("AB", chars.ToString ());
}

[Test]
public void WriteHex_Span_LowerCase ()
{
Span<char> chars = stackalloc char [2];
HexUtilities.WriteHex (chars, 0xab, upperCase: false);
Assert.AreEqual ("ab", chars.ToString ());
}

[Test]
public void WriteHex_Span_WritesExactlyTwoChars ()
{
Span<char> chars = stackalloc char [4];
chars.Fill ('_');
HexUtilities.WriteHex (chars.Slice (1, 2), 0x0f);
Assert.AreEqual ("_0F_", chars.ToString ());
}

[Test]
public void WriteHex_Span_EveryByteValue ()
{
Span<char> chars = stackalloc char [2];
for (int i = 0; i < 256; i++) {
HexUtilities.WriteHex (chars, (byte) i);
Assert.AreEqual (i.ToString ("X2"), chars.ToString (), $"Mismatch at {i}.");
}
}

[Test]
public void WriteHex_TextWriter_UpperCase ()
{
var writer = new StringWriter ();
HexUtilities.WriteHex (writer, 0x00);
HexUtilities.WriteHex (writer, 0x0f);
HexUtilities.WriteHex (writer, 0xff);
Assert.AreEqual ("000FFF", writer.ToString ());
}

[Test]
public void WriteHex_TextWriter_LowerCase ()
{
var writer = new StringWriter ();
HexUtilities.WriteHex (writer, 0x00, upperCase: false);
HexUtilities.WriteHex (writer, 0x0f, upperCase: false);
HexUtilities.WriteHex (writer, 0xff, upperCase: false);
Assert.AreEqual ("000fff", writer.ToString ());
}

[Test]
public void WriteHex_TextWriter_EveryByteValue ()
{
var writer = new StringWriter ();
for (int i = 0; i < 256; i++) {
HexUtilities.WriteHex (writer, (byte) i);
}
var expected = string.Concat (Enumerable.Range (0, 256).Select (i => i.ToString ("X2")));
Assert.AreEqual (expected, writer.ToString ());
}

[Test]
public void WriteHex_TextWriter_NullThrows ()
{
Assert.Throws<ArgumentNullException> (() => HexUtilities.WriteHex (writer: null, value: 0x00));
}

[TestCase (0)]
[TestCase (1)]
public void WriteHex_Span_TooShortThrows (int length)
{
var destination = new char [length];
Assert.Throws<ArgumentException> (() => HexUtilities.WriteHex (destination.AsSpan (), 0xAB));
}

[Test]
public void WriteHex_Span_LongerThanTwoOnlyWritesTwo ()
{
var destination = new char [] { 'x', 'x', 'x', 'x' };
HexUtilities.WriteHex (destination.AsSpan (), 0xAB);
Assert.AreEqual ("ABxx", new string (destination));
}

[Test]
public void ToHexString_Empty ()
{
Assert.AreEqual ("", HexUtilities.ToHexString (ReadOnlySpan<byte>.Empty));
}

[Test]
public void ToHexString_UpperCase ()
{
var bytes = new byte [] { 0x00, 0x01, 0x0f, 0x10, 0x7f, 0x80, 0xab, 0xff };
Assert.AreEqual ("00010F107F80ABFF", HexUtilities.ToHexString (bytes));
}

[Test]
public void ToHexString_LowerCase ()
{
var bytes = new byte [] { 0x00, 0x01, 0x0f, 0x10, 0x7f, 0x80, 0xab, 0xff };
Assert.AreEqual ("00010f107f80abff", HexUtilities.ToHexString (bytes, upperCase: false));
}

[Test]
public void ToHexString_EveryByteValue ()
{
var bytes = Enumerable.Range (0, 256).Select (i => (byte) i).ToArray ();
var expected = BitConverter.ToString (bytes).Replace ("-", "");
Assert.AreEqual (expected, HexUtilities.ToHexString (bytes));
Assert.AreEqual (expected.ToLowerInvariant (), HexUtilities.ToHexString (bytes, upperCase: false));
}

// The implementation stackallocs up to 128 chars (64 bytes) and heap allocates beyond that
[TestCase (63)]
[TestCase (64)]
[TestCase (65)]
[TestCase (1024)]
public void ToHexString_CrossesStackallocThreshold (int length)
{
var bytes = new byte [length];
new Random (42).NextBytes (bytes);
var expected = BitConverter.ToString (bytes).Replace ("-", "");
Assert.AreEqual (expected, HexUtilities.ToHexString (bytes));
}
}
}
Loading