Generic Methods
Generic methods are methods that carry type parameters in addition to their “normal” method parameters. These type parameters are subject to all the rules governing the type parameters of generic types, discussed in Chapter 11. This simplifies the discussion of generic methods significantly; therefore, this chapter will be brief.
The generic parameters of generic methods, as in the case of generic types, are limited to representing only types and can be constrained in the same way. The scope of the generic method’s type parameters is the scope of the method itself, which includes the method’s signature and body. Methods don’t have members or inheritance attributes, which simplifies the discussion even further.
Like in the case of generic types, the introduction of genericity does not affect the classification of the methods proposed in Chapter 10. We still deal with static and instance methods (independent and dependent of the parent type’s instance, respectively) and with virtual and nonvirtual instance methods, only now any method can be generic or not.
The genericity of methods is in no way related to the genericity of their owner types. You can have a nongeneric method of a generic type or a generic method of a nongeneric type. The fact that a method’s signature and body reference the type parameters of the parent type doesn’t make the method generic. What makes a method generic is the presence of its own type parameter list.
Generic Method Metadata
Recall that to define a nongeneric method, you need to supply its name, its parent, its signature, and its flags—and, of course, the method’s body, unless the method is abstract, internal to the CLR, CLR generated, or P/Invoked from an unmanaged module.
To define a generic method, as in the case of generic types, you need also to supply the list of type parameters and define the constraints of each type parameter.
Again, as in the case of generic types, the generic methods can be referenced only in the form of their instantiations.
Figure 12-1 shows the full mutual reference graph between the metadata tables involved in method defining and referencing. This figure is similar to Figure 10-1, except I omitted the tables GenericParam and GenericParamConstraint in Figure 12-1 because these tables are irrelevant to the nongeneric methods. The arrows indicate cross-table referencing by means of metadata tokens and RIDs.
Figure 12-1. Metadata tables participating in method definition and referencing
The tables specific to the representation of generic methods are GenericParam, GenericParamConstraint, and MethodSpec. I discussed the first two tables in Chapter 11, so I won’t repeat this discussion here. This leaves the MethodSpec table.
MethodSpec Metadata Table
The MethodSpec table carries the information about generic method instantiations. Unlike generic type instantiations represented by instantiation signatures stored as TypeSpecs, the instantiations of generic methods have to be represented by two signatures because the method itself, generic or not, has a signature describing its calling convention, return type, and parameter types. This signature is referenced from the Method and MemberRef tables (as offset in the #Blob stream). The MethodSpec table provides the link of a generic method to the second signature—the instantiation signature.
Each record in the MethodSpec table has two entries:
The MethodSpec records don’t need to be sorted in any metadata model.
Signatures of Generic Methods
The signature of a generic method differs from the signature of a nongeneric method, described in Chapter 8, and has the structure of
<gen_method_sig> ::= <callconv_gen_method> <num_of_type_params> <num_of_args>
<return_type> [<arg_type>[, <arg_type>]*]
where <callconv_gen_method> is the method’s calling convention ORed with IMAGE_CEE_CS_CALLCONV_GENERIC (0x10) and where <num_of_type_params> is a compressed number of type parameters of this generic method (again, as long as this number is not over 127, “compressed number” is simply a byte containing this number).
The method calling conventions acceptable for generic methods are limited to CALLCONV_GENERIC possibly in combination with CALLCONV_HASTHIS and CALLCONV_EXPLICITTHIS. The vararg generic methods are not supported, and the nongeneric vararg methods in generic types are not either.
The <return_type> and <arg_type> items of a generic method signature can be (or can contain) references to the method’s type parameters. Such references have the form {E_T_MVAR, <ordinal>}, where E_T_MVAR = 0x1E and <ordinal> is a compressed zero-based ordinal of the method’s type parameter. As you may recall from Chapter 11, the references to the type parameters of generic types in signatures have the form {E_T_VAR, <ordinal>}, where E_T_VAR = 0x13. So, you can declare a generic method of a generic class and easily tell a reference to the class’s type parameter number N from a reference to the method’s type parameter number N.
The ILAsm notation for referencing the method’s type parameters is !!<ordinal> or !!<name>, where <name> is the name of the type parameter and <ordinal> is the parameter’s number (zero-based) in the type parameter list. The generic type’s type parameters, as you remember, are referenced in ILAsm as !<ordinal> or !<name>.
There is no need for a special ILAsm keyword denoting IMAGE_CEE_CS_CALLCONV_GENERIC because the definitions and references to generic methods have a specific syntax. I’ll come to that in a moment, but now, while on the subject of signatures, let me show you what the instantiation signatures look like.
An instantiation signature of a generic method carries only information about the types the method is instantiated with, so its structure is relatively simple:
<gen_method_inst_sig> ::= <callconv_inst> <num_of_type_args>
<type_arg_type>[,<type_arg_type>]*
Here, <callconv_inst> is IMAGE_CEE_CS_CALLCONV_GENERICINST (0x0A), <num_of_type_args> is a compressed number of type arguments (cannot be 0), and <type_arg_type> is a single encoded type representing the respective type argument.
Defining Generic Methods in ILAsm
The ILasm syntax for defining a generic type is as follows:
<gen_method_def> ::=
.method<flags> <call_conv> <ret_type> <name> <<gen_params> >(<arg_list>) <impl>
{ <method_body> }
For example,
.method public static!!T GetMedian<T>(!!T[] tarray)
{
ldarg.0
dup
ldlen
ldc.i4.1
shr
ldelem!!T
ret
}
As in the case of generic types, the only difference between a generic method definition and nongeneric method definition is the presence of the <gen_params> clause (enclosed in angular brackets). I described this clause in detail in Chapter 11, and the only difference between generic types and generic methods in this regard is that the constraint flags + (covariance) and – (contravariance) are not applicable to the type parameters of a generic method.
The implementation flags <impl> of a generic method definition cannot be native, unmanaged, internalcall, or runtime, which leaves cil and managed, and as you know, cil and managed are default flags and do not need to be specified.
The <flags> clause of a generic method definition cannot contain the pinvokeimpl flag. And you know already that <call_conv> of a generic method cannot be vararg.
Class constructors (.cctor) cannot be generic because they are not called explicitly, so there is no way to specify a type argument for a .cctor. The instance constructors (.ctor), which are called explicitly (in the newobj instruction), can be generic.
Calling Generic Methods
Strictly speaking, you cannot call (or otherwise reference) a generic method; you can call only an instantiation of a generic method. It is the same story as with the generic types.
When calling (or otherwise referencing) a generic method instantiation directly, you need to specify its signature as it was defined, not as it became in the instantiation. It is the same rule that applies to calling nongeneric methods of generic types: if a parameter or return type of the method is declared as a “type parameter (of class or method) number 0,” it should be specified as such at the call site, no matter what type substitutes for the type parameter number 0. For example,
.method public static void Exec()
{
.entrypoint
ldc.i4.3
newarr string
... // Fill in the string array here
call!!0 GetMedian< string>(!!0[]) // Execute direct call
call[mscorlib]System.Console::WriteLine(string)
ret
}
Calling a generic method instantiation indirectly is a different story. To call a method (generic or not) indirectly, you need to load a function pointer to this method and then execute an indirect call on this function pointer. You reference the generic method instantiation only when you load the function pointer to it; after that you work with the pointer and not with the instantiation. The function pointer itself and the indirect call instruction carry the signature of the method as it became an instantiation. They have to because they carry no reference to the method instantiation. The following example illustrates this point:
.method public static void Exec()
{
.entrypoint
.locals init(string[] sarr, method string*(string[]) fptr)
ldc.i4.3
newarr string
stloc.0 // Store string vector
... // Fill in the string array here
ldftn!!0 GetMedian< string>(!!0[]) // Load ptr to instantiation
stloc.1 // Store function pointer
...
ldloc.0 // Load string vector
ldloc.1 // Load function pointer
calli string(string[]) // Execute indirect call. Note the signature
call[mscorlib]System.Console::WriteLine(string)
ret
}
The previous code snippets are slightly modified parts of the sample Genfptr.il, which you can download from the Apress web site:
.assembly extern mscorlib { auto}
.assembly genfptr {}
.module genfptr.exe
.typedef method void[mscorlib]System.Console::WriteLine(string) as PrintString
.method public static!!T GetMedian<T>(!!T[] tarray)
{
ldstr"GetMedian<T> called"
call PrintString
ldarg.0
dup
ldlen
ldc.i4.1
shr
ldelem!!T
ret
}
.method public static!!T Invoke<T>(method!!T*(!!T[]) medFunc, !!T[] tarr)
{
ldstr"Invoke<T> called"
call PrintString
ldarg.1
ldarg.0
calli!!T (!!T[])
ret
}
#define CALL_VIA_INVOKE
.method public static void Exec()
{
.entrypoint
#ifdef CALL_VIA_INVOKE
ldftn!!0 GetMedian< string>(!!0[])
#endif
ldc.i4.3
newarr string
dup
dup
dup
ldc.i4.0
ldstr"One"
stelem.ref
ldc.i4.1
ldstr"Two"
stelem.ref
ldc.i4.2
ldstr"Three"
stelem.ref
#ifdef CALL_VIA_INVOKE
call!!0 Invoke< string>(method!!0*(!!0[]), !!0[])
#else
ldftn!!0 GetMedian< string>(!!0[])
calli string(string[])
#endif
call PrintString
ret
}
Overriding Virtual Generic Methods
Declaring generic virtual methods is similar to declaring nonvirtual methods (just add type parameters!):
.class interface public abstract IX
{
.method public abstract virtual int32Do<T>(!!T theT) {}
}
Overriding the generic virtual methods, however, is severely limited compared to nongeneric virtual methods (including those of generic types): a generic virtual method can override (or can be overridden by) only a generic method of the same generic arity—not a (or by a) nongeneric method and not by an instantiation of a generic method. This goes for both implicit and explicit overriding.
Think about the meaning of a virtual generic method. Let’s consider the “most virtual” case—an abstract virtual method. An abstract virtual method is a contract between the superclass and all possible subclasses requiring all subclasses to supply an implementation of that method. By making the virtual abstract method generic, the superclass is requiring subclasses to provide an infinite number of implementations (one for each possible instantiation with various type arguments). The only mechanism that a subclass can use to provide an infinite number of implementations is by providing the method implementation as a generic “template.”
For example, the following implicit override of IX::Do is acceptable:
.class public A implements IX
{
...
.method public virtual final int32Do<T>(!!T argT) // Implicit override
{
...
}
...
}
Explicitly overriding generic virtual methods is slightly more complex than even explicitly overriding virtual methods of generic classes: you have to supply the signature of the overridden method and the generic arity of the overridden method itself (not the generic arity of the overridden method’s class). So, the explicit override directives for generic virtual methods look as follows (the construct <[1]> following the overridden method’s name is the method’s generic arity):
.class public B implements IX
{
...
.method public virtual final int32DoIt<T>(!!T argT) //Explicit override
{
.override method instance int32IX::Do<[1]>(!!0)// !!0 is type arg of Do
...
}
...
}
.class public C implements IX
{
// Explicit override, long form:
.override method instance int32IX::Do<[1]>(!!0)// !!0 is type arg of Do
with method instance int32 .this::DoIt<[1]>(!!0)// !!0 is type arg of DoIt
.method public virtual final int32DoIt<T>(!!T argT)
{
...
}
...
}
To reiterate, overrides of generic methods by generic methods of different arity (including nongeneric methods) or by instantiations of generic methods are illegal.
The type parameters of the overriding method cannot be constrained more restrictively than the type parameters of the overridden method. The reason for this requirement is simple. Suppose you override the method void A<T>() with the method void B<U>() and constrain U more restrictively than T. Constraining a type parameter means narrowing the possible choices of instantiation arguments, which means there will be types {X i } that can substitute for T but not for U. When you call virtually the instantiation of the overridden method, the CLR checks the type argument compliance with the overridden method’s constraints because it’s the method that is called. So, you can call virtually void A<X i >(), which means you will be calling in fact void B<X i >(), violating the constraints of U.
For example, the following code is wrong:
.class interface public abstract IX
{
.method public abstract virtual int32Do<T>(!!T) {}// T unconstrained
}
...
.class public A implements IX
{
...
.method public virtual int32Do< .ctor T>(!!T t)// T constrained
{
...
}
...
}
The previous code snippets are slightly modified fragments of the sample Gen_virt.il, which you can download from the Apress web site. The sample also shows the definition and usage of a generic .ctor:
.assembly extern mscorlib { auto}
.assembly gen_virt{}
.module gen_virt.exe
#define DEFLT_CTOR " .method public specialname void .ctor()
{ ldarg.0; call instance void .base:: .ctor(); ret;}"
.typedef method void[mscorlib]System.Console::WriteLine(string) as PrintString
.typedef method void[mscorlib]System.Console::WriteLine(int32) as PrintInt
.typedef[mscorlib]System.Type as SysType
// Non-generic interface with generic method
.class public interface IX
{
.method public virtual abstract int32Do<T>(!!T theT){}
}
// Implicit override of generic virtual method
.class public A implements IX
{
DEFLT_CTOR
.method public virtual int32Do<T>(!!T theT)
{
ldarga.s theT
constrained.!!T
callvirt instance string object::ToString()
call PrintString
ldc.i4.2
ret
}
}
// Explicit override of generic method, short form
.class public B implements IX
{
DEFLT_CTOR
.method public virtual int32DoIt<T>(!!T theT)
{
.override method instance int32IX::Do<[1]>(!!0)
ldarga.s theT
constrained.!!T
callvirt instance string object::ToString()
call PrintString
ldc.i4.3
ret
}
}
// Generic instance constructor and
// Explicit override of generic method, long form
.class public C implements IX
{
.method public specialname void .ctor<U>(!!U u)
{
ldarg.0
call instance void .base:: .ctor()
ldtoken!!U
call class SysType SysType::GetTypeFromHandle(
valuetype[mscorlib]System.RuntimeTypeHandle)
callvirt instance string SysType::ToString()
call PrintString
ldarga.s u
constrained.!!U
callvirt instance string object::ToString()
call PrintString
ret
}
.override method instance int32IX::Do<[1]>(!!0)
with method instance int32 .this::DoIt<[1]>(!!0)
.method public virtual int32DoIt<T>(!!T theT)
{
ldarga.s theT
constrained.!!T
callvirt instance string object::ToString()
call PrintString
ldc.i4.4
ret
}
}
// The executing method
.method public static void Exec()
{
.entrypoint
newobj instance void A:: .ctor()
ldstr"Hehe"
callvirt instance int32IX::Do< string>(!!0)
call PrintInt
newobj instance void B:: .ctor()
ldstr"Haha"
callvirt instance int32IX::Do< string>(!!0)
call PrintInt
ldstr"Huhu"
newobj instance void C::.ctor< string>(!!0)
ldstr"Hoho"
callvirt instance int32IX::Do< string>(!!0)
call PrintInt
ret
}
Summary of the Metadata Validity Rules
One metadata table is specific to the generic methods—the MethodSpec table. The records of this table contain two entries:
The signature of a generic method has the calling convention bit IMAGE_CEE_CS_CALLCONV_GENERIC (0x10) set.
The generic methods cannot have a vararg calling convention.
The type parameters of a generic method should not have the constraint flags + (covariance) and – (contravariance). These constraints are not applicable to the type parameters of generic methods.
The flags of a generic method definition cannot contain the pinvokeimpl flag.
The implementation flags of a generic method definition cannot be native, unmanaged, internalcall, or runtime.
Class constructors (.cctor) cannot be generic.
18.191.208.91