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

Error in Eval using a referenced assembly with unloading enabled. #355

Closed
MUN1Z opened this issue Dec 6, 2023 · 7 comments
Closed

Error in Eval using a referenced assembly with unloading enabled. #355

MUN1Z opened this issue Dec 6, 2023 · 7 comments

Comments

@MUN1Z
Copy link

MUN1Z commented Dec 6, 2023

Hello @oleg-shilo , i am trying reload my script files. I have a assembly loaded called Libs, all my scripts uses this Libs with reference in Roslyn Eval method.

image

Could not load file or assembly 'ℛ*1240c384-fa14-4563-9208-e649e77e3a95#1-0, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the specified file.

The Eval method do not find the referenced assembly before addition of "IsAssemblyUnloadingEnabled" flag in CompileCode method.

If i remove the flag, the Eval find the assembly and works fine, but i cannot unload the asm assembly:

image

image

@MUN1Z
Copy link
Author

MUN1Z commented Dec 6, 2023

This is a example project to reproduce the issue: https://github.com/MUN1Z/CsSrcipt.unload.issue

@oleg-shilo
Copy link
Owner

oleg-shilo commented Dec 6, 2023

This is a very interesting problem and it's not an easy task to solve...but possible. I have provided the code sample for that.

Anyway, let's try.

Though, in your first code snippet you are preparing for unloading the referenced assembly asm, what kinda doesn't make sense as you indicated you want to reference it from multiple eval-scrips.

Next, if you remove IsAssemblyUnloadingEnabled from your shared script then you are fine and your code will run and you will be able to unload.

Though with the very current API you cannot unload assembly if your eval-script does not return anything. Thus the API needs to change to allow that. I have done the change (af55aab) and it will be available in the very next release. For your convenience I have provided the extension method for that so you do not have to wait.

Next, you need to be careful with eval type of scenarios. Every time an instance of the tipe from a give assembly typecasted to dynamic it becomes un-unloadable. It is either a bug or a feature of CLR. But we cannot change it. Ironically avoiding dynamic and relying on interfaces arguably is a better approach anyway. EvalAndUnload also takes care of that.

Note in the code sample below I had to call GC.Collect explicitly and deliberatly place it outside of the host-call method. Just to demonstrate the fact that the eval-script is unloaded.

And below is the complete working sample that shows the use of the shared script form the unloadable eval-script:

I do recommend avoid using eval and using LoadMethod instead. It's more manageable (useDelegate option).

using System.Reflection;
using Microsoft.CodeAnalysis;
using CSScripting;
using CSScriptLib;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var useDelegate = false;

            for (int i = 0; i < 20; i++)
            {
                Console.WriteLine("Loaded assemblies count: " + AppDomain.CurrentDomain.GetAssemblies().Count());

                call_UnloadAssembly(useDelegate);

                GC.Collect();
            }
        }

        static Assembly printer_asm;

        static void call_UnloadAssembly(bool useDelegate)
        {
            var info = new CompileInfo { RootClass = "Printing", AssemblyFile = "Printer.dll" };

            if (printer_asm == null)
                printer_asm = CSScript.Evaluator
                                      .CompileCode(@"using System;
                                                     using System.Diagnostics;
                                                     public class Printer
                                                     {
                                                         public static void Print() =>
                                                             Console.WriteLine(""Printing..."");
                                                     }", info);
            if (useDelegate)
            {
                var script = CSScript.Evaluator
                                     .With(eval => eval.IsAssemblyUnloadingEnabled = true)
                                     .ReferenceAssembly(printer_asm)
                                     .LoadMethod<ICalc>(@"public int Sum(int a, int b)
                                                          {
                                                              Printing.Printer.Print();
                                                              return a+b;
                                                          }");
                script.Sum(1, 2);
                script.GetType().Assembly.Unload();
            }
            else
            {
                var result = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true)
                                     .ReferenceAssembly(printer_asm)
                                     .EvalAndUnload("Printing.Printer.Print();");
            }
        }
    }

    public interface ICalc
    {
        int Sum(int a, int b);
    }
}

static class Extension
{
    public static object EvalAndUnload(this IEvaluator evaluator, string scriptText, bool unloadableDependency = true)
    {
        var info = new CompileInfo { CodeKind = SourceCodeKind.Script };
        var asm = evaluator.CompileCode(scriptText, info);

