File: System\Runtime\CompilerServices\ClassConstructorRunner.cs
Web Access
Project: src\src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

using Internal.Runtime;
using Internal.Runtime.CompilerHelpers;

namespace System.Runtime.CompilerServices
{
    internal static partial class ClassConstructorRunner
    {
        //==============================================================================================================
        // Ensures the class constructor for the given type has run.
        //
        // Called by the runtime when it finds a class whose static class constructor has probably not run
        // (probably because it checks in the initialized flag without thread synchronization).
        //
        // The context structure passed by reference lives in the image of one of the application's modules.
        // The contents are thus fixed (do not require pinning) and the address can be used as a unique
        // identifier for the context.
        //
        // This guarantee is violated in one specific case: where a class constructor cycle would cause a deadlock. If
        // so, per ECMA specs, this method returns without guaranteeing that the .cctor has run.
        //
        // No attempt is made to detect or break deadlocks due to other synchronization mechanisms.
        //==============================================================================================================

        private static unsafe object CheckStaticClassConstructionReturnGCStaticBase(StaticClassConstructionContext* context, object gcStaticBase)
        {
            EnsureClassConstructorRun(context);
            return gcStaticBase;
        }

        private static unsafe IntPtr CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext* context, IntPtr nonGcStaticBase)
        {
            EnsureClassConstructorRun(context);
            return nonGcStaticBase;
        }

        private static unsafe object CheckStaticClassConstructionReturnThreadStaticBase(TypeManagerSlot* pModuleData, int typeTlsIndex, StaticClassConstructionContext* context)
        {
            object threadStaticBase = ThreadStatics.GetThreadStaticBaseForType(pModuleData, typeTlsIndex);
            if (context->cctorMethodAddress != 0)
                EnsureClassConstructorRun(context);
            return threadStaticBase;
        }

        public static unsafe void EnsureClassConstructorRun(StaticClassConstructionContext* pContext)
        {
            IntPtr pfnCctor = pContext->cctorMethodAddress;
            NoisyLog("EnsureClassConstructorRun, context={0}, thread={1}", pContext, CurrentManagedThreadId);

            // If we were called from JIT helper, this check is redundant but harmless. This is in case someone within classlib
            // (cough, Reflection) needs to call this explicitly.
            if (pfnCctor == 0)
            {
                NoisyLog("Cctor already run, context={0}, thread={1}", pContext, CurrentManagedThreadId);
                return;
            }

            CctorHandle cctor = Cctor.GetCctor(pContext);
            Cctor[] cctors = cctor.Array;
            int cctorIndex = cctor.Index;
            try
            {
                Lock cctorLock = cctors[cctorIndex].Lock;
                if (DeadlockAwareAcquire(cctor, pContext))
                {
                    int currentManagedThreadId = CurrentManagedThreadId;
                    try
                    {
                        NoisyLog("Acquired cctor lock, context={0}, thread={1}", pContext, currentManagedThreadId);

                        cctors[cctorIndex].HoldingThread = currentManagedThreadId;
                        if (pContext->cctorMethodAddress != 0)  // Check again in case some thread raced us while we were acquiring the lock.
                        {
                            TypeInitializationException priorException = cctors[cctorIndex].Exception;
                            if (priorException != null)
                                throw priorException;
                            try
                            {
                                NoisyLog("Calling cctor, context={0}, thread={1}", pContext, currentManagedThreadId);

                                ((delegate*<void>)pfnCctor)();

                                NoisyLog("Set type inited, context={0}, thread={1}", pContext, currentManagedThreadId);

                                pContext->cctorMethodAddress = 0;
                            }
                            catch (Exception e)
                            {
                                TypeInitializationException wrappedException = new TypeInitializationException(null, SR.TypeInitialization_Type_NoTypeAvailable, e);
                                cctors[cctorIndex].Exception = wrappedException;
                                throw wrappedException;
                            }
                        }
                    }
                    finally
                    {
                        cctors[cctorIndex].HoldingThread = ManagedThreadIdNone;
                        NoisyLog("Releasing cctor lock, context={0}, thread={1}", pContext, currentManagedThreadId);

                        cctorLock.Exit();
                    }
                }
                else
                {
                    // Cctor cycle resulted in a deadlock. We will break the guarantee and return without running the
                    // .cctor.
                }
            }
            finally
            {
                Cctor.Release(cctor);
            }
            NoisyLog("EnsureClassConstructorRun complete, context={0}, thread={1}", pContext, CurrentManagedThreadId);
        }

