diff --git a/change/react-native-windows-2020-04-23-11-35-11-MS_FixDotNetNative.json b/change/react-native-windows-2020-04-23-11-35-11-MS_FixDotNetNative.json new file mode 100644 index 00000000000..6145c7787dd --- /dev/null +++ b/change/react-native-windows-2020-04-23-11-35-11-MS_FixDotNetNative.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Fixed .Net Native crash for custom struct deserialization", + "packageName": "react-native-windows", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-04-23T18:35:11.572Z" +} diff --git a/vnext/Microsoft.ReactNative.SharedManaged/JSValueReaderGenerator.cs b/vnext/Microsoft.ReactNative.SharedManaged/JSValueReaderGenerator.cs index 0751707a124..c246f58aec6 100644 --- a/vnext/Microsoft.ReactNative.SharedManaged/JSValueReaderGenerator.cs +++ b/vnext/Microsoft.ReactNative.SharedManaged/JSValueReaderGenerator.cs @@ -99,6 +99,9 @@ private static MethodInfo MethodOf(string methodName, params Type[] typeArgs) => 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 = @@ -211,13 +214,33 @@ private static Delegate GenerateReadValueForEnum(Type valueType) : null; } + public static void ReadClass(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.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, out Type value) => + // (IJSValueReader reader) => // { - // value = new Type(); + // Type value = new Type(); // if (reader.ValueType == JSValueType.Object) // { // while (reader.GetNextObjectProperty(out string propertyName)) @@ -232,41 +255,50 @@ private static Delegate GenerateReadValueForClass(Type valueType) // } // } // } + // + // return value; // } - var valueTypeInfo = valueType.GetTypeInfo(); - bool isStruct = valueTypeInfo.IsValueType && !valueTypeInfo.IsEnum; - bool isClass = valueTypeInfo.IsClass; - if (isStruct || (isClass && valueType.GetConstructor(Type.EmptyTypes) != null)) - { - var fields = - from field in valueType.GetFields(BindingFlags.Public | BindingFlags.Instance) - where !field.IsInitOnly - select new { field.Name, Type = field.FieldType }; - var properties = - from property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance) - let propertySetter = property.SetMethod - where propertySetter.IsPublic - select new { property.Name, Type = property.PropertyType }; - var members = fields.Concat(properties).ToArray(); - - return ReadValueDelegateOf(valueType).CompileLambda( - Parameter(typeof(IJSValueReader), out var reader), - Parameter(valueType.MakeByRefType(), out var value), - value.Assign(New(valueType)), - 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)))))); - } + 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.IsPublic + select new { property.Name, Type = property.PropertyType }; + var members = fields.Concat(properties).ToArray(); - return null; + 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(IJSValueReader reader); + + // This class provides constant time access to the ReadClass delegate. + static class JSValueReaderReadClassOf + { + public static ReadClassDelegate ReadClass = + (ReadClassDelegate)JSValueReaderGenerator.GenerateReadClassDelegate(typeof(T)); + } }