Dynamic Callbacks In Plugins

June 11th, 2008 by Patrick Boucher - Viewed 10877 times -




I really like applications that provide rich SDKs for it’s users to play around with. I also like event based systems and callbacks. These are all things that make me absolutely love writing up plugins inside XSI. But there is one small problem, I find, with XSI’s callbacks.

Callbacks in XSI plugins are functions in the global scope who’s name must be defined exactly the way XSI expects to find them.

I would have preferred callbacks to be supplied by the user in a way that allows them to have any name and in a way that allows them to be changed on the fly. In all fairness, Softimage probably has some very good reasons for the way they did things.

So this is where we, as users, exploit the power that has been given to us and take things into our own hands. Here is my implementation of dynamic, interchangeable, replaceable callbacks.

Code Time!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import win32com.client
from win32com.client import constants as c
from vg.xsi.dynamic import *
 
def XSILoadPlugin( in_reg ):
	in_reg.Author = "Patrick Boucher"
	in_reg.Name = "dynCbCommandPlugin"
	in_reg.Major = 1
	in_reg.Minor = 0
	in_reg.RegisterCommand("dynCbCommand", "dynCbCommand")
	return True
 
def XSIUnloadPlugin( in_reg ):
	strPluginName = in_reg.Name
	Application.LogMessage(str(strPluginName) + str(" has been unloaded."),c.siVerbose)
	return True
 
def dynCbCommand_Init( in_ctxt ):
	oCmd = in_ctxt.Source
	oCmd.Description = ""
	oCmd.ReturnValue = True
	return True
 
@dynamic
def dynCbCommand_Execute():
	Application.LogMessage('In original callback!')
	setCallback(dynCbCommand_Execute, callbackTwo)
	return True
 
def callbackTwo():
	Application.LogMessage('In %s!' % callback(dynCbCommand_Execute).func_name)
	setCallback(dynCbCommand_Execute, callbackThree)
	return True	
 
def callbackThree():
	Application.LogMessage('In callback three!')
	restoreCallback('dynCbCommand_Execute')
	return True	
 
setCallback(dynCbCommand_Execute, callbackTwo)

The first time you run this command it will run the code in callbackTwo and switch itself to new code. The next time it will run callbackThree and the next time it will run the original callback code. It will then, with each consecutive invocation of the command, continue on in this loop. This plugin, as you have guessed, is just a demo of the dynamic callbacks.

The lines you have to concentrate on are the following:

from vg.xsi.dynamic import *

and

@dynamic

These are the lines that provide the core of the functionality which is imported from the vg.xsi.dynamic module. Here it is…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import sys
 
__all__ = ['dynamic', 'setCallback', 'callback', 'restoreCallback']
 
class Wrapper(object):
    def __init__(self, cb):
        self.__defaultCb = cb
        self.__cb = cb
 
    def call(self, *args, **kwargs):
        return self.__cb(*args, **kwargs)
 
    def setCallback(self, newCb):
        self.__cb = newCb
 
    def callback(self):
        return self.__cb
 
    def restoreCallback(self):
        self.__cb = self.__defaultCb
 
def dynamic(func):
    cbWrap = Wrapper(func)
    return cbWrap.call
 
def setCallback(bound, newCb):
    bound = _getCallable(bound)
    newCb = _getCallable(newCb)
    return bound.im_self.setCallback(newCb)
 
def callback(bound):
    bound = _getCallable(bound)
    return bound.im_self.callback()
 
def restoreCallback(bound):
    bound = _getCallable(bound)
    return bound.im_self.restoreCallback()
 
def _getCallable(req):
    if callable(req):
        return req
 
    if type(req) is str:
        g = sys._getframe(2).f_globals
        return g[req]

I’ve removed any comments or docstrings for the sake of brevity.

About Decorators

The import line I said was of particular interest. Well it is but only in the sense that it will allow you to use the module in which the features are implemented… Moving on quickly.

The real fun is with the @dynamic decorator. What is a decorator and what happens when we use one (or this one in particular)? Before we look at what a decorator is and does, let’s look at what happens when we define a function.

