Chapter 12

image

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.

9781430267614_Fig12-01.jpg

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:

  • Method (coded token of type MethodDefOrRef): A token of the generic method definition being instantiated (references the Method or MemberRef table).
  • Instantiation (offset in the #Blob stream): The instantiation signature, described in the next section of this chapter, cannot be 0.

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:

  • Method (coded token of type MethodDefOrRef): A token of the instantiated generic method definition. Must be a valid reference to the Method or MemberRef table.
  • Instantiation (offset in the #Blob stream): The instantiation signature must be a valid nonzero offset.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.208.91