Skip to content

Instantly share code, notes, and snippets.

@Guiorgy
Last active November 24, 2022 14:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Guiorgy/55853aa6821a5f00d463b5fd6cafba05 to your computer and use it in GitHub Desktop.
Save Guiorgy/55853aa6821a5f00d463b5fd6cafba05 to your computer and use it in GitHub Desktop.
An "evil" C# wrapper class that allows the mutation of the internal string object
/// <summary>
/// <strong>This class uses unsafe code and is only meant as a thought experiment. Do NOT use this in production!</strong>
/// <para/>
/// A wrapper to a string object that allows the mutation of the said string.
/// </summary>
public sealed class MutableString
{
/**
* We are assuming that if we allocate a char[] array and write it in a specific way that
* taking a reference to one of it's elements, we can "fool" .NET into thinking that that's
* a managed string object.
*
* The assumed string object memory format:
* [prepadding (0 bytes)], [metadata (12 bytes)] (where last 4 bytes (int) is the string length), [length x chars], [\0]
* Note: The string object also needs a padding at the end for allignment, but the char[] array allocation takes care of that.
*/
const int metadataPaddedLength = 12; // Bytes of metadata + padding
const int metadataPaddedLength16 = metadataPaddedLength / 2; // Chars of metadata + padding
const int metadataPaddedLength32 = metadataPaddedLength16 / 2; // Int32s of metadata + padding
const int prepaddingLength = 0; // Bytes of padding
const int prepaddingLength16 = prepaddingLength / 2; // Chars of padding
const int prepaddingLength32 = prepaddingLength16 / 2; // Int32s of padding
const int metadataLength = metadataPaddedLength - prepaddingLength; // Bytes of metadata
const int metadataLength16 = metadataPaddedLength16 - prepaddingLength16; // Chars of metadata
const int metadataLength32 = metadataPaddedLength32 - prepaddingLength32; // Int32s of metadata
const int lengthPosition = metadataPaddedLength - 4; // Offset to the first byte of length
const int lengthPosition16 = lengthPosition / 2; // Offset to the first char of length
const int lengthPosition32 = lengthPosition16 / 2; // Offset to length
private readonly string _string; // The internal string object that we mutate
private readonly char[] _memory; // The memory holding our string object (including its metadata)
private readonly bool _autoZeroTerminate; // Whether we should always zero terminate ('\0') when mutating
private MutableString(char[] memory, int length, bool autoZeroTerminate = true)
{
if (length <= 0 || memory.Length - metadataPaddedLength16 < length) throw new ArgumentOutOfRangeException(nameof(length));
_memory = memory;
_string = string.Empty;
_autoZeroTerminate = autoZeroTerminate;
unsafe
{
fixed (char* mp = memory)
{
// Copy string metadata to the new memory
fixed (char* sp = _string) for (int i = 0; i < metadataLength16 - 2; i++) mp[i + prepaddingLength16] = sp[i - metadataLength16];
// Set the desired string length
*(int*)&mp[lengthPosition16] = length;
// Zero terminate the string (if requsted)
if (autoZeroTerminate) mp[length + metadataPaddedLength16] = '\0';
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
// Change the reference of the internal string from string.Empty to our memory
fixed (string* sp = &_string) *(uint*)sp = (uint)&mp[prepaddingLength16];
#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
}
}
Debug.Assert(_string.Length == length);
}
/// <summary>
/// Initialize the mutable string with the given value.
/// </summary>
/// <param name="str">The initial value of the mutable string.</param>
/// <param name="autoZeroTerminate">If true, automatically zero terminate when resizing the string.</param>
public MutableString(string str, bool autoZeroTerminate = true) : this(AllocateFrom(str), str.Length, autoZeroTerminate) { }
private static char[] AllocateFrom(string str)
{
// Allocate memory of the string length + metadata + 1 for zero termination
char[] memory = new char[str.Length + metadataPaddedLength16 + 1];
str.CopyTo(0, memory, metadataPaddedLength16, str.Length);
return memory;
}
/// <summary>
/// Encapsulates a method that receives a span of objects of type <typeparamref>Tin</typeparamref>
/// and a state object of type <typeparamref>TArg</typeparamref>, and returns an object of type <typeparamref>Tout</typeparamref>.
/// </summary>
/// <typeparam name="Tin">The type of the objects in the span.</typeparam>
/// <typeparam name="Tout">The type of the objects to return.</typeparam>
/// <typeparam name="TArg">The type of the object that represents the state.</typeparam>
/// <param name="span">A span of objects of type <typeparamref>Tin</typeparamref>.</param>
/// <param name="arg">A state object of type <typeparamref>TArg</typeparamref>.</param>
/// <returns>Object of type <typeparamref>Tout</typeparamref></returns>
public delegate Tout SpanFunction<Tin, Tout, in TArg>(Span<Tin> span, TArg arg);
/// <summary>
/// Creates a new mutable string with a specific capacity and initializes it after creation by using the specified callback.
/// </summary>
/// <typeparam name="TState">The type of the element to pass to <paramref>callback</paramref>.</typeparam>
/// <param name="capacity">The capacity of the mutable string to create.</param>
/// <param name="state">The element to pass to <paramref>callback</paramref>.</param>
/// <param name="callback">A callback to initialize the string.</param>
/// <param name="autoZeroTerminate">If true, automatically zero terminate when resizing the string.</param>
/// <returns>A MutableString object.</returns>
/// <exception cref="ArgumentNullException">Throws when <paramref>callback</paramref> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Throws when <paramref>capacity</paramref> is non-positive,
/// or the returned length from the <paramref>callback</paramref> exceeds capacity.</exception>
public static MutableString Create<TState>(int capacity, TState state, SpanFunction<char, int, TState> callback, bool autoZeroTerminate = true)
{
if (callback == null) throw new ArgumentNullException(nameof(callback));
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
// Allocate memory of the string length + metadata + 1 for zero termination
char[] memory = new char[capacity + metadataPaddedLength16 + 1];
// Call the callback with a Span to our memory not including the metadata at the start and zero char at the end
int length = callback(new Span<char>(memory, metadataPaddedLength16, capacity - metadataPaddedLength16 - 1), state);
return new MutableString(memory, length, autoZeroTerminate);
}
/// <summary>
/// Creates a new mutable string with a specific capacity and initializes it after creation by using the specified callback.
/// </summary>
/// <typeparam name="TState">The type of the element to pass to <paramref>callback</paramref>.</typeparam>
/// <param name="capacity">The capacity of the mutable string to create.</param>
/// <param name="state">The element to pass to <paramref>callback</paramref>.</param>
/// <param name="callback">A callback to initialize the string.</param>
/// <param name="autoZeroTerminate">If true, automatically zero terminate when resizing the string.</param>
/// <returns>A MutableString object.</returns>
/// <exception cref="ArgumentNullException">Throws when <paramref>callback</paramref> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Throws when <paramref>capacity</paramref> is non-positive.</exception>
public static MutableString Create<TState>(int capacity, TState state, System.Buffers.SpanAction<char, TState> callback, bool autoZeroTerminate = true)
{
if (callback == null) throw new ArgumentNullException(nameof(callback));
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
// Allocate memory of the string length + metadata + 1 for zero termination
char[] memory = new char[capacity + metadataPaddedLength16 + 1];
// Call the callback with a Span to our memory not including the metadata at the start and zero char at the end
callback(new Span<char>(memory, metadataPaddedLength16, capacity - metadataPaddedLength16 - 1), state);
return new MutableString(memory, capacity, autoZeroTerminate);
}
/// <summary>
/// Gets the number of characters in the current string object.
/// </summary>
public int Length => _string.Length;
/// <summary>
/// Gets the number of characters allocated for string object.
/// </summary>
public int Capacity => _memory.Length - metadataPaddedLength16 - 1; // Capacity not including the metadata at the start and zero char at the end
/// <summary>
/// Returns the internal instance of string; no actual conversion is performed.
/// </summary>
/// <returns>The internal instance of string</returns>
public override string ToString() => _string;
public static implicit operator string(MutableString ms) => ms.ToString();
public static implicit operator MutableString(string str) => new(str);
/// <summary>
/// Creates a new (mutable) span over the string.
/// </summary>
public Span<char> Chars => _memory.AsSpan(metadataPaddedLength16, _string.Length);
/// <summary>
/// Creates a new (mutable) span over the allocated char[] array.
/// </summary>
public Span<char> AllChars => _memory.AsSpan(metadataPaddedLength16, _memory.Length - metadataPaddedLength16 - 1);
/// <summary>
/// Resizes the string.
/// </summary>
/// <param name="length">The new length of the string</param>
/// <exception cref="ArgumentOutOfRangeException">Throws when <paramref>length</paramref> is non-positive or exceeds the allocated size.</exception>
public void Resize(int length)
{
if (length <= 0 || _memory.Length - metadataPaddedLength16 - 1 < length) throw new ArgumentOutOfRangeException(nameof(length));
unsafe
{
fixed (char* mp = _memory)
{
// Set the desired string length
*(int*)&mp[lengthPosition16] = length;
// Zero terminate the string (if requsted)
if (_autoZeroTerminate) mp[length + metadataPaddedLength16] = '\0';
}
}
}
}
@Guiorgy
Copy link
Author

