-
Notifications
You must be signed in to change notification settings - Fork 32
CommonLarcenyEvents
This page will document adding CLR event handling to CommonLarceny.
- Wrap methods in an object so they can be passed as arguments to other methods
- Specify the return type and argument types of the method
- Perfect for "anonymous" invocation
delegate result-type identifier ([parameters]);
public delegate void SimpleDelegate ()
public delegate int ButtonClickHandler (object obj1, object obj2)
Publisher/subscriber pattern: User Control (publisher) declares a delegate, declares an Event based on the delegate, fires the event. Application, Control or other subscriber is notified by publisher when event is fired and invokes designated event handler(s).
- Event Handlers in the .NET Framework return void and take two parameters
- The first parameter is the source of the event; that is the publishing object
- The second parameter is an object derived from the EventArgs type
- Events are properties of the class publishing the event
- The keyword event controls how the event property is accessed by the subscribing classes.
public class Form1 : Form
{
private Button button1;
private Label label1;
public Form1()
{
// constructor code
this.button1.Click += new EventHandler(button1_Click);
}
private void button1_Click(object sender, EventArgs e)
{
this.label1.Text = "Button Clicked!";
}
}
The following code represents the IL code generated by ilasm from the above C# code.
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// other constructor code ...
ldfld class [System.Windows.Forms]System.Windows.Forms.Button WindowsApplication1.Form1::button1
ldarg.0
ldftn instance void WindowsApplication1.Form1::button1_Click(object, class [mscorlib]System.EventArgs)
newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::add_Click(class [mscorlib]System.EventHandler)
// ...
}
Explanation of the above code:
- Push a reference to the value of the
button1
field onto the stack - Push the
this
instance of Form1 onto the stack - Push an unmanaged pointer to the native code implementing the
button1_Click
method onto the stack - Construct an EventHandler object and pushes it onto the stack:
* First argument is the
this
instance of Form1 * Second argument is the unmanaged pointer to thebutton1_Click
method - Call the virtual
add_Click
method on the instance of button1 using the previously definedEventHandler
(the method is virtual because it is defined by the Control class, IL uses the type of button1 to find correct method)
Ideally, we'd like to be able to attach event handlers to events in Common Larceny. Here is an example of how one might do it:
(enable-dotnet!)
(define button1 (System.Windows.Forms.Button.))
(set-.Top$! button1 50)
(define label1 (System.Windows.Forms.Label.))
(set-.Name$! label1 "label1")
(define form1 (System.Windows.Forms.Form.))
(.AddRange (.Controls$ form1) (vector label1 button1))
(define button1-click
(lambda (sender args)
(set-.Text$! button1 "Button was clicked!")))
(define eh (System.EventHandler. form1 button1-click))
(.add_Click button1 eh)
(System.Windows.Forms.Application.Run form1)
However, when we construct the System.EventHandler
object, it expects the unmanaged pointer to the button1-click method, so this line currently fails to run.
PnkFelix pointed out an important fact. Rather than simply attach the method to the event, we also need to capture the environment in which the method was created. This will allow the method body to reference identifiers like button1
. If we only created the event handler based on the pointer to the method, as .NET does, we couldn't capture the environment, too.
In the CL runtime system, the Procedure
object is used to represent a closure, or a lambda expression. Instead of having the Form
object subscribe to the event, we will have the Procedure
object that represents the method that handles the event subscribe to the event. Then when the event is fired, the procedure object is notified, and is able to execute its method with knowledge of the environment within which it was defined.
There are dozens of events in the CLR, and many require that their handlers conform to special method signatures. Specifically, instead of the standard second parameter of the type EventArgs
, some other subclass of EventArgs
is required, and carries additional information about the event, such as which keyboard button was pressed in the case of a KeyDown
event.
JoeMarshall had included a separate method in the Procedure class for each possible event in the CLR. These were all being preprocessed out by the c-pre-processor, but they provided insight as to how he was attempting to handle events at one point in time. While each overloaded verson of represented a different event handler method, all the bodies were the same.
So, I instead created a method that consumes information about an event and generates an appropriate dynamic handler based on the argument types required by the handler. It does so via reflection and JIT generation of IL code.
Additionally, we needed a way to get at this method from the Scheme world, so I created an additional FFI syscall (25) that can be used to attach
an event handler to an object. The syscall takes three arguments, the object that publishes the event, a string representing the name of the event, and the procedure that will subscribe to and handle the event. The syscall looks up the event via reflection, and passes information about the event to the procedure's makeEventHandler
method, which then returns the appropriate delegate created via the process mentioned above. Lastly, the syscall adds the dynamically generated event handler to the publishing object's event handler list, and we are on our merry way to handling events in CommonLarceny.
Here are two examples of how to attach handlers to events. The first uses the lower-level dotnet-ffi syntax (and is much faster).
(define form-type (find-clr-type "System.Windows.Forms.Form"))
(define form-controls-property (clr/%get-property form-type "Controls" (vector)))
(define control-type (find-clr-type "System.Windows.Forms.Control"))
(define controls-collection-type (find-clr-type "System.Windows.Forms.Control+ControlCollection"))
(define button-type (find-clr-type "System.Windows.Forms.Button"))
(define button-text-property (clr/%get-property button-type "Text" (vector)))
(define button-top-property (clr/%get-property button-type "Top" (vector)))
(define label-type (find-clr-type "System.Windows.Forms.Label"))
(define label-text-property (clr/%get-property label-type "Text" (vector)))
(define message-box-type (find-clr-type "System.Windows.Forms.MessageBox"))
(define form1 (clr/%invoke-constructor (clr/%get-constructor form-type (vector)) (vector)))
(define form1-controls (clr/%property-ref form-controls-property form1 (vector)))
(define button1 (clr/%invoke-constructor (clr/%get-constructor button-type (vector)) (vector)))
(define label1 (clr/%invoke-constructor (clr/%get-constructor label-type (vector)) (vector)))
(clr/%property-set! button-text-property button1 (clr/string->foreign "Click Me") (vector))
(clr/%property-set! button-top-property button1 (clr/int->foreign 50) (vector))
(clr/%property-set! label-text-property label1 (clr/string->foreign "no click received") (vector))
(define (button1-click sender args)
(clr/%property-set! label-text-property label1 (clr/string->foreign "Click Received Successfully!") (vector)))
(define (add-controls collection controls)
(for-each (lambda (c)
(clr/%invoke
(clr/%get-method controls-collection-type "Add" (vector control-type))
collection
(vector c)))
controls))
(add-controls form1-controls (list label1 button1))
(define (add-event-handler publisher event-name procedure)
(clr/%add-event-handler publisher event-name (clr/%foreign-box procedure)))
(add-event-handler button1 "Click" button1-click)
(define (demo-event-handling)
(clr/%invoke (clr/%get-method form-type "ShowDialog" (vector)) form1 (vector)))
The second example uses the JavaDot syntax and the generic/multimethod dispatch system and is much slower. Be sure to call (enable-dotnet!)
before running it:
(define (add-event-handler publisher event-name procedure)
(clr/%add-event-handler (clr-object/clr-handle publisher) event-name (clr/%foreign-box procedure)))
(write "Defining Button...")
(define button1 (System.Windows.Forms.Button.))
(write "Defining Label...")
(define label1 (System.Windows.Forms.Label.))
(write "Defining Form...")
(define form1 (System.Windows.Forms.Form.))
(write "Defining Timer...")
(define countdown-timer (System.Windows.Forms.Timer.))
(define *initial-time-remaining* 10)
(define time-remaining *initial-time-remaining*)
(define (reset-time-remaining!) (set! time-remaining *initial-time-remaining*))
(define (update-label) (set-.Text$! label1 (number->string time-remaining)))
(define (start-timer)
(.Start countdown-timer)
(set-.Text$! button1 "Stop Timer"))
(define (stop-timer)
(.Stop countdown-timer)
(set-.Text$! button1 "Start Timer")
(reset-time-remaining!)
(update-label))
(define (decrement-timer)
(set! time-remaining (- time-remaining 1))
(update-label))
(define (countdown-expire)
(stop-timer)
(System.Windows.Forms.MessageBox.Show "Time expired!"))
(define (init-components)
(set-.Top$! button1 50)
(set-.Interval$! countdown-timer 1000)
(.AddRange (.Controls$ form1) (vector label1 button1))
(add-event-handler button1 "Click"
(lambda (sender args)
(if (.Enabled$ countdown-timer) (stop-timer) (start-timer))))
(add-event-handler countdown-timer "Tick"
(lambda (sender args)
(if (> time-remaining 0) (decrement-timer) (countdown-expire))))
(stop-timer))
(define (demo-event-handling)
(init-components)
(.ShowDialog form1))
Demo1 : examples/CommonLarceny/event-handling-demo1.sch Demo2 : examples/CommonLarceny/event-handling-demo2.sch
|| || (load ...) || REPL || ||Demo1 || Yes || Yes || ||Demo2* || Yes || Yes ||
|| X || (load X.sch )
|| (compile-file X.sch) (load X.fasl)
|| REPL ||
||Demo1 || Yes || Yes || Yes ||
||Demo2* || Yes || Yes || Yes ||
Additionally, after compiling the demos using compile-file
running CommonLarceny.exe -- X.fasl
works for both demos.
|| X || (load X.sch )
|| (compile-file X.sch) (load X.fasl)
|| REPL ||
||Demo1 || Y || Y || Y ||
||Demo2* || Y || NO** || Y ||
- An inital call to
(enable-dotnet!)
is made before compiling/loading the second demo.
** There seems to be a memory leak/infinite loop somewhere. I left it running for 4+ hours and the CommonLarceny process grew to over 400MB in memory consumption and hadn't gotten past displaying the line "Source file: examples/CommonLarceny/event-handling-demo2.sch"
-
Mon Nov 13 02:54:21-0500 2006 pnkfelix is now reminded that it is unlikely (perhaps impossible by design) for the Twobit fasl to compile the second demo successfully, because the Twobit fasl will use the usual-syntactic-environment, which (IIRC) does not include the necessary macros for javadot syntax.
- That said, the fact that it is infinite looping/memory leaking is quite distressing.
- PnkFelix does not think this should delay the 0.93 release
- In particular, the docs could just say that use of the Twobit fasl is for internal development use only. Perhaps they should say this across the board.
- The REPL under Twobit fasl may/should still be capable of running the demo, though, if we care to test it. (But see about note about how we should just not support Twobit fasl at all...)
We need to look into detaching event handlers from events ...