0

I have a 3rd party scripting engine contained in a session in my code. The engine takes any delegate and makes it available to it's script with the same signature.

Now I want to have plugins that provide these delegates for the engine, but I also want extra data from the session without it showing up in the script.

The script consuming the delegate should have no idea about the session, but the plugin implementing it does. The plugin writer should be free to use any number or types of arguments for the plugin delegates, so I need to do this dynamically at run time.

For example:

//from available plugin delegates
delegate bool SendMessage(Session info, string ip, int port, string message);
delegate void LogMessage(Session info, string message);

//to create script delegates
delegate bool SendMessage(string ip, int port, string message);
delegate void LogMessage(string message);

So when the script engine calls LogMessage("Test") it should invoke LogMessage(mysession, "Test") in the plugin.

I found information on curry for adding defaults to delegates and Reflection could create the delegates, but how can they be fit together to accomplish this?

EDIT: full length example

public class Session
{
    //Some metadata here
}

public class Plugin
{
    private delegate bool SendMessage(Session info, string ip, int port, string message);
    private delegate void LogMessage(Session info, string message);

    public Delegate[] GetFunctions()
    {
        return new Delegate[] { new SendMessage(HandleSendMessage), new LogMessage(HandleLogMessage) };
    }

    private bool HandleSendMessage(Session info, string ip, int port, string message)
    {
        Console.WriteLine($"SEND {ip}:{port} >> \"{message}\"");
        return true;
    }

    private void HandleLogMessage(Session info, string message)
    {
        Console.WriteLine($"LOG \"{message}\"");
    }
}

//stand-in for 3rd party code
public class Engine
{
    private IEnumerable<Delegate> _functions = null;

    public void Add(IEnumerable<Delegate> functions)
    {
        //ignore this code, just simulating 3rd party behavior
        _functions = functions;
    }

    public void Execute()
    {
        //ignore this code, just simulating 3rd party behavior
        foreach (Delegate function in _functions)
        {
            ParameterInfo[] fparams = function.Method.GetParameters();
            int n = fparams.Count();
            object[] args = new object[n];
            for (int i = 0; i < n; i++)
            {
                if (string.Compare(fparams[i].Name, "ip") == 0)
                {
                    args[i] = "127.0.0.1";
                }
                else if (string.Compare(fparams[i].Name, "port") == 0)
                {
                    args[i] = 80;
                }
                else if (string.Compare(fparams[i].Name, "message") == 0)
                {
                    args[i] = "Some message";
                }
                else if (string.Compare(fparams[i].Name, "info") == 0)
                {
                    Console.WriteLine("Error this should not be here");
                    args[i] = null;
                }
            }
            function.DynamicInvoke(args);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Plugin p = new Plugin(); //assume this instead comes from Assembly.Load(..) and Activator.CreateInstance(..)
        Engine e = new Engine(); //stand-in for 3rd party code
        List<Delegate> newDelegates = new List<Delegate>();

        foreach (Delegate d in p.GetFunctions())
        {
            //QUESTION: create a new delegate same as (d) minus the first param (Session info) 
            //QUESTION: link the new delegate to (d) and set (Session info) to some value

            newDelegates.Add(d); //add new delegate instead of (d)
        }

        e.Add(newDelegates);
        e.Execute();
    }
}

EDIT 2: Progress update

I can now create a delegate type with less variables then the original

/// <summary>
/// Based on code from user svick [https://stackoverflow.com/questions/9505117/creating-delegates-dynamically-with-parameter-names]
/// </summary>
class DelegateTypeFactory
{
    private readonly ModuleBuilder _module;

    public DelegateTypeFactory()
    {
        //Build in-memory assembly to contain the new types
        AssemblyBuilder assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("DelegateTypeFactory"), AssemblyBuilderAccess.RunAndCollect);
        _module = assembly.DefineDynamicModule("DelegateTypeFactory");
    }

    public Type CreateDelegateType(MethodInfo method)
    {
        //Create new name for the type to avoid clashes
        string nameBase = string.Format("{0}{1}", method.DeclaringType.Name, method.Name);
        string name = GetUniqueName(nameBase);

        //Create the toolset to make the new type
        TypeBuilder builder = _module.DefineType(name, TypeAttributes.Sealed | TypeAttributes.Public, typeof(MulticastDelegate));
        ConstructorBuilder constructor = builder.DefineConstructor(MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(object), typeof(IntPtr) });
        constructor.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);

        //define the methods params and filter unwanted param
        ParameterInfo[] parameters = method.GetParameters();
        parameters = parameters.Where(p => p.ParameterType != typeof(Session)).ToArray();

        //design the method signature
        MethodBuilder invokeMethod = builder.DefineMethod("Invoke", MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.Public, method.ReturnType, parameters.Select(p => p.ParameterType).ToArray());
        invokeMethod.SetImplementationFlags(MethodImplAttributes.CodeTypeMask);
        for (int i = 0; i < parameters.Length; i++)
        {
            invokeMethod.DefineParameter(i + 1, ParameterAttributes.None, parameters[i].Name);
        }

        //Return the newly created delegate type
        return builder.CreateType();
    }

    private string GetUniqueName(string nameBase)
    {
        int number = 2;
        string name = nameBase;
        while (_module.GetType(name) != null)
        {
            name = $"{nameBase}{number++}";
        }
        return name;
    }
}