        //=========================================================================================================
        // Return value:
        //   true   - lock acquired.
        //   false  - deadlock detected. Lock not acquired.
        //=========================================================================================================
        private static unsafe bool DeadlockAwareAcquire(CctorHandle cctor, StaticClassConstructionContext* pContext)
        {
            const int WaitIntervalSeedInMS = 1;      // seed with 1ms and double every time through the loop
            const int WaitIntervalLimitInMS = WaitIntervalSeedInMS << 7; // limit of 128ms

            int waitIntervalInMS = WaitIntervalSeedInMS;

            int cctorIndex = cctor.Index;
            Cctor[] cctors = cctor.Array;
            Lock lck = cctors[cctorIndex].Lock;
            if (lck.IsHeldByCurrentThread)
                return false;     // Thread recursively triggered the same cctor.

            if (lck.TryEnter(waitIntervalInMS))
                return true;

            // We couldn't acquire the lock. See if this .cctor is involved in a cross-thread deadlock.  If so, break
            // the deadlock by breaking the guarantee - we'll skip running the .cctor and let the caller take his chances.
            int currentManagedThreadId = CurrentManagedThreadId;
            int unmarkCookie = -1;
            try
            {
                // We'll spin in a forever-loop of checking for a deadlock state, then waiting a short time, then
                // checking for a deadlock state again, and so on. This is because the BlockedRecord info has a built-in
                // lag time - threads don't report themselves as blocking until they've been blocked for a non-trivial
                // amount of time.
                //
                // If the threads are deadlocked for any reason other a class constructor cycling, this loop will never
                // terminate - this is by design. If the user code inside the class constructors were to
                // deadlock themselves, then that's a bug in user code.
                for (; ; )
                {
                    using (s_cctorGlobalLock.EnterScope())
                    {
                        // Ask the guy who holds the cctor lock we're trying to acquire who he's waiting for. Keep
                        // walking down that chain until we either discover a cycle or reach a non-blocking state. Note
                        // that reaching a non-blocking state is not proof that we've avoided a deadlock due to the
                        // BlockingRecord reporting lag.
                        CctorHandle cctorWalk = cctor;
                        int chainStepCount = 0;
                        for (; chainStepCount < Cctor.Count; chainStepCount++)
                        {
                            int cctorWalkIndex = cctorWalk.Index;
                            Cctor[] cctorWalkArray = cctorWalk.Array;

                            int holdingThread = cctorWalkArray[cctorWalkIndex].HoldingThread;
                            if (holdingThread == currentManagedThreadId)
                            {
                                // Deadlock detected.  We will break the guarantee and return without running the .cctor.
                                DebugLog("A class constructor was skipped due to class constructor cycle. context={0}, thread={1}",
                                    pContext, currentManagedThreadId);

                                // We are maintaining an invariant that the BlockingRecords never show a cycle because,
                                // before we add a record, we first check for a cycle.  As a result, once we've said
                                // we're waiting, we are committed to waiting and will not need to skip running this
                                // .cctor.
                                Debug.Assert(unmarkCookie == -1);
                                return false;
                            }

                            if (holdingThread == ManagedThreadIdNone)
                            {
                                // No one appears to be holding this cctor lock. Give the current thread some more time
                                // to acquire the lock.
                                break;
                            }

                            cctorWalk = BlockingRecord.GetCctorThatThreadIsBlockedOn(holdingThread);
                            if (cctorWalk.Array == null)
                            {
                                // The final thread in the chain appears to be blocked on nothing. Give the current
                                // thread some more time to acquire the lock.
                                break;
                            }
                        }

                        // We don't allow cycles in the BlockingRecords, so we must always enumerate at most each entry,
                        // but never more.
                        Debug.Assert(chainStepCount < Cctor.Count);

                        // We have not discovered a deadlock, so let's register the fact that we're waiting on another
                        // thread and continue to wait.  It is important that we only signal that we are blocked after
                        // we check for a deadlock because, otherwise, we give all threads involved in the deadlock the
                        // opportunity to break it themselves and that leads to "ping-ponging" between the cctors
                        // involved in the cycle, allowing intermediate cctor results to be observed.
                        //
                        // The invariant here is that we never 'publish' a BlockingRecord that forms a cycle.  So it is
                        // important that the look-for-cycle-and-then-publish-wait-status operation be atomic with
                        // respect to other updates to the BlockingRecords.
                        if (unmarkCookie == -1)
                        {
                            NoisyLog("Mark thread blocked, context={0}, thread={1}", pContext, currentManagedThreadId);

                            unmarkCookie = BlockingRecord.MarkThreadAsBlocked(currentManagedThreadId, cctor);
                        }
                    } // _cctorGlobalLock scope

                    if (waitIntervalInMS < WaitIntervalLimitInMS)
                        waitIntervalInMS *= 2;

                    // We didn't find a cycle yet, try to take the lock again.
                    if (lck.TryEnter(waitIntervalInMS))
                        return true;
                } // infinite loop
            }
            finally
            {
                if (unmarkCookie != -1)
                {
                    NoisyLog("Unmark thread blocked, context={0}, thread={1}", pContext, currentManagedThreadId);
                    BlockingRecord.UnmarkThreadAsBlocked(unmarkCookie);
                }
            }
        }