Guiorgy commented Nov 23, 2022

Demo 1

string expected = "Hello World!";
MutableString ms = expected;
string s = ms;
Console.WriteLine("{0} == {1}: {2}", s, expected, s == expected); // Hello World! == Hello World!: True

ms.Resize(5);
Console.WriteLine(s); // Hello

ms.Resize(12);
ms.Chars[5] = ' '; // When we resized the string to 5 above, it automatically inserted a null character ('\0') in this position
Console.WriteLine(s); // Hello World!

ms.Chars[4] = ' ';
Console.WriteLine(s); // Hell  World!

Demo 2

string expected = "Hello World!";
MutableString ms = new MutableString(expected, false);
string s = ms;
Console.WriteLine("{0} == {1}: {2}", s, expected, s == expected); // Hello World! == Hello World!: True

ms.Resize(5);
Console.WriteLine(s); // Hello

ms.Resize(12);
Console.WriteLine(s); // Hello World!

ms.Chars[4] = ' ';
Console.WriteLine(s); // Hell  World!

Demo 3

string expected = "Hello World!";
MutableString ms = MutableString.Create(50, expected, (chars, state) =>
{
    state.CopyTo(chars);
    return state.Length;
}, false);
string s = ms;
Console.WriteLine("{0} == {1}: {2}", s, expected, s == expected); // Hello World! == Hello World!: True

ms.Resize(5);
Console.WriteLine(s); // Hello

ms.Resize(12);
Console.WriteLine(s); // Hello World!

ms.Chars[4] = ' ';
Console.WriteLine(s); // Hell  World!

Demo 4

string expected = "Hello World!";
MutableString ms = MutableString.Create(50, expected, (chars, state) => state.CopyTo(chars), false);
string s = ms;
Console.WriteLine("{0} == {1}: {2}", s, expected, s == expected); // Hello World! == Hello World!: False
ms.Resize(12);
Console.WriteLine("{0} == {1}: {2}", s, expected, s == expected); // Hello World! == Hello World!: True

ms.Resize(5);
Console.WriteLine(s); // Hello

ms.Resize(12);
Console.WriteLine(s); // Hello World!

ms.Chars[4] = ' ';
Console.WriteLine(s); // Hell  World!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment