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

How to release collectable assembly (cannot null belonging type)? #10149

Closed
JiriCepelkaFirstLineSoftware opened this issue Jan 25, 2019 · 4 comments
Labels
doc-enhancement Improve the current content [org][type][category] dotnet-framework/svc waiting-on-feedback Waiting for feedback from SMEs before they can be merged

Comments

@JiriCepelkaFirstLineSoftware
Copy link

JiriCepelkaFirstLineSoftware commented Jan 25, 2019

The documentation states

You must release all objects that represent parts of the assembly. The ModuleBuilder that defines T keeps a reference to the TypeBuilder, and the AssemblyBuilder object keeps a reference to the ModuleBuilder, so references to these objects must be released. Even the existence of a LocalBuilder or an ILGenerator used in the construction of T prevents unloading.

To do so I have prepared some methods:

private static IEnumerable<MemberInfo> GetMemberInfo(object obj)
{
        if(obj == null)
        {
                return Enumerable.Empty<MemberInfo>();
        }

        var type = obj.GetType();
        if (type.IsPrimitive || type == typeof(string) || type == typeof(object))
        {
                return Enumerable.Empty<MemberInfo>();
        }            
        return GetMemberInfo(type);
}

private static IEnumerable<MemberInfo> GetMemberInfo(Type type)
{
        if (type.IsPrimitive || type == typeof(string) || type == typeof(object) || type == null)
        {
                return Enumerable.Empty<MemberInfo>();
        }

        var memberInfos = type.GetMembers(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
                                        .Where(mi => mi.MemberType == MemberTypes.Property || mi.MemberType == MemberTypes.Field);

        memberInfos.Concat(
                memberInfos.OfType<PropertyInfo>()
                        .Select(pi => type.GetField($"<{pi.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static))
                        .Where(x => x != null));

        return memberInfos;
}
   
public static void NullAllValues(MemberInfo memberInfo, object obj)
{
        if(obj == null)
        {
                return;
        }

        var objType = obj.GetType();
        if (objType.IsPrimitive || objType == typeof(string) || objType == typeof(object))
        {
                return;
        }
        
        FieldInfo fieldInfo = memberInfo as FieldInfo;
        if (fieldInfo != null)
        {
                if(fieldInfo.FieldType == typeof(string) ||fieldInfo.FieldType.IsPrimitive || fieldInfo.FieldType == typeof(object))
                {
                        return;
                }

                if (fieldInfo.FieldType.IsArray)
                {
                        if (fieldInfo.FieldType.GetElementType().IsPrimitive)
                        {
                                return;
                        }
                }                               
        }
        
        PropertyInfo propertyInfo = memberInfo as PropertyInfo;
        if (propertyInfo != null) {

                if (propertyInfo.PropertyType == typeof(string) || propertyInfo.PropertyType.IsPrimitive || propertyInfo.PropertyType == typeof(object))
                {
                        return;
                }

                if (propertyInfo.PropertyType.IsArray)
                {
                        if (propertyInfo.PropertyType.GetElementType().IsPrimitive)
                        {
                                return;
                        }
                }                
        }

        if (fieldInfo != null)
        {   
                try
                {
                        var value = fieldInfo.GetValue(obj);

                        if (value == null)
                        {
                                return;
                        }
                        
                        if (fieldInfo.FieldType.IsArray)
                        {
                                object[] array = (object[])fieldInfo.GetValue(obj);

                                if (array == null)
                                {
                                        return;
                                }

                                foreach (var index in Enumerable.Range(0, array.Length))
                                {
                                        foreach (var mi in GetMemberInfo(obj: array[index]))
                                        {
                                                NullAllValues(mi, array[index]);
                                        }
                                }

                                return;
                        }
                        

                        fieldInfo.SetValue(obj, null, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField | BindingFlags.Public | BindingFlags.Static, null, null);
                        return;
                }

                catch
                {
                        return;
                }
        }

        if (propertyInfo != null)
        {   
                try
                {
                        if (propertyInfo.PropertyType.IsArray)
                        {
                                object[] array = (object[])propertyInfo.GetValue(obj);

                                if (array == null)
                                {
                                        return;
                                }

                                foreach (var index in Enumerable.Range(0, array.Length))
                                {
                                        foreach (var mi in GetMemberInfo(obj: array[index]))
                                        {
                                                NullAllValues(mi, array[index]);
                                        }
                                }

                                return;
                        }

                        var value = propertyInfo.GetValue(obj);

                        if (value == null)
                        {
                                return;
                        }

                        propertyInfo.SetValue(obj, null, BindingFlags.Instance | BindingFlags.SetProperty | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, null, null);
                        return;
                }
                catch
                {
                        return;
                }
        }
}

It could be said it works. At least until there is created type by TypeBuilder. This is because of no setters available on Type and no option to null backing fields (reflection does not find them).

static void Main(string[] args)
{
        const string collectibleOne = "CollectibleOne";
        const string someMethod = "SomeMethod";

        var collAssem = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(collectibleOne), AssemblyBuilderAccess.RunAndCollect);

        ModuleBuilder moduleBuilder = collAssem.DefineDynamicModule("SomeModule");
        TypeBuilder typeBuilder = moduleBuilder.DefineType("EventType", TypeAttributes.Class | TypeAttributes.Public);

        MethodBuilder methodBuilder = typeBuilder.DefineMethod(someMethod, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HasSecurity, CallingConventions.Standard, typeof(void), new[] { typeof(object), typeof(EventArgs) });
        methodBuilder.CreateMethodBody(new byte[50], 50);

        //Type type = typeBuilder.CreateType();           

        foreach (var mi in GetMemberInfo(obj: methodBuilder))
        {
                NullAllValues(mi, methodBuilder);
        }
        methodBuilder = null;

        foreach (var mi in GetMemberInfo(obj: typeBuilder))
        {
                NullAllValues(mi, typeBuilder);
        }
        typeBuilder = null;

        foreach (var mi in GetMemberInfo(obj: moduleBuilder))
        {
                NullAllValues(mi, moduleBuilder);
        }
        moduleBuilder = null;

        //foreach (var mi in GetMemberInfo(obj: type))
        //{
        //    NullAllValues(mi, type);
        //}
        //type = null;

        foreach (var mi in GetMemberInfo(obj: collAssem))
        {
                NullAllValues(mi, collAssem);
        }
        collAssem = null;

        GC.WaitForPendingFinalizers();
        GC.Collect();
     
        foreach (var assem in AppDomain.CurrentDomain.GetAssemblies())
        {
                if (assem.GetName().Name == collectibleOne)
                {   
                        throw new Exception("Found!");
                }
        }
}

If you uncomment the code you will get exception. No option to get backing fields with no option to set props without setters mean the type cannot be nulled so it will retain some references to assembly that could not be collected.

My questions:
• Am I wrong?
• If so how to achieve this?
• Is the documentation wrong?
• If so how the collectable assembly can be collected?

If you are in doubt you can try to comment out foreachs calling the NullAllValues. You will get exception too.

There is also post about this on Software Engineering.


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

@JiriCepelkaFirstLineSoftware JiriCepelkaFirstLineSoftware changed the title How to release collectiable assembly (cannot null the type of it)? How to release collectiable assembly (cannot null beloging type)? Jan 25, 2019
@JiriCepelkaFirstLineSoftware JiriCepelkaFirstLineSoftware changed the title How to release collectiable assembly (cannot null beloging type)? How to release collectiable assembly (cannot null belonging type)? Jan 25, 2019
@Adriien-M
Copy link

Adriien-M commented Aug 19, 2019

I'm creating dynamic assemblies too but they are never collected...
I tried this very simple example in a test project, it doesn't work either:

using System;
using System.Reflection;
using System.Reflection.Emit;

class RunAndCollectDemo
{
  static void Main()
  {
    Type type = CreateType();
    object obj = Activator.CreateInstance(type);
    Console.WriteLine(obj);
    WeakReference weak = new WeakReference(type);
    type = null;
    obj = null;
    Console.WriteLine("type = " + weak.Target);
    GC.Collect();
    Console.WriteLine("type = " + weak.Target);
  }

  static Type CreateType()
  {
        static Type CreateType()
        {
            AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("foo"), AssemblyBuilderAccess.RunAndCollect);
            ModuleBuilder modb = ab.DefineDynamicModule("foo.dll");
            TypeBuilder tb = modb.DefineType("Foo");
            return tb.CreateType();
        }
  }

Indeed the WeakReference is still alive...

@JiriCepelkaFirstLineSoftware did you solved this issue or is it a bug from .NET Core?

I saw that a class AssemblyLoadContext exists however, it is not possible to create a dynamic assembly elsewhere that in the current AppDomain if I understand correctly? So it's not possible to unload our dynamic assembly...

@mairaw mairaw added the doc-enhancement Improve the current content [org][type][category] label Dec 6, 2019
@Thraka
Copy link
Contributor

Thraka commented Feb 3, 2020

@GrabYourPitchforks @steveharter

Can either one of you answer these questions?

@Thraka Thraka added waiting-on-feedback Waiting for feedback from SMEs before they can be merged and removed ⌚ Not Triaged Not triaged labels Feb 3, 2020
@steveharter
Copy link
Member

Sorry missing notifications from this repro. It works better if an actual issue is created in https://github.com/dotnet/runtime/issues.

This issue is discussed in dotnet/runtime#29842

For @Adriien-M's example, I verified it works if build in Release mode and a loop is added:

        for (int i = 0; i < 10; i++)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine($"{i} type = { weak.Target}");
        }

@steveharter steveharter changed the title How to release collectiable assembly (cannot null belonging type)? How to release collectable assembly (cannot null belonging type)? Sep 2, 2020
@steveharter
Copy link
Member

@JiriCepelkaFirstLineSoftware I ran your example on .NET Framework 4.7.2 and it did not hit the throw line:

                if (assem.GetName().Name == collectibleOne)
                {   
                        throw new Exception("Found!");
                }

Closing this; please log an issue in https://github.com/dotnet/runtime/issues or add to dotnet/runtime#29842

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
doc-enhancement Improve the current content [org][type][category] dotnet-framework/svc waiting-on-feedback Waiting for feedback from SMEs before they can be merged
Projects
None yet
Development

No branches or pull requests

7 participants