        //==============================================================================================================
        // These structs are allocated on demand whenever the runtime tries to run a class constructor. Once the
        // the class constructor has been successfully initialized, we reclaim this structure. The structure is long-
        // lived only if the class constructor threw an exception.
        //==============================================================================================================
        private unsafe struct Cctor
        {
            public Lock Lock;
            public TypeInitializationException Exception;
            public int HoldingThread;
            private int _refCount;
            private StaticClassConstructionContext* _pContext;

            //==========================================================================================================
            // Gets the Cctor entry associated with a specific class constructor context (creating it if necessary.)
            //==========================================================================================================
            public static CctorHandle GetCctor(StaticClassConstructionContext* pContext)
            {
#if DEBUG
                const int Grow = 2;
#else
                const int Grow = 10;
#endif

                // WASMTODO: Remove this when the Initialize method gets called by the runtime startup
#if TARGET_WASM
                if (s_cctorGlobalLock == null)
                {
                    Interlocked.CompareExchange(ref s_cctorGlobalLock, new Lock(useTrivialWaits: true), null);
                }
                if (s_cctorArrays == null)
                {
                    Interlocked.CompareExchange(ref s_cctorArrays, new Cctor[10][], null);
                }
#endif // TARGET_WASM

                using (s_cctorGlobalLock.EnterScope())
                {
                    Cctor[]? resultArray = null;
                    int resultIndex = -1;

                    if (s_count != 0)
                    {
                        // Search for the cctor context in our existing arrays
                        for (int cctorIndex = 0; cctorIndex < s_cctorArraysCount; ++cctorIndex)
                        {
                            Cctor[] segment = s_cctorArrays[cctorIndex];
                            for (int i = 0; i < segment.Length; i++)
                            {
                                if (segment[i]._pContext == pContext)
                                {
                                    resultArray = segment;
                                    resultIndex = i;
                                    break;
                                }
                            }
                            if (resultArray != null)
                                break;
                        }
                    }
                    if (resultArray == null)
                    {
                        // look for an empty entry in an existing array
                        for (int cctorIndex = 0; cctorIndex < s_cctorArraysCount; ++cctorIndex)
                        {
                            Cctor[] segment = s_cctorArrays[cctorIndex];
                            for (int i = 0; i < segment.Length; i++)
                            {
                                if (segment[i]._pContext == default(StaticClassConstructionContext*))
                                {
                                    resultArray = segment;
                                    resultIndex = i;
                                    break;
                                }
                            }
                            if (resultArray != null)
                                break;
                        }
                        if (resultArray == null)
                        {
                            // allocate a new array
                            resultArray = new Cctor[Grow];
                            if (s_cctorArraysCount == s_cctorArrays.Length)
                            {
                                // grow the container
                                Array.Resize(ref s_cctorArrays, (s_cctorArrays.Length * 2) + 1);
                            }
                            // store the array in the container, this cctor gets index 0
                            s_cctorArrays[s_cctorArraysCount] = resultArray;
                            s_cctorArraysCount++;
                            resultIndex = 0;
                        }

                        Debug.Assert(resultArray[resultIndex]._pContext == default(StaticClassConstructionContext*));
                        resultArray[resultIndex]._pContext = pContext;
                        resultArray[resultIndex].Lock = new Lock(useTrivialWaits: true);
                        s_count++;
                    }

                    Interlocked.Increment(ref resultArray[resultIndex]._refCount);
                    return new CctorHandle(resultArray, resultIndex);
                }
            }

