Friday, June 7, 2013

Sasa.TypeConstraint and IL Rewriting - Generic Constraints (No Longer) Forbidden in C#

This is the twenty second post in my ongoing series covering the abstractions in Sasa. Previous posts:

It's well known that C# forbids certain core types from appearing as constraints, namely System.Enum, System.Delegate, and System.Array. Sasa.TypeConstraint are two purely declarative abstract classes that are recognized by Sasa's ilrewrite tool, and which permit programs to specify the above forbidden constraints. This pattern is used throughout Sasa to implement functionality allowed by the CLR but that would normally be forbidden by C#.

There are two definitions of TypeConstraint, one with only a single type parameter, and one with two type parameters. The second extended definition is unfortunately required to express some corner cases. It's primarily used in Sasa.Operators, and it's generally only needed if you're going to use some methods defined with TypeConstraint within the same assembly. If you can factor out those constrained methods into a separate assembly, you shouldn't ever need it.

Sasa's ilrewrite tool basically crawls the generated assembly IL and erases all references to TypeConstraint wherever it appears. In type signatures, a T : TypeConstraint<Foo> then becomes T : Foo.

Sasa.TypeConstraint.Value

The Sasa.TypeConstraint.Value property allows code compiled assuming a value of type TypeConstraint<Foo> to access the underlying value of type Foo. The ilrewrite tool erases calls to this property as well, leaving the access to the raw value. The following example is actually the definition of Sasa.Func.Combine:

public static T Combine<T>(this T first, T second)
    where T : TypeConstraint<Delegate>
{
    return Delegate.Combine(first.Value, second.Value) as T;
}

The ilrewrite tool erases all references to TypeConstraint so the final output is exactly:

public static T Combine<T>(this T first, T second)
    where T : Delegate
{
    return Delegate.Combine(first, second) as T;
}

TypeConstraint also defines implicit conversion from T to TypeConstraint<T> and TypeConstraint<T, TBase>, so you should never have to construct such an instance manually. If you forget to run ilrewrite on an assembly, attempting to construct or access any members of TypeConstraint will throw a NotSupportedException naming the assembly that needs rewriting.

Sasa's ilrewrite Tool

Sasa's ilrewrite tool has a fairly straightforward interface:

Usage:
ilrewrite /dll:<.dll or .exe> [/verify] [/key:<.snk file>] [/debug]

  /dll:    path to the file that will be rewritten.
  /verify: ensure the rewritten IL passes verification tests.
  /debug:  rewrite debugging symbols too.
  /key:    optional .snk key file used to sign all code.

Any options not listed above will simply be ignored by ilrewrite. The /verify option runs the platform's "peverify" tool to ensure the output passes the CLR verification tests. The /key option additionally creates strongly named assemblies from the given key file. This way you can keep strong names, you just need to defer signing to ilrewrite.

I should also note that this is how Sasa is complying with the LGPL while also providing strongly named assemblies with a privately held key. The LGPL stipulates that Sasa users ought to be able to replace my assemblies with their own whenever they wish, and this is possible using ilrewrite. An assembly that was built against my Sasa.dll simply needs to pass through ilrewrite that's given a different Sasa.dll signed with another key, and the output assembly will then be bound to the new assembly and key. After a brief exchange with a associate member of the EFF, these terms seemed satisfactory, although I should note that he isn't licensed to practice law, and his opinion does not constitute an official EFF response on this issue.

To integrate ilrewrite into my VS builds, I simply place ilrewrite in a bin\ folder under the root folder of my solution, then add the following line to my post-build event:

..\..\..\bin\ilrewrite /verify /dll:$(TargetFileName) /key:..\..\..\key.snk /$(ConfigurationName)

When running a DEBUG build, $(ConfigurationName) specifies the /DEBUG option, and any other configuration specifies an unknown option that ilrewrite simply ignores.

Sasa.Raw: Building on Sasa's Constrained Operations

From time to time, it may be necessary to build upon the constrained operations provided by Sasa, ie. defining a generic but different Func.Combine from the one above, but which takes some additional parameters. Here you'll run into a little trouble because you'll need to specify TypeConstraint<T> on your function, but the rewritten function in Sasa.dll is expecting a T. For this reason, Sasa also ships with Sasa.Raw.dll, which is Sasa.dll prior to rewriting and signing. This means all the TypeConstraint IL is intact, and you can write your extensions as needed.

The step by step procedure is:

  1. Link to Sasa.Raw.dll, not Sasa.dll, when building your project.
  2. In the post-build event, delete Sasa.Raw.dll and copy over Sasa.dll.
  3. In the post-build event, run ilrewrite as you normally would.

This is exactly the same procedure you'd follow when replacing my Sasa release with someone else's. For instance, here's the post-build event for Sasa.Reactive:

copy ..\..\..\bin\Sasa.dll .
del Sasa.Raw.dll
..\..\..\bin\ilrewrite /verify /dll:$(TargetFileName) /key:..\..\..\Sasa.snk /$(ConfigurationName)

If there's anything unclear about any of the above, please don't hesitate to ask here or on the Sasa help forum.

No comments: