Chapter 15

image

Events and Properties

Events and properties are special metadata components that are intended to make life easier for the high-level language compilers. The most intriguing feature of events and properties is that the JIT compiler and the execution engine are completely unaware of them. Can you recall any IL instruction that deals with an event or a property? That’s because none exist.

To understand the indifference of the JIT compiler and the execution engine toward events and properties, you need to understand the way these items are implemented.

Events and Delegates

The managed events I’m talking about here are not synchronization elements, similar to Win32/Win64 event objects. Rather, they more closely resemble Visual Basic events and On<event> functions. Managed events provide a means to describe the asynchronous execution of methods, initiated by certain other methods.

Figure 15-1 illustrates the general sequence of activities. A program unit—known as the publisher, or source, of an event—defines the event. You can think of this program unit as a class, for the sake of simplicity. Other program units (classes)—known as subscribers, event listeners, or event sinks—define the methods to be executed when the event occurs and pass this information to the event publisher. When the event publisher raises, or fires, the event by calling a special method, all the subscriber’s methods associated with this event are executed.

9781430267614_Fig15-01.jpg

Figure 15-1. The interaction of an event publisher and subscribers

In a nutshell, to implement a managed event, you need an entity that can collect the callback methods (event handlers) from the event subscribers and execute these methods when the publisher executes a method that signifies the event.

There is a type (a class) that is designed to do exactly that: the delegate type, which was discussed in Chapter 7. As you might remember, delegates are classes derived from the class [mscorlib]System.MulticastDelegate and play the role of “politically correct” type-safe function pointers in the managed world. The actual function pointer to a delegated method is passed to the delegate as an argument of its constructor, and the delegated method can subsequently be executed by calling the delegate’s Invoke method.

What Chapter 7 doesn’t mention is that several delegates can be aggregated into one delegate. Calling the Invoke method of such an aggregated delegate invokes all the delegated methods that make up the aggregate—which is exactly what you need to implement an event.

The [mscorlib]System.MulticastDelegate class defines the virtual methodsCombineImpl and RemoveImpl, adding a delegate (more exactly, the callback function pointer it carries) to the aggregate (more exactly, to the list of callback function pointers the aggregate carries) and removing a delegate from the aggregate, respectively. These methods are defined in Mscorlib.dll as follows. (I have omitted the resolution scope [mscorlib] of delegate types here because the methods are defined in the same assembly; this doesn’t mean you can omit the resolution scope when you refer to these types in your assemblies.)

.method family hidebysig final virtual
     instance class System.Delegate
     CombineImpl(class System.Delegate follow) cil managed
{ ... }
.method family hidebysig final virtual
     instance class System.Delegate
     RemoveImpl(class System.Delegate 'value') cil managed
{ ... }

The methods take the object reference to the delegate being added (or removed) as the argument and return the object reference to the aggregated delegate. The parameter and return type of both methods is System.Delegate rather than System.MulticastDelegate, but this isn’t contradictory: System.MulticastDelegate is derived from System.Delegate and hence can be used in its stead.

The principle of the delegate-based implementation of an event is more or less clear. Each event subscriber creates a delegate representing its event handler and then subscribes to the event by combining the handler delegate with the aggregate delegate, held by the event publisher. To raise the event, the publisher simply needs to call the Invoke method of the aggregate delegate, and everybody is happy.

One question remains, though: what does the publisher’s aggregate delegate look like before any event subscriber has subscribed to the event? The answer is, it doesn’t exist at all. The aggregate delegate is a result of combining the subscribers’ handler delegates. As long as there are no subscribers, the publisher’s aggregate delegate does not exist. This poses a certain problem: CombineImpl is an instance method, which has to be called on the instance of the aggregated delegate, and hence each subscriber must worry about whether it is the first in line (in other words, whether the aggregated delegate exists yet). That’s why the subscribers usually use the static methods Combine and Remove, inherited by System.MulticastDelegate from System.Delegate:

.method public hidebysig static class System.Delegate
     Combine(class System.Delegate a,
         class System.Delegate b)
{ ... }
.method public hidebysig static class System.Delegate
     Remove(class System.Delegate source,
        class System.Delegate 'value')
{ ... }

If one of the arguments of these methods is a null reference, the methods simply return the non-null argument. If both arguments are null references, the methods return a null reference. If the arguments are incompatible—that is, if the delegated methods have different signatures—Combine, which internally calls CombineImpl, throws an Argument exception, and Remove, which internally calls RemoveImpl, simply returns the aggregated delegate unchanged.

In general, delegates are fascinating types with more features than this book can discuss. The best way to learn more about delegates first-hand is to disassemble Mscorlib.dll and have a look at how System.Delegate and System.MulticastDelegate are implemented and used. The same advice is applicable to other Microsoft .NET Framework classes you happen to be interested in: when in doubt, disassemble the respective DLL, and see for yourself.

Events, of course, can be implemented without delegates. But given the functionality needed to implement events, I don’t see why anyone would waste time on an alternative implementation when the delegates offer a complete and elegant solution.

MANAGED SYNCHRONIZATION ELEMENTS

You’re probably wondering whether managed code has any elements equivalent to the synchronization events and APIs of the unmanaged world. It does, although this aspect is unrelated to the events discussed in this chapter. The synchronization elements of the managed world are implemented as classes of the .NET Framework class library. You can learn a lot about them by disassembling Mscorlib.dll and looking at the namespace System.Threading—and especially at the WaitHandle class of this namespace. (You’ve already encountered the System.Threading.WaitHandle class in the discussion of asynchronous invocation of delegates in Chapter 7.) The WaitHandle class is central to the entire class system of the System.Threading namespace and implements such methods as WaitOne, WaitAll, and WaitAny. Sounds familiar, doesn’t it? The AutoResetEvent, ManualResetEvent, and Mutex classes, derived from the WaitHandle class, are also worth a glance.

Event Metadata

To define an event, you need to know the event type, which, as a rule, is derived from [mscorlib]System.MulticastDelegate; the methods associated with the event (methods to subscribe to the event, to unsubscribe, to fire the event, and perhaps to carry out other tasks you might define); and, of course, the class defining the event. The events are never referenced in IL instructions, so you needn’t worry about the syntax for referencing the events.

The event metadata group includes the Event, EventMap, TypeDef, TypeRef, TypeSpec, Method, and MethodSemantics tables. Figure 15-2 shows the mutual references between the tables of this group.

9781430267614_Fig15-02.jpg

Figure 15-2. The event metadata group

The Event Table

The Event table has the associated token type mdtEvent (0x14000000). An Event record has three entries:

  • EventFlags (2-byte unsigned integer): Binary flags of the event characteristics.
  • Name (offset in the #Strings stream): The name of the event, which must be a simple name no longer than 1,023 bytes in UTF-8 encoding.
  • EventType (coded token of type TypeDefOrRef): The type associated with the event. The coded token indexes a TypeDef, TypeRef, or TypeSpec record. The class indexed by this token is either a delegate or a class providing the necessary functionality similar to that of a delegate.

Only two flag values are defined for events, and only one of them can be set explicitly:

  • specialname (0x0200): The event is special in some way, as specified by the name.
  • rtspecialname (0x0400): The event has a special name reserved for the internal use of the common language runtime. This flag can’t be set explicitly. The IL disassembler outputs this flag for information purposes, but the IL assembler ignores the keyword.

Only two flags? What about the event’s accessibility—is it public or protected or something else?

An event is never accessed from IL, remember? Its associated methods are, and they sure have the accessibility flags.

To my knowledge, the primary use of these event flags is to mark deleted events in edit-and-continue scenarios. When an event record is marked as deleted, both flags are set, and its name is changed to _Deleted. Some compilers, however, might find certain uses for the specialname flag. After all, an event as a metadata item exists solely for the benefit of the compilers.

The EventMap Table

The EventMap table provides mapping between the classes defining the events (the TypeDef table) and the events themselves (the Event table). An EventMap record has two entries:

  • Parent (record index [RID] in the TypeDef table): The type declaring the events.
  • EventList (RID in the Event table): The beginning of the events declared by the type indexed by the Parent entry. The mechanism of addressing the events in this case is identical to the mechanism used by TypeDef records to address the Method and Field records belonging to a certain TypeDef. In the optimized metadata model (the #~ stream), the records in the Event table are ordered by the declaring type. In the unoptimized model (the #- stream), the event records are not so ordered, and an intermediate lookup metadata table, EventPtr, is used. (Chapter 5 describes the metadata models and intermediate tables.)

The MethodSemantics Table

The MethodSemantics metadata table connects events and properties with their associated methods and provides information regarding the type of this association. A record in this table has three entries:

  • Semantic (2-byte unsigned integer): The kind of method association.
  • Method (RID in the Method table): The index of the associated method.
  • Association (coded token of type HasSemantics): A token indexing an event or a property the method is associated with.

The Semantic entry can have the following values, which look like binary flags but in fact are mutually exclusive:

  • msSetter (0x0001): The method sets a value of a property.
  • msGetter (0x0002): The method retrieves a value of a property.
  • msOther (0x0004): The method has another meaning for a property or an event
  • msAddOn (0x0008): The method subscribes to an event.
  • msRemoveOn (0x0010): The method removes the subscription to an event.
  • msFire (0x0020): The method fires an event.

The same method can be associated in different capacities with different events or properties. An event must have at least one subscribing method and one unsubscribing method. These methods return void and have one parameter of the same type as the event’s associated type (the EventType entry of the respective Event record). The Visual C# and Visual Basic compilers use uniform naming for subscribing and unsubscribing methods: add_<event_name> and remove_<event_name>, respectively. In addition, these compilers mark these methods with the specialname flag.

An event may (or may not) have a firing method. The firing method usually boils down to an invocation of the delegate implementing the event. The Visual C# and Visual Basic compilers, for example, never bother to define a firing method for an event—that is, the method invoking the delegate is there, but it is never associated with the event as a firing method. Such an approach contains certain logic: the firing method is a purely internal affair of the event publisher and need not be exposed to the event subscribers. And since the compilers, as a rule, use the event metadata to facilitate subscription and unsubscription, associating a firing method with an event is not necessary. If an event does have an associated firing method, however, this method must return void.

Event Declaration

In ILAsm, the syntax for an event declaration is

.event<class_ref> <name> { <method_semantics_decl>* }

where <class_ref> represents the type associated with the event, <name> is a simple name, and

<method_semantics_decl> ::= <semantics> <method_ref>
<semantics> ::= .addon| .removeon| .fire| .other

The following is an example of an event declaration:

// The delegate implementing the event
.class public sealed MyEventImpl
    extends[mscorlib]System.MulticastDelegate
{
   .method public specialname
        void  .ctor(object obj,
               native int'method') runtime{ }
   .method public virtual void
        Invoke(int32EventCode, string Msg) runtime
        { }
}
// The event publisher
.typedef class[mscorlib]System.Delegate as delegate
.class public A
{
   .field private class MyEventImpl evImpl
               // Aggregate delegate
   .method public specialname void .ctor()
   {
      ldarg.0
      dup
      call instance void .base::.ctor()
      ldnull
      stfld class MyEventImpl A::evImpl
      ret
   }
   .method public void Subscribe(class MyEventImpl aHandler)
   {
      ldarg.0
      dup
      ldfld class MyEventImpl A::evImpl
      ldarg.1
      call delegate
             delegate::Combine( delegate,
             delegate )
      castclass MyEventImpl
      stfld class MyEventImpl A::evImpl
      ret
   }
   .method public void Unsubscribe(class MyEventImpl aHandler)
   {
      ldarg.0
      dup
      ldfld class MyEventImpl A::evImpl
      ldarg.1
      call delegate
             delegate::Remove( delegate,
             delegate )
      castclass MyEventImpl
      stfld class MyEventImpl A::evImpl
      ret
   }
   .method public void Raise(int32EventCode, string Msg)
   {
      ldarg.0
      ldfld class MyEventImpl A::evImpl
      ldarg.1
      ldarg.2
      callvirt void MyEventImpl::Invoke(int32, string)
      ret
   }
   .method public bool HasSubscribers()
   {
      ldc.i4.0
      ldarg.0
      ldfld class MyEventImpl A::evImpl
      brnull L1
      pop
      ldc.i4.1
     L1:   ret
   }
   .event MyEventImpl MyEvent
   {
     .addon instance void A::Subscribe(class MyEventImpl)
     .removeon instance void A::Unsubscribe(class MyEventImpl)
     .fire instance void A::Raise(int32, string)
     .other instance bool A::HasSubscribers()
   }
   // Other class members
   ...
}// The end of the publisher class
// The event subscriber
.class public B
{
  .method public void MyEvtHandler(int32EventCode, string Msg)
  {
     //  If EventCode > 100, print the message
     ldarg.1
     ldc.i4100
     ble.s Return
     ldarg.2
     call void[mscorlib]System.Console::WriteLine(string)
   Return:
     ret
  }
  .method private void SubscribeToMyEvent(class A Publisher)
  {
    // Publisher->Subscribe(new MyEventImpl( this,
    //      (int)(this->MyEvtHandler)))
    .locals init (class MyEventImpl v)
    ldarg.0
    ldftn instance void .this::MyEvtHandler(int32, string)
    newobj instance void MyEventImpl:: .ctor(object, native int)
    stloc.0
    ldarg.1
    ldloc.0
    call instance void A::Subscribe(class MyEventImpl)
    ret
  }
  // Other class members
  ...
}// The end of the subscriber class

Property Metadata

Properties are considerably less fascinating than events. Typically, a property is some characteristic of the class that declares it—for example, the value of a private field—accessible only through the so-called accessor methods. Because of this, the only aspects of a property the common language runtime is concerned with are the property’s accessors.

Let’s suppose that a property is based on a private field (you might also have heard the phrase backed by a private field). Let’s also suppose that both read and write accessors are defined. What is the sense in declaring such a property when you could simply make the field public and be done with it? At least two reasons argue for declaring it: the accessors can run additional checks to ensure that the field has valid values at all times, and the accessors can fire events signaling that the property has been changed or accessed. I’m sure you can think of other reasons for implementing properties, even leaving aside cases in which the property has no backing field or has only a read accessor or only a write accessor.

A property’s read and write accessors are referred to as getters and setters, respectively. The Visual C# and Visual Basic compilers follow these naming conventions for the property accessors: setters are named set_<property_name>, and getters are named get_<property_name>. Both methods are marked with the specialname flag.

The property metadata group includes the following tables: Property, PropertyMap, TypeDef, Method, MethodSemantics, and Constant. Figure 15-3 shows the structure of the property metadata group. The following sections describe the Property and PropertyMap tables. I discussed the MethodSemantics table in the preceding section of this chapter, and Chapter 9 contains information about the Constant table.

9781430267614_Fig15-03.jpg

Figure 15-3. The property metadata group

The Property Table

The Property table has the associated token type mdtProperty (0x17000000), and its records have three entries:

  • PropFlags (2-byte unsigned integer): Binary flags of the property characteristics.
  • Name (offset in the #Strings stream): The name of the property, which must be a simple name no longer than 1,023 bytes in UTF-8 encoding.
  • Type (offset in the #Blob stream): The property signature. It’s about the only occurrence in the metadata when an entry named Type has nothing to do with type (TypeDef, TypeRef, or TypeSpec). Why this entry couldn’t be called Signature (which it is) remains mystery to me.

So then, the Type entry holds an offset to the property signature residing in the #Blob metadata stream. The structure of the property signature is similar to that of the method signature, except that the calling convention is IMAGE_CEE_CS_CALLCONV_PROPERTY (0x08). The return type and the parameter types of the property should correspond to those of the getter and setter, respectively. The runtime, of course, pays no attention to what the property signature looks like, but the compilers do care.

Three flag values are defined for properties, and as in the case of events, only one of them can be set explicitly:

  • specialname (0x0200): The property is special in some way, as specified by the name.
  • rtspecialname (0x0400): The property has a special name reserved for the internal use of the common language runtime. This flag can’t be set explicitly.
  • [no ILAsm keyword] (0x1000): The property has a default value, which resides in the Constant table; that is, the Constant table contains a record, the Parent entry of which refers to this property.

Similar to the events, property’s accessibility is defined by the accessibility of its accessor methods (for example, public getter and protected setter). A property itself cannot be referenced from IL and hence has no need for accessibility flags.

Like the event flags, the specialname and rtspecialname flags are used by the runtime for marking deleted properties in edit-and-continue scenarios. The deleted property name is changed to _Deleted*. The flag 0x1000 is set by the metadata emission API when a Constant record is emitted for this property, signifying the property’s default value.

The PropertyMap Table

The PropertyMap table serves the same purpose for properties as the EventMap table does for events: it provides mapping between the TypeDef table and the Property table. A PropertyMap record has two entries:

  • Parent (RID in the TypeDef table): The type declaring the properties.
  • PropertyList (RID in the Property table): The beginning of the properties declared by the type referenced by the Parent entry.

In the unoptimized model (the #- stream), an intermediate lookup metadata table, PropertyPtr, is used to remap the properties so that they look as if they were ordered by parent.

Property Declaration

The ILAsm syntax for a property declaration is

.property<flags> <ret_type> <name>(<param_type>[,<param _type>*] )
      [ <const_decl> ]  { <method_semantics_decl >* }

where

<method_semantics_decl> ::= <semantics> <method_ref>
<semantics> ::= .set | .get | .other
<const_decl> ::=  = <const_type> [ ( <value> ) ]

The <ret_type> and the sequence of <param_type> nonterminals define the property’s signature. <semantics> defines the kind of the associated methods: .set for the setter, .get for the getter, and .other for any other method defined for this property. The optional <const_decl> is the declaration of the property’s default value, similar to that of a field or a method parameter. The parent of the property is the class in whose scope the property is declared, as is the case for other class members (fields, methods, and events).

Now, as an exercise, let’s declare a simple property:

.class public A
{
   // theTally is the backing field of the property Tally
   .field private uint32theTally = int32(0xFFFFFFFF)
   // Constructor: set theTally to 0xFFFFFFFF (not used yet)
   .method public void .ctor()
   {
      ldarg.0
      dup
      call instance void .base:: .ctor()
      ldc.i40xFFFFFFFF
      stfld uint32A::theTally
      ret
   }
   // Setter: set Tally to Val if Val is not 0xFFFFFFFF
   .method public void set_Tally(uint32Val)
   {
      ldarg.1
      ldc.i40xFFFFFFFF
      beq.s Return
      ldarg.0
      ldarg.1
      stfld uint32A::theTally// set the backing field
   Return:
      ret
   }
   // Getter: return the value of Tally
   .method public uint32get_Tally()
   {
      ldarg.0
      ldfld uint32A::theTally// get the backing field
      ret
   }
   //  Other method: reset the value of Tally to 0xFFFFFFFF
   .method public void reset_Tally()
   {
      ldarg.0
      ldc.i40xFFFFFFFF
      stfld uint32A::theTally
      ret
   }
   .property uint32Tally(uint32)
     = int32(0xFFFFFFFF)
   {
      .set instance void A::set_Tally(uint32)
      .get instance uint32A::get_Tally()
      .other instance void A::reset_Tally()
   }
}// The end of class A

Summary of Metadata Validity Rules

The event-related and property-related metadata tables are Event, EventMap, Property, PropertyMap, Method, MethodSemantics, TypeDef, TypeRef, and Constant. Earlier chapters have discussed the validity rules for Method, TypeDef, TypeRef, and Constant tables. The records of the remaining tables have the following entries:

  • The Event table: EventFlags, Name, and EventType
  • The EventMap table: Parent and EventList
  • The Property table: PropFlags, Name, and Type
  • The PropertyMap table: Parent and PropertyList
  • The MethodSemantics table: Semantic, Method, and Association

Event Table Validity Rules

  • The EventFlags entry must contain 0, must have the specialname flag set (0x0200), or must have both the specialname and rtspecialname flags set (0x0600).
  • The Name entry must contain a valid offset in the #Strings stream, indexing a string no longer than 1,023 bytes in UTF-8 encoding.
  • If the specialname and rtspecialname flags are set, the event name must be _Deleted*.
  • No duplicate records—those with the same name belonging to the same class—can exist unless the event name is _Deleted*.
  • The EventType entry must contain a valid reference to the TypeDef or TypeRef table.

EventMap Table Validity Rules

  • The Parent entry must hold a valid reference to the TypeDef table.
  • The EventList entry must hold a valid reference to the Event table.

Property Table Validity Rules

  • The PropFlags entry must contain 0 or a combination of the binary flags specialname (0x0200), rtspecialname (0x0400), and 0x1000.
  • If the rtspecialname flag is set, the specialname flag must also be set.
  • If the 0x1000 flag is set, the Constant table must contain a valid record whose Parent entry holds the reference to this Property record, and vice versa.
  • The Name entry must contain a valid offset in the #Strings stream, indexing a string no longer than 1,023 bytes in UTF-8 encoding.
  • If the specialname and rtspecialname flags are set, the property name must be _Deleted*.
  • No duplicate records—those with the same name and signature belonging to the same class—can exist unless the property name is _Deleted*.
  • The Type entry must contain a valid offset in the #Blob stream, indexing a valid property signature. Chapter 7 discussed the validity rules for property signatures.

PropertyMap Table Validity Rules

  • The Parent entry must hold a valid reference to the TypeDef table.
  • The PropertyList entry must hold a valid reference to the Property table.

MethodSemantics Table Validity Rules

  • The Semantic entry must contain one of the following values: msSetter (0x0001), msGetter (0x0002), msOther (0x0004), msAddOn (0x0008), msRemoveOn (0x0010), and msFire (0x0020).
  • The Method entry must contain a valid index to the Method table.
  • The Association entry must contain a valid reference to the Event or Property table.
  • If the Semantic entry contains msSetter or msGetter, the Association entry must reference the Property table.
  • If the Semantic entry contains msAddOn, msRemoveOn, or msFire, the Association entry must reference the Event table.
  • No duplicate records that have the same Method and Association entries can exist.
  • No duplicate records that have the same Association and Semantic entries can exist unless the Semantic entry contains msOther.
  • For each Event record referenced in the Association entry, the table can contain one and only one MethodSemantics record with a Semantic entry equal to msAddOn.
  • For each Event record referenced in the Association entry, the table can contain one and only one MethodSemantics record with a Semantic entry equal to msRemoveOn.
  • For each Event record referenced in the Association entry, the table can contain no more than one MethodSemantics record with a Semantic entry equal to msFire.
..................Content has been hidden....................

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