            public static int Count
            {
                get
                {
                    Debug.Assert(s_cctorGlobalLock.IsHeldByCurrentThread);
                    return s_count;
                }
            }

            public static void Release(CctorHandle cctor)
            {
                using (s_cctorGlobalLock.EnterScope())
                {
                    Cctor[] cctors = cctor.Array;
                    int cctorIndex = cctor.Index;
                    if (0 == Interlocked.Decrement(ref cctors[cctorIndex]._refCount))
                    {
                        if (cctors[cctorIndex].Exception == null)
                        {
                            cctors[cctorIndex] = default;
                            s_count--;
                        }
                    }
                }
            }
        }

        private struct CctorHandle
        {
            public CctorHandle(Cctor[] array, int index)
            {
                _array = array;
                _index = index;
            }

            public Cctor[] Array { get { return _array; } }
            public int Index { get { return _index; } }

            private Cctor[] _array;
            private int _index;
        }

        //==============================================================================================================
        // Keeps track of threads that are blocked on a cctor lock (alas, we don't have ThreadLocals here in
        // System.Private.CoreLib so we have to use a side table.)
        //
        // This is used for cross-thread deadlock detection.
        //
        // - Data is only entered here if a thread has been blocked past a certain timeout (otherwise, it's certainly
        //   not participating of a deadlock.)
        // - Reads and writes to _blockingRecord are guarded by _cctorGlobalLock.
        // - BlockingRecords for individual threads are created on demand. Since this is a rare event, we won't attempt
        //   to recycle them directly (however,
        //   ManagedThreadId's are themselves recycled pretty quickly - and threads that inherit the managed id also
        //   inherit the BlockingRecord.)
        //==============================================================================================================
        private struct BlockingRecord
        {
            public int ManagedThreadId;     // ManagedThreadId of the blocked thread
            public CctorHandle BlockedOn;

            public static int MarkThreadAsBlocked(int managedThreadId, CctorHandle blockedOn)
            {
#if DEBUG
                const int Grow = 2;
#else
                const int Grow = 10;
#endif
                using (s_cctorGlobalLock.EnterScope())
                {
                    s_blockingRecords ??= new BlockingRecord[Grow];
                    int found;
                    for (found = 0; found < s_nextBlockingRecordIndex; found++)
                    {
                        if (s_blockingRecords[found].ManagedThreadId == managedThreadId)
                            break;
                    }
                    if (found == s_nextBlockingRecordIndex)
                    {
                        if (s_nextBlockingRecordIndex == s_blockingRecords.Length)
                        {
                            BlockingRecord[] newBlockingRecords = new BlockingRecord[s_blockingRecords.Length + Grow];
                            for (int i = 0; i < s_blockingRecords.Length; i++)
                            {
                                newBlockingRecords[i] = s_blockingRecords[i];
                            }
                            s_blockingRecords = newBlockingRecords;
                        }
                        s_blockingRecords[s_nextBlockingRecordIndex].ManagedThreadId = managedThreadId;
                        s_nextBlockingRecordIndex++;
                    }
                    s_blockingRecords[found].BlockedOn = blockedOn;
                    return found;
                }
            }

