public bool TryEmitDeserialiseCode(ILGenerator ilg, ThrowBlockGatherer throwBlocks, ICollection <string> errors, string name, Type targetType, LocalBuilder value, LocalBuilder unpacker, LocalBuilder contextLocal, DasherContext context, UnexpectedFieldBehaviour unexpectedFieldBehaviour) { var lblValidateArrayLength = ilg.DefineLabel(); var lblExit = ilg.DefineLabel(); // read the array length var count = ilg.DeclareLocal(typeof(int)); ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Ldloca, count); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryReadArrayLength); ilg.Emit(OpCodes.Brtrue_S, lblValidateArrayLength); // test for empty map (empty type) ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryPeekEmptyMap); throwBlocks.ThrowIfFalse(() => { ilg.Emit(OpCodes.Ldstr, "Union values must be encoded as an array for property \"{0}\" of type \"{1}\""); ilg.Emit(OpCodes.Ldstr, name); ilg.LoadType(value.LocalType); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object); ilg.LoadType(targetType); ilg.Emit(OpCodes.Newobj, Methods.DeserialisationException_Ctor_String_Type); ilg.Emit(OpCodes.Throw); }); ilg.Emit(OpCodes.Ldnull); ilg.Emit(OpCodes.Stloc, value); ilg.Emit(OpCodes.Br, lblExit); // ensure we have two items in the array ilg.MarkLabel(lblValidateArrayLength); ilg.Emit(OpCodes.Ldloc, count); ilg.Emit(OpCodes.Ldc_I4_2); throwBlocks.ThrowIfNotEqual(() => { ilg.Emit(OpCodes.Ldstr, "Union array should have 2 elements (not {0}) for property \"{1}\" of type \"{2}\""); ilg.Emit(OpCodes.Ldloc, count); ilg.Emit(OpCodes.Box, typeof(int)); ilg.Emit(OpCodes.Ldstr, name); ilg.LoadType(value.LocalType); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object_Object); ilg.LoadType(targetType); ilg.Emit(OpCodes.Newobj, Methods.DeserialisationException_Ctor_String_Type); ilg.Emit(OpCodes.Throw); }); // read the serialised type name var typeName = ilg.DeclareLocal(typeof(string)); ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Ldloca, typeName); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryReadString); throwBlocks.ThrowIfFalse(() => { ilg.Emit(OpCodes.Ldstr, "Unable to read union type name for property \"{0}\" of type \"{1}\""); ilg.Emit(OpCodes.Ldstr, name); ilg.LoadType(value.LocalType); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object); ilg.LoadType(targetType); ilg.Emit(OpCodes.Newobj, Methods.DeserialisationException_Ctor_String_Type); ilg.Emit(OpCodes.Throw); }); var success = true; // loop through types within the union, looking for a matching type name var labelNextType = ilg.DefineLabel(); foreach (var type in value.LocalType.GetGenericArguments()) { var expectedTypeName = UnionEncoding.GetTypeName(type); ilg.Emit(OpCodes.Ldloc, typeName); ilg.Emit(OpCodes.Ldstr, expectedTypeName); ilg.Emit(OpCodes.Call, Methods.String_Equals_String_String); // continue if this type doesn't match the union's values ilg.Emit(OpCodes.Brfalse, labelNextType); // we have a match // read the value var readValue = ilg.DeclareLocal(type); if (!DeserialiserEmitter.TryEmitDeserialiseCode(ilg, throwBlocks, errors, name, targetType, readValue, unpacker, context, contextLocal, unexpectedFieldBehaviour)) { errors.Add($"Unable to deserialise union member type {type}"); success = false; } // create the union ilg.Emit(OpCodes.Ldloc, readValue); ilg.Emit(OpCodes.Call, value.LocalType.GetMethod(nameof(Union <int, int> .Create), new[] { type })); // store it in the result value ilg.Emit(OpCodes.Stloc, value); // exit the loop ilg.Emit(OpCodes.Br, lblExit); ilg.MarkLabel(labelNextType); labelNextType = ilg.DefineLabel(); } // If we exhaust the loop, throws ilg.MarkLabel(labelNextType); throwBlocks.Throw(() => { // TODO include received type name in error message and some more general info ilg.Emit(OpCodes.Ldstr, "No match on union type"); ilg.Emit(OpCodes.Newobj, Methods.Exception_Ctor_String); ilg.Emit(OpCodes.Throw); }); ilg.MarkLabel(lblExit); return(success); }
public bool TryEmitSerialiseCode(ILGenerator ilg, ThrowBlockGatherer throwBlocks, ICollection <string> errors, LocalBuilder value, LocalBuilder packer, LocalBuilder contextLocal, DasherContext context) { // write header ilg.Emit(OpCodes.Ldloc, packer); ilg.Emit(OpCodes.Ldc_I4_2); ilg.Emit(OpCodes.Call, Methods.Packer_PackArrayHeader); // TODO might be faster if we a generated class having members for use with called 'Union<>.Match' var typeObj = ilg.DeclareLocal(typeof(Type)); ilg.Emit(OpCodes.Ldloc, value); ilg.Emit(OpCodes.Callvirt, value.LocalType.GetProperty(nameof(Union <int, int> .Type)).GetMethod); ilg.Emit(OpCodes.Stloc, typeObj); // write type name ilg.Emit(OpCodes.Ldloc, packer); ilg.Emit(OpCodes.Ldloc, typeObj); ilg.Emit(OpCodes.Call, Methods.UnionEncoding_GetTypeName); ilg.Emit(OpCodes.Call, Methods.Packer_Pack_String); var success = true; // loop through types within the union, looking for a match var doneLabel = ilg.DefineLabel(); var labelNextType = ilg.DefineLabel(); foreach (var type in value.LocalType.GetGenericArguments()) { ilg.LoadType(type); ilg.Emit(OpCodes.Ldloc, typeObj); ilg.Emit(OpCodes.Call, Methods.Object_Equals_Object_Object); // continue if this type doesn't match the union's values ilg.Emit(OpCodes.Brfalse, labelNextType); // we have a match // get the value var valueObj = ilg.DeclareLocal(type); ilg.Emit(OpCodes.Ldloc, value); ilg.Emit(OpCodes.Callvirt, value.LocalType.GetProperty(nameof(Union <int, int> .Value)).GetMethod); ilg.Emit(type.GetTypeInfo().IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, type); ilg.Emit(OpCodes.Stloc, valueObj); // write value if (!SerialiserEmitter.TryEmitSerialiseCode(ilg, throwBlocks, errors, valueObj, packer, context, contextLocal)) { errors.Add($"Unable to serialise union member type {type}"); success = false; } ilg.Emit(OpCodes.Br, doneLabel); ilg.MarkLabel(labelNextType); labelNextType = ilg.DefineLabel(); } ilg.MarkLabel(labelNextType); throwBlocks.Throw(() => { ilg.Emit(OpCodes.Ldstr, "No match on union type"); ilg.Emit(OpCodes.Newobj, Methods.Exception_Ctor_String); ilg.Emit(OpCodes.Throw); }); ilg.MarkLabel(doneLabel); return(success); }
public bool TryEmitDeserialiseCode(ILGenerator ilg, ThrowBlockGatherer throwBlocks, ICollection <string> errors, string name, Type targetType, LocalBuilder value, LocalBuilder unpacker, LocalBuilder contextLocal, DasherContext context, UnexpectedFieldBehaviour unexpectedFieldBehaviour) { if (!TryValidateComplexType(targetType, errors)) { return(false); } var constructors = targetType.GetConstructors(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance); Debug.Assert(constructors.Length == 1, "Should have one constructor (validation should be performed before call)"); var constructor = constructors[0]; var parameters = constructor.GetParameters(); void ThrowException() { ilg.LoadType(targetType); ilg.Emit(OpCodes.Newobj, Methods.DeserialisationException_Ctor_String_Type); ilg.Emit(OpCodes.Throw); } #region Initialise locals for constructor args var valueLocals = new LocalBuilder[parameters.Length]; var valueSetLocals = new LocalBuilder[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; valueLocals[i] = ilg.DeclareLocal(parameter.ParameterType); valueSetLocals[i] = ilg.DeclareLocal(typeof(int)); if (parameter.HasDefaultValue) { // set default values on params if (NullableValueProvider.IsNullableValueType(parameter.ParameterType)) { ilg.Emit(OpCodes.Ldloca, valueLocals[i]); if (parameter.DefaultValue == null) { ilg.Emit(OpCodes.Initobj, parameter.ParameterType); } else { ilg.LoadConstant(parameter.DefaultValue); ilg.Emit(OpCodes.Call, parameter.ParameterType.GetConstructor(new[] { parameter.ParameterType.GetGenericArguments().Single() })); } } else if (parameter.DefaultValue == null && parameter.ParameterType.GetTypeInfo().IsValueType) { // When DefaultValue is null for a parameter of value type, the programmer must have // stated default(T) as the default value. ilg.Emit(OpCodes.Ldloca, valueLocals[i]); ilg.Emit(OpCodes.Initobj, valueLocals[i].LocalType); } else { ilg.LoadConstant(parameter.DefaultValue); ilg.Emit(OpCodes.Stloc, valueLocals[i]); } // set 'valueSet' to true // note we use the second LSb to indicate a default value ilg.Emit(OpCodes.Ldc_I4_2); ilg.Emit(OpCodes.Stloc, valueSetLocals[i]); } else { // set 'valueSet' to false ilg.Emit(OpCodes.Ldc_I4_0); ilg.Emit(OpCodes.Stloc, valueSetLocals[i]); } } #endregion #region Read map length var mapSize = ilg.DeclareLocal(typeof(int)); { // MsgPack messages may be single values, arrays, maps, or any arbitrary // combination of these types. Our convention is to require messages to // be encoded as maps where the key is the property name. // // MsgPack maps begin with a header indicating the number of pairs // within the map. We read this here. ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Ldloca, mapSize); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryReadMapLength); // If false was returned, then the next MsgPack value is not a map throwBlocks.ThrowIfFalse(() => { // Check if it's a null ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryReadNull); var lblNotNull = ilg.DefineLabel(); ilg.Emit(OpCodes.Brfalse_S, lblNotNull); { // value is null ilg.Emit(OpCodes.Ldnull); ilg.Emit(OpCodes.Ret); } ilg.MarkLabel(lblNotNull); ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Call, Methods.Unpacker_HasStreamEnded_Get); var lblNotEmpty = ilg.DefineLabel(); ilg.Emit(OpCodes.Brfalse_S, lblNotEmpty); ilg.Emit(OpCodes.Ldstr, "Data stream empty"); ThrowException(); ilg.MarkLabel(lblNotEmpty); ilg.Emit(OpCodes.Ldstr, "Message must be encoded as a MsgPack map, not \"{0}\"."); ilg.PeekFormatString(unpacker); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object); ThrowException(); }); } #endregion #region Loop through each key/value pair in the map { #region Initialise loop // Create a loop counter, initialised to zero var loopIndex = ilg.DeclareLocal(typeof(long)); ilg.Emit(OpCodes.Ldc_I4_0); ilg.Emit(OpCodes.Conv_I8); ilg.Emit(OpCodes.Stloc, loopIndex); // Create labels to jump to within the loop var lblLoopTest = ilg.DefineLabel(); // Comparing counter to map size var lblLoopExit = ilg.DefineLabel(); // The first instruction after the loop var lblLoopStart = ilg.DefineLabel(); // The first instruction within the loop // Run the test first ilg.Emit(OpCodes.Br, lblLoopTest); // Mark the first instruction within the loop ilg.MarkLabel(lblLoopStart); #endregion #region Read field name // Although MsgPack allows map keys to be of any arbitrary type, our convention // is to require keys to be strings. We read the key here. var key = ilg.DeclareLocal(typeof(string)); { ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Ldloca, key); ilg.Emit(OpCodes.Call, Methods.Unpacker_TryReadString); // If false was returned, the data stream ended throwBlocks.ThrowIfFalse(() => { ilg.Emit(OpCodes.Ldstr, "Data stream ended."); ThrowException(); }); } #endregion #region Match name to expected field, then attempt to deserialise by expected type // Build a chain of if/elseif/elseif... blocks for each of the expected fields. // It could be slightly more efficient here to generate a O(log(N)) tree-based lookup, // but that would take quite some engineering. Let's see if there's a significant perf // hit here or not first. var lblEndIfChain = ilg.DefineLabel(); // TODO how much perf diff if properties sorted lexicographically, so no searching? Label?nextLabel = null; for (var parameterIndex = 0; parameterIndex < parameters.Length; parameterIndex++) { // Mark the beginning of the next block, as used if the previous block's condition failed if (nextLabel != null) { ilg.MarkLabel(nextLabel.Value); } nextLabel = ilg.DefineLabel(); // Compare map's key with this parameter's name in a case insensitive way ilg.Emit(OpCodes.Ldloc, key); ilg.Emit(OpCodes.Ldstr, parameters[parameterIndex].Name); ilg.Emit(OpCodes.Ldc_I4_5); // StringComparison.OrdinalIgnoreCase ilg.Emit(OpCodes.Callvirt, Methods.String_Equals_String_StringComparison); // If the key doesn't match this property, go to the next block ilg.Emit(OpCodes.Brfalse, nextLabel.Value); #region Test for duplicate fields // Verify we haven't already seen a value for this parameter { // Mask out the LSb and see if it is set. If so, we've seen this property // already in this message, which is invalid. ilg.Emit(OpCodes.Ldloc, valueSetLocals[parameterIndex]); ilg.Emit(OpCodes.Ldc_I4_1); ilg.Emit(OpCodes.And); throwBlocks.ThrowIfTrue(() => { ilg.Emit(OpCodes.Ldstr, "Encountered duplicate field \"{0}\" for type \"{1}\"."); ilg.Emit(OpCodes.Ldloc, key); ilg.Emit(OpCodes.Ldstr, targetType.Name); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object); ThrowException(); }); // Record the fact that we've seen this property ilg.Emit(OpCodes.Ldloc, valueSetLocals[parameterIndex]); ilg.Emit(OpCodes.Ldc_I4_1); ilg.Emit(OpCodes.Or); ilg.Emit(OpCodes.Stloc, valueSetLocals[parameterIndex]); } #endregion if (!DeserialiserEmitter.TryEmitDeserialiseCode(ilg, throwBlocks, errors, parameters[parameterIndex].Name, targetType, valueLocals[parameterIndex], unpacker, context, contextLocal, unexpectedFieldBehaviour)) { throw new Exception($"Unable to deserialise values of type {valueLocals[parameterIndex].LocalType} from MsgPack data."); } ilg.Emit(OpCodes.Br, lblEndIfChain); } if (nextLabel != null) { ilg.MarkLabel(nextLabel.Value); } #region Process unexpected field // If we got here then the property was not recognised. Either throw or ignore, depending upon configuration. if (unexpectedFieldBehaviour == UnexpectedFieldBehaviour.Throw) { throwBlocks.Throw(() => { ilg.Emit(OpCodes.Ldstr, "Encountered unexpected field \"{0}\" of MsgPack format \"{1}\" for CLR type \"{2}\"."); ilg.Emit(OpCodes.Ldloc, key); ilg.PeekFormatString(unpacker); ilg.Emit(OpCodes.Ldstr, targetType.Name); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object_Object); ThrowException(); }); } else { // skip unexpected value ilg.Emit(OpCodes.Ldloc, unpacker); ilg.Emit(OpCodes.Call, Methods.Unpacker_SkipValue); } #endregion ilg.MarkLabel(lblEndIfChain); #endregion #region Evaluate loop and branch to start unless finished // Increment the loop index ilg.Emit(OpCodes.Ldloc, loopIndex); ilg.Emit(OpCodes.Ldc_I4_1); ilg.Emit(OpCodes.Conv_I8); ilg.Emit(OpCodes.Add); ilg.Emit(OpCodes.Stloc, loopIndex); // Loop condition ilg.MarkLabel(lblLoopTest); ilg.Emit(OpCodes.Ldloc, loopIndex); ilg.Emit(OpCodes.Ldloc, mapSize); ilg.Emit(OpCodes.Conv_I8, mapSize); // If the loop is done, jump to the first instruction after the loop ilg.Emit(OpCodes.Beq_S, lblLoopExit); // Jump back to the start of the loop ilg.Emit(OpCodes.Br, lblLoopStart); // Mark the end of the loop ilg.MarkLabel(lblLoopExit); #endregion } #endregion #region Verify all required values either specified or have a default value var paramName = ilg.DeclareLocal(typeof(string)); for (var i = 0; i < valueSetLocals.Length; i++) { // Store the name of the parameter we are inspecting // for use in the exception later. ilg.Emit(OpCodes.Ldstr, parameters[i].Name); ilg.Emit(OpCodes.Stloc, paramName); // If any value is zero then neither a default nor specified value // exists for that parameter, and we cannot continue. ilg.Emit(OpCodes.Ldloc, valueSetLocals[i]); ilg.Emit(OpCodes.Ldc_I4_0); throwBlocks.ThrowIfEqual(() => { ilg.Emit(OpCodes.Ldstr, "Missing required field \"{0}\" for type \"{1}\"."); ilg.Emit(OpCodes.Ldloc, paramName); ilg.Emit(OpCodes.Ldstr, targetType.Name); ilg.Emit(OpCodes.Call, Methods.String_Format_String_Object_Object); ThrowException(); }); } // If we got here, all value exist and we're good to continue. #endregion #region Construct final complex object via its constructor // Push all values onto the execution stack foreach (var valueLocal in valueLocals) { ilg.Emit(OpCodes.Ldloc, valueLocal); } // Call the target type's constructor ilg.Emit(OpCodes.Newobj, constructor); ilg.Emit(OpCodes.Stloc, value); #endregion return(true); }