Usage:

DelegateTypeFactory factory = new ConsoleApplication1.DelegateTypeFactory();
Type newDelegateType = factory .CreateDelegateType(originalDelegate.Method);

However how one could instantiating the new delegate and make it call the original delegate with the default session value eludes me

Kris Vanherck
  • 245
  • 1
  • 10

1 Answers1

1

It seems like you have Plugins passing delegates into the Engine.

The engine then invokes the plugins dynamically.

You can do this with closures, but the plugin would have to create the closure since it is creating the delegate. So 3rd party developers could use this technique as well, it would be up to them. If they don't need any extra objects available in the delegate they don't have to.

It would be transparent to the Engine that the delegate has captured other variables.

I see in your main you have comments that indicate you're thinking about mutating the plugin functions there. I don't know how you would do it there since you wouldn't know what paramaters the Plugin author intended to be in/visible.

So I wrote this to allow the Plugin to decide what it wants to hide.

I left your Handle* methods the way you wrote them, but they do have access to the Session objects if required.

public class Session
{
    //Some metadata here
}

public class Plugin
{
    private delegate bool SendMessage(string ip, int port, string message);
    private delegate void LogMessage(string message);

    public Delegate[] GetFunctions()
    {
        var sessionInfo = new Session();
        return new Delegate[] { new SendMessage(HandleSendMessage(sessionInfo)), new LogMessage(HandleLogMessage(sessionInfo)) };
    }

    private SendMessage HandleSendMessage(Session info)
    {
        return delegate (string ip, int port, string message)
        {
            Console.WriteLine($"SEND {ip}:{port} >> \"{message}\"");
            return true;
        };
    }

    private LogMessage HandleLogMessage(Session info)
    {
        return delegate (string message)
        {
            Console.WriteLine($"LOG \"{message}\"");
        };
    }
}

//stand-in for 3rd party code
public class Engine
{
    private IEnumerable<Delegate> _functions = null;

    public void Add(IEnumerable<Delegate> functions)
    {
        //ignore this code, just simulating 3rd party behavior
        _functions = functions;
    }

    public void Execute()
    {
        //ignore this code, just simulating 3rd party behavior
        foreach (Delegate function in _functions)
        {
            ParameterInfo[] fparams = function.Method.GetParameters();
            int n = fparams.Count();
            object[] args = new object[n];
            for (int i = 0; i < n; i++)
            {
                if (string.Compare(fparams[i].Name, "ip") == 0)
                {
                    args[i] = "127.0.0.1";
                }
                else if (string.Compare(fparams[i].Name, "port") == 0)
                {
                    args[i] = 80;
                }
                else if (string.Compare(fparams[i].Name, "message") == 0)
                {
                    args[i] = "Some message";
                }
                else if (string.Compare(fparams[i].Name, "info") == 0)
                {
                    Console.WriteLine("Error this should not be here");
                    args[i] = null;
                }
            }
            function.DynamicInvoke(args);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Plugin p = new Plugin(); //assume this instead comes from Assembly.Load(..) and Activator.CreateInstance(..)
        Engine e = new Engine(); //stand-in for 3rd party code
        List<Delegate> newDelegates = new List<Delegate>();

        foreach (Delegate d in p.GetFunctions())
        {
            //QUESTION: create a new delegate same as (d) minus the first param (Session info) 
            //QUESTION: link the new delegate to (d) and set (Session info) to some value

            newDelegates.Add(d); //add new delegate instead of (d)
        }

        e.Add(newDelegates);
        e.Execute();

    }
}
Ashley Pillay
  • 868
  • 4
  • 9
  • I added a full length example, hope that clarifies the question. I had not found closure, looks interesting, but can it get me all the way? I cannot type something like `Func` because I do not know what delegates the plugins have (3rd party code), so it would need to be possible to generate the signature based on reflection. – Kris Vanherck Aug 25 '17 at 12:51
  • This works, but a bit too advanced for our target plugin-writer audience :( . We ended up building a generalized `invoke(string function, string[] params)` delegate for the script engine that got translate to the normal plugin delegates in the invoke implementation. We then pre-load scripts into the engine that generate wrapper functions around the invoke to match the plugin delegate (without the session). – Kris Vanherck Aug 31 '17 at 11:51