dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.96k stars 4.02k forks source link

CSharpScript.RunAsync fail with exception System.NotSupportedException: A non-collectible assembly may not reference a collectible assembly #72366

Open MoreDone opened 7 months ago

MoreDone commented 7 months ago

Version Used:

roslyn 4.8.0 runtime net8.0

Steps to Reproduce:

  1. Create tow project, main project run as console exe and another as hotfix plugin which can dynamic load and reload later.
  2. Main project use CSharpScript.RunAsync implement a repl console, which can handle input as command or repl.
  3. Hotfix project create a class which can be a parameter of CSharpScript.RunAsync of globals, then can invoke directly in console.
  4. In main project use AssemblyLoadContext load hotfix assembly by stream and the isCollectible paramater set true.
  5. Run the main exe, input any valid expression like 1+1 or Test() and so on, then exception is thrown.

part of code of load hotfix plugin :

    private void LoadPlugin()
    {
        this.UnloadPlugin();

        this.m_AssemblyLoadContext = new AssemblyLoadContext("Hotfix", IsPluginCollectible);

        // load plugin assembly from stream.
        var dllByte = File.ReadAllBytes(PluginFile);
        var pdbBytes = File.ReadAllBytes(Path.ChangeExtension(PluginFile, ".pdb"));
        this.m_Plugin = this.m_AssemblyLoadContext.LoadFromStream(new MemoryStream(dllByte), new MemoryStream(pdbBytes));

        var type = this.m_Plugin.GetType("ReplHotfix.Hotfix");
        this.Interactor.Initialize(Activator.CreateInstance(type));
    }

    private void UnloadPlugin()
    {
        if (IsPluginCollectible)
        {
            this.m_AssemblyLoadContext?.Unload();
        }

        this.m_Plugin = null;
        this.m_AssemblyLoadContext = null;

        GC.Collect();
    }

part of code of initialize interactive:

    public void Initialize(object globals)
    {
        this.m_ResetScriptState = true;

        this.m_Loader?.Dispose();
        this.m_Loader = new InteractiveAssemblyLoader();
        this.m_Loader.RegisterDependency(globals.GetType().Assembly);

        this.m_Global = globals;
        this.m_Options = ScriptOptions.Default
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(Directory.GetCurrentDirectory()))
            .AddReferences(typeof(Interactor).Assembly)
            // plugin assembly is load from stream, not contains location info.
            .AddReferences(MetadataReference.CreateFromFile(App.PluginFile))
            .AddImports("System", "System.IO", "System.Linq", "System.Collections.Generic", "Repl", "ReplHotfix");
    }

    private async Task Execute(string input)
    {
        input = input.Trim();
        if (string.IsNullOrWhiteSpace(input))
        {
            return;
        }

        // execute repl if input not a command
        if (!ExecuteCommand(input))
        {
            try
            {
                if (this.m_ScriptState == null)
                {
                    this.m_ScriptState = await CSharpScript.Create(input, this.m_Options, this.m_Global.GetType(), this.m_Loader)
                        .RunAsync(this.m_Global);
                }
                else
                {
                    this.m_ScriptState = await this.m_ScriptState.ContinueWithAsync(input, this.m_Options);
                }

                if (this.m_ScriptState != null)
                {
                    Console.WriteLine(this.m_ScriptState.ReturnValue);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        // state will be reset of operation reload
        if (this.m_ResetScriptState)
        {
            this.m_ResetScriptState = false;
            this.m_ScriptState = null;
        }
    }

There is the whole project Demo.zip

Expected Behavior: What I want to do is, invoke same methods of assemblies which is dynamic load and later unload or reload if I modify some code, directly just like coding script. I use AssemblyLoadContext to create collectible assemblies, so that the old assemblies can unload to avoid memory leak. There are some command, quit or Quit() will stop app, reload or Reload() will reload plugin assemblies, and in hotfix project can add any custom function or class for invoking in console directory.

Actual Behavior:

  1. It work successful in net6.0, but from net7.0 there is an exception thrown.
  2. Maybe this chage cause this exception https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/7.0/collectible-assemblies
  3. When I view the source code, found class CoreAssemblyLoaderImpl.LoadContext which inherit AssemblyLoadContext is not collectiable and CoreAssemblyLoaderImpl.Dispose do nothing, these may result the problem?
  4. ScriptState depend my plugin assemblies, but it not unload, that will keep some reference, it may lead to assemblies can't unload actually.

My Demand:

  1. Can ScriptState support unload. There may exist multi ScriptStates simultaneously, when there exist multi sessions, and if the session is close the ScriptState should be clear.
  2. Can CoreAssemblyLoaderImpl support collectible or InteractiveAssemblyLoader support a parameter for custom AssemblyLoadContext
zh6335901 commented 1 month ago

Roslyn scripting would create a assembly everytime that cannot be unloaded when you run script, It can results memory leak also. You can see 41722.

This problem exists for a long time, I don't know why Microsoft doesn't intend to fix it. I use Roslyn scripting to implement flow execution in a RPA project, And I am trying to solve it now.