/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport.ExceptionHandling;
using Amazon.Lambda.RuntimeSupport.Helpers;
using Amazon.Lambda.RuntimeSupport.Serializers;
namespace Amazon.Lambda.RuntimeSupport.Bootstrap
{
///
/// Builds user delegate from the handler information.
///
internal class InvokeDelegateBuilder
{
private readonly InternalLogger _logger;
private readonly HandlerInfo _handler;
private readonly MethodInfo _customerMethodInfo;
private Type CustomerOutputType { get; set; }
public InvokeDelegateBuilder(InternalLogger logger, HandlerInfo handler, MethodInfo customerMethodInfo)
{
_logger = logger;
_handler = handler;
_customerMethodInfo = customerMethodInfo;
}
///
/// Constructs the invoke delegate using Expressions
///
/// Serialize & Deserialize calls are only made when a serializer is provided.
/// Context is only passed when customer method has context parameter.
/// Lambda return type can be void.
///
/// (inStream, context, outStream) =>
/// {
/// var input = serializer.Deserialize(inStream);
/// var output = handler(input, context);
/// return serializer.Serialize(output);
/// }
///
///
/// Wrapped customer object.
/// Instance of lambda input & output serializer.
/// Action delegate pointing to customer's handler.
public Action ConstructInvokeDelegate(object customerObject, object customerSerializerInstance, bool isPreJit)
{
var inStreamParameter = Expression.Parameter(Types.StreamType, "inStream");
var outStreamParameter = Expression.Parameter(Types.StreamType, "outStream");
var contextParameter = Expression.Parameter(typeof(ILambdaContext), "context");
_logger.LogDebug($"UCL : Constructing input expression");
var inputExpression = BuildInputExpressionOrNull(customerSerializerInstance, inStreamParameter, out var iLambdaContextType);
if (isPreJit)
{
_logger.LogInformation("PreJit: inputExpression");
UserCodeInit.InitDeserializationAssembly(inputExpression, inStreamParameter);
}
_logger.LogDebug($"UCL : Constructing context expression");
var contextExpression = BuildContextExpressionOrNull(iLambdaContextType, contextParameter);
_logger.LogDebug($"UCL : Constructing handler expression");
var handlerExpression = CreateHandlerCallExpression(customerObject, inputExpression, contextExpression);
_logger.LogDebug($"UCL : Constructing output expression");
var outputExpression = CreateOutputExpression(customerSerializerInstance, outStreamParameter, handlerExpression);
if (isPreJit)
{
_logger.LogInformation("PreJit: outputExpression");
UserCodeInit.InitSerializationAssembly(outputExpression, outStreamParameter, CustomerOutputType);
}
_logger.LogDebug($"UCL : Constructing final expression");
var finalExpression = Expression.Lambda>(outputExpression, inStreamParameter, contextParameter, outStreamParameter);
#if DEBUG
var finalExpressionDebugView = typeof(Expression)
.GetTypeInfo()
.GetProperty("DebugView", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(finalExpression);
_logger.LogDebug($"UCL : Constructed final expression:\n'{finalExpressionDebugView}'");
#endif
_logger.LogDebug($"UCL : Compiling final expression");
return finalExpression.Compile();
}
///
/// Creates an expression to convert incoming Stream to the customer method inputs.
/// If customer method takes no inputs or only takes ILambdaContext, return null.
///
/// Instance of lambda input & output serializer.
/// Input stream parameter.
/// Type of context passed for the invocation.
/// Expression that deserializes incoming stream to the customer method inputs or null if customer method takes no input.
/// Thrown when customer method inputs don't meet lambda requirements.
private Expression BuildInputExpressionOrNull(object customerSerializerInstance, Expression inStreamParameter, out Type iLambdaContextType)
{
Type inputType = null;
iLambdaContextType = null;
// get input types
var inputTypes = _customerMethodInfo.GetParameters().Select(pi => pi.ParameterType).ToArray();
// check if there are too many parameters
if (inputTypes.Length > 2)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.MethodTooManyParams,
_customerMethodInfo.Name, _handler.TypeName);
}
// if two parameters, check that the second input is ILambdaContext
if (inputTypes.Length == 2)
{
if (!Types.IsILambdaContext(inputTypes[1]))
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.MethodSecondParamNotContext,
_customerMethodInfo.Name, _handler.TypeName, Types.ILambdaContextTypeName);
}
iLambdaContextType = inputTypes[1];
inputType = inputTypes[0];
}
// if one input, check if input is ILambdaContext
else if (inputTypes.Length == 1)
{
if (Types.IsILambdaContext(inputTypes[0]))
{
iLambdaContextType = inputTypes[0];
}
else
{
inputType = inputTypes[0];
}
}
if (inputType != null)
{
// deserializer.Deserialize(inStream)
return CreateDeserializeExpression(customerSerializerInstance, inputType, inStreamParameter);
}
if (iLambdaContextType != null)
{
_logger.LogDebug($"UCL : Validating iLambdaContextType");
UserCodeValidator.ValidateILambdaContextType(iLambdaContextType);
}
return null;
}
///
/// Generates an expression if iLambdaContextType is not null, or returns null
///
/// Type of context passed for the invocation.
/// Expression that defines context parameter.
/// Expression that defines context parameter if iLambdaContextType is not null, or returns null
private static Expression BuildContextExpressionOrNull(Type iLambdaContextType, Expression contextParameter)
{
// If the Lambda function does not have a context parameter, don't build
// an expression to construct it.
return iLambdaContextType == null ? null : contextParameter;
}
///
/// Creates expression to invoke the customer method.
/// If input or context expressions are not null, those expressions are
/// passed into the method.
///
/// Wrapped customer object.
/// Input expression that defines customer input.
/// Context expression that defines context passed for the invocation.
/// Expression that unwraps customer object.
private Expression CreateHandlerCallExpression(object customerObject, Expression inputExpression, Expression contextExpression)
{
Expression customerObjectConstant = null;
if (customerObject != null)
{
// code: [customerObject]
customerObjectConstant = Expression.Constant(customerObject);
}
var inputs = new List();
if (inputExpression != null)
{
inputs.Add(inputExpression);
}
if (contextExpression != null)
{
inputs.Add(contextExpression);
}
// [customerObject].handler([input|context|input, context])
Expression handlerCallExpression = Expression.Call(customerObjectConstant, _customerMethodInfo, inputs);
var outputType = _customerMethodInfo.ReturnType;
var taskTType = GetTaskTSubclassOrNull(outputType);
var outputIsTaskT = taskTType != null;
var outputIsTask = Types.TaskType.GetTypeInfo().IsAssignableFrom(outputType);
if (outputIsTaskT)
{
// code: (Task)[customerObject].handler(...)
var handlerConvert = Expression.Convert(handlerCallExpression, taskTType);
// code: [customerObject].handler(...).GetAwaiter().GetResult()
handlerCallExpression = Expression.Call(Expression.Call(handlerConvert, taskTType.GetMethod("GetAwaiter"), null), "GetResult", null);
}
else if (outputIsTask)
{
// code: (Task)([customerObject].handler(...))
var handlerConvert = Expression.Convert(handlerCallExpression, Types.TaskType);
// code: [customerObject].handler(...).GetAwaiter().GetResult()
handlerCallExpression = Expression.Call(Expression.Call(handlerConvert, Types.TaskType.GetMethod("GetAwaiter"), null), "GetResult", null);
}
return handlerCallExpression;
}
///
/// Creates the final expression. If there is no output data, the final expression
/// is just the handler call expression. Otherwise, the final expression is the
/// serialization expression operating on the handler call expression.
///
/// Instance of lambda input & output serializer.
/// Expression that defines customer output.
/// Expression that defines customer handler call.
/// Expression that serializes customer method output to outgoing stream.
private Expression CreateOutputExpression(object customerSerializerInstance, Expression outStreamParameter, Expression handlerCallExpression)
{
var outputType = _customerMethodInfo.ReturnType;
var taskTType = GetTaskTSubclassOrNull(outputType);
var isTaskT = taskTType != null;
var isTask = Types.TaskType.GetTypeInfo().IsAssignableFrom(outputType);
var isVoid = outputType == Types.VoidType;
var hasData = (!isVoid && !isTask) || isTaskT;
if (hasData)
{
if (isTaskT)
{
outputType = taskTType.GenericTypeArguments[0];
}
// serializer.Serialize(outputData, outStream)
return CreateSerializeExpression(customerSerializerInstance, outputType, handlerCallExpression, outStreamParameter);
}
CustomerOutputType = outputType;
return handlerCallExpression;
}
///
/// Retrieves the Task<T> type that the given type subclasses,
/// or null if the type does not subclass Task<T>
///
///
///
private static Type GetTaskTSubclassOrNull(Type type)
{
if (type == null)
return null;
if (type.IsConstructedGenericType)
{
var genericDefinition = type.GetGenericTypeDefinition();
if (genericDefinition == Types.TaskTType)
return type;
}
var baseType = type.GetTypeInfo().BaseType;
return GetTaskTSubclassOrNull(baseType);
}
///
/// Generates an expression to serialize customer method result into the output stream
///
/// Instance of lambda input & output serializer.
/// Customer input type.
/// Expression that define customer object.
/// Expression that defines customer output.
/// Expression that serializes returned object to output stream.
/// Thrown when customer input is serializable & serializer instance is null.
private Expression CreateSerializeExpression(object customerSerializerInstance, Type dataType, Expression customerObject, Expression outStreamParameter)
{
// generic types, null for String and Stream converters
Type[] genericTypes = null;
var converter = customerSerializerInstance;
Type iLambdaSerializerType = null;
// subclasses of Stream are allowed as customer method output
if (Types.StreamType.GetTypeInfo().IsAssignableFrom(dataType))
{
converter = StreamSerializer.Instance;
}
else
{
if (customerSerializerInstance == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.SerializeMissingAttribute,
_handler.AssemblyName, _handler.MethodName, dataType.FullName, typeof(LambdaSerializerAttribute).FullName);
}
iLambdaSerializerType = customerSerializerInstance
.GetType()
.GetTypeInfo()
.GetInterface(Types.ILambdaSerializerTypeName);
genericTypes = new[] {dataType};
}
// code: serializer
Expression converterExpression = Expression.Constant(converter);
if (iLambdaSerializerType != null)
{
// code: (ILambdaSerializer)serializer
converterExpression = Expression.Convert(converterExpression, iLambdaSerializerType);
}
// code: [(ILambdaSerializer)]serializer.Serialize[](handler(...), outStream)
Expression serializeCall = Expression.Call(
converterExpression, // variable
"Serialize", // method name
genericTypes, // generic type
customerObject, // arg1 - customer object
outStreamParameter); // arg2 - out stream
return serializeCall;
}
///
/// Generates an expression to deserialize incoming data to customer method input
///
/// Instance of lambda input & output serializer.
/// Customer input type.
/// Input expression that defines customer input.
/// Expression that deserializes incoming data to customer method input.
/// Thrown when customer serializer doesn't match with expected serializer definition
private Expression CreateDeserializeExpression(object customerSerializerInstance, Type dataType, Expression inStream)
{
// generic types, null for String and Stream converters
Type[] genericTypes = null;
var converter = customerSerializerInstance;
if (dataType == Types.StreamType)
{
converter = StreamSerializer.Instance;
}
else
{
if (customerSerializerInstance == null)
{
throw LambdaExceptions.ValidationException(Errors.UserCodeLoader.DeserializeMissingAttribute,
_handler.AssemblyName, _handler.MethodName, dataType.FullName, typeof(LambdaSerializerAttribute).FullName);
}
genericTypes = new[] {dataType};
}
// code: serializer
var serializer = Expression.Constant(converter);
// code: serializer.Deserializer[](inStream)
Expression deserializeCall = Expression.Call(
serializer, // variable
"Deserialize", // method name
genericTypes, // generic type
inStream); // arg1 - input stream
return deserializeCall;
}
}
}