oleg-shilo / cs-script

C# scripting platform
http://www.cs-script.net
MIT License
1.62k stars 235 forks source link

All assemblies loaded twice? #259

Closed axelriet closed 2 years ago

axelriet commented 2 years ago

Hi,

I'm just trying out CS script and I observed the following strange behavior on .NET 5.0: after executing the following line

CSScript.Evaluator.CreateDelegate("...")

All the assemblies on my app domain are loaded twice?

Repro:

                Console.WriteLine($"{AppDomain.CurrentDomain.GetAssemblies().Length} assemblies loaded");

                var log = CSScript.Evaluator.CreateDelegate(@"void Log(string message)
                                                              {
                                                                  Console.WriteLine(message);
                                                              }");

                Console.WriteLine($"{AppDomain.CurrentDomain.GetAssemblies().Length} assemblies loaded");

75 assemblies loaded 168 assemblies loaded

This causes problems. Please advise.

Thanks, Axel

oleg-shilo commented 2 years ago

Hi Alex,

Indeed pragmatic assembly loading is a tricky objective to achieve. The problem is that neither Roslyn nor CLR are not particularly "collaborative" about that. In fact, Roslyn does not offer even any guideline for avoiding excessive assembly loading at all and instead simply demonstrates how to execute a script (any sort) and ... leave it in the memory forever.

CS-Script, understandably, is bound by these limitations. Though it offers a few measures to assist the users with what I would call "economical" assembly loading.

As the most direct measure you can always unload the script assembly after you are done with it:

dynamic script = CSScript.Evaluator
                         .With(eval => eval.IsAssemblyUnloadingEnabled = true)
                         .LoadMethod(@"public object func()
                                       {
                                           return new[] {0,5};
                                       }");

var result = (int[])script.func();

var asm_type = (Type)script.GetType();

asm_type.Assembly.Unload();

For more than 15 years the code above was not possible but thanks to .NET Core 3 the assembly unloading became a reality. Though it comes with the price - referencing IsCollectible assemblies by other assemblies is problematic. But if your scripts do not reference other scripts then you are fine.

There is an additional CS-Script own caching mechanism, which resembles Python caching. You were trying to use this approach. Though it was designed for caching the execution of the full-bodied scripts (.CompileCode(...)) but not methods or delegates (your case). It also requires the assembly to be file- not memory-based. Otherwise, CLR will always treat it as a new assembly. Thus you will need to create a small wrapper method to achieve the desired functionality:

dynamic load_method(string code)
{
    var info = new CompileInfo { AssemblyFile = @$".\{method.GetHashCode()}.dll" };

    Assembly asm = CSScript.RoslynEvaluator
                            .With(e => e.IsCachingEnabled = true)
                            .CompileCode(@"using System;
                                            public class Script
                                            {
                                                " + code + @"
                                            }",
                                            info);

    return asm.CreateObject("*");
}

. . .

var code = @"void Log(string message)
                {
                    Console.WriteLine(message);
                }";

var script1 = load_method(code);
var before = AppDomain.CurrentDomain.GetAssemblies().Count();

var script2 = load_method(code);
var after = AppDomain.CurrentDomain.GetAssemblies().Count();

I marked your issue as an "enhancement" as I believe the caching interface can be extended to cover delegates scenarios more naturally.

oleg-shilo commented 2 years ago

Done The release v4.2.0 you can do caching for all IEvaluator.Compile* and IEvaluator.Load* and without the use of CompileInfo

axelriet commented 2 years ago

Thanks Oleg! I'm using CS-Script Core 2.0.0, I'll try again.