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