12

In C# I have following methods defined in given class (non static):

int MyMethod(ScriptEngine script, int a, int b) {
    return a + b;
}

void MyMethod2(ScriptEngine script, string c) {
    // do something with c
}

I want to create wrapping lambda / Action / Delegate / MethodInfo (all are accepted by script engine) that automatically passes ScriptEngine and this from given, predefined variables.

So far I've experimenting with:

// With overloads up to 16 template parameters
Action<T1> Wrap<T1>(Action<ScriptEngine, T1> func, ScriptEngine script) { 
     return (Action<T1>) ((t1) => func(script, t1));
}

But when called on MyMethod2 I've got The type arguments for method … cannot be inferred from the usage. Try specifying the type arguments explicitly. If I specify the template arguments explicity, it works, but I want to avoid such specification.

Is there any other way (not neccesery following my solution) I can create such wrapper automatically (or semi-automatically)?

It is worth mentioning that there is a dedicated abstract method void RegisterAll(ScriptEngine script) that can register required members of given subclass.

Here is an example of what I am trying to achieve:

class ScriptEngine { // Stub to have complete example, actual implementation is defined elsewhere
    void RegisterApi(string name, MethodInfo methodInfo) { }
    void RegisterApi(string name, Delegate delegateSelf) { }
}

class Api {
    int MyMethod(ScriptEngine script, int a, int b) {
        return a + b;
    }

    void MyMethod2(ScriptEngine script, string c) {
        // do something with c
    }

    void RegisterAll(ScriptEngine script) {
         // Is there any way to shorten this up (not providing MyMethod twice, not providing template arguments?)
        script.RegisterApi(nameof(MyMethod), (Delegate)Wrap<string>(MyMethod, script));
    }

}

The problem is how to improve this RegisterApi method so it:

  • Takes the method only once
  • Does not require specifying arguments via template method
Gimby
  • 5,095
  • 2
  • 35
  • 47
PiotrK
  • 4,210
  • 6
  • 45
  • 65
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/223160/discussion-on-question-by-piotrk-c-create-lambda-over-given-method-that-injects). – Samuel Liew Oct 16 '20 at 13:33

2 Answers2

2

There is actually another solution that does not involve emiting new Expressions (could fail on iOS!)

First, let us define following wrapper:

    private class Wrapper
    {
        public readonly object container;
        public readonly MethodInfo method;
        public readonly ScriptEngine engine;

        public Wrapper(object container, MethodInfo method, ScriptEngine engine)
        {
            this.container = container;
            this.method = method;
            this.engine = engine;
        }

        public Action CreateAction()
        {
            return () => method.Invoke(container, new object[] { engine });
        }
        public Action<T1> CreateAction<T1>()
        {
            return (arg1) => method.Invoke(container, new object[] { engine, arg1 });
        }
        // etc
    }

Now you can register method like that:

        var type = typeof(Wrapper);
        var instance = Activator.CreateInstance(type, new object[] { container, methodInfo, engine });
        MethodInfo methodActual = null;
        if (methodInfo.ReturnType == typeof(void))
        {
            var methods = type.GetMethods().Where(x => x.Name == "CreateAction");

            foreach (var method in methods)
            {
                if (method.GetGenericArguments().Length == methodInfo.GetParameters().Length - 1)
                {
                    methodActual = method.MakeGenericMethod(methodInfo.GetParameters().Skip(1).Select(x => x.ParameterType).ToArray());
                }
            }
        }
        var actionToRegister = methodActual.Invoke(instance, new object[0]);
PiotrK
  • 4,210
  • 6
  • 45
  • 65
1

The solution is to use Expression Tree to create a simple delegate wrapper.

        var parameterEngine = Expression.Constant(script, typeof(ScriptEngine));
        var parameterCaller = Expression.Constant(this, this.GetType());

        // Build parameters, skipping the first. Note that trailing "ToList" is important!
        var parameters = methodInfo.GetParameters().Skip(1).Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToList();

        // Inject parameter
        var mergedParameters = new List<Expression>();
        mergedParameters.Add(parameterEngine);
        mergedParameters.AddRange(parameters);

        // Bind together
        var call = Expression.Call(parameterCaller, methodInfo, mergedParameters);
        var expression = Expression.Lambda(call, parameters);
        var method = expression.Compile();

        // Method can now be registered
PiotrK
  • 4,210
  • 6
  • 45
  • 65