09/17/2020

As I mentioned in an earlier post, I wrote a visual editor for building \Psi programs. For most nodes in a \Psi program the inputs/outputs are known ahead of time. For instance, my editor exposes a node for performing a Gaussian blur. The inputs to that node is the image to be blurred. This makes writing the code for that node pretty simple:

var inputStreamName1 = this.inputs[0].Connections[0].Parent.GetOutputStreamName(this.inputs[0].Connections[0]);
Emitter<Shared<Microsoft.Psi.Imaging.Image>> emitter1 = ctx.connectors[inputStreamName1] as Emitter<Shared<Microsoft.Psi.Imaging.Image>>;
var he = Microsoft.Psi.OpenCV.PsiOpenCV.GaussianBlur(emitter1, (int)GetProperty("WindowSize"), (double)GetProperty("Sigma"));
ctx.connectors.Add(GetOutputStreamName(outputs[0]), he.Out);

As can be seen by the bolded text, I know that the emitter has a type Shared<Microsoft.Psi.Imaging.Image>.

However, for some operators, I don't know in advance what the type is. This is where I use C#'s reflection support to call the correct function. For instance, I have a node for \Psi's "return" generator that simply returns an arbitrary (type-wise) object as its output. In order to make this work, I have to run code similar to the following:

object[] args = { ctx.pipeline, value };
var methods = typeof(Generators).GetMethods();
foreach (var method in methods)
{
    if (method.Name == "Return")
    {
        Type[] argTypes = new Type[] { value.GetType() };
        var repeatMethod = method.MakeGenericMethod(argTypes);
        object prod = repeatMethod.Invoke(null, args);
        object outprod = prod.GetType().GetProperty("Out").GetValue(prod);
        ctx.connectors.Add(GetOutputStreamName(outputs[0]), outprod);
        break;
    }

}

As shown the example above, we first need to walk across all the methods (GetMethods()) exposed by type Generators and find the correct method. We then need to make a generic method (MakeGenericMethod) using the types based on the value type. We then have to call the generic method using the arguments we supplied (Invoke()).

This works for the most part. Where this becomes super difficult is when the arguments to the method are multilevel templates and the method we are attempting to call is an extension method. The first problem is finding the correct extension method. We can do this by enumerating over the extension class to find the correct method using something like:

var ms = typeof(Microsoft.Psi.Operators).GetMethods();
foreach (var m in ms)
{
    if (m.Name.Contains("Join"))
    {
        // Possibly use this method
    }
}

One problem with this approach is that there can be many overloads of Join<> (e.g. Join<TPrimary,TSecondary>, Join(TPrimary,TSecondary1,TSecondary2> etc) and we need to find the correct one.

The second problem is that we might have to construct new types to pass to MakeGenericMethod. For instance, here is one of the signatures for one version of Join<>:

public static IProducer<(TPrimary, TSecondary)> Join<TPrimary, TSecondary>(
    this IProducer<TPrimary> primary,
    IProducer<TSecondary> secondary,
    RelativeTimeInterval relativeTimeInterval,
    DeliveryPolicy<TPrimary> primaryDeliveryPolicy = null,
    DeliveryPolicy<TSecondary> secondaryDeliveryPolicy = null)

Notice that the first and second arguments are a template using the TPrimary/TSecondary template specification (i.e. IProducer<TPrimary>). Thus in order to call the generic method we need to do something like:

var q = typeof(IProducer<>);
var i0 = q.MakeGenericType(inputType0);
var i1 = q.MakeGenericType(inputType1);
var qd = typeof(DeliveryPolicy<>);
var q0 = qd.MakeGenericType(inputType0);
var q1 = qd.MakeGenericType(inputType1);
Type[] argTypes = new Type[] { i0, i1, typeof(RelativeTimeInterval), q0, q1};
var repeatMethod = m.MakeGenericMethod(argTypes);

The upshot of all this, is that using reflection to call heavily templatized methods is a royal PITA.

Luckily, there is a much simpler solution. Simpler create a new type! Then instantiate that object, which does all the heavy lifting. This way we avoid all the nested nastiness of types on the method we are trying to call. Below is the implementation for PsiEditorJoin<> using this pattern. This allows us to easily call Join() without worrying about all its various implementations. We let the compiler figure it out.


class PsiEditorJoin<T1,T2>
{
        public void Preview(ref PreviewContext ctx, List<DispConnector> inputs, List<DispConnector> outputs, string outputStreamName)
        {
                var inName0 = inputs[0].Connections[0].Parent.GetOutputStreamName(inputs[0].Connections[0]);
                var emitter0 = ctx.connectors[inName0] as Emitter<T1>;
                var inName1 = inputs[1].Connections[0].Parent.GetOutputStreamName(inputs[1].Connections[0]);
                var emitter1 = ctx.connectors[inName1] as Emitter<T2>;
                var firstArg = emitter0.Join(emitter1, new RelativeTimeInterval(TimeSpan.FromSeconds(-1),         TimeSpan.FromSeconds(1)));
                ctx.connectors.Add(outputStreamName, firstArg.Out);
                inputs[0].ValueType = typeof(T1);
                inputs[1].ValueType = typeof(T2);
                outputs[0].ValueType = typeof((T1, T2));
        }
}

public override void Preview(ref PreviewContext ctx)
{
        if (!visited)
        {
                visited = true;
                base.Preview(ref ctx);
                var inputType0 = this.inputs[0].Connections[0].ValueType;
                var inputType1 = this.inputs[1].Connections[0].ValueType;
                var d1 = typeof(PsiEditorJoin<,>);
                System.Type[] typeArgs = { inputType0, inputType1 };
                var objType = d1.MakeGenericType(typeArgs);
                object o = System.Activator.CreateInstance(objType);

                var previewMethodInfo = objType.GetMethod("Preview");
                object[] parameters = { ctx, this.inputs, this.outputs, GetOutputStreamName(outputs[0]) };
                previewMethodInfo.Invoke(o, parameters);
        }
}

This is way cleaner and simpler to understand.