// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; namespace System.Runtime.Loader { public partial class AssemblyLoadContext { private enum InternalState { /// /// The ALC is alive (default) /// Alive, /// /// The unload process has started, the Unloading event will be called /// once the underlying LoaderAllocator has been finalized /// Unloading } private static readonly Dictionary> s_allContexts = new Dictionary>(); private static long s_nextId; #region private data members // If you modify any of these fields, you must also update the // AssemblyLoadContextBaseObject structure in object.h // synchronization primitive to protect against usage of this instance while unloading private readonly object _unloadLock; private event Func _resolvingUnmanagedDll = null!; private event Func _resolving = null!; private event Action _unloading = null!; private readonly string? _name; // Contains the reference to VM's representation of the AssemblyLoadContext private readonly IntPtr _nativeAssemblyLoadContext; // Id used by s_allContexts private readonly long _id; // Indicates the state of this ALC (Alive or in Unloading state) private InternalState _state; private readonly bool _isCollectible; #endregion protected AssemblyLoadContext() : this(false, false, null) { } protected AssemblyLoadContext(bool isCollectible) : this(false, isCollectible, null) { } public AssemblyLoadContext(string? name, bool isCollectible = false) : this(false, isCollectible, name) { } private protected AssemblyLoadContext(bool representsTPALoadContext, bool isCollectible, string? name) { // Initialize the VM side of AssemblyLoadContext if not already done. _isCollectible = isCollectible; _name = name; // The _unloadLock needs to be assigned after the IsCollectible to ensure proper behavior of the finalizer // even in case the following allocation fails or the thread is aborted between these two lines. _unloadLock = new object(); if (!isCollectible) { // For non collectible AssemblyLoadContext, the finalizer should never be called and thus the AssemblyLoadContext should not // be on the finalizer queue. GC.SuppressFinalize(this); } // If this is a collectible ALC, we are creating a weak handle tracking resurrection otherwise we use a strong handle var thisHandle = GCHandle.Alloc(this, IsCollectible ? GCHandleType.WeakTrackResurrection : GCHandleType.Normal); var thisHandlePtr = GCHandle.ToIntPtr(thisHandle); _nativeAssemblyLoadContext = InitializeAssemblyLoadContext(thisHandlePtr, representsTPALoadContext, isCollectible); // Add this instance to the list of alive ALC lock (s_allContexts) { _id = s_nextId++; s_allContexts.Add(_id, new WeakReference(this, true)); } } ~AssemblyLoadContext() { // Use the _unloadLock as a guard to detect the corner case when the constructor of the AssemblyLoadContext was not executed // e.g. due to the JIT failing to JIT it. if (_unloadLock != null) { // Only valid for a Collectible ALC. Non-collectible ALCs have the finalizer suppressed. Debug.Assert(IsCollectible); // We get here only in case the explicit Unload was not initiated. Debug.Assert(_state != InternalState.Unloading); InitiateUnload(); } } private void RaiseUnloadEvent() { // Ensure that we raise the Unload event only once Interlocked.Exchange(ref _unloading, null!)?.Invoke(this); } private void InitiateUnload() { RaiseUnloadEvent(); // When in Unloading state, we are not supposed to be called on the finalizer // as the native side is holding a strong reference after calling Unload lock (_unloadLock) { Debug.Assert(_state == InternalState.Alive); var thisStrongHandle = GCHandle.Alloc(this, GCHandleType.Normal); var thisStrongHandlePtr = GCHandle.ToIntPtr(thisStrongHandle); // The underlying code will transform the original weak handle // created by InitializeLoadContext to a strong handle PrepareForAssemblyLoadContextRelease(_nativeAssemblyLoadContext, thisStrongHandlePtr); _state = InternalState.Unloading; } lock (s_allContexts) { s_allContexts.Remove(_id); } } public IEnumerable Assemblies { get { foreach (Assembly a in GetLoadedAssemblies()) { AssemblyLoadContext? alc = GetLoadContext(a); if (alc == this) { yield return a; } } } } // Event handler for resolving native libraries. // This event is raised if the native library could not be resolved via // the default resolution logic [including AssemblyLoadContext.LoadUnmanagedDll()] // // Inputs: Invoking assembly, and library name to resolve // Returns: A handle to the loaded native library public event Func ResolvingUnmanagedDll // TODO-NULLABLE: Should all events use nullable delegate types? { add { _resolvingUnmanagedDll += value; } remove { _resolvingUnmanagedDll -= value; } } // Event handler for resolving managed assemblies. // This event is raised if the managed assembly could not be resolved via // the default resolution logic [including AssemblyLoadContext.Load()] // // Inputs: The AssemblyLoadContext and AssemblyName to be loaded // Returns: The Loaded assembly object. public event Func Resolving // TODO-NULLABLE: Should all events use nullable delegate types? { add { _resolving += value; } remove { _resolving -= value; } } public event Action Unloading // TODO-NULLABLE: Should all events use nullable delegate types? { add { _unloading += value; } remove { _unloading -= value; } } #region AppDomainEvents // Occurs when an Assembly is loaded internal static event AssemblyLoadEventHandler AssemblyLoad; // TODO-NULLABLE: Should all events use nullable delegate types? // Occurs when resolution of type fails internal static event ResolveEventHandler TypeResolve; // TODO-NULLABLE: Should all events use nullable delegate types? // Occurs when resolution of resource fails internal static event ResolveEventHandler ResourceResolve; // TODO-NULLABLE: Should all events use nullable delegate types? // Occurs when resolution of assembly fails // This event is fired after resolve events of AssemblyLoadContext fails internal static event ResolveEventHandler AssemblyResolve; // TODO-NULLABLE: Should all events use nullable delegate types? #endregion public static AssemblyLoadContext Default => DefaultAssemblyLoadContext.s_loadContext; public bool IsCollectible { get { return _isCollectible;} } public string? Name { get { return _name;} } public override string ToString() => "\"" + Name + "\" " + GetType().ToString() + " #" + _id; public static IEnumerable All { get { _ = AssemblyLoadContext.Default; // Ensure default is initialized List>? alcList = null; lock (s_allContexts) { // To make this thread safe we need a quick snapshot while locked alcList = new List>(s_allContexts.Values); } foreach (WeakReference weakAlc in alcList) { if (weakAlc.TryGetTarget(out AssemblyLoadContext? alc)) { yield return alc; } } } } // Helper to return AssemblyName corresponding to the path of an IL assembly public static AssemblyName GetAssemblyName(string assemblyPath) { if (assemblyPath == null) { throw new ArgumentNullException(nameof(assemblyPath)); } return AssemblyName.GetAssemblyName(assemblyPath); } // Custom AssemblyLoadContext implementations can override this // method to perform custom processing and use one of the protected // helpers above to load the assembly. protected virtual Assembly? Load(AssemblyName assemblyName) { return null; } #if !CORERT [System.Security.DynamicSecurityMethod] // Methods containing StackCrawlMark local var has to be marked DynamicSecurityMethod public Assembly LoadFromAssemblyName(AssemblyName assemblyName) { if (assemblyName == null) throw new ArgumentNullException(nameof(assemblyName)); // Attempt to load the assembly, using the same ordering as static load, in the current load context. StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; return Assembly.Load(assemblyName, ref stackMark, this); } #endif // These methods load assemblies into the current AssemblyLoadContext // They may be used in the implementation of an AssemblyLoadContext derivation public Assembly LoadFromAssemblyPath(string assemblyPath) { if (assemblyPath == null) { throw new ArgumentNullException(nameof(assemblyPath)); } if (PathInternal.IsPartiallyQualified(assemblyPath)) { throw new ArgumentException(SR.Argument_AbsolutePathRequired, nameof(assemblyPath)); } lock (_unloadLock) { VerifyIsAlive(); return InternalLoadFromPath(assemblyPath, null); } } public Assembly LoadFromNativeImagePath(string nativeImagePath, string? assemblyPath) { if (nativeImagePath == null) { throw new ArgumentNullException(nameof(nativeImagePath)); } if (PathInternal.IsPartiallyQualified(nativeImagePath)) { throw new ArgumentException(SR.Argument_AbsolutePathRequired, nameof(nativeImagePath)); } if (assemblyPath != null && PathInternal.IsPartiallyQualified(assemblyPath)) { throw new ArgumentException(SR.Argument_AbsolutePathRequired, nameof(assemblyPath)); } lock (_unloadLock) { VerifyIsAlive(); return InternalLoadFromPath(assemblyPath, nativeImagePath); } } public Assembly LoadFromStream(Stream assembly) { return LoadFromStream(assembly, null); } public Assembly LoadFromStream(Stream assembly, Stream? assemblySymbols) { if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); } int iAssemblyStreamLength = (int)assembly.Length; if (iAssemblyStreamLength <= 0) { throw new BadImageFormatException(SR.BadImageFormat_BadILFormat); } // Allocate the byte[] to hold the assembly byte[] arrAssembly = new byte[iAssemblyStreamLength]; // Copy the assembly to the byte array assembly.Read(arrAssembly, 0, iAssemblyStreamLength); // Get the symbol stream in byte[] if provided byte[]? arrSymbols = null; if (assemblySymbols != null) { var iSymbolLength = (int)assemblySymbols.Length; arrSymbols = new byte[iSymbolLength]; assemblySymbols.Read(arrSymbols, 0, iSymbolLength); } lock (_unloadLock) { VerifyIsAlive(); return InternalLoad(arrAssembly, arrSymbols); } } // This method provides a way for overriders of LoadUnmanagedDll() to load an unmanaged DLL from a specific path in a // platform-independent way. The DLL is loaded with default load flags. protected IntPtr LoadUnmanagedDllFromPath(string unmanagedDllPath) { if (unmanagedDllPath == null) { throw new ArgumentNullException(nameof(unmanagedDllPath)); } if (unmanagedDllPath.Length == 0) { throw new ArgumentException(SR.Argument_EmptyPath, nameof(unmanagedDllPath)); } if (PathInternal.IsPartiallyQualified(unmanagedDllPath)) { throw new ArgumentException(SR.Argument_AbsolutePathRequired, nameof(unmanagedDllPath)); } return InternalLoadUnmanagedDllFromPath(unmanagedDllPath); } // Custom AssemblyLoadContext implementations can override this // method to perform the load of unmanaged native dll // This function needs to return the HMODULE of the dll it loads protected virtual IntPtr LoadUnmanagedDll(string unmanagedDllName) { //defer to default coreclr policy of loading unmanaged dll return IntPtr.Zero; } public void Unload() { if (!IsCollectible) { throw new InvalidOperationException(SR.AssemblyLoadContext_Unload_CannotUnloadIfNotCollectible); } GC.SuppressFinalize(this); InitiateUnload(); } internal static void OnProcessExit() { lock (s_allContexts) { foreach (var alcAlive in s_allContexts) { if (alcAlive.Value.TryGetTarget(out AssemblyLoadContext? alc)) { alc.RaiseUnloadEvent(); } } } } private void VerifyIsAlive() { if (_state != InternalState.Alive) { throw new InvalidOperationException(SR.AssemblyLoadContext_Verify_NotUnloading); } } private static AsyncLocal? s_asyncLocalCurrent; /// Nullable current AssemblyLoadContext used for context sensitive reflection APIs /// /// This is an advanced setting used in reflection assembly loading scenarios. /// /// There are a set of contextual reflection APIs which load managed assemblies through an inferred AssemblyLoadContext. /// * /// * /// * /// * /// /// When CurrentContextualReflectionContext is null, the AssemblyLoadContext is inferred. /// The inference logic is simple. /// * For static methods, it is the AssemblyLoadContext which loaded the method caller's assembly. /// * For instance methods, it is the AssemblyLoadContext which loaded the instance's assembly. /// /// When this property is set, the CurrentContextualReflectionContext value is used by these contextual reflection APIs for loading. /// /// This property is typically set in a using block by /// . /// /// The property is stored in an AsyncLocal<AssemblyLoadContext>. This means the setting can be unique for every async or thread in the process. /// /// For more details see https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md /// public static AssemblyLoadContext? CurrentContextualReflectionContext { get { return s_asyncLocalCurrent?.Value; } } private static void SetCurrentContextualReflectionContext(AssemblyLoadContext? value) { if (s_asyncLocalCurrent == null) { Interlocked.CompareExchange?>(ref s_asyncLocalCurrent, new AsyncLocal(), null); } s_asyncLocalCurrent!.Value = value; // Remove ! when compiler specially-recognizes CompareExchange for nullability } /// Enter scope using this AssemblyLoadContext for ContextualReflection /// A disposable ContextualReflectionScope for use in a using block /// /// Sets CurrentContextualReflectionContext to this instance. /// /// /// Returns a disposable ContextualReflectionScope for use in a using block. When the using calls the /// Dispose() method, it restores the ContextualReflectionScope to its previous value. /// public ContextualReflectionScope EnterContextualReflection() { return new ContextualReflectionScope(this); } /// Enter scope using this AssemblyLoadContext for ContextualReflection /// Set CurrentContextualReflectionContext to the AssemblyLoadContext which loaded activating. /// A disposable ContextualReflectionScope for use in a using block /// /// Sets CurrentContextualReflectionContext to to the AssemblyLoadContext which loaded activating. /// /// /// Returns a disposable ContextualReflectionScope for use in a using block. When the using calls the /// Dispose() method, it restores the ContextualReflectionScope to its previous value. /// public static ContextualReflectionScope EnterContextualReflection(Assembly? activating) { if (activating == null) return new ContextualReflectionScope(null); AssemblyLoadContext? assemblyLoadContext = GetLoadContext(activating); if (assemblyLoadContext == null) { // All RuntimeAssemblies & Only RuntimeAssemblies have an AssemblyLoadContext throw new ArgumentException(SR.Arg_MustBeRuntimeAssembly, nameof(activating)); } return assemblyLoadContext.EnterContextualReflection(); } /// Opaque disposable struct used to restore CurrentContextualReflectionContext /// /// This is an implmentation detail of the AssemblyLoadContext.EnterContextualReflection APIs. /// It is a struct, to avoid heap allocation. /// It is required to be public to avoid boxing. /// /// [EditorBrowsable(EditorBrowsableState.Never)] public struct ContextualReflectionScope : IDisposable { private readonly AssemblyLoadContext? _activated; private readonly AssemblyLoadContext? _predecessor; private readonly bool _initialized; internal ContextualReflectionScope(AssemblyLoadContext? activating) { _predecessor = AssemblyLoadContext.CurrentContextualReflectionContext; AssemblyLoadContext.SetCurrentContextualReflectionContext(activating); _activated = activating; _initialized = true; } public void Dispose() { if (_initialized) { // Do not clear initialized. Always restore the _predecessor in Dispose() // _initialized = false; AssemblyLoadContext.SetCurrentContextualReflectionContext(_predecessor); } } } private Assembly? ResolveSatelliteAssembly(AssemblyName assemblyName) { // Called by native runtime when CultureName is not empty Debug.Assert(assemblyName.CultureName?.Length > 0); string satelliteSuffix = ".resources"; if (assemblyName.Name == null || !assemblyName.Name.EndsWith(satelliteSuffix, StringComparison.Ordinal)) return null; string parentAssemblyName = assemblyName.Name.Substring(0, assemblyName.Name.Length - satelliteSuffix.Length); Assembly parentAssembly = LoadFromAssemblyName(new AssemblyName(parentAssemblyName)); AssemblyLoadContext parentALC = GetLoadContext(parentAssembly)!; string parentDirectory = Path.GetDirectoryName(parentAssembly.Location)!; string assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!, $"{assemblyName.Name}.dll"); if (Internal.IO.File.InternalExists(assemblyPath)) { return parentALC.LoadFromAssemblyPath(assemblyPath); } else if (Path.IsCaseSensitive) { assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!.ToLowerInvariant(), $"{assemblyName.Name}.dll"); if (Internal.IO.File.InternalExists(assemblyPath)) { return parentALC.LoadFromAssemblyPath(assemblyPath); } } return null; } } internal sealed class DefaultAssemblyLoadContext : AssemblyLoadContext { internal static readonly AssemblyLoadContext s_loadContext = new DefaultAssemblyLoadContext(); internal DefaultAssemblyLoadContext() : base(true, false, "Default") { } } internal sealed class IndividualAssemblyLoadContext : AssemblyLoadContext { internal IndividualAssemblyLoadContext(string name) : base(false, false, name) { } } }