Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you change the contents of a (immutable) string via an unsafe method?

Tags:

c#

I know that strings are immutable and any changes to a string simply creates a new string in memory (and marks the old one as free). However, I'm wondering if my logic below is sound in that you actually can, in a round-a-bout fashion, modify the contents of a string.

const string baseString = "The quick brown fox jumps over the lazy dog!";

//initialize a new string
string candidateString = new string('\0', baseString.Length);

//Pin the string
GCHandle gcHandle = GCHandle.Alloc(candidateString, GCHandleType.Pinned);

//Copy the contents of the base string to the candidate string
unsafe
{
    char* cCandidateString = (char*) gcHandle.AddrOfPinnedObject();
    for (int i = 0; i < baseString.Length; i++)
    {
        cCandidateString[i] = baseString[i];
    }
}

Does this approach indeed change the contents candidateString (without creating a new candidateString in memory) or does the runtime see through my tricks and treat it as a normal string?

like image 495
Brian Mitchell Avatar asked Sep 08 '15 18:09

Brian Mitchell


People also ask

Can a String object be mutated?

Actually, it is possible to mutate strings (and other immutable objects). It requires reflection and is very, very dangerous and should never ever be used unless you're actually interested in destroying the program.

Is strings mutable or immutable?

String is an example of an immutable type. A String object always represents the same string. StringBuilder is an example of a mutable type.

How do you mutate a string in Java?

StringBuilder is a mutable sequence of characters. StringBuilder is used when we want to modify Java strings in-place. StringBuffer is a thread-safe equivalent similar of StringBuilder . StringBuilder has methods such as append , insert , or replace that allow to modify strings.

What's immutable in C# String objects are immutable?

There is a term called immutable, which means the state of an object can't be changed after is has been created. A string is an immutable type. The statement that a string is immutable means that, once created, it is not altered by changing the value assigned to it.


1 Answers

Your example works just fine, thanks to several elements:

  • candidateString lives in the managed heap, so it's safe to modify. Compare this with baseString, which is interned. If you try to modify the interned string, unexpected things may happen. There's no guarantee that string won't live in write-protected memory at some point, although it seems to work today. That would be pretty similar to assigning a constant string to a char* variable in C and then modifying it. In C, that's undefined behavior.

  • You preallocate enough space in candidateString - so you're not overflowing the buffer.

  • Character data is not stored at offset 0 of the String class. It's stored at an offset equal to RuntimeHelpers.OffsetToStringData.

    public static int OffsetToStringData
    {
        // This offset is baked in by string indexer intrinsic, so there is no harm
        // in getting it baked in here as well.
        [System.Runtime.Versioning.NonVersionable] 
        get {
            // Number of bytes from the address pointed to by a reference to
            // a String to the first 16-bit character in the String.  Skip 
            // over the MethodTable pointer, & String 
            // length.  Of course, the String reference points to the memory 
            // after the sync block, so don't count that.  
            // This property allows C#'s fixed statement to work on Strings.
            // On 64 bit platforms, this should be 12 (8+4) and on 32 bit 8 (4+4).
    #if WIN32
            return 8;
    #else
            return 12;
    #endif // WIN32
        }
    }
    

    Except...

  • GCHandle.AddrOfPinnedObject is special cased for two types: string and array types. Instead of returning the address of the object itself, it lies and returns the offset to the data. See the source code in CoreCLR.

    // Get the address of a pinned object referenced by the supplied pinned
    // handle.  This routine assumes the handle is pinned and does not check.
    FCIMPL1(LPVOID, MarshalNative::GCHandleInternalAddrOfPinnedObject, OBJECTHANDLE handle)
    {
        FCALL_CONTRACT;
    
        LPVOID p;
        OBJECTREF objRef = ObjectFromHandle(handle);
    
        if (objRef == NULL)
        {
            p = NULL;
        }
        else
        {
            // Get the interior pointer for the supported pinned types.
            if (objRef->GetMethodTable() == g_pStringClass)
                p = ((*(StringObject **)&objRef))->GetBuffer();
            else if (objRef->GetMethodTable()->IsArray())
                p = (*((ArrayBase**)&objRef))->GetDataPtr();
            else
                p = objRef->GetData();
        }
    
        return p;
    }
    FCIMPLEND
    

In summary, the runtime lets you play with its data and doesn't complain. You're using unsafe code after all. I've seen worse runtime messing than that, including creating reference types on the stack ;-)

Just remember to add one additional \0 after all the characters (at offset Length) if your final string is shorter than what's allocated. This won't overflow, each string has an implicit null character at the end to ease interop scenarios.


Now take a look at how StringBuilder creates a string, here's StringBuilder.ToString:

[System.Security.SecuritySafeCritical]  // auto-generated
public override String ToString() {
    Contract.Ensures(Contract.Result<String>() != null);

    VerifyClassInvariant();

    if (Length == 0)
        return String.Empty;

    string ret = string.FastAllocateString(Length);
    StringBuilder chunk = this;
    unsafe {
        fixed (char* destinationPtr = ret)
        {
            do
            {
                if (chunk.m_ChunkLength > 0)
                {
                    // Copy these into local variables so that they are stable even in the presence of race conditions
                    char[] sourceArray = chunk.m_ChunkChars;
                    int chunkOffset = chunk.m_ChunkOffset;
                    int chunkLength = chunk.m_ChunkLength;

                    // Check that we will not overrun our boundaries. 
                    if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length)
                    {
                        fixed (char* sourcePtr = sourceArray)
                            string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index"));
                    }
                }
                chunk = chunk.m_ChunkPrevious;
            } while (chunk != null);
        }
    }
    return ret;
}

Yes, it uses unsafe code, and yes, you can optimize yours by using fixed, as this type of pinning is much more lightweight than allocating a GC handle:

const string baseString = "The quick brown fox jumps over the lazy dog!";

//initialize a new string
string candidateString = new string('\0', baseString.Length);

//Copy the contents of the base string to the candidate string
unsafe
{
    fixed (char* cCandidateString = candidateString)
    {
        for (int i = 0; i < baseString.Length; i++)
            cCandidateString[i] = baseString[i];
    }
}

When you use fixed, the GC only discovers an object needs to be pinned when it stumbles upon it during a collection. If there's no collection going on, the GC isn't even involved. When you use GCHandle, a handle is registered in the GC each time.

like image 162
Lucas Trzesniewski Avatar answered Oct 04 '22 20:10

Lucas Trzesniewski