views:

82

answers:

2

Hey!

In IronPython I am trying to call a PythonFunction with different numbers of arguments from C#. For instance;

I want to do:

def foo(a, b):
     print a, b

def bar(a, b, c = None):
     print a, b, c

p = App.DynamicEvent()
p.addHandler(foo)
p.addHandler(bar)
p.invoke("Not", "Working")

where addHandler takes a single argument and somehow stores it in a list of methods to be invoked and invoke has a signature like this:

public virtual void invoke(params object[] tArgs)

Because I want to avoid making it specific to the PythonEngine (and thus engine.Operations.Invoke()), I've tried several ways of storing and implementing these things as delegates but I think the crux of my problem is that I don't know how to store some kind of MulticastDelegate base type that is compatible with a PythonFunction?

Perhaps I want to implement my own DynamicInvoke method? Any thoughts and experience would be greatly appreciated!

The reason for wanting to do this is that I want to transparently map calls made from a sealed Javascript engine into IronPython via C#. i.e. in the Javascript call: Client.doThing("something", 4, {"key:"value"}) and handle it in the python with:

def doThing(s, i, d):
    pass

using the following dynamic event binding:

doThingEvent = App.DynamicEvent()
doThingEvent.addHandler(doThing)
WebBrowser.handleMethod("doThing", doThingEvent);
+2  A: 

You can pass a PythonFunction as a delegate for example by casting to an Action<...>

i.e. in Python by doing something like:

import sys
import clr
import System
from System import *

def foo(a, b):
     print a, b

def bar(a, b, c = "N.A."):
     print a, b, c

p = App.DynamicEvent()
p.AddHandler( Action[object,object](foo) )
p.AddHandler( Action[object,object,object](bar) )
p.DynamicInvoke("Not", "Working")

In this way your p can be a MulticastDelegate. Obviously this implies:

  1. All the passed delegates must have the same signature
  2. you can pass Python function with at most 16 parameters (because Action support 16 params maximum)

For the first problem I think you need to write your own "Delegates invoker", something like:

//
// WARNING: very very rough code here !!
//
public class DynamicEvent
{
    List<Delegate> delegates;

    public DynamicEvent()
    {
        delegates = new List<Delegate>();
    }
    public void AddHandler(Delegate dlgt)
    {
        delegates.Add(dlgt);
    }
    public void Invoke(params object[] args)
    {
        foreach (var del in delegates)
        {
            var parameters = del.Method.GetParameters();
            // check parameters number
            if (args.Length != parameters.Length)
                continue; // go to next param

            // check parameters assignability
            bool assignable = true;
            for (int i = 0; i < args.Length; i++)
            {
                if (!parameters[i].ParameterType.IsInstanceOfType(args[i]))
                {
                    assignable = false;
                    break; // stop looping on parameters
                }
            }
            if (!assignable)
                continue; // go to next param

            // OK it seems compatible, let's invoke
            del.DynamicInvoke(args);
        }
    }
}

N.B.:

the part checking delegate-parameters compatibility is wrong, is just to give you an idea.

The problem is that I don't know how to check, at runtime, whether a list of object args is compatible with a delegate signature... maybe we should check IronPython source code :)

digEmAll
Do you mean `Action[object,object,object](bar)`?
Callum Rogers
Yes, sorry. Edited thanks :)
digEmAll
A: 

My first idea was as digEmAll suggested but I came up with a better solution that doesn't require the casting to System.Action or any invoke-time type checking or branching. Just overload addHandler to take a PythonFunction as well as a Delegate, meaning you can use your original code:

def foo(a, b):
     print a, b

def bar(a, b, c = None):
     print a, b, c

p = DynamicEvent(engine)
p.addHandler(foo)
p.addHandler(bar)

p.invoke("a", "b")
p.invoke("a", "b", "c")

With this C# code:

public class DynamicEvent
{
    private Dictionary<int, Action<object[]>> delegates = new Dictionary<int, Action<object[]>>();
    public ScriptEngine Engine { get; set; }


    public DynamicEvent(ScriptEngine engine)
    {
        Engine = engine;
    }

    public void addHandler(PythonFunction pythonFunction)
    {
        int args = (int) pythonFunction.func_code.co_nlocals;
        delegates.Add(args, a => Engine.Operations.Invoke(pythonFunction, a));
    }

    public void addHandler(Delegate d)
    {
        int args = d.Method.GetParameters().Length;
        delegates.Add(args, a => d.DynamicInvoke(a));
    }

    public void invoke(params object[] args)
    {
        Action<object[]> action;
        if(!delegates.TryGetValue(args.Length, out action))
            throw new ArgumentException("There is no handler that takes " + args.Length + " arguments!");

        action(args);
    }
}

Note that you need to add the engine to the script's scope so you can use it in the constructor.

Hope that helped!


Note: It is possible to get a Delegate and execute it from a PythonFunction like this:

Delegate target = pythonFunction.__code__.Target;
var result = target.DynamicInvoke(new object[] {pythonFunction, args});

but it is more platform dependant than using Engine.Operations.Invoke() and the Delegate signature is different to a 'regular' Delegate.

Callum Rogers
Thanks for the answers guys (and sorry about the delay in getting back to you both).
John
My issue with using this solution is twofold really. (1) I cannot remove the delegate without storing an extra reference to the PythonFunction (not too much of an issue). (2) It inserts script host specific functionality into part of my codebase that is more generic.
John
My compromise is to write a subclass that adds the DynamicEvent with script host specific handling ability (as above), but implement and override in each script host that is supported. Not ideal as I am sure you can imagine... but it does let me call transparently across script hosts if not add/remove handlers across them... still it seems surprising that this was not supported by default as some implicit conversion between a callable and a delegate... alas :)
John
@John: Thanks for getting back to me and I hope you get everything working fine!
Callum Rogers