1
2
def func():
    pass

When a function is defined, what is happening is that an identifier is created in your current scope and this identifier is given the name of your function. Now let’s add the decorator. A decorator is just a simple function, but a function who’s purpose is very specific and a function that must return a callable (another function most of the time).

1
2
3
@myDecorator
def func():
    pass

What happens here is that the identifier func is still created but what lies ‘in’ this identifier is not the function itself but the result of passing said function to myDecorator. To help you visualize, the above code could just as easily been written like so:

1
2
3
def func():
    pass
func = myDecorator(func)

Let’s Get Specific

The decorator @dynamic takes your callback and wraps it inside a Wrapper object and returns the call method of this new object. When XSI tries to call what it thinks is just a simple callback it is actually calling the call method of an object that in turn calls your original callback. Whew!

This means that you now have the liberty of querying and editing the callback object and telling it to call something different depending on context. This functionality is provided by the setCallback, callback and restoreCallback functions.

These three functions either take the callback function objects or the callback’s names (as a string) as arguments.

There are some more particularities to this code such as the use of sys._getframe, the im_self attribute or the use of the call method on the Wrapper object instead of implementing __call__ but I think they might be outside the scope of this article. If there are requests for it I can expand in the comments or another post.

Meanwhile you can download the example. Drop the plugin into a plugin folder (user, factory or workgroup) and drop the vg folder somewhere in your Python path.

Cheers!

5 Responses to “Dynamic Callbacks In Plugins”

  1. I should have mentioned that this idea started boiling in my subconscious after this thread on the XSI Mailing list: http://tinyurl.com/4cde9w.

  2. Patrick – if you get a chance could you go into more detail on sys._getframe, im_self and the use of the call method on the Wrapper object? Fascinating stuff…

  3. Hey Julian,

    about im_self:

    “Special read-only attributes: im_self is the class instance object, im_func is the function object; im_class is the class of im_self for bound methods, or the class that asked for the method for unbound methods);”

    See User-defined method -> http://www.python.org/doc/2.2.3/ref/types.html

    sys._getframe:

    “- A new function (python 2.1) sys._getframe(), returns the stack frame pointer of
    the caller. This is intended only as a building block for
    higher-level mechanisms such as string interpolation.

    http://www.python.org/download/releases/2.1/NEWS.txt

  4. Xavier covers two points accurately, if tersely.

    As for the third, I don’t know if it is a COM thing or an XSI thing but the callbacks in XSI Plugins can’t just be callable items as defined by Python, they have to be methods or functions. If the callbacks could have been any callable we could have changed the call method in the Wrapper object to __call__ and we could have implemented the decorator as follows:

    1
    2
    
    def dynamic(func):
        return Wrapper(func)

    By doing it this way, the callback would have been the wrapper object itself and it would have eliminated the need to use im_self to go from the bound method to the instance.

    In my opinion, if we could have done it that way, it would have made for cleaner code.

    Hope this helps.

  5. niko says:

    Salut Patrick,

    I need to create some dynamically named functions (defs) for an adaptable creature setup. Those functions are OnChanged Event PPG functions of a customProp inside a plugin. So they need to be callable as “%s_%s_OnChanged” % (customProp.Name, dynamicParameter.Name). I’ve downloaded your plugin and have tried to understand it but as you know I’m not a pro when it comes to classes and I’m still a bit lost. :S

    I am wondering how to hack your code:

    1- Which function do I need to modify to be able to pass customProp.Name and dynamicParameter.Name as arguments and it would give me back a callable function named: “%s_%s_OnChanged” % (customProp.Name, dynamicParameter.Name) ?

    2- Which function I have to put my actual code into (the code that the properly renamed function would use – this function would need to know it’s own dynamicParameter.Name to be able to find some parts of the Rig it’s controlling) ?

    3- What is the best way to create and organise these functions so that I can have different code (some FK/IK switching code and some completely different code for Elbow Locking as an example) depending on different dynamicParameter.Name?

    Thanks for any help!

    niko