Skip to content

Allow default ALC to be unloadable for plugin scenarios #1633

Closed as not planned

Description

@jazzdelightsme

There is a native app; let’s call it Foo.exe. Foo can host arbitrary plugins. I wrote a plugin primarily using managed code, but to get loaded into Foo, I provide a native DLL with certain exports, and then when those exports are called, I load up the CLR and run my managed code. Life was good.

Now .NET Core has come along. I think things have progressed far enough (for example, I was waiting for C++/CLI support) that I could move my code onto .NET Core—after all, that’s the live branch of .NET. However, there's a big snag.

In my old .NET code, I relied on my native host being able to create an alternate AppDomain to load my managed code in. That way, when my plugin got unloaded, I could unload the AppDomain. I had to leave the CLR alive, but my binaries could be deleted, and/or replaced, and/or reloaded.

I have heard that now we have the concept of a “collectible” assembly load context. But what that means is that I will need to have another C# shim DLL that stays loaded permanently, in order to host the “real” DLL in a collectible context, so that it can dump it when desired. And having that shim DLL be “pinned” in place for the lifetime of the process is highly undesirable—it means I can’t actually easily replace my plugin on disk.

In other words, being able to create collectible AssemblyLoadContexts for plugins is great, when the plugin host is a .NET Core application. But if the plugin host is native... it doesn't quite fit the bill.

I think .NET Core should be a viable technology to write add-ins for any application, but to do that credibly, it will need to be able to fully unload the add-ins. Perhaps this could be as simple as providing a way for the hostfxr-created AssemblyLoadContext to be collectible, I don't know. And of course the next-level goal would be to extend unloadability to coreclr itself, and side-by-side (imagine two plugins, each using a different coreclr).

TIA!