            public static void UnmarkThreadAsBlocked(int blockRecordIndex)
            {
                // This method must never throw
                s_cctorGlobalLock.Enter();
                s_blockingRecords[blockRecordIndex].BlockedOn = new CctorHandle(null, 0);
                s_cctorGlobalLock.Exit();
            }

            public static CctorHandle GetCctorThatThreadIsBlockedOn(int managedThreadId)
            {
                Debug.Assert(s_cctorGlobalLock.IsHeldByCurrentThread);
                for (int i = 0; i < s_nextBlockingRecordIndex; i++)
                {
                    if (s_blockingRecords[i].ManagedThreadId == managedThreadId)
                        return s_blockingRecords[i].BlockedOn;
                }
                return new CctorHandle(null, 0);
            }

            private static BlockingRecord[] s_blockingRecords;
            private static int s_nextBlockingRecordIndex;
        }

        private static int CurrentManagedThreadId => ManagedThreadId.Current;
        private const int ManagedThreadIdNone = ManagedThreadId.IdNone;

        private static Lock s_cctorGlobalLock;

        // These three  statics are used by ClassConstructorRunner.Cctor but moved out to avoid an unnecessary
        // extra class constructor call.
        //
        // Because Cctor's are mutable structs, we have to give our callers raw references to the underlying arrays
        // for this collection to be usable.  This also means once we place a Cctor in an array, we can't grow or
        // reallocate the array.
        private static Cctor[][] s_cctorArrays;
        private static int s_cctorArraysCount;
        private static int s_count;

        // Eager construction called from LibraryInitialize Cctor.GetCctor uses _cctorGlobalLock.
        internal static void Initialize()
        {
            s_cctorArrays = new Cctor[10][];
            s_cctorGlobalLock = new Lock(useTrivialWaits: true);
        }

        [Conditional("ENABLE_NOISY_CCTOR_LOG")]
        private static unsafe void NoisyLog(string format, StaticClassConstructionContext* pContext, int threadId)
        {
            // We cannot utilize any of the typical number formatting code because it triggers globalization code to run
            // and this cctor code is layered below globalization.
#if DEBUG
            Debug.WriteLine(format, ToHexString((IntPtr)pContext), ToHexString(threadId));
#endif // DEBUG
        }

        [Conditional("DEBUG")]
        private static unsafe void DebugLog(string format, StaticClassConstructionContext* pContext, int threadId)
        {
            // We cannot utilize any of the typical number formatting code because it triggers globalization code to run
            // and this cctor code is layered below globalization.
#if DEBUG
            Debug.WriteLine(format, ToHexString((IntPtr)pContext), ToHexString(threadId));
#endif
        }

        // We cannot utilize any of the typical number formatting code because it triggers globalization code to run
        // and this cctor code is layered below globalization.
#if DEBUG
        private static string ToHexString(int num)
        {
            return ToHexStringUnsignedLong((ulong)num, false, 8);
        }
        private static string ToHexString(IntPtr num)
        {
            return ToHexStringUnsignedLong((ulong)num, false, 16);
        }
        private static char GetHexChar(uint u)
        {
            if (u < 10)
                return unchecked((char)('0' + u));

            return unchecked((char)('a' + (u - 10)));
        }
        public static unsafe string ToHexStringUnsignedLong(ulong u, bool zeroPrepad, int numChars)
        {
            char[] chars = new char[numChars];

            int i = numChars - 1;

            for (; i >= 0; i--)
            {
                chars[i] = GetHexChar((uint)(u % 16));
                u /= 16;

                if ((i == 0) || (!zeroPrepad && (u == 0)))
                    break;
            }

            string str;
            fixed (char* p = &chars[i])
            {
                str = new string(p, 0, numChars - i);
            }
            return str;
        }
#endif
    }
}