Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run delegate in different process

Tags:

c#

.net

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?..

like image 245
Alex Zhukovskiy Avatar asked Jul 21 '26 02:07

Alex Zhukovskiy


1 Answers

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 :)

like image 199
Luaan Avatar answered Jul 22 '26 15:07

Luaan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!