3

In C# we have beautiful CodeDomProvider class, that allows us to dynamically create some exe file from sources, run it and so on.

Problem is when we don't have such sources. For example, I have a delegate (Action<string> for example), I want to compile an exe, that takes first argument from command line, and executes passed Action<string> with it. In more complex situation, I have Func<string[], string>, Which takes passed command line arguments, does something and writes something in standard output.

I want something like string ExecuteOutOfProcess(Func<string[], string> func, string[] args), that compiles an exe, runs it with provided arguments, get result from standard output and then returns it as result. Ideally, it should be more generic, and for example use TResult ExecuteOutOfProcess<T,TResult>(Func<T, TResult> func, T input), and it should internally de- and serialize everything, transparently for calling code.

Is there something to achieve it? Becuase the only way to do something similar is just write a decompiler, then get C# sources from delegate, then use those source with CodeDomProvider, that parse sources again... There is no way to pass expression directly in compiler?..

Alex Zhukovskiy
  • 9,565
  • 11
  • 75
  • 151
  • Not clear what you want to achieve. You may Assembly.Load your compiled exe/dll module in to your program and pass any Action<> from the program to any function in your compiled and loaded module. – Sergey L May 11 '16 at 11:51
  • What code are you trying to execute? The general case is pretty much impossible, but there might be a more specific case that would be doable. For example, if the delegate refers to a function (inputs -> outputs) with serializable inputs and outputs, you simplify the problem quite a bit. – Luaan May 11 '16 at 15:09
  • @SergeyL I have a 64 bit host process, I should run some code that interop with 32 bit dlls. Now I should write my own little applications, that just are proxy between 64 host and 32 slave dll. It's annoying to write it again and again, and I'm looking for something, that automatize those interation. – Alex Zhukovskiy May 11 '16 at 15:10
  • I cannot imagine why this is getting upvotes. The general form is going to be incredibly nasty to the point where the only viable solution is going to be attuned to the specific use case, and the use case is not given. – Joshua May 11 '16 at 15:42
  • 1
    @Joshua >> The general form is going to be incredibly nasty - it's your opinion, but is not definitly truth. – Alex Zhukovskiy May 11 '16 at 15:51

2 Answers2

3

This is actually quite tricky. But you can simplify it quite a bit if you make sure that it's used in a certain way:

  • Only allow calling static functions that have serializable arguments and return values, and don't touch any other managed state.
  • Since you want 64-bit -> 32-bit interop, have the functions declared in an assembly that's set as AnyCPU

Within these constraints, you can use a simple trick: send the type and method name of whatever you're trying to execute, and your helper test runner can load the type using the assembly qualified name and invoke the method. To send the required data, you can use something like WCF or memory mapped files, for example.

A very simple (and fragile) example:

public static async Task<T> Run<T>(Func<T> func)
{
    var mapName = Guid.NewGuid().ToString();

    using (var mapFile = MemoryMappedFile.CreateNew(mapName, 65536))
    {
        using (var stream = mapFile.CreateViewStream())
        using (var bw = new BinaryWriter(stream))
        {
            bw.Write(func.Method.DeclaringType.AssemblyQualifiedName);
            bw.Write(func.Method.Name);

            if (func.Target == null)
            {
                bw.Write(0);
            }
            else
            {
                using (var ms = new MemoryStream())
                {
                    new BinaryFormatter().Serialize(ms, func.Target);
                    var data = ms.ToArray();
                    bw.Write(data.Length);
                    bw.Write(data);
                }
            }
        }

        using (var process = Process.Start(new ProcessStartInfo("LambdaRunner", mapName) { UseShellExecute = false, CreateNoWindow = true }))
        {
            process.EnableRaisingEvents = true;
            await process.WaitForExitAsync();

            switch (process.ExitCode)
            {
                case -10: throw new Exception("Type not accessible.");
                case -11: throw new Exception("Method not accessible.");
                case -12: throw new Exception("Unexpected argument count.");
                case -13: throw new Exception("Target missing.");
                case 0: break;
            }
        }

        using (var stream = mapFile.CreateViewStream())
        {
            return (T)(object)new BinaryFormatter().Deserialize(stream);
        }
    }
}

The helper runner executable looks like this:

static int Main(string[] args)
{
    var mapName = args[0];

    using (var mapFile = MemoryMappedFile.OpenExisting(mapName))
    {
        string typeAqn;
        string methodName;
        byte[] target;

        using (var stream = mapFile.CreateViewStream())
        using (var br = new BinaryReader(stream))
        {
            typeAqn = br.ReadString();
            methodName = br.ReadString();
            target = br.ReadBytes(br.ReadInt32());
        }

        var type = Type.GetType(typeAqn);
        if (type == null) return -10;

        var method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.InvokeMethod);
        if (method == null) return -11;
        if (method.GetParameters().Length > 0) return -12;

        object returnValue;

        if (target.Length == 0)
        {
            if (!method.IsStatic) return -13;

            returnValue = method.Invoke(null, new object[0]);
        }
        else
        {
            object targetInstance;
            using (var ms = new MemoryStream(target)) targetInstance = new BinaryFormatter().Deserialize(ms);

            returnValue = method.Invoke(targetInstance, new object[0]);
        }

        using (var stream = mapFile.CreateViewStream())
            new BinaryFormatter().Serialize(stream, returnValue);

        return 0;
    }
}

Example usage:

static string HelloWorld1()
{
    return "Hello world!";
}

static async Task RunTest<T>(int num, Func<Task<T>> func)
{
    try
    {
        Console.WriteLine($"Test {num}: {await func()}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Test {num} failed: {ex.Message}");
    }
}

[Serializable]
public struct Fun
{
    public string Text;
    public int Number;

    public override string ToString() => $"{Text} ({Number})";
}

static async Task MainAsync(string[] args)
{
    await RunTest(1, () => Runner.Run(HelloWorld1));
    await RunTest(2, () => Runner.Run(() => "Hello world from a lambda!"));
    await RunTest(3, () => Runner.Run(() => 42));
    await RunTest(4, () => Runner.Run(() => new Fun{Text = "I also work!", Number = 42}));
}

If you can keep yourself within the constraints I outlined, this will work pretty well - just make sure to also add proper error handling. Sadly, there's no simple way to ensure that the functions you're trying to call are "pure" - if there's a dependency on some static state somewhere, it's not going to work properly (that is, it will not use the static state in your process, but rather have its own, whatever that means).

You'll have to decide if this approach is worth it in your case or not. It may make things simpler, it might make them much worse :)

Luaan
  • 62,244
  • 7
  • 97
  • 116
0

You can convert your expression into Expression Tree:

https://blogs.msdn.microsoft.com/charlie/2008/01/31/expression-tree-basics/

and then compile it:

https://msdn.microsoft.com/en-us/library/bb345362(v=vs.110).aspx

Mind yourself, your expression might end up being a closure.

zmechanic
  • 1,842
  • 21
  • 27