react-native-windows/vnext/Microsoft.ReactNative.Managed/JSValueReaderGenerator.cs

341 строка
14 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using static System.Linq.Expressions.Expression;
using static Microsoft.ReactNative.Managed.JSValueGenerator;
namespace Microsoft.ReactNative.Managed
{
public static class JSValueReaderGenerator
{
// The current assembly to ensure we always register the basic type readers
private static readonly Assembly s_currentAssembly = typeof(JSValueReaderGenerator).Assembly;
private static readonly ConcurrentDictionary<Assembly, bool> s_registeredAssemblies = new ConcurrentDictionary<Assembly, bool>();
private static readonly ConcurrentDictionary<Type, MethodInfo> s_codeGenerateGenericExtensionMethods = new ConcurrentDictionary<Type, MethodInfo>();
private static readonly Lazy<KeyValuePair<Type, MethodInfo>[]> s_allMethods;
private static readonly Lazy<IReadOnlyDictionary<Type, MethodInfo>> s_nonGenericMethods;
private static readonly Lazy<IReadOnlyDictionary<Type, SortedList<Type, MethodInfo>>> s_genericMethods;
public static void RegisterAssembly(Assembly assembly)
{
// UnitTests re-register over and over, safe to skip if already added.
if (s_registeredAssemblies.ContainsKey(assembly))
{
return;
}
s_registeredAssemblies.GetOrAdd(assembly, true);
// Fail programs that register after we started serializing values.
if (s_allMethods.IsValueCreated)
{
throw new InvalidOperationException("Cannot register assemblies dynamically after the first value is serialized.");
}
}
public static void RegisterCodeGeneratorGenericExtensionMethod(Type type, MethodInfo method)
{
if (!method.IsGenericMethod)
{
throw new InvalidOperationException("Cannot register non generic methods.");
}
s_codeGenerateGenericExtensionMethods.TryAdd(type, method);
}
static JSValueReaderGenerator()
{
s_registeredAssemblies.GetOrAdd(s_currentAssembly, true);
// Get all extension ReadValue methods for IJSValueReader and JSValue.
// The first parameter must be IJSValueReader or JSValue.
// The second parameter must be an 'out' parameter and must not be a generic parameter T.
// This is to avoid endless recursion because ReadValue with generic parameter T
// is the one who calls the JSValueReaderGenerator.
s_allMethods = new Lazy<KeyValuePair<Type, MethodInfo>[]>(() =>
{
var extensionMethods =
from assembly in s_registeredAssemblies.Keys
from type in assembly.GetTypes()
let typeInfo = type.GetTypeInfo()
where typeInfo.IsSealed && !typeInfo.IsGenericType && !typeInfo.IsNested
from member in type.GetMember(nameof(JSValueReader.ReadValue), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
let method = member as MethodInfo
where (method != null) && method.IsDefined(typeof(ExtensionAttribute), inherit: false)
let parameters = method.GetParameters()
where parameters.Length == 2
&& (parameters[0].ParameterType == typeof(IJSValueReader)
|| parameters[0].ParameterType == typeof(JSValue))
&& parameters[1].IsOut
let dataType = parameters[1].ParameterType.GetElementType()
where !dataType.IsGenericParameter
select new KeyValuePair<Type, MethodInfo>(dataType, method);
return extensionMethods.ToArray();
});
// Get all non-generic ReadValue extension methods.
// They are easy to match and we can put them in a dictionary.
s_nonGenericMethods = new Lazy<IReadOnlyDictionary<Type, MethodInfo>>(() =>
s_allMethods.Value.Where(p => !p.Value.IsGenericMethod).ToDictionary(p => p.Key, p => p.Value));
// Get all generic ReadValue extension methods.
// Group them by generic type definitions and sort them by having more specific methods getting first.
// The matching for generic types is more complicated: we first match the generic type definition,
// and then walk the sorted list and try to match types one by one.
// Note that the parameter could be an array of generic type. The array is not a generic type.
s_genericMethods = new Lazy<IReadOnlyDictionary<Type, SortedList<Type, MethodInfo>>>(() =>
{
var genericMethods =
from pair in s_allMethods.Value.Union(s_codeGenerateGenericExtensionMethods)
where pair.Value.IsGenericMethod
let type = pair.Key
let keyType = type.GetTypeInfo().IsGenericType ? type.GetGenericTypeDefinition()
: type.IsArray ? typeof(Array)
: throw new InvalidOperationException($"Unsupported argument type {type}")
group pair by keyType into g
select new KeyValuePair<Type, SortedList<Type, MethodInfo>>(
g.Key, new SortedList<Type, MethodInfo>(
g.ToDictionary(p => p.Key, p => p.Value), GenericTypeComparer.Default));
return genericMethods.ToDictionary(p => p.Key, p => p.Value);
});
}
public static MethodInfo ReadArgsOf(Type[] typeArgs)
{
var readArgsMethod =
from member in typeof(JSValueReader).GetMember(
nameof(JSValueReader.ReadArgs), BindingFlags.Static | BindingFlags.Public)
let method = member as MethodInfo
let isGeneric = method.IsGenericMethod
where method != null
&& method.IsDefined(typeof(ExtensionAttribute), inherit: false)
let parameters = method.GetParameters()
where parameters.Length == typeArgs.Length + 1
&& parameters[0].ParameterType == typeof(IJSValueReader)
select isGeneric ? method.MakeGenericMethod(typeArgs) : method;
return readArgsMethod.First();
}
static MethodInfo GetNextObjectProperty =>
typeof(IJSValueReader).GetMethod(nameof(IJSValueReader.GetNextObjectProperty));
private static MethodInfo MethodOf(string methodName, params Type[] typeArgs) =>
typeof(JSValueReaderGenerator).GetMethod(methodName).MakeGenericMethod(typeArgs);
static string ValueType => nameof(IJSValueReader.ValueType);
static TypeWrapper ReadValueDelegateOf(Type typeArg) =>
new TypeWrapper(typeof(ReadValueDelegate<>).MakeGenericType(typeArg));
static TypeWrapper ReadClassDelegateOf(Type typeArg) =>
new TypeWrapper(typeof(ReadClassDelegate<>).MakeGenericType(typeArg));
static MethodInfo ReadValueOf(Type typeArg)
{
var readValueNoParamMethod =
from member in typeof(JSValueReader).GetMember(
nameof(JSValueReader.ReadValue), BindingFlags.Static | BindingFlags.Public)
let method = member as MethodInfo
where method != null
&& method.IsGenericMethod
&& method.IsDefined(typeof(ExtensionAttribute), inherit: false)
let parameters = method.GetParameters()
where parameters.Length == 1
&& parameters[0].ParameterType == typeof(IJSValueReader)
select method;
return readValueNoParamMethod.First().MakeGenericMethod(typeArg);
}
static MethodInfo JSValueReadFrom => typeof(JSValue).GetMethod(nameof(JSValue.ReadFrom));
public static Delegate GenerateReadValueDelegate(Type valueType)
{
return GenerateFromExtension(valueType)
?? GenerateReadValueFromGenericExtension(valueType)
?? GenerateReadValueForEnum(valueType)
?? GenerateReadValueForClass(valueType)
?? throw new Exception($"Cannot generate ReadValue delegate for type {valueType}");
}
// Creates a delegate from the ReadValue non-generic extension method
private static Delegate GenerateFromExtension(Type valueType)
{
if (s_nonGenericMethods.Value.TryGetValue(valueType, out MethodInfo methodInfo))
{
var extendedType = methodInfo.GetParameters()[0].ParameterType;
if (extendedType == typeof(IJSValueReader))
{
return methodInfo.CreateDelegate(typeof(ReadValueDelegate<>).MakeGenericType(valueType));
}
else if (extendedType == typeof(JSValue))
{
return GenerateReadValueFromJSValueExtension(valueType, methodInfo);
}
}
return null;
}
private static Delegate GenerateReadValueFromGenericExtension(Type valueType)
{
var keyType = valueType.GetTypeInfo().IsGenericType ? valueType.GetGenericTypeDefinition()
: valueType.IsArray ? typeof(Array)
: null;
if (keyType != null
&& s_genericMethods.Value.TryGetValue(keyType, out SortedList<Type, MethodInfo> candidateMethods))
{
foreach (var candidateMethod in candidateMethods)
{
var genericType = candidateMethod.Key;
var methodInfo = candidateMethod.Value;
var genericArgs = methodInfo.GetGenericArguments();
if (TryMatchGenericType(valueType, genericType, genericArgs, out Type[] typeArgs))
{
var genericMethod = methodInfo.MakeGenericMethod(typeArgs);
var extendedType = methodInfo.GetParameters()[0].ParameterType;
if (extendedType == typeof(IJSValueReader))
{
return genericMethod.CreateDelegate(typeof(ReadValueDelegate<>).MakeGenericType(valueType));
}
else if (extendedType == typeof(JSValue))
{
return GenerateReadValueFromJSValueExtension(valueType, genericMethod);
}
}
}
}
return null;
}
// Creates a delegate from the JSValue ReadValue non-generic extension method
private static Delegate GenerateReadValueFromJSValueExtension(Type valueType, MethodInfo methodInfo)
{
// Generate code that looks like:
//
// (IJSValueReader reader, out Type value) =>
// {
// var jsValue = JSValue.ReadFrom(reader);
// jsValue.ReadValue(out value);
// }
return ReadValueDelegateOf(valueType).CompileLambda(
Parameter(typeof(IJSValueReader), out var reader),
Parameter(valueType.MakeByRefType(), out var value),
Variable(typeof(JSValue), out var jsValue, Call(JSValueReadFrom, reader)),
jsValue.CallExt(methodInfo, value));
}
// It cannot be an extension method because it would conflict with the generic
// extension method that uses T value type.
public static void ReadEnum<TEnum>(IJSValueReader reader, out TEnum value) where TEnum : Enum
{
value = (TEnum)(object)reader.ReadValue<int>();
}
private static Delegate GenerateReadValueForEnum(Type valueType)
{
// Creates a delegate from the ReadEnum method
return valueType.GetTypeInfo().IsEnum
? MethodOf(nameof(ReadEnum), valueType).CreateDelegate(typeof(ReadValueDelegate<>).MakeGenericType(valueType))
: null;
}
public static void ReadClass<TClass>(IJSValueReader reader, out TClass value) where TClass : new()
{
// .Net Native seems to have a bug that does not allow to have 'out' or 'ref' parameters.
// We use a return value to work around this issue.
value = JSValueReaderReadClassOf<TClass>.ReadClass(reader);
}
private static Delegate GenerateReadValueForClass(Type valueType)
{
var valueTypeInfo = valueType.GetTypeInfo();
bool isStruct = valueTypeInfo.IsValueType && !valueTypeInfo.IsEnum;
bool isClass = valueTypeInfo.IsClass;
if (isStruct || (isClass && valueType.GetConstructor(Type.EmptyTypes) != null))
{
return MethodOf(nameof(ReadClass), valueType).CreateDelegate(typeof(ReadValueDelegate<>).MakeGenericType(valueType));
}
return null;
}
internal static Delegate GenerateReadClassDelegate(Type classType)
{
// Generate code that looks like:
//
// (IJSValueReader reader) =>
// {
// Type value = new Type();
// if (reader.ValueType == JSValueType.Object)
// {
// while (reader.GetNextObjectProperty(out string propertyName))
// {
// // For each object field or property
// switch(propertyName)
// {
// case "Field1": value.Field1 = reader.ReadValue<Field1Type>(); break;
// case "Field2": value.Field2 = reader.ReadValue<Field2Type>(); break;
// case "Prop1": value.Prop1 = reader.ReadValue<Prop1Type>(); break;
// case "Prop2": value.Prop2 = reader.ReadValue<Prop2Type>(); break;
// }
// }
// }
//
// return value;
// }
var fields =
from field in classType.GetFields(BindingFlags.Public | BindingFlags.Instance)
where !field.IsInitOnly
select new { field.Name, Type = field.FieldType };
var properties =
from property in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
let propertySetter = property.SetMethod
where propertySetter != null && propertySetter.IsPublic
select new { property.Name, Type = property.PropertyType };
var members = fields.Concat(properties).ToArray();
return ReadClassDelegateOf(classType).CompileLambda(
Parameter(typeof(IJSValueReader), out var reader),
Variable(classType, out var value, New(classType)),
IfThen(Equal(reader.Property(ValueType), Constant(JSValueType.Object)),
ifTrue: AutoBlock(
Variable(typeof(string), out var propertyName),
While(reader.Call(GetNextObjectProperty, propertyName),
(members.Length != 0)
? Switch(propertyName,
members.Select(member => SwitchCase(
value.SetPropertyStatement(member.Name, reader.CallExt(ReadValueOf(member.Type))),
Constant(member.Name))).ToArray()) as Expression
: Default(typeof(void))))),
value.AsExpression);
}
}
//============================================================================
// .Net Native has a bug that it fails with a 'ref' parameters in generated
// lambdas. To work around this issue we use a return value for a ReadValue
// generated against a class or struct.
//============================================================================
delegate T ReadClassDelegate<T>(IJSValueReader reader);
// This class provides constant time access to the ReadClass delegate.
static class JSValueReaderReadClassOf<T>
{
public static ReadClassDelegate<T> ReadClass =
(ReadClassDelegate<T>)JSValueReaderGenerator.GenerateReadClassDelegate(typeof(T));
}
}