My current Perl Ironman Challenge status is: My Ironman Badge

Saturday, May 2, 2009

POE + Moose with POEx::Role::SessionInstantiation | You, too, can do evil dark magic with Perl | IRC is a better friend than Google

And deeper down the rabbit hole we go.

So it all started in IRC the other day this past week, with Martijn van Beers talking about using MooseX::POE to rebuild Client::TCP. And I had a kind of epiphany. What if POE::Session were actually a Moose::Role. Think about it. What if you could take any object and turn it into a Session with out invoking the arcana yourself? That would be pretty cool right? Suddenly, the object_states of POE::Session become a first class concept. Subclassing a Session because you just want to make a couple of tweaks to a couple of methods becomes as simple of providing around advice for those methods. Your object becomes the ultimate POE cyborg. But how could a Role be so transmutative. With lots of fuckin' poking and prodding anything is possible.

So I shared this little epiphany and started hacking on it that night. Until dawn. And by then, I had a very simple working solution. After consulting some great POE::Session implementations on the CPAN such as POE::Session::Cascading and POE::Session::PlainCall (and stealing their ideas), I figured out that I needed to call session_alloc to register with the Kernel, and also to implement _invoke_state to receive events. So I did.

Inside the Role, I provide a dummy BUILD sub and then after advice for said BUILD sub (in case the consuming class has a BUILD and overrides my dummy one) to register with the Kernel. That took a little bit of brain power, but was oddly enough provided by Martijn again by regurgitating that exact information I needed from the #moose channel about this very topic. Pure awesomesauce. Next was _invoke_state. I spent a little time digesting the Class::MOP documentation (which is quite good), in order to figure out how take a given string for a state/event to be invoked and actually do that. And it was pretty dumb simple: find_method_by_name(). Follow the normal POE::Session behavior of sending things to _default if there is no event found and we are set.

Proof of concept complete. Time to flesh it out a bit. I wanted to be able to change events at runtime. So what does the Kernel do to make that happen? It calls _register_state on the Session. Groovy. I go back to the Class::MOP docs to figure out how to add a method to a class: add_method(). Well that's just too damn easy! I expand my little proof of concept tests to include using $poe_kernel->state('new_state' sub {}); and everything is peachy keen. Except, what about removing? Ah, look. remove_method(). Awesome.

I go off to parade my effort around and that is when Rocco Caputo points out a giant glaring naivety: I am changing the actual class, not the instance that Session represents. Adding a state to one instance effectively adds it to EVERY other instance. Holy leaky Sessions, batman! So back to the drawing board.

What about subclassing? Anonymous subclasses per instance. Sounds evil. Will it work though? I give a whirl. My 'new_state' test is passing. My 'remove_method()' test fails. WTF? I double check the meta object. Yup, has_method() tells me that my anonymous subclass does not have that method. But of course, further up the inheritance tree, my parent does. So find_method_by_name and, hell, invoking the method /works/. At this point, I am bitching. Both ruby and ecmascript support per instance manipulation of methods. Sigh. So I am desperate. I travel to the land of #moose and wail and generally gnash my teeth. That is when Chris Prather had this bang up idea, so evil, so magical, so awesome, that he deserves full credit for it: anonymous class CLONES. Not subclasses, but actual, full out, clones. I consult Class::MOP::Class and Moose::Meta::Class. I see all of the tools available for what I need.

Queue generic john williams evil march music.

So while I was doing this, I stumbled upon a horrible side effect to my evil, evil, subclassing/cloning ways: it breaks POE. So I do some spelunking into the guts of POE to figure out what is going on. Come to find out, POE uses the stringification of your Session object to basically track it internally. And since my cloning creates an anonymous class and I rebless the Session into said class, my stringification changes. Not all of it mind you, because the memory address is still the same. I still have the same object, it is just wearing a bunny suit instead of a santa claus suit.

Ultimately I was breaking POE encapsulation anyhow because I needed to store the newly reblessed Session object into the current slot, but down the road, the mismatch between the stringifications was killing me. And so the evil gets deeper. I overload "". Add an attribute to the Role to store the original stringification. But everything was still broken. After some deduction (print statements instead of the debugger because the debugger was giving me a SEGV, for the lose), I determine that while my sub routine for overloading stringification was indeed composed into the class, the magic of its execution was not happening. Hrm. Querying #moose again returns positive: Shawn Moore notices in the source of overload.pm that it does some symbol table hackery to enable it's magic:

${__PACKAGE__ . "::OVERLOAD"}{dummy}++;
*{__PACKAGE__ . "::()"} = sub {};

And obviously, Role composition doesn't invoke this particular incantation. So I throw it into "after 'BUILD'", and make sure that the symbol table hackery takes place in the anonymous clone classes, too. Now the magic happens in my composed class, which means now my stringification evil works, which means that POE now works, which means it all works! Well, in the simple cases.

There is always more. I wanted to add tests to make sure that composing an anonymous class with the Role wouldn't fail for some weird reason. And so I added that while inside one of the events for the test Session. And, surprise, it fails. But not because it is an anonymous class. I was trying to set an alias on the new anonymous Session from the constructor (the alias attribute has a trigger to call alias_set() on the Kernel) and that alias was getting added to another pre-existing Session, my parent. Silly me. I was still inside my parent's context. POE helpfully switches contexts between Sessions so that when you are constructing a Session, its _start will fire within it's own context, but I was jumping the gun. I didn't have a valid context yet and therefore my alias was getting added to my parent. Add some checks. Make sure to clear/restore my own context after each invocation (to handle parent/child situations). And I am set.

And along the while I also added tracing that looks just like POE::Sessions tracing, but a bit more complicated. Tracing happens on a per method basis and each method actually has around advice applied to it dynamically. I know, not exactly the most performance oriented way to do it. It will likely change to a simple statement just before invocation.

Whew. It works though. You can see the evil arcana incarnate out on github or clone the repo from me here. After I finish up the docs and get it prepared and ready for the CPAN, I'll likely look at doing Real Work with it next. Maybe start an experimental branch of POE::Component::Jabber using it.

No comments:

Post a Comment