Yesterday, C# MVP Job Skeet blogged about a  way to add generic constraints for enums and delegates. He noticed that, while the C# compiler forbids such constructions, it is perfectly possibly in MSIL. And he is fully right with this.

Jon uses a post-build step to add modify generic constraints. His process: ILDASM, then find-and-replace, then ILASM. That’s possible because the requirement is pretty easy.

But here he challenged me:

I've looked at PostSharp and CCI; both look way more complicated than the above.

Sure, PostSharp would be more complex, but Jon’s ILASM-based solution works only in best cases. There are plenty of things that you have to keep in mind while building a post-build step that modifies MSIL. For instance, how will ILASM find referenced assemblies? What with signed assemblies?  Sure, you can find a solution to everything – after all, PostSharp uses ILASM as a back-end (contrarily to rumors, it does not use ILDASM). But, believe me, addressing these side issues will prove more difficult than the core job itself.

And anyway, why would it be so complex to implement these rather simple requirements using PostSharp? PostSharp is designed so that easy jobs get easily done.

I took the challenge and implemented an add-in adding arbitrary constraints to generic parameters. I measured time. It took me 30 minutes. NDepend counts 12 lines of code.

It’s based on custom attributes. PostSharp loves custom attributes. I defined a custom attribute AddGenericConstraintAttribute that can be applied to generic parameters. Here is how it’s used:

public static class EnumHelper
{
    public static T[] GetValues<[AddGenericConstraint( typeof(Enum) )] T>() 
       where T : struct
    {
        return (T[]) Enum.GetValues( typeof(T) );
    }
}

The project can be downloaded from http://postsharp-user-samples.googlecode.com/files/AddGenericConstraint.zip. In order to use the plug-in, follow the documentation.

Here are the principal steps I followed to create this plug-in:

1.       Create a new solution AddGenericConstraint.sln.

2.       Create a new class library project AddGenericConstraint.csproj. This project will be the public interface of the plug-in.

3.       Create the class AddGenericConstraintAttribute.cs:

[AttributeUsage( AttributeTargets.GenericParameter )]
public sealed class AddGenericConstraintAttribute : Attribute
{
    public AddGenericConstraintAttribute( Type type )
    {
    }
}

4.       Create a new class library project AddGenericConstraint.Impl.csproj. This will contain the plug-in implementation.

5.       Add references to PostSharp.Public.dll, PostSharp.Core.dll, AddGenericConstraint.csproj.

6.       Create a class AddGenericConstraintTask. This is the real implementation class. This is the hard point.

public class AddGenericConstraintTask : Task
{
    public override bool Execute()
    {
        // Get the type AddGenericConstraintAttribute.
        ITypeSignature addGenericConstraintAttributeType = 
            this.Project.Module.FindType(
               typeof(AddGenericConstraintAttribute),
               BindingOptions.OnlyDefinition | BindingOptions.DontThrowException );

        if ( addGenericConstraintAttributeType == null )
        {
            // The type is not referenced in the current module. There cannot be a custom attribute
            // of this type, so we are done.
            return true;
        }

        // Enumerate custom attributes of type AddGenericConstraintAttribute.
        AnnotationRepositoryTask annotationRepository = 
            AnnotationRepositoryTask.GetTask( this.Project );

        IEnumerator customAttributesEnumerator =
            annotationRepository.GetAnnotationsOfType( 
              addGenericConstraintAttributeType.GetTypeDefinition(), false );

        while ( customAttributesEnumerator.MoveNext() )
        {
            // Get the target of the custom attribute.
            GenericParameterDeclaration target = (GenericParameterDeclaration)
                customAttributesEnumerator.Current.TargetElement;
 
            // Get the value of the parameter of the custom attribute constructor.
            ITypeSignature constraintType = (ITypeSignature)
                customAttributesEnumerator.Current.Value.
                 ConstructorArguments[0].Value.Value;

            // Add a generic constraint.
            target.Constraints.Add( constraintType );

            // Remove the custom attribute.
            ((CustomAttributeDeclaration) customAttributesEnumerator.Current).Remove();
        }

        return base.Execute();
    }
}

7.       Add an XML file AddGenericConstraint.psplugin to the project. This file will describe your plug-in. In file properties, specify “Copy to Output Directory: Copy Always”.

<?xml version="1.0" encoding="utf-8" ?>
<PlugIn xmlns="http://schemas.postsharp.org/1.0/configuration">
  <TaskType Name="AddGenericConstraint" Implementation="AddGenericConstraint.Impl.AddGenericConstraintTask, AddGenericConstraint.Impl" Phase="Transform">
    <Dependency TaskType="Remove" Position="After"/>
    <Dependency TaskType="AnnotationRepository"/>
  </TaskType>
</PlugIn>

8.       Go back to project AddGenericConstraint. Add a reference to PostSharp.Public.dll.  Add the following on the top of the class AddGenericConstraintAttribute to bind the custom attribute to its implementation:

[RequirePostSharp( "AddGenericConstraint", "AddGenericConstraint" )]

9.       Create a test project. Add references to PostSharp.Public.dll and AddGenericConstraint.csproj. In project properties, add directory “..AddGenericConstraint.ImplinDebug” to reference paths.

10.   You are done.

What’s the catch? Maybe that, if you don’t know PostSharp Core and MSIL, it will take you days to come with the 12 lines of code that form the implementation. But when you got the knowledge, you are incredibly productive. And all the issues caused by the integration in the build process are solved for you. Believe me, after 5 years of existence, there is a huge knowledge behind PostSharp integration.

Happy PostSharping!

-gael

PS. It took me longer to write this blog than the implementation itself.

 

Comments (7) -

Alex Yakunin
Alex Yakunin
9/11/2009 5:36:45 PM #

Very technical, but really cool post ;)

Pavel Savara
Pavel Savara
9/12/2009 12:20:09 AM #

Hi Gael,

good stuff :-) Now, please let me challenge you a little bit more.

There is way how to export function as native export from managed dll.
.corflags 0x00000010
.vtfixup [1] int32
For more details look at this article
[url]www.codeproject.com/.../DllExporter.aspx[/url]

Exactly as you mentioned, I use search and replace to achieve this.

Bear in ming that it could be done for 32 and 64 bit version, but separately.
(Bonus points if you figure out how to export native function for Mono and Linux, but that's probably far out of scope of poshsharp as well as this challenge)

Cheers and enjoy
Pavel

Gael Fraiteur
Gael Fraiteur
9/12/2009 8:18:16 AM #

Hi Pavel,

Thank you for your feedback.
Unfortunately, PostSharp does not implement VT fixups.

-gael

Hermann
Hermann
9/13/2009 10:34:41 AM #

Wow, Gael, this is awesome. It's amazing that you can use PostSharp to "fix" a language.

Hermann
Hermann
9/13/2009 2:48:56 PM #

Does AddGenericConstraint work with Delegates?
Another really cool usecase is for arithmetic methods where you can constrain the type to be a numerical type (e.g. int, double, decimal, etc.) by setting AttributeUsage.AllowMultiple to True. This would save having to do tons of overloads for every possible numerical type.

Gael Fraiteur
Gael Fraiteur
9/13/2009 5:26:07 PM #

Yes, it works with delegates. I added a test to SVN.

Robert Giesecke
Robert Giesecke
10/7/2009 8:32:51 PM #

Hi Pavel,
I have written an MSBuild task that does this for me.
Makes it fairly transparent and the project will work seamlessly on other machines, since it doesn't need anything besides VS.Net.

You can find it here:
sites.google.com/.../unmanagedexports

Comments are closed