Last active
November 24, 2022 14:53
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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'; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Demo 1
Demo 2
Demo 3
Demo 4