Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AssemblyLoadContext cant initialize in WPF application method. #47704

Closed
Reknij opened this issue Feb 1, 2021 · 14 comments
Closed

AssemblyLoadContext cant initialize in WPF application method. #47704

Reknij opened this issue Feb 1, 2021 · 14 comments
Labels
area-AssemblyLoader-coreclr untriaged New issue has not been triaged by the area owner

Comments

@Reknij
Copy link

Reknij commented Feb 1, 2021

Framework: .Net 5
This is code from microsoft docs. Please note the comments in the code

class TestAssemblyLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;

    public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
    }

    protected override Assembly Load(AssemblyName name)
    {
        string assemblyPath = _resolver.ResolveAssemblyToPath(name);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }
        return null;
    }
}

This is the loading and unloading method (I modified the code from microsoft doc)

private static string pluginPath = @"C:\PluginFolder\MyPlugin.dll"; //interface is IPlugin
private static TestAssemblyLoadContext talc; //the assemblyloadcontext
[MethodImpl(MethodImplOptions.NoInlining)]
static void LoadIt(string assemblyPath)
{
    talc = new TestAssemblyLoadContext(pluginPath); //create instance
    Assembly a = talc.LoadFromAssemblyPath(assemblyPath); 
    //assemblyPath cant delete now because is loaded
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void UnloadIt()
{
    talc.Unload();
    for (int i = 0; i < 10; i++)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
    //if success unload, file on assemblyPath can success delete.
    File.Delete(assemblyPath);
}

This is part of a wpf program code

public MainWindow()
{
    InitializeComponent();

    LoadIt(pluginPath); //loaded assembly
    UnloadIt(pluginPath); //success unload assembly, file assembly can delete.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

If the unloading code is not together with the loading code

public MainWindow()
{
    InitializeComponent();

    LoadIt(pluginPath); //loaded assembly
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    UnloadIt(pluginPath); //unsuccessful. File assembly cant delete.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

But if the loading code is not called in the Wpf initialization function "MainWindow()" or wpf application event e.g. Loaded event, ContentRendered event, Activated event, it will work.

public MainWindow()
{
    InitializeComponent();
}

private void LoadButton_Click(object sender, RoutedEventArgs e)
{
    LoadIt(pluginPath); //load plugin with button click event.
}

private void UnloadButton_Click(object sender, RoutedEventArgs e)
{
    UnloadIt(pluginPath) //It successful unload, assembly file can be deleted.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

Can anyone tell me why? Could this be a bug?

Note: I have not used the LoadIt function elsewhere. Use it only once.
In order to confirm whether it is a problem with my project, I put the TestAssemblyLoadContext class, LoadIt function and UnloadIt function into the new WpfApp to test and get exactly the same result.

@dotnet-issue-labeler dotnet-issue-labeler bot added area-AssemblyLoader-coreclr untriaged New issue has not been triaged by the area owner labels Feb 1, 2021
@ghost
Copy link

ghost commented Feb 1, 2021

Tagging subscribers to this area: @vitek-karas, @agocke, @CoffeeFlux
See info in area-owners.md if you want to be subscribed.

Issue Details

Framework: .Net 5
This is code from microsoft docs. Please note the comments in the code

class TestAssemblyLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;

    public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
    }

    protected override Assembly Load(AssemblyName name)
    {
        string assemblyPath = _resolver.ResolveAssemblyToPath(name);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }
        return null;
    }
}

This is the loading and unloading method (I modified the code from microsoft doc)

private static string pluginPath = @"C:\PluginFolder\MyPlugin.dll"; //interface is IPlugin
private static TestAssemblyLoadContext talc; //the assemblyloadcontext
[MethodImpl(MethodImplOptions.NoInlining)]
static void LoadIt(string assemblyPath)
{
    talc = new TestAssemblyLoadContext(pluginPath); //create instance
    Assembly a = talc.LoadFromAssemblyPath(assemblyPath); 
    //assemblyPath cant delete now because is loaded
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void UnloadIt()
{
    talc.Unload();
    for (int i = 0; i < 10; i++)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
    //if success unload, file on assemblyPath can success delete.
    File.Delete(assemblyPath);
}

This is part of a wpf program code

public MainWindow()
{
    InitializeComponent();

    LoadIt(pluginPath); //loaded assembly
    UnloadIt(pluginPath); //success unload assembly, file assembly can delete.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

If the unloading code is not together with the loading code

public MainWindow()
{
    InitializeComponent();

    LoadIt(pluginPath); //loaded assembly
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    UnloadIt(pluginPath); //unsuccessful. File assembly cant delete.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

But if the loading code is not called in the Wpf initialization function "MainWindow()" or wpf application event e.g. Loaded event, ContentRendered event, Activated event, it will work.

public MainWindow()
{
    InitializeComponent();
}

private void LoadButton_Click(object sender, RoutedEventArgs e)
{
    LoadIt(pluginPath); //load plugin with button click event.
}

private void UnloadButton_Click(object sender, RoutedEventArgs e)
{
    UnloadIt(pluginPath) //It successful unload, assembly file can be deleted.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

Can anyone tell me why? Could this be a bug?

Note: I have not used the LoadIt function elsewhere. Use it only once.
In order to confirm whether it is a problem with my project, I put the TestAssemblyLoadContext class, LoadIt function and UnloadIt function into the new WpfApp to test and get exactly the same result.

Author: JinkerLeong
Assignees: -
Labels:

area-AssemblyLoader-coreclr, untriaged

Milestone: -

@vitek-karas
Copy link
Member

In .NET Core unloadability is "cooperative", it means the program needs to release all references to objects and types from the assemblies to be unloaded. So it's not just about the code driving the load/unload, but also about the plugin itself. For example if the plugin registers a callback on the application object, there will be a reference from the application object to the plugin - which will keep the plugin around and it won't unload. So the while the code above looks OK, the plugin can do all kinds of things which will prevent unload from being successful.

There are ways to debug this - see the guide here: https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability#debug-unloading-issues

There could also be issues with the framework itself (both WPF as well as the core framework) which might prevent the plugin from unloading. A typical example are components which hold on to global caches - the cache would then hold the reference to the plugin. If you find that to be the case here, please create an issue so that we can track/fix it.

@Reknij
Copy link
Author

Reknij commented Feb 1, 2021

In .NET Core unloadability is "cooperative", it means the program needs to release all references to objects and types from the assemblies to be unloaded. So it's not just about the code driving the load/unload, but also about the plugin itself. For example if the plugin registers a callback on the application object, there will be a reference from the application object to the plugin - which will keep the plugin around and it won't unload. So the while the code above looks OK, the plugin can do all kinds of things which will prevent unload from being successful.

There are ways to debug this - see the guide here: https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability#debug-unloading-issues

There could also be issues with the framework itself (both WPF as well as the core framework) which might prevent the plugin from unloading. A typical example are components which hold on to global caches - the cache would then hold the reference to the plugin. If you find that to be the case here, please create an issue so that we can track/fix it.

I'm pretty sure there is no problem with my plugin, I tested it before. Plugin interface like this

public interface IPlugin
{
     string result { get; }
}

The problem now is that if I load window-related functions, I cannot unload AssemblyLoadContext in other functions.

I tested loading AssemblyLoadContext in window-related functions, and then unloading, there is no problem. This is what bothers me.

Therefore, I think that if the AssemblyLoadContext is loaded into a window-related function, it will cause some unexpected results.

@vitek-karas
Copy link
Member

The plugin interface itself is not very telling. It depends what it actually does when it executes.

But you're right in general - WPF has issues when used from non-default load contexts. Some of it can be worked around by using contextual reflection overrides: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext.entercontextualreflection?view=net-5.0. Also see the design doc for this feature here: https://github.com/dotnet/runtime/blob/master/docs/design/features/AssemblyLoadContext.ContextualReflection.md.

Unloading is basically yet another set of requirement on top of correct secondary load context support. It's very possible that WPF has some issues in this area as well.

@netcorefan1
Copy link

I found this thread after searching on possible solutions when not all expected assemblies are unloaded.
Time ago I also wanted to do some "wpf load and unload on demand", but it turned out that it is not a supported scenario (and not something expected to be supported in the future). Not sure if it also involve WinForms or any other frameworks.
I suppose this is still valid with .Net 5, right?
This is the link to the issue where you can find more info.

@Reknij
Copy link
Author

Reknij commented Feb 2, 2021

I gradually found possible solutions with the help of vitek-karas, but failed.
Then, I found that changing the loading method in the code can solve my needs.
I mainly load the plug-in when opening the software, and then I can uninstall it, as shown in my question
Change the following code:

static void LoadIt(string assemblyPath)
{
    talc = new TestAssemblyLoadContext(pluginPath); //create instance
    Assembly a = talc.LoadFromAssemblyPath(assemblyPath); 
    //assemblyPath cant delete now because is loaded
}

to

static void LoadIt(string assemblyPath)
{
    talc = new TestAssemblyLoadContext(pluginPath); //create instance
    using (FileStream fs=  new FileStream(pluginPath, FileMode.Open))
    {
        Assembly a = talc.LoadFromStream(fs);
        //assemblyPath successful delete now!
        //Because it load from stream no the assembly file.
    }
}

Use the code to test in MainWindow()

public MainWindow()
{
    InitializeComponent();

    LoadIt(pluginPath); //loaded assembly
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    UnloadIt(pluginPath); //successful. File assembly delete work.
}

private static TestAssemblyLoadContext talc;
LoadIt(){...}
UnloadIt(){...}

So far, my software is running well, and there are no problems loading and unloading.

@netcorefan1
Copy link

Are you trying to just unload your plugin or your plugin along with the wpf assemblies that your plugin loaded? Because in the first case your plugin should unload just fine, but the wpf assemblies will stay loaded for the full lifetime of your application.
@vitek-karas may correct me If I am wrong, but this was when I reported this problem with 3.1 and I suppose should also apply to .Net 5.

@vitek-karas
Copy link
Member

Because in the first case your plugin should unload just fine

I wish this were true. Unfortunately the frameworks are still full of issues which cause plugins to fail to unload (hold onto references of things in the plugin). There's a story tracking the work to make frameworks work well with secondary load contexts: #43544.

@netcorefan1
Copy link

It seems to be that the situation became worse over the time. Now Wpf tries to keep more assemblies as possible, included the plugin. But not only Wpf, in my case the following assemblies loaded from a third party assembly failed to unload:

System.Runtime.InteropServices.RuntimeInformation.dll
System.Linq.dll
System.Collections.dll
netstandard.dll
System.Collections.Concurrent.dll
Microsoft.Win32.Primitives.dll
System.IO.FileSystem.dll
System.Memory.dll
System.Runtime.CompilerServices.Unsafe.dll
System.Threading.dll

My plugin and most of the other assemblies unload fine. Do you know if the assemblies in this list are problematic? I suspect .netstandard can play a role. I also suspect that the third party app does not fully cooperate and may keep some reference (even if all of their main assemblies are successfully unloaded). Is there a way to force unload when cooperation is not 100% full?

@vitek-karas
Copy link
Member

All these assemblies are framework assemblies - there's no way to unload them. I don't know how you load them, but the only really supported configuration is to load them to the default load context - which means they can't unload.

As discussed in some other issues, there's no support for unloading frameworks (either as a whole or per-assembly). In fact there's no support to "load" frameworks dynamically either (if you start with non-WindowsDesktop app, there's no way to add WindowsDesktop framework to it).

If you measure "unload" by listing all assemblies loaded into the process and before/after comparison - that is not very telling. Framework assemblies are lazy loaded (just like anything else), so the fact that the plugin caused the assembly to load doesn't mean anything but that it was the first to need it - it might be that the host would eventually load it anyway.

Is there a way to force unload when cooperation is not 100% full?

No. And this is by design. Force unloading assemblies is VERY tricky business. .NET Framework did implement it - it was very expensive to do, and even after multiple releases and endless bug fixing it still wasn't 100% - it could lead to data corruption or random crashes in the extreme cases. (Just as an example, what do you do if the assembly in question is running code on some thread and that thread is stuck in native code - how do you "force" that to end without potential data corruption).

Cooperative unload is also problematic (as seen by this issue for example), but at least it doesn't have really bad consequences (data corruption/crashes). The current unload implementation in the runtime basically guarantees no corruption of anything - but the compromise is that it doesn't guarantee unload.

@Reknij
Copy link
Author

Reknij commented Feb 3, 2021

Are you trying to just unload your plugin or your plugin along with the wpf assemblies that your plugin loaded?

I just uninstalled the plugin, not the wpf assembly, as @vitek-karas said, they are all loaded into AssemblyLoadContext.Default.
My problem is that when creating the context and loading the assembly in the relevant function of the Wpf program, there will be a problem, as if the assembly file seems to be locked.

I think my program should have successfully uninstalled them. I tested it like this (The code used is the modified code shown above):

  1. Record the memory occupied by the wpf program without any plugins loaded
  2. In the relevant functions of the WPF program, load 2000 sample plugins to their respective AssemblyLoadContext, and record the memory occupied after loading. At this time, the memory usage has increased a lot
  3. Unload them all, and the occupied memory is reduced to the state when no plugins is loaded

The program did not reopen in the above test.

Maybe I am not strict with this test because I don't know how to test it. But I think this should successfully uninstall the assembly.

@netcorefan1
Copy link

@JinkerLeong Well, in your specific case you need to constantly run multiple plugins at the same time, therefore it makes perfectly sense to leave WPF loaded. Regarding the locked files, I recently found a thread (not sure where, but I believe in this repo) where this problem could be related to visual studio debugger locking assemblies. I am not sure if they have already fixed this, but you may want to retry without debugger attached.

@vitek-karas Sometimes I ask myself if there are some assemblies that are not framework assemblies except for the ones represented from an empty dll project. Eh eh
The way I load them is the most basic one (I am not sure if I should do something more advanced). I don't specifically call them separately, but I reference a third party lib that cause them to be loaded.

var ctx = new CustomContext(dllPath)
var ass = ctx.LoadFromAssemblyPath(dllPath);
var type = ass.GetType("NameSpace.Class1");
var mi = type?.GetMethod("MyMethod");
var inst = Activator.CreateInstance(type);
mi.Invoke(inst, Array.Empty<object>());

public class CustomContext : AssemblyLoadContext
{
        private readonly AssemblyDependencyResolver _resolver;
        public CustomContext(string path) : base(true)
        {
            _resolver = new AssemblyDependencyResolver(path);
        }
            
        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }

            return IntPtr.Zero;
        }
}

I am trying to do a complete switch to .Net 5 and eliminate any dependency from the old .Net Framework, but I am afraid that .net standard is the bridge that is causing the whole runtime loaded.

If you measure "unload" by listing all assemblies loaded into the process and before/after comparison - that is not very telling.
Framework assemblies are lazy loaded (just like anything else), so the fact that the plugin caused the assembly to load doesn't mean anything but that it was the first to need it - it might be that the host would eventually load it anyway.

Yes, this is something that I should never forget. It's highly likely that a good number of unloaded assemblies are needed from the client app anyway. My mistake is the wrong assumption that when an assembly is fully unloaded, then "everything is good, clean and without any memory left here and there".

No. And this is by design. Force unloading assemblies is VERY tricky business. .NET Framework did implement it - it was very expensive to do, and even after multiple releases and endless bug fixing it still wasn't 100% - it could lead to data corruption or random crashes in the extreme cases. (Just as an example, what do you do if the assembly in question is running code on some thread and that thread is stuck in native code - how do you "force" that to end without potential data corruption).

Cooperative unload is also problematic (as seen by this issue for example), but at least it doesn't have really bad consequences (data corruption/crashes). The current unload implementation in the runtime basically guarantees no corruption of anything - but the compromise is that it doesn't guarantee unload.

I totally agree. It's just like real humans. Forcing for a full cooperation can result in a total lack of cooperation.

@Reknij
Copy link
Author

Reknij commented Feb 3, 2021

Regarding the locked files, I recently found a thread (not sure where, but I believe in this repo) where this problem could be related to visual studio debugger locking assemblies. I am not sure if they have already fixed this, but you may want to retry without debugger attached.

Ok, when I tried to run the release, the error locking the assembly file disappeared. This surprised me. I didn't expect the problem to be here.

@Reknij Reknij closed this as completed Feb 3, 2021
@vitek-karas
Copy link
Member

My mistake is the wrong assumption that when an assembly is fully unloaded, then "everything is good, clean and without any memory left here and there".

Maybe a better way to think about it in terms of load contexts (that's how it's implemented). Your statement above should be true when speaking about things loaded into the unloaded context. If there are any memory (or other) leaks from it, then it's a bug which should be fixed.

But the code in the custom load context is not isolated from the rest of the app so it can cause stuff to happen outside of the load context, for example loading more code into some other context (typically Default), increased memory allocation by other components (frameworks sometimes have caches which don't have an eviction policy for example) and so on. Some of these problems are sort of new to .NET Core (loading to Default for example, since .NET Framework had that solved differently), some of them are basically the same as before, but maybe a bit more visible (.NET Framework could "cut" direct references to unloaded code/types, so the caches would still be larger, but the stuff they point to would not longer be there).

@ghost ghost locked as resolved and limited conversation to collaborators Mar 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-AssemblyLoader-coreclr untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

3 participants