        // note that the leading dot is missing "css_Root" vs ".css_Root"; this is required for asms compiled with inloadable context
        var entryPointType = asm.GetType($"{info.RootClass}", true, false);

        var entryPointMethod = entryPointType?.GetTypeInfo().GetDeclaredMethod("<Factory>") ?? throw new InvalidOperationException("Script entry point method could be found.");

        var submissionFactory = (Func<object[], Task<object>>)entryPointMethod.CreateDelegate(typeof(Func<object[], Task<object>>));

        var result = submissionFactory.Invoke(new object[] { null, null }).Result;

        asm.Unload();

        return result;
    }
}

image

@MUN1Z
Copy link
Author

MUN1Z commented Dec 6, 2023

I understand, I'm not at home right now but in a little while I'll run the example you sent, thank you very much @oleg-shilo .

Regarding the use of Eval, it was the closest I could get to simulating the behavior of lua scripts that I use on Tibia servers.

I need the developer who will create the scripts to be free to create and call objects, variables, methods, create his own business rule in that script and override one or two delegates to in the end return the script instance to be stored and called when some player performs a certain action.

but I will analyze the use of the load method calmly, it may be possible to change it if I change some behaviors in the scripts.

I'm going to leave an example of the pattern that is used in lua and as I'm recreating it in eval, I want to make it very similar so I can convert these lua scripts to csx automatically in the future.

image

Regarding reloading scripts, they will be executed when a player with permission needs to speak a specific command in the game, for example /reload scripts, or via terminal on the server, execute the same command.

@MUN1Z
Copy link
Author

MUN1Z commented Dec 6, 2023

@oleg-shilo I tested it now, so the same problem still occurs, the problem occurs when you put "IsAssemblyUnloadingEnabled = true" in the printer assembly and reference the assembly in Eval.

In my case I need to reload both the assemblies created from scripts and the scripts that only use those assembly.

image

image

@oleg-shilo
Copy link
Owner

oleg-shilo commented Dec 7, 2023

Correct. That's why I did not marked the shared assembly for unloading.

Though, in your first code snippet you are preparing for unloading the referenced assembly asm, what kinda doesn't make sense as you indicated you want to reference it from multiple eval-scrips.

That was an architectural reason but it also served an additional purpose: Roslyn is not able to reference assemblies that are loaded with the "collectable" context.

Thus you may want to consider not unloading the assemblies at all or not unloading the shared scripts/assemblies. After all it might not be such a pressure on your memory as it might seem. But it only you can answer this question.

BTW, it's only CS-Script who is trying to do something about unloading. Roslyn completely expects you to do eval and forget about it. Completely ignoring the fact that a tiny assembly has been created.
And before .NETCore MS did not even provide a mechanism for unloading at all.

You can also consider an early days technique that CS-Script implemented when true unloading was not available. You cna group your eval jobs in to batches hosted in a dedicated AppDomain. And when the job is doen then just unload the domain with all these eval-assemblies in it.

I have already captures some of those considerations here

@kimdiego2098
Copy link

Next, you need to be careful with eval type of scenarios. Every time an instance of the tipe from a give assembly typecasted to dynamic it becomes un-unloadable. It is either a bug or a feature of CLR. But we cannot change it. Ironically avoiding dynamic and relying on interfaces arguably is a better approach anyway. EvalAndUnload also takes care of that.

This is an unbearable bug, if we keep compiling dynamic methods (with dynamic type) but cannot uninstall them, will there be memory leaks and other situations

@oleg-shilo
Copy link
Owner

This is an unbearable bug,...

Agree. Though most likely Roslyn team would reply to that "It's not a bug but a feature" :)
.NET roadmap was always very rigid when it comes to dynamic syntax features. And unloading is not the only hiccup with Roslyn.

Mind you, in late 2002, I lodged the feature request on MS Connect for unloading dynamically loaded assemblies. Interestingly enough the ticket handler (.NET ASP lead) agreed with my request but admitted... "it would be too hard to implement". But 17 years later they did it. So it came a long way...

Anyway, indeed uncontrolled evals may lead to effective memory leaks. So it's your call. You can be OK with it as long as your set of dynamic assemblies is fixed. Or if you prefer, you can switch to the unloadable options as I mentioned (e.g. using interfaces, in many cases, it's a preferred option for architectural reasons).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants