Bug with Expression.GetDelegateType and user defined struct as ref

Apr 19, 2010 at 12:49 PM
Edited Apr 19, 2010 at 12:50 PM

I think I've discovered what seems to be a bug in GetDelegateType if you use it with a user defined struct that is made into a ref type, this example should illustrate it (in F#):

 

type Foo =
  struct
    val mutable test : int
  end

let fooAsRef = typeof<Foo>.MakeByRefType()
let typsRef = [|typeof<int>; typeof<string>; typeof<obj>; fooAsRef|]

let typRef1 = System.Linq.Expressions.Expression.GetDelegateType(typsRef)
let typRef2 = System.Linq.Expressions.Expression.GetDelegateType(typsRef)

typRef1 = typRef2 // false

Basically what happens is that GetDelegateType returns a new type for every call, even if the input types are identical. This does not happen if you replace "fooAsRef" with either built in value type as ref or not, a user value type that is not a ref, or a normal heap type (as ref or not). Seems to only present itself with user defined structs as ref types.

Example output of the above snipped (copied from F# Interactive):

 

val typRef1 : Type = Delegate4$87
val typRef2 : Type = Delegate4$88

> 

val typRef1 : Type = Delegate4$89
val typRef2 : Type = Delegate4$90

> 

val typRef1 : Type = Delegate4$91
val typRef2 : Type = Delegate4$92

> 

val typRef1 : Type = Delegate4$93
val typRef2 : Type = Delegate4$94

> 

val typRef1 : Type = Delegate4$95
val typRef2 : Type = Delegate4$96

> 

val typRef1 : Type = Delegate4$97
val typRef2 : Type = Delegate4$98

> 

val typRef1 : Type = Delegate4$99
val typRef2 : Type = Delegate4$100

And it just keeps on trucking, every time you request a DelegateType it creates a new one.

Apr 19, 2010 at 12:55 PM
Edited Apr 19, 2010 at 12:56 PM

Quick update, I tried the same scenario in C#

namespace ConsoleApplication2 {
	struct Foo {
		public int val;
	}

	class Program {
		static void Main(string[] args) {
			var fooAsRef = typeof(Foo).MakeByRefType();
			var typsRef = new[] { typeof(int), typeof(string), typeof(object), fooAsRef };
			
			var typRef1 = System.Linq.Expressions.Expression.GetDelegateType(typsRef);
			var typRef2 = System.Linq.Expressions.Expression.GetDelegateType(typsRef);
			var typRef3 = System.Linq.Expressions.Expression.GetDelegateType(typsRef);
			var typRef4 = System.Linq.Expressions.Expression.GetDelegateType(typsRef);

			var isSame = typRef1 == typRef2;
		}
	}
}

And it gives the same error, none of four typeRefN variables are the same delegate type, a new one gets created for each call to .GetDelegateType

Apr 20, 2010 at 12:57 AM

> I think I've discovered what seems to be a bug in GetDelegateType if you use it with a user defined struct that is made into a ref type, this example should illustrate it (in F#):  …

> Basically what happens is that GetDelegateType returns a new type for every call, even if the input types are identical. This does not happen if you replace "fooAsRef" with either built in value type as ref, or a normal heap type (as ref or not). Seems to only present itself with user defined structs.

This is unfortunately by design. We used to cache all types created by GetDelegateType. But CLR 4.0 added a new feature called “collectable types”, and our caching was preventing those types from being garbage collected. Since there is (or was) no way to tell if a type is collectable, the solution was to whitelist types in mscorlib and System.Core. For any other type, it’s not safe for us to hold onto a reference to it, so we’ll always create a new delegate.

Now what you can do is copy the caching logic and cache them yourself. In that case they’re your types, so hopefully you’ll know whether it makes sense to cache the delegates or not =). I suspect there is a helper that does this somewhere in Microsoft.Dynamic. But I can’t recall what it is. Maybe Dino or Tomas can chime in. If there isn’t, then DelegateHelpers in the DLR source is the right place to look.

- John

Coordinator
Apr 20, 2010 at 7:42 PM

Looking at the code and reviewing the spec, I'm wondering about making a stronger claim in the spec.  Today it says:

1.1.1 GetDelegateType Method

This helper method creates Type objects for delegate types.  It determines whether to return a Func or Action based on whether the last argument, the return type, is void.  If possible, this returns a built in Func or Action type, as GetFuncType or GetActionType would.  If necessary, this generates a new delegate type.

If invoked on the same type sequence as seen previously in an App Domain, this may return the same delegate object returned the first time, but there is no guarantee on this behavior.  If caching is important to the performance of your compiler, you should implement your own caching.

This method is useful when calling the Lambda factories, and it is what the Lambda factories call when you do not supply the delegate type.

Looking at the code, we only cache Action<…> and Func<…> types if the arity matches the built-in types in .NET 4.0 and there are no byref generic arg types.  We also only cache types we have to create when, as John said, the parameter and return types are composed only of types in mscorlib and system (our approximation for ensuring they aren't collectible).  I'm not sure at the time why we made such a loose claim in the spec other than to have the out to change our caching, but it seems we could capture what we really do here so that it is more clear what's a bug and when you need to take our code and add your own type caching (or simply remove the checks that limit our caching).  If you know you're not using collectible types, it doesn't matter.

Bill

From: jmesserly [mailto:notifications@codeplex.com]
Sent: Monday, April 19, 2010 4:57 PM
To: Bill Chiles
Subject: Re: Bug with Expression.GetDelegateType and user defined struct as ref [dlr:209768]

From: jmesserly

> I think I've discovered what seems to be a bug in GetDelegateType if you use it with a user defined struct that is made into a ref type, this example should illustrate it (in F#):

> Basically what happens is that GetDelegateType returns a new type for every call, even if the input types are identical. This does not happen if you replace "fooAsRef" with either built in value type as ref, or a normal heap type (as ref or not). Seems to only present itself with user defined structs.

This is unfortunately by design. We used to cache all types created by GetDelegateType. But CLR 4.0 added a new feature called “collectable types”, and our caching was preventing those types from being garbage collected. Since there is (or was) no way to tell if a type is collectable, the solution was to whitelist types in mscorlib and System.Core. For any other type, it’s not safe for us to hold onto a reference to it, so we’ll always create a new delegate.

Now what you can do is copy the caching logic and cache them yourself. In that case they’re your types, so hopefully you’ll know whether it makes sense to cache the delegates or not =). I suspect there is a helper that does this somewhere in Microsoft.Dynamic. But I can’t recall what it is. Maybe Dino or Tomas can chime in. If there isn’t, then DelegateHelpers in the DLR source is the right place to look.

- John

Read the full discussion online.

To add a post to this discussion, reply to this email (dlr@discussions.codeplex.com)

To start a new discussion for this project, email dlr@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com