// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.ClearScript.Util;
namespace Microsoft.ClearScript
{
internal static partial class DelegateFactory
{
public static Delegate CreateProc(ScriptEngine engine, object target, int argCount)
{
if ((argCount < 0) || (argCount > maxArgCount))
{
throw new ArgumentException("Invalid argument count", nameof(argCount));
}
var typeArgs = Enumerable.Repeat(typeof(object), argCount).ToArray();
return CreateDelegate(engine, target, procTemplates[argCount].MakeSpecificType(typeArgs));
}
public static Delegate CreateFunc(ScriptEngine engine, object target, int argCount)
{
if ((argCount < 0) || (argCount > maxArgCount))
{
throw new ArgumentException("Invalid argument count", nameof(argCount));
}
var typeArgs = Enumerable.Repeat(typeof(object), argCount).Concat(typeof(TResult).ToEnumerable()).ToArray();
return CreateDelegate(engine, target, funcTemplates[argCount].MakeSpecificType(typeArgs));
}
public static TDelegate CreateDelegate(ScriptEngine engine, object target)
{
return (TDelegate)(object)CreateDelegate(engine, target, typeof(TDelegate));
}
public static Delegate CreateDelegate(ScriptEngine engine, object target, Type delegateType)
{
MiscHelpers.VerifyNonNullArgument(target, nameof(target));
if (!typeof(Delegate).IsAssignableFrom(delegateType))
{
throw new ArgumentException("Invalid delegate type", nameof(delegateType));
}
var method = delegateType.GetMethod("Invoke");
if (method is null)
{
throw new ArgumentException("Invalid delegate type (invocation method not found)", nameof(delegateType));
}
var parameters = method.GetParameters();
if (parameters.Length > maxArgCount)
{
throw new ArgumentException("Invalid delegate type (parameter count too large)", nameof(delegateType));
}
var paramTypes = parameters.Select(param => param.ParameterType).ToArray();
if (paramTypes.Any(paramType => paramType.IsByRef))
{
return CreateComplexDelegate(engine, target, delegateType);
}
return CreateSimpleDelegate(engine, target, delegateType);
}
private static Delegate CreateSimpleDelegate(ScriptEngine engine, object target, Type delegateType)
{
var method = delegateType.GetMethod("Invoke");
var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray();
Type shimType;
if (method.ReturnType == typeof(void))
{
var typeArgs = paramTypes.Concat(delegateType.ToEnumerable()).ToArray();
shimType = procShimTemplates[paramTypes.Length].MakeSpecificType(typeArgs);
}
else
{
var typeArgs = paramTypes.Concat(new[] { method.ReturnType, delegateType }).ToArray();
shimType = funcShimTemplates[paramTypes.Length].MakeSpecificType(typeArgs);
}
var shim = (DelegateShim)Activator.CreateInstance(shimType, engine, target);
return shim.Delegate;
}
private static Delegate CreateComplexDelegate(ScriptEngine engine, object target, Type delegateType)
{
var method = delegateType.GetMethod("Invoke");
var parameters = method.GetParameters();
var paramTypes = parameters.Select(param => param.ParameterType).ToArray();
var innerParamTypes = new Type[parameters.Length];
for (var index = 0; index < parameters.Length; index++)
{
var paramType = paramTypes[index];
if (parameters[index].IsOut)
{
innerParamTypes[index] = typeof(OutArg<>).MakeSpecificType(paramType.GetElementType());
}
else if (paramType.IsByRef)
{
innerParamTypes[index] = typeof(RefArg<>).MakeSpecificType(paramType.GetElementType());
}
else
{
innerParamTypes[index] = paramType;
}
}
Type innerDelegateType;
if (method.ReturnType == typeof(void))
{
innerDelegateType = procTemplates[innerParamTypes.Length].MakeSpecificType(innerParamTypes);
}
else
{
var typeArgs = innerParamTypes.Concat(method.ReturnType.ToEnumerable()).ToArray();
innerDelegateType = funcTemplates[innerParamTypes.Length].MakeSpecificType(typeArgs);
}
var paramExprs = paramTypes.Select((paramType, index) => Expression.Parameter(paramType, "a" + index)).ToArray();
var varExprs = innerParamTypes.Select((paramType, index) => Expression.Variable(paramType, "v" + index)).ToArray();
var topExprs = new List();
for (var index = 0; index < varExprs.Length; index++)
{
if (paramTypes[index].IsByRef)
{
var constructor = innerParamTypes[index].GetConstructor(new[] { paramTypes[index].GetElementType() });
topExprs.Add(Expression.Assign(varExprs[index], Expression.New(constructor, paramExprs[index])));
}
else
{
topExprs.Add(Expression.Assign(varExprs[index], paramExprs[index]));
}
}
var innerDelegate = CreateSimpleDelegate(engine, target, innerDelegateType);
// ReSharper disable once CoVariantArrayConversion
var invokeExpr = Expression.Invoke(Expression.Constant(innerDelegate), varExprs);
var finallyExprs = new List();
for (var index = 0; index < varExprs.Length; index++)
{
if (paramTypes[index].IsByRef)
{
var member = innerParamTypes[index].GetProperty("Value");
var resultExpr = Expression.MakeMemberAccess(varExprs[index], member);
finallyExprs.Add(Expression.Assign(paramExprs[index], resultExpr));
}
}
var finallyBlockExpr = Expression.Block(finallyExprs);
topExprs.Add(Expression.TryFinally(invokeExpr, finallyBlockExpr));
var topBlockExpr = Expression.Block(method.ReturnType, varExprs, topExprs);
return Expression.Lambda(delegateType, topBlockExpr, paramExprs).Compile();
}
private abstract class DelegateShim
{
public abstract Delegate Delegate { get; }
protected ScriptEngine Engine { get; }
protected DelegateShim(ScriptEngine engine)
{
Engine = engine;
}
protected static bool GetAllByValue(params Type[] types)
{
return !types.Any(type => typeof(IByRefArg).IsAssignableFrom(type));
}
protected static object GetArgValue(object arg)
{
return (arg is IByRefArg byRefArg) ? byRefArg.Value : arg;
}
protected void SetArgValue(object arg, object value)
{
if (arg is IByRefArg byRefArg)
{
byRefArg.SetValue(Engine, value);
}
}
protected static object GetCompatibleTarget(Type delegateType, object target)
{
if ((target is Delegate del) && (del.GetType() != delegateType))
{
// The target is a delegate of a different type from the one we are creating.
// Normally we expect the target to be a script function (COM object), but
// since we'll be invoking it dynamically, there's no need to restrict it.
// However, if the target is a delegate of a type that's inaccessible to this
// assembly, dynamic invocation fails. Strangely, delegate type accessibility
// doesn't seem to affect delegate creation, so we can work around this by
// creating a compatible delegate based on the properties of the target.
return Delegate.CreateDelegate(delegateType, del.Target, del.Method);
}
return target;
}
}
private abstract class ProcShim : DelegateShim
{
private readonly Action invoker;
protected ProcShim(ScriptEngine engine)
: base(engine)
{
if (engine is null)
{
invoker = action => action();
}
else
{
invoker = engine.SyncInvoke;
}
}
protected void Invoke(Action action)
{
invoker(action);
}
}
private abstract class FuncShim : DelegateShim
{
private readonly Func, TResult> invoker;
protected FuncShim(ScriptEngine engine)
: base(engine)
{
if (engine is null)
{
invoker = func => func();
}
else
{
invoker = engine.SyncInvoke;
}
}
protected TResult Invoke(Func func)
{
return invoker(func);
}
}
}
}