Activity

  1. timhe commented on Jan 21, 2020

    @timhe

    WinDbg/WinDbgX debugger extensions could greatly benefit from the ability to unload. The Mex Debugger Extension would like to move to .NET core, but without the ability to unload, it would be a hard sell

  2. jeffschwMSFT commented on Jan 30, 2020

    @jeffschwMSFT
    Member
  3. added this to the Future milestone on Jan 30, 2020
  4. removed
    untriagedNew issue has not been triaged by the area owner
    on Jan 30, 2020
  5. agocke commented on Apr 19, 2024

    @agocke
    Member

    It sounds like the main request here is to be able to unload CoreCLR itself when it is loaded into a native application.

    We don't have any plans to support that scenario. Unloading Native AOT DLLs might be more feasible, but is still not in the backlog. If you're already using Native AOT successfully and think this is feature would heavily improve the experience, I think we can open a new issue for that.

  6. jazzdelightsme commented on Apr 19, 2024

    @jazzdelightsme
    ContributorAuthor

    @agocke You have misunderstood the Issue.

    It sounds like the main request here is to be able to unload CoreCLR itself

    No, that is not the request at all. It has never been possible to unload the CLR, yet I explicitly described how in old .NET Framework I was able to write an unloadable plugin in C# (even though the CLR itself must stay loaded). But in .NET Core, I don't see how it is possible anymore to write an unloadable / reloadable plugin for a native host, because you have to have a managed shim to do the ALC stuff... which then itself cannot be unloaded.

    (ALCs have their own problems, too. 😢)

  7. jazzdelightsme commented on Apr 19, 2024

    @jazzdelightsme
    ContributorAuthor

    Here is the desired workflow for writing a plugin for a native app:

    1. Write some plugin code (in C#, yay).
    2. Build it.
    3. Load it into the native application through whatever its plugin mechanism is.
    4. Test it.
    5. Oops, there was a bug.
    6. Unload the plugin.
    7. Goto step 1.

    Step 6 was possible in .NET Framework, but is no longer possible. When I get to step 2 the second time around and try to build my code, there will be a sharing violation with the shim assembly that does the ALC stuff.

  8. agocke commented on Apr 19, 2024

    @agocke
    Member

    I see, so the request here is to have the "default" ALC be unloadable, that is you want the entry-point assembly for your plugin to be unloadable.

  9. reopened this on Apr 19, 2024
  10. changed the title [-].NET Core can host plugins, but can't BE a plugin[/-] [+]Allow default ALC to be unloadable for plugin scenarios[/+] on Apr 19, 2024
  11. 8 remaining items

  12. jazzdelightsme commented on Apr 24, 2024

    @jazzdelightsme
    ContributorAuthor

    Thanks, @jkotas.

    As I understand it, if a Unity plugin were to host the PowerShell runtime, or any other code that uses Assembly.Load, then the plugin would not be unloadable, because some random set of assemblies would accidentally end up in the default ALC. Is that correct? I suppose they just don't care about that problem? Or perhaps they just don't have any such assemblies--it's all new, all built on .NET, specifically for Unity, and no legacy dependencies? If they are happy, then I'm happy for them, too.

    It is very hard to write code that gracefully recovers from being aborted at any point by non-cooperative appdomain unload.

    I agree (aside: I would go even further and call it fundamentally impossible to do completely, at least on Windows, because on Windows, the process is the reliability boundary). But I don't need or want that feature. I really don't want AppDomains brought back. I just want to be able to unload my plugin, which happened to have called Assembly.Load. (And remember, I can't just rewrite my plugin to not use Assembly.Load, because it's actually the PowerShell runtime that calls Assembly.Load.) And I don't understand why the feeling that asking for this is tantamount to asking for AppDomains to be brought back, or asking for the CLR itself to be unloadable, or any other impossible-or-never-going-to-happen thing.

    People wanted to do plugins, so y'all came up with a way to do plugins (ALCs), great! But the way you did it seems to be that not only the plugin but also all of its downstream dependencies has to have been purpose-written to stay in the correct ALC... and I just don't have the power to rewrite the world like that.

    Is that actually what you think is the right course of action? That I should propose to the PowerShell team that they should update their code to become ALC-aware? And any other similar dependencies? (It seems like it would benefit a lot more people with a lot less effort and a lot less bugs to make ALCs work for everyone without requiring them all to change code.)

  13. jkotas commented on Apr 24, 2024

    @jkotas
    Member

    any other code that uses Assembly.Load, then the plugin would not be unloadable,

    Assembly.Load(string) is not a problematic API for unloadability. This API gets the ALC of its caller and forwards the request to it. It is then up to that ALC to decide whether to load the dependency as unloadable or not. (For example, the BCL assemblies should be loaded as unloadable into the default ALC.)

    There are other APIs that are problematic like Assembly.LoadFrom that have to be avoided in unloadable plugins. I would not call it purposely written. The amount of the code that needs to be aware of ALCs and unloadability is typically very small.

  14. jkotas commented on Apr 25, 2024

    @jkotas
    Member

    That I should propose to the PowerShell team that they should update their code to become ALC-aware?

    Yes. It is up to every package maintainer to decide whether it makes sense to invest into making their package ALC-aware and unloadable.

    It is very similar to supporting multiple OSes. Some packages work great on all OSes. Some packages are Windows-specific and the maintainers are not interested investing into support for non-Windows OSes.

  15. agocke commented on Apr 25, 2024

    @agocke
    Member

    Ok, it sounds like this issue and the linked one are both asking for a feature of “non-cooperative” assembly unload. I agree with @jkotas — we will likely never build such a feature. I’m going to close both of these issues to signal the appropriate guidance.

    In .NET core, all plugins and their dependencies must be safe for unloading if they want to support unloading.

  16. jazzdelightsme commented on Apr 25, 2024

    @jazzdelightsme
    ContributorAuthor

    Assembly.Load(string) is not a problematic API for unloadability.

    @jkotas: yes, yes; I assumed you would understand what I meant. What I meant was "the Assembly load methods that for whatever reason have been defined as loading stuff into the default ALC--you know, the ones that are causing all the problems here."

    It is very similar to supporting multiple OSes.

    Yes, but it sure would be nice if a .NET change made a whole bunch more stuff "just work" on other OSes.

    it sounds like this issue and the linked one are both asking for a feature of “non-cooperative” assembly unload.

    @agocke: I don't understand where the disconnect is here: I absolutely do not want non-cooperative unload. (In fact, I went so far as to call it impossible to do completely; AppDomains were doomed in that respect from day 1!)

    It is frustrating that it feels like you keep misunderstanding what I am asking for. I do not want to unload the CLR. I do not want AppDomains brought back. I do not want non-cooperative unload.

    If you came and explained "look, here is why Assembly.LoadFile has to load into the Default ALC; there is no alternate design that would work", that would be helpful. I've read the design docs (this one and this one), and I'm not seeing why that would be. (In fact the design of the "contextual reflection" stuff seems directly aimed at my problem, but what I didn't see explained was why e.g. Assembly.LoadFile is not updated to use this facility, to just automatically use the ALC of the calling assembly if there isn't a different one set.)

    Or if you said there are no resources or not enough customers that want what I'm asking for, or something like that, that would be understandable, too.

    But I don't want this closed out because you think I'm asking for something different. So for the record, again: I do not want non-cooperative unload.

    All that said, I appreciate your transparency in your intention to not do anything about this.

  17. jkotas commented on Apr 25, 2024

    @jkotas
    Member

    Assembly.LoadFrom and Assembly.LoadFile APIs exist for compatibility with .NET Framework, to make most of the .NET Framework libraries that use these APIs work. @agocke has a pending doc update to make it clear https://github.com/dotnet/dotnet-api-docs/pull/9764/files#diff-f317cbaa519e4a60571d2ab44a4fb991bbfd90941c69552f5eb3ddb6c2739454R4636 .

    There are many ways that this compat layer is imperfect. We are not interested in churning the behavior of these API in .NET Core. Any change in behavior of these APIs is a breaking change.

    here is why Assembly.LoadFile has to load into the Default ALC

    Nit: Assembly.LoadFile does not load into the Default ALC. It loads into its own isolated non-collectible ALC that is the best approximation of the .NET Framework behavior of this API.

  18. agocke commented on Apr 25, 2024

    @agocke
    Member

    It is frustrating that it feels like you keep misunderstanding what I am asking for. I do not want to unload the CLR. I do not want AppDomains brought back. I do not want non-cooperative unload.

    You are misunderstanding what I mean by non-cooperative unload. I mean that you want to be able to unload assemblies which may load other assemblies that have not been modified to be "Unload-compatible." Modifying assemblies to only use unload-safe APIs and architectures is what I mean by cooperation.

    The problem that you're hitting is precisely that one of the dependencies of your library (powershell) is non-cooperative in assembly unload. We will not support that.

  19. jazzdelightsme commented on Apr 25, 2024

    @jazzdelightsme
    ContributorAuthor

    I see, thanks for the explanation. What I did not (still do not) understand is why certain APIs were deemed unload-UNsafe in the first place. PowerShell is not doing anything "crazy"; just loading an assembly. But, according to Jan, that design is basically frozen, so nothing to do about it now.

  20. agocke commented on Apr 25, 2024

    @agocke
    Member

    The fundamental limitation on unloadability is that there can be no live GC references into the assembly that you want to unload. There are many APIs which could cause this, for instance a static cache of types could have a type from a collectible assembly added to it, and unless that cache is cleared, the target assembly could not unload.

    Assembly.LoadFile is a good example of the problem here. If you load an assembly using LoadFile it loads into a non-collectible ALC. If you pass a reference to any type into that ALC, and it's then stored, then you will keep an object reference alive in the unloadable ALC, and that will keep your original ALC loaded.

    This is why targets need to be "ALC-aware." There are some operations which can create static roots, and those assemblies need to be careful not to use them.

  21. jazzdelightsme commented on Apr 27, 2024

    @jazzdelightsme
    ContributorAuthor

    What you describe (type references crossing ALC boundaries) makes total sense. What does NOT make sense (to me) is why the design of ALCs (which you do not plan to change, so this is an academic discussion at this point, but hopefully will serve as a guidepost for some future technology decisions) made a bunch of existing "self-contained" code (PowerShell does not deliberately call into any other AppDomain or ALC or other such concept) suddenly become crossing these lines, by definition, by defining LoadFile to cross an ALC boundary. I'm sure there was a reason; I just don't like it, because it "moved the lines" for existing code (and, for me, in a way that I can't effectively deal with it).

  22. jkotas commented on Apr 28, 2024

    @jkotas
    Member

    by defining LoadFile to cross an ALC boundary

    Assembly.LoadFile is not part of the core ALC design. The only reason why this API exists is compatibility with .NET Framework.

    The implementation of Assembly.LoadFile is quite simple:

    public static Assembly LoadFile(string path)
    {
    ArgumentNullException.ThrowIfNull(path);
    if (PathInternal.IsPartiallyQualified(path))
    {
    throw new ArgumentException(SR.Format(SR.Argument_AbsolutePathRequired, path), nameof(path));
    }
    string normalizedPath = Path.GetFullPath(path);
    Assembly? result;
    lock (s_loadfile)
    {
    if (s_loadfile.TryGetValue(normalizedPath, out result))
    return result;
    // we cannot check for file presence on BROWSER. The files could be embedded and not physically present.
    #if !TARGET_BROWSER && !TARGET_WASI
    if (!File.Exists(normalizedPath))
    throw new FileNotFoundException(SR.Format(SR.FileNotFound_LoadFile, normalizedPath), normalizedPath);
    #endif // !TARGET_BROWSER && !TARGET_WASI
    AssemblyLoadContext alc = new IndividualAssemblyLoadContext($"Assembly.LoadFile({normalizedPath})");
    result = alc.LoadFromAssemblyPath(normalizedPath);
    s_loadfile.Add(normalizedPath, result);
    }
    return result;
    }
    . How would you change the implementation of Assembly.LoadFile to address your issue while keeping the behavior compatible with .NET Framework as much as possible?

  23. jazzdelightsme commented on Apr 29, 2024

    @jazzdelightsme
    ContributorAuthor

    Pseudocode:

    AssemblyLoadContext alc = null;
    if (AssemblyLoadContext.CurrentContextualReflectionContext != null)
    {
        alc = AssemblyLoadContext.CurrentContextualReflectionContext;
    }
    else
    {
        alc = new IndividualAssemblyLoadContext($"Assembly.LoadFile({normalizedPath})"); 
    }

    (and of course similar changes would be needed in the other places in that file that new up one-off ALCs)

    (and also, before new-ing up that one-off ALC, I wonder about checking AssemblyLoadContext.GetLoadContext for the calling assembly?)

    I don't think it's a question of compatibility with .NET Framework--that is easy, because this change is 100% exactly as compatible with .NET Framework as the unchanged code is, because AssemblyLoadContext.CurrentContextualReflectionContext did not exist there, so nobody could be setting it.

    For more modern code (post-.NET Framework) (including, importantly, where the host is modern, but the calling code is circa .NET Framework) it only changes the behavior in cases where the program seems to have clearly indicated that it wants the "current ALC" to be a particular ALC.

  24. jkotas commented on Apr 30, 2024

    @jkotas
    Member

    I don't think it's a question of compatibility with .NET Framework

    The question is whether this behavior would make more .NET Framework libraries compatible.

    You have mentioned Powershell. Where is the Assembly.LoadFile call in Powershell that this would "fix"?

  25. locked and limited conversation to collaborators on May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions