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.
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.
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:
Only two flag values are defined for events, and only one of them can be set explicitly:
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:
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:
The Semantic entry can have the following values, which look like binary flags but in fact are mutually exclusive:
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.
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:
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:
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:
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:
Event Table Validity Rules
EventMap Table Validity Rules
Property Table Validity Rules
PropertyMap Table Validity Rules
MethodSemantics Table Validity Rules
3.144.172.233