From 8e7a68a993fa5eccb1f4104449f0c839a0cee2a5 Mon Sep 17 00:00:00 2001 From: David Engel Date: Thu, 16 Jun 2022 18:21:48 -0700 Subject: [PATCH 1/7] Remove union overlay design and use reflection in SqlTypeWorkarounds --- .../Data/SqlTypes/SqlTypeWorkarounds.cs | 340 +++++++++++++----- 1 file changed, 252 insertions(+), 88 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index 2cbd875cf8..57a6a161ad 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -6,8 +6,9 @@ using System.Data.SqlTypes; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Runtime.Serialization; using System.Xml; using Microsoft.Data.SqlClient; @@ -93,6 +94,8 @@ internal static DateTime SqlDateTimeToDateTime(int daypart, int timepart) #endregion #region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation + private static readonly Func s_sqlMoneyfactory = CtorHelper.CreateFactory(); // binds to SqlMoney..ctor(long, int) if it exists + /// /// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists /// only to distinguish this constructor from the constructor that takes a long. @@ -100,130 +103,291 @@ internal static DateTime SqlDateTimeToDateTime(int daypart, int timepart) /// internal static SqlMoney SqlMoneyCtor(long value, int ignored) { - var c = default(SqlMoneyCaster); - - // Same behavior as the internal SqlMoney.ctor(long, bool) overload - c.Fake._fNotNull = true; - c.Fake._value = value; + SqlMoney val; + if (s_sqlMoneyfactory is not null) + { + val = s_sqlMoneyfactory(value); + } + else + { + val = new SqlMoney(((decimal)value) / 10000); + } - return c.Real; + return val; } internal static long SqlMoneyToSqlInternalRepresentation(SqlMoney money) { - var c = default(SqlMoneyCaster); - c.Real = money; + return SqlMoneyHelper.GetSqlMoneyToLong(money); + } + + internal static class SqlMoneyHelper + { + private static readonly MethodInfo s_toSqlInternalRepresentation = GetFastSqlMoneyToLong(); - // Same implementation as the internal SqlMoney.ToSqlInternalRepresentation implementation - if (money.IsNull) + internal static long GetSqlMoneyToLong(SqlMoney money) { - throw new SqlNullValueException(); + if (s_toSqlInternalRepresentation is not null) + { + try + { + return (long)s_toSqlInternalRepresentation.Invoke(money, null); + } + catch + { + // If an exception occurs for any reason, swallow & use the fallback code path. + } + } + + return FallbackSqlMoneyToLong(money); } - return c.Fake._value; - } - [StructLayout(LayoutKind.Sequential)] - private struct SqlMoneyLookalike // exact same shape as SqlMoney, but with accessible fields - { - internal bool _fNotNull; - internal long _value; - } + private static MethodInfo GetFastSqlMoneyToLong() + { + MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, + null, CallingConventions.Any, new Type[] { }, null); - [StructLayout(LayoutKind.Explicit)] - private struct SqlMoneyCaster - { - [FieldOffset(0)] - internal SqlMoney Real; - [FieldOffset(0)] - internal SqlMoneyLookalike Fake; + if (toSqlInternalRepresentation is not null && toSqlInternalRepresentation.ReturnType == typeof(long)) + { + return toSqlInternalRepresentation; + } + + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.GetFastSqlMoneyToLong | Info | SqlMoney.ToSqlInternalRepresentation() not found. Less efficient fallback method will be used."); + return null; // missing the expected method - cannot use fast path + } + + // Used in case we can't use a [Serializable]-like mechanism. + private static long FallbackSqlMoneyToLong(SqlMoney value) + { + if (value.IsNull) + { + return default; + } + else + { + decimal data = value.ToDecimal(); + return (long)(data * 10000); + } + } } #endregion #region Work around inability to access SqlDecimal._data1/2/3/4 internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) { - // Extract the four data elements from SqlDecimal. - var c = default(SqlDecimalCaster); - c.Real = d; - data1 = c.Fake._data1; - data2 = c.Fake._data2; - data3 = c.Fake._data3; - data4 = c.Fake._data4; + SqlDecimalHelper.Decompose(d, out data1, out data2, out data3, out data4); } - [StructLayout(LayoutKind.Sequential)] - private struct SqlDecimalLookalike // exact same shape as SqlDecimal, but with accessible fields + private static class SqlDecimalHelper { - internal byte _bStatus; - internal byte _bLen; - internal byte _bPrec; - internal byte _bScale; - internal uint _data1; - internal uint _data2; - internal uint _data3; - internal uint _data4; - } + private static readonly bool s_canUseFastPath = GetFastDecomposers(ref s_fiData1, ref s_fiData2, ref s_fiData3, ref s_fiData4); + private static FieldInfo s_fiData1; + private static FieldInfo s_fiData2; + private static FieldInfo s_fiData3; + private static FieldInfo s_fiData4; - [StructLayout(LayoutKind.Explicit)] - private struct SqlDecimalCaster - { - [FieldOffset(0)] - internal SqlDecimal Real; - [FieldOffset(0)] - internal SqlDecimalLookalike Fake; + internal static void Decompose(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) + { + if (s_canUseFastPath) + { + try + { + data1 = (uint)s_fiData1.GetValue(value); + data2 = (uint)s_fiData2.GetValue(value); + data3 = (uint)s_fiData3.GetValue(value); + data4 = (uint)s_fiData4.GetValue(value); + return; + } + catch + { + // If an exception occurs for any reason, swallow & use the fallback code path. + } + } + + FallbackDecomposer(value, out data1, out data2, out data3, out data4); + } + + private static bool GetFastDecomposers(ref FieldInfo fiData1, ref FieldInfo fiData2, ref FieldInfo fiData3, ref FieldInfo fiData4) + { + // This takes advantage of the fact that for [Serializable] types, the member fields are implicitly + // part of the type's serialization contract. This includes the fields' names and types. By default, + // [Serializable]-compliant serializers will read all the member fields and shove the data into a + // SerializationInfo dictionary. We mimic this behavior in a manner consistent with the [Serializable] + // pattern, but much more efficiently. + // + // In order to make sure we're staying compliant, we need to gate our checks to fulfill some core + // assumptions. Importantly, the type must be [Serializable] but cannot be ISerializable, as the + // presence of the interface means that the type wants to be responsible for its own serialization, + // and that member fields are not guaranteed to be part of the serialization contract. Additionally, + // we need to check for [OnSerializing] and [OnDeserializing] methods, because we cannot account + // for any logic which might be present within them. + + if (!typeof(SqlDecimal).IsSerializable) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); + return false; // type is not serializable - cannot use fast path assumptions + } + + if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal))) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); + return false; // type contains custom logic - cannot use fast path assumptions + } + + foreach (var method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute))) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); + return false; // type contains custom logic - cannot use fast path assumptions + } + } + + // GetSerializableMembers filters out [NonSerialized] fields for us automatically. + + foreach (var candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) + { + if (candidate is FieldInfo fi && fi.FieldType == typeof(uint)) + { + if (fi.Name == "m_data1") + { + fiData1 = fi; + } + else if (fi.Name == "m_data2") + { + fiData2 = fi; + } + else if (fi.Name == "m_data3") + { + fiData3 = fi; + } + else if (fi.Name == "m_data4") + { + fiData4 = fi; + } + } + } + + if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); + return false; // missing one of the expected member fields - cannot use fast path assumptions + } + + return true; + } + + // Used in case we can't use a [Serializable]-like mechanism. + private static void FallbackDecomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) + { + if (value.IsNull) + { + data1 = default; + data2 = default; + data3 = default; + data4 = default; + } + else + { + int[] data = value.Data; // allocation + data4 = (uint)data[3]; // write in reverse to avoid multiple bounds checks + data3 = (uint)data[2]; + data2 = (uint)data[1]; + data1 = (uint)data[0]; + } + } } #endregion #region Work around inability to access SqlBinary.ctor(byte[], bool) - internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) - { - // Construct a SqlBinary without allocating/copying the byte[]. This provides - // the same behavior as SqlBinary.ctor(byte[], bool). - var c = default(SqlBinaryCaster); - c.Fake._value = value; - return c.Real; - } + private static readonly Func s_sqlBinaryfactory = CtorHelper.CreateFactory(); // binds to SqlBinary..ctor(byte[], bool) if it exists - [StructLayout(LayoutKind.Sequential)] - private struct SqlBinaryLookalike + internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) { - internal byte[] _value; - } + SqlBinary val; + if (s_sqlBinaryfactory is not null) + { + val = s_sqlBinaryfactory(value); + } + else + { + val = new SqlBinary(value); + } - [StructLayout(LayoutKind.Explicit)] - private struct SqlBinaryCaster - { - [FieldOffset(0)] - internal SqlBinary Real; - [FieldOffset(0)] - internal SqlBinaryLookalike Fake; + return val; } #endregion #region Work around inability to access SqlGuid.ctor(byte[], bool) + private static readonly Func s_sqlGuidfactory = CtorHelper.CreateFactory(); // binds to SqlGuid..ctor(byte[], bool) if it exists + internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) { - // Construct a SqlGuid without allocating/copying the byte[]. This provides - // the same behavior as SqlGuid.ctor(byte[], bool). - var c = default(SqlGuidCaster); - c.Fake._value = value; - return c.Real; - } + SqlGuid val; + if (s_sqlBinaryfactory is not null) + { + val = s_sqlGuidfactory(value); + } + else + { + val = new SqlGuid(value); + } - [StructLayout(LayoutKind.Sequential)] - private struct SqlGuidLookalike - { - internal byte[] _value; + return val; } + #endregion - [StructLayout(LayoutKind.Explicit)] - private struct SqlGuidCaster + private static class CtorHelper { - [FieldOffset(0)] - internal SqlGuid Real; - [FieldOffset(0)] - internal SqlGuidLookalike Fake; + // Returns null if .ctor(TValue, TIgnored) cannot be found. + // Caller should have fallback logic in place in case the API doesn't exist. + internal unsafe static Func CreateFactory() where TInstance : struct + { + try + { + ConstructorInfo fullCtor = typeof(TInstance).GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, + null, new[] { typeof(TValue), typeof(TIgnored) }, null); + if (fullCtor is not null) + { + // Need to use fnptr rather than delegate since MulticastDelegate expects to point to a MethodInfo, + // not a ConstructorInfo. The convention for invoking struct ctors is that the caller zeros memory, + // then passes a ref to the zeroed memory as the implicit arg0 "this". We don't need to worry + // about keeping this pointer alive; the fact that we're instantiated over TInstance will do it + // for us. + // + // On Full Framework, creating a delegate to InvocationHelper before invoking it for the first time + // will cause the delegate to point to the pre-JIT stub, which has an expensive preamble. Instead, + // we invoke InvocationHelper manually with a captured no-op fnptr. We'll then replace it with the + // real fnptr before creating a new delegate (pointing to the real codegen, not the stub) and + // returning that new delegate to our caller. + + static void DummyNoOp(ref TInstance @this, TValue value, TIgnored ignored) + { } + + IntPtr fnPtr; + TInstance InvocationHelper(TValue value) + { + TInstance retVal = default; // ensure zero-inited + ((delegate* managed)fnPtr)(ref retVal, value, default); + return retVal; + } + + fnPtr = (IntPtr)(delegate* managed)(&DummyNoOp); + InvocationHelper(default); // no-op to trigger JIT + + fnPtr = fullCtor.MethodHandle.GetFunctionPointer(); // replace before returning to caller + return InvocationHelper; + } + } + catch + { + } + + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.CtorHelper.CreateFactory | Info | {0}..ctor({1}, {2}) not found. Less efficient fallback method will be used.", typeof(TInstance).Name, typeof(TValue).Name, typeof(TIgnored).Name); + return null; // factory not found or an exception occurred + } } - #endregion } } From 54685055d187f885a3e0ab4215cb54c655316e48 Mon Sep 17 00:00:00 2001 From: David Engel Date: Fri, 17 Jun 2022 15:23:01 -0700 Subject: [PATCH 2/7] Refactor to use delegates --- .../src/Microsoft.Data.SqlClient.csproj | 2 + .../Data/SqlTypes/SqlTypeWorkarounds.cs | 144 ++++++++++-------- tools/props/Versions.props | 5 + tools/specs/Microsoft.Data.SqlClient.nuspec | 2 + 4 files changed, 93 insertions(+), 60 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index eed46e0d1c..670a052889 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -959,6 +959,8 @@ + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index 57a6a161ad..5c3160e83d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Xml; @@ -23,9 +24,9 @@ namespace Microsoft.Data.SqlTypes internal static class SqlTypeWorkarounds { #region Work around inability to access SqlXml.CreateSqlXmlReader - private static readonly XmlReaderSettings s_defaultXmlReaderSettings = new XmlReaderSettings() { ConformanceLevel = ConformanceLevel.Fragment }; - private static readonly XmlReaderSettings s_defaultXmlReaderSettingsCloseInput = new XmlReaderSettings() { ConformanceLevel = ConformanceLevel.Fragment, CloseInput = true }; - private static readonly XmlReaderSettings s_defaultXmlReaderSettingsAsyncCloseInput = new XmlReaderSettings() { Async = true, ConformanceLevel = ConformanceLevel.Fragment, CloseInput = true }; + private static readonly XmlReaderSettings s_defaultXmlReaderSettings = new() { ConformanceLevel = ConformanceLevel.Fragment }; + private static readonly XmlReaderSettings s_defaultXmlReaderSettingsCloseInput = new() { ConformanceLevel = ConformanceLevel.Fragment, CloseInput = true }; + private static readonly XmlReaderSettings s_defaultXmlReaderSettingsAsyncCloseInput = new() { Async = true, ConformanceLevel = ConformanceLevel.Fragment, CloseInput = true }; internal const SqlCompareOptions SqlStringValidSqlCompareOptionMask = SqlCompareOptions.IgnoreCase | SqlCompareOptions.IgnoreWidth | @@ -118,31 +119,30 @@ internal static SqlMoney SqlMoneyCtor(long value, int ignored) internal static long SqlMoneyToSqlInternalRepresentation(SqlMoney money) { - return SqlMoneyHelper.GetSqlMoneyToLong(money); + return SqlMoneyHelper.s_sqlMoneyToLong(ref money); } - internal static class SqlMoneyHelper + private static class SqlMoneyHelper { - private static readonly MethodInfo s_toSqlInternalRepresentation = GetFastSqlMoneyToLong(); + internal delegate long SqlMoneyToLongDelegate(ref SqlMoney @this); + internal static readonly SqlMoneyToLongDelegate s_sqlMoneyToLong = GetSqlMoneyToLong(); - internal static long GetSqlMoneyToLong(SqlMoney money) + internal static SqlMoneyToLongDelegate GetSqlMoneyToLong() { - if (s_toSqlInternalRepresentation is not null) - { + SqlMoneyToLongDelegate del = null; try { - return (long)s_toSqlInternalRepresentation.Invoke(money, null); + del = GetFastSqlMoneyToLong(); } catch { // If an exception occurs for any reason, swallow & use the fallback code path. } - } - return FallbackSqlMoneyToLong(money); + return del ?? FallbackSqlMoneyToLong; } - private static MethodInfo GetFastSqlMoneyToLong() + private static SqlMoneyToLongDelegate GetFastSqlMoneyToLong() { MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, @@ -150,7 +150,21 @@ private static MethodInfo GetFastSqlMoneyToLong() if (toSqlInternalRepresentation is not null && toSqlInternalRepresentation.ReturnType == typeof(long)) { - return toSqlInternalRepresentation; + // On Full Framework, invoking the MethodInfo first before wrapping + // a delegate around it will produce better codegen. We don't need + // to inspect the return value; we just need to call the method. + + _ = toSqlInternalRepresentation.Invoke(new SqlMoney(0), new object[0]); + + // Now create the delegate. This is an open delegate, meaning the + // "this" parameter will be provided as arg0 on each call. + + var del = (SqlMoneyToLongDelegate)toSqlInternalRepresentation.CreateDelegate(typeof(SqlMoneyToLongDelegate), target: null); + + // Now we can cache the delegate and invoke it over and over again. + // Note: the first parameter to the delegate is provided *byref*. + + return del; } SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.GetFastSqlMoneyToLong | Info | SqlMoney.ToSqlInternalRepresentation() not found. Less efficient fallback method will be used."); @@ -158,7 +172,7 @@ private static MethodInfo GetFastSqlMoneyToLong() } // Used in case we can't use a [Serializable]-like mechanism. - private static long FallbackSqlMoneyToLong(SqlMoney value) + private static long FallbackSqlMoneyToLong(ref SqlMoney value) { if (value.IsNull) { @@ -176,39 +190,30 @@ private static long FallbackSqlMoneyToLong(SqlMoney value) #region Work around inability to access SqlDecimal._data1/2/3/4 internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) { - SqlDecimalHelper.Decompose(d, out data1, out data2, out data3, out data4); + SqlDecimalHelper.s_decompose(d, out data1, out data2, out data3, out data4); } private static class SqlDecimalHelper { - private static readonly bool s_canUseFastPath = GetFastDecomposers(ref s_fiData1, ref s_fiData2, ref s_fiData3, ref s_fiData4); - private static FieldInfo s_fiData1; - private static FieldInfo s_fiData2; - private static FieldInfo s_fiData3; - private static FieldInfo s_fiData4; + internal delegate void Decomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4); + internal static readonly Decomposer s_decompose = GetDecomposer(); - internal static void Decompose(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) + private static Decomposer GetDecomposer() { - if (s_canUseFastPath) + Decomposer decomposer = null; + try { - try - { - data1 = (uint)s_fiData1.GetValue(value); - data2 = (uint)s_fiData2.GetValue(value); - data3 = (uint)s_fiData3.GetValue(value); - data4 = (uint)s_fiData4.GetValue(value); - return; - } - catch - { - // If an exception occurs for any reason, swallow & use the fallback code path. - } + decomposer = GetFastDecomposer(); + } + catch + { + // If an exception occurs for any reason, swallow & use the fallback code path. } - FallbackDecomposer(value, out data1, out data2, out data3, out data4); + return decomposer ?? FallbackDecomposer; } - private static bool GetFastDecomposers(ref FieldInfo fiData1, ref FieldInfo fiData2, ref FieldInfo fiData3, ref FieldInfo fiData4) + private static Decomposer GetFastDecomposer() { // This takes advantage of the fact that for [Serializable] types, the member fields are implicitly // part of the type's serialization contract. This includes the fields' names and types. By default, @@ -225,57 +230,76 @@ private static bool GetFastDecomposers(ref FieldInfo fiData1, ref FieldInfo fiDa if (!typeof(SqlDecimal).IsSerializable) { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); - return false; // type is not serializable - cannot use fast path assumptions + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); + return null; // type is not serializable - cannot use fast path assumptions } if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal))) { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); - return false; // type contains custom logic - cannot use fast path assumptions + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); + return null; // type contains custom logic - cannot use fast path assumptions } - foreach (var method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + foreach (MethodInfo method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute))) { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); - return false; // type contains custom logic - cannot use fast path assumptions + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); + return null; // type contains custom logic - cannot use fast path assumptions } } // GetSerializableMembers filters out [NonSerialized] fields for us automatically. - foreach (var candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) + FieldInfo fiData1 = null, fiData2 = null, fiData3 = null, fiData4 = null; + foreach (MemberInfo candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) { if (candidate is FieldInfo fi && fi.FieldType == typeof(uint)) { if (fi.Name == "m_data1") - { - fiData1 = fi; - } + { fiData1 = fi; } else if (fi.Name == "m_data2") - { - fiData2 = fi; - } + { fiData2 = fi; } else if (fi.Name == "m_data3") - { - fiData3 = fi; - } + { fiData3 = fi; } else if (fi.Name == "m_data4") - { - fiData4 = fi; - } + { fiData4 = fi; } } } if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null) { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposers | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); - return false; // missing one of the expected member fields - cannot use fast path assumptions + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); + return null; // missing one of the expected member fields - cannot use fast path assumptions } - return true; + Type refToUInt32 = typeof(uint).MakeByRefType(); + DynamicMethod dm = new( + name: "sqldecimal-decomposer", + returnType: typeof(void), + parameterTypes: new[] { typeof(SqlDecimal), refToUInt32, refToUInt32, refToUInt32, refToUInt32 }, + restrictedSkipVisibility: true); // perf: JITs method at delegate creation time + + ILGenerator ilGen = dm.GetILGenerator(); + ilGen.Emit(OpCodes.Ldarg_1); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData1); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_2); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData2); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_3); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData3); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_S, (byte)4); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData4); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ret); + + return (Decomposer)dm.CreateDelegate(typeof(Decomposer), null /* target */); } // Used in case we can't use a [Serializable]-like mechanism. diff --git a/tools/props/Versions.props b/tools/props/Versions.props index f8ee837453..26740ed4fb 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -53,6 +53,11 @@ 4.3.0 + + + 4.7.0 + 4.7.0 + [1.24.0,2.0.0) diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index b4412f3e75..f51407864d 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -75,6 +75,8 @@ When using NuGet 3.x this package requires at least version 3.4. + + From 4cd514fc3f9b14defdfbc2effa255f71a87ae655 Mon Sep 17 00:00:00 2001 From: David Engel Date: Tue, 21 Jun 2022 17:43:38 -0700 Subject: [PATCH 3/7] Bug fix --- .../src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index 5c3160e83d..b55e9e899d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -349,7 +349,7 @@ internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) { SqlGuid val; - if (s_sqlBinaryfactory is not null) + if (s_sqlGuidfactory is not null) { val = s_sqlGuidfactory(value); } From 413ef427ad093d5979c9b4ba1dbb7f4856379182 Mon Sep 17 00:00:00 2001 From: David Engel Date: Wed, 29 Jun 2022 12:47:06 -0700 Subject: [PATCH 4/7] Leave union overlay method intact for netcore --- .../src/Microsoft.Data.SqlClient.csproj | 1 + .../SqlTypes/SqlTypeWorkarounds.netcore.cs | 158 ++++++++ .../netfx/src/Microsoft.Data.SqlClient.csproj | 1 + .../Data/SqlTypes/SqlTypeWorkarounds.netfx.cs | 346 ++++++++++++++++++ .../Data/SqlTypes/SqlTypeWorkarounds.cs | 322 +--------------- 5 files changed, 507 insertions(+), 321 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs create mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 670a052889..3b23705075 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -644,6 +644,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs new file mode 100644 index 0000000000..99f8e46787 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Xml; +using Microsoft.Data.SqlClient; + +namespace Microsoft.Data.SqlTypes +{ + /// + /// This type provides workarounds for the separation between System.Data.Common + /// and Microsoft.Data.SqlClient. The latter wants to access internal members of the former, and + /// this class provides ways to do that. We must review and update this implementation any time the + /// implementation of the corresponding types in System.Data.Common change. + /// + internal static partial class SqlTypeWorkarounds + { + #region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation + /// + /// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists + /// only to distinguish this constructor from the constructor that takes a long. + /// Used only internally. + /// + internal static SqlMoney SqlMoneyCtor(long value, int ignored) + { + var c = default(SqlMoneyCaster); + + // Same behavior as the internal SqlMoney.ctor(long, bool) overload + c.Fake._fNotNull = true; + c.Fake._value = value; + + return c.Real; + } + + internal static long SqlMoneyToSqlInternalRepresentation(SqlMoney money) + { + var c = default(SqlMoneyCaster); + c.Real = money; + + // Same implementation as the internal SqlMoney.ToSqlInternalRepresentation implementation + if (money.IsNull) + { + throw new SqlNullValueException(); + } + return c.Fake._value; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SqlMoneyLookalike // exact same shape as SqlMoney, but with accessible fields + { + internal bool _fNotNull; + internal long _value; + } + + [StructLayout(LayoutKind.Explicit)] + private struct SqlMoneyCaster + { + [FieldOffset(0)] + internal SqlMoney Real; + [FieldOffset(0)] + internal SqlMoneyLookalike Fake; + } + #endregion + + #region Work around inability to access SqlDecimal._data1/2/3/4 + internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) + { + // Extract the four data elements from SqlDecimal. + var c = default(SqlDecimalCaster); + c.Real = d; + data1 = c.Fake._data1; + data2 = c.Fake._data2; + data3 = c.Fake._data3; + data4 = c.Fake._data4; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SqlDecimalLookalike // exact same shape as SqlDecimal, but with accessible fields + { + internal byte _bStatus; + internal byte _bLen; + internal byte _bPrec; + internal byte _bScale; + internal uint _data1; + internal uint _data2; + internal uint _data3; + internal uint _data4; + } + + [StructLayout(LayoutKind.Explicit)] + private struct SqlDecimalCaster + { + [FieldOffset(0)] + internal SqlDecimal Real; + [FieldOffset(0)] + internal SqlDecimalLookalike Fake; + } + #endregion + + #region Work around inability to access SqlBinary.ctor(byte[], bool) + internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) + { + // Construct a SqlBinary without allocating/copying the byte[]. This provides + // the same behavior as SqlBinary.ctor(byte[], bool). + var c = default(SqlBinaryCaster); + c.Fake._value = value; + return c.Real; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SqlBinaryLookalike + { + internal byte[] _value; + } + + [StructLayout(LayoutKind.Explicit)] + private struct SqlBinaryCaster + { + [FieldOffset(0)] + internal SqlBinary Real; + [FieldOffset(0)] + internal SqlBinaryLookalike Fake; + } + #endregion + + #region Work around inability to access SqlGuid.ctor(byte[], bool) + internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) + { + // Construct a SqlGuid without allocating/copying the byte[]. This provides + // the same behavior as SqlGuid.ctor(byte[], bool). + var c = default(SqlGuidCaster); + c.Fake._value = value; + return c.Real; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SqlGuidLookalike + { + internal byte[] _value; + } + + [StructLayout(LayoutKind.Explicit)] + private struct SqlGuidCaster + { + [FieldOffset(0)] + internal SqlGuid Real; + [FieldOffset(0)] + internal SqlGuidLookalike Fake; + } + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 93b2fe8ca9..12323365bf 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -653,6 +653,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs new file mode 100644 index 0000000000..7c2e97d1cd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs @@ -0,0 +1,346 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Xml; +using Microsoft.Data.SqlClient; + +namespace Microsoft.Data.SqlTypes +{ + /// + /// This type provides workarounds for the separation between System.Data.Common + /// and Microsoft.Data.SqlClient. The latter wants to access internal members of the former, and + /// this class provides ways to do that. We must review and update this implementation any time the + /// implementation of the corresponding types in System.Data.Common change. + /// + internal static partial class SqlTypeWorkarounds + { + #region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation + private static readonly Func s_sqlMoneyfactory = CtorHelper.CreateFactory(); // binds to SqlMoney..ctor(long, int) if it exists + + /// + /// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists + /// only to distinguish this constructor from the constructor that takes a long. + /// Used only internally. + /// + internal static SqlMoney SqlMoneyCtor(long value, int ignored) + { + SqlMoney val; + if (s_sqlMoneyfactory is not null) + { + val = s_sqlMoneyfactory(value); + } + else + { + val = new SqlMoney(((decimal)value) / 10000); + } + + return val; + } + + internal static long SqlMoneyToSqlInternalRepresentation(SqlMoney money) + { + return SqlMoneyHelper.s_sqlMoneyToLong(ref money); + } + + private static class SqlMoneyHelper + { + internal delegate long SqlMoneyToLongDelegate(ref SqlMoney @this); + internal static readonly SqlMoneyToLongDelegate s_sqlMoneyToLong = GetSqlMoneyToLong(); + + internal static SqlMoneyToLongDelegate GetSqlMoneyToLong() + { + SqlMoneyToLongDelegate del = null; + try + { + del = GetFastSqlMoneyToLong(); + } + catch + { + // If an exception occurs for any reason, swallow & use the fallback code path. + } + + return del ?? FallbackSqlMoneyToLong; + } + + private static SqlMoneyToLongDelegate GetFastSqlMoneyToLong() + { + MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, + null, CallingConventions.Any, new Type[] { }, null); + + if (toSqlInternalRepresentation is not null && toSqlInternalRepresentation.ReturnType == typeof(long)) + { + // On Full Framework, invoking the MethodInfo first before wrapping + // a delegate around it will produce better codegen. We don't need + // to inspect the return value; we just need to call the method. + + _ = toSqlInternalRepresentation.Invoke(new SqlMoney(0), new object[0]); + + // Now create the delegate. This is an open delegate, meaning the + // "this" parameter will be provided as arg0 on each call. + + var del = (SqlMoneyToLongDelegate)toSqlInternalRepresentation.CreateDelegate(typeof(SqlMoneyToLongDelegate), target: null); + + // Now we can cache the delegate and invoke it over and over again. + // Note: the first parameter to the delegate is provided *byref*. + + return del; + } + + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.GetFastSqlMoneyToLong | Info | SqlMoney.ToSqlInternalRepresentation() not found. Less efficient fallback method will be used."); + return null; // missing the expected method - cannot use fast path + } + + // Used in case we can't use a [Serializable]-like mechanism. + private static long FallbackSqlMoneyToLong(ref SqlMoney value) + { + if (value.IsNull) + { + return default; + } + else + { + decimal data = value.ToDecimal(); + return (long)(data * 10000); + } + } + } + #endregion + + #region Work around inability to access SqlDecimal._data1/2/3/4 + internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) + { + SqlDecimalHelper.s_decompose(d, out data1, out data2, out data3, out data4); + } + + private static class SqlDecimalHelper + { + internal delegate void Decomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4); + internal static readonly Decomposer s_decompose = GetDecomposer(); + + private static Decomposer GetDecomposer() + { + Decomposer decomposer = null; + try + { + decomposer = GetFastDecomposer(); + } + catch + { + // If an exception occurs for any reason, swallow & use the fallback code path. + } + + return decomposer ?? FallbackDecomposer; + } + + private static Decomposer GetFastDecomposer() + { + // This takes advantage of the fact that for [Serializable] types, the member fields are implicitly + // part of the type's serialization contract. This includes the fields' names and types. By default, + // [Serializable]-compliant serializers will read all the member fields and shove the data into a + // SerializationInfo dictionary. We mimic this behavior in a manner consistent with the [Serializable] + // pattern, but much more efficiently. + // + // In order to make sure we're staying compliant, we need to gate our checks to fulfill some core + // assumptions. Importantly, the type must be [Serializable] but cannot be ISerializable, as the + // presence of the interface means that the type wants to be responsible for its own serialization, + // and that member fields are not guaranteed to be part of the serialization contract. Additionally, + // we need to check for [OnSerializing] and [OnDeserializing] methods, because we cannot account + // for any logic which might be present within them. + + if (!typeof(SqlDecimal).IsSerializable) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); + return null; // type is not serializable - cannot use fast path assumptions + } + + if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal))) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); + return null; // type contains custom logic - cannot use fast path assumptions + } + + foreach (MethodInfo method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute))) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); + return null; // type contains custom logic - cannot use fast path assumptions + } + } + + // GetSerializableMembers filters out [NonSerialized] fields for us automatically. + + FieldInfo fiData1 = null, fiData2 = null, fiData3 = null, fiData4 = null; + foreach (MemberInfo candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) + { + if (candidate is FieldInfo fi && fi.FieldType == typeof(uint)) + { + if (fi.Name == "m_data1") + { fiData1 = fi; } + else if (fi.Name == "m_data2") + { fiData2 = fi; } + else if (fi.Name == "m_data3") + { fiData3 = fi; } + else if (fi.Name == "m_data4") + { fiData4 = fi; } + } + } + + if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null) + { + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); + return null; // missing one of the expected member fields - cannot use fast path assumptions + } + + Type refToUInt32 = typeof(uint).MakeByRefType(); + DynamicMethod dm = new( + name: "sqldecimal-decomposer", + returnType: typeof(void), + parameterTypes: new[] { typeof(SqlDecimal), refToUInt32, refToUInt32, refToUInt32, refToUInt32 }, + restrictedSkipVisibility: true); // perf: JITs method at delegate creation time + + ILGenerator ilGen = dm.GetILGenerator(); + ilGen.Emit(OpCodes.Ldarg_1); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData1); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_2); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData2); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_3); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData3); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ldarg_S, (byte)4); // eval stack := [UInt32&] + ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] + ilGen.Emit(OpCodes.Ldfld, fiData4); // eval stack := [UInt32&] [UInt32] + ilGen.Emit(OpCodes.Stind_I4); // eval stack := + ilGen.Emit(OpCodes.Ret); + + return (Decomposer)dm.CreateDelegate(typeof(Decomposer), null /* target */); + } + + // Used in case we can't use a [Serializable]-like mechanism. + private static void FallbackDecomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) + { + if (value.IsNull) + { + data1 = default; + data2 = default; + data3 = default; + data4 = default; + } + else + { + int[] data = value.Data; // allocation + data4 = (uint)data[3]; // write in reverse to avoid multiple bounds checks + data3 = (uint)data[2]; + data2 = (uint)data[1]; + data1 = (uint)data[0]; + } + } + } + #endregion + + #region Work around inability to access SqlBinary.ctor(byte[], bool) + private static readonly Func s_sqlBinaryfactory = CtorHelper.CreateFactory(); // binds to SqlBinary..ctor(byte[], bool) if it exists + + internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) + { + SqlBinary val; + if (s_sqlBinaryfactory is not null) + { + val = s_sqlBinaryfactory(value); + } + else + { + val = new SqlBinary(value); + } + + return val; + } + #endregion + + #region Work around inability to access SqlGuid.ctor(byte[], bool) + private static readonly Func s_sqlGuidfactory = CtorHelper.CreateFactory(); // binds to SqlGuid..ctor(byte[], bool) if it exists + + internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) + { + SqlGuid val; + if (s_sqlGuidfactory is not null) + { + val = s_sqlGuidfactory(value); + } + else + { + val = new SqlGuid(value); + } + + return val; + } + #endregion + + private static class CtorHelper + { + // Returns null if .ctor(TValue, TIgnored) cannot be found. + // Caller should have fallback logic in place in case the API doesn't exist. + internal unsafe static Func CreateFactory() where TInstance : struct + { + try + { + ConstructorInfo fullCtor = typeof(TInstance).GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, + null, new[] { typeof(TValue), typeof(TIgnored) }, null); + if (fullCtor is not null) + { + // Need to use fnptr rather than delegate since MulticastDelegate expects to point to a MethodInfo, + // not a ConstructorInfo. The convention for invoking struct ctors is that the caller zeros memory, + // then passes a ref to the zeroed memory as the implicit arg0 "this". We don't need to worry + // about keeping this pointer alive; the fact that we're instantiated over TInstance will do it + // for us. + // + // On Full Framework, creating a delegate to InvocationHelper before invoking it for the first time + // will cause the delegate to point to the pre-JIT stub, which has an expensive preamble. Instead, + // we invoke InvocationHelper manually with a captured no-op fnptr. We'll then replace it with the + // real fnptr before creating a new delegate (pointing to the real codegen, not the stub) and + // returning that new delegate to our caller. + + static void DummyNoOp(ref TInstance @this, TValue value, TIgnored ignored) + { } + + IntPtr fnPtr; + TInstance InvocationHelper(TValue value) + { + TInstance retVal = default; // ensure zero-inited + ((delegate* managed)fnPtr)(ref retVal, value, default); + return retVal; + } + + fnPtr = (IntPtr)(delegate* managed)(&DummyNoOp); + InvocationHelper(default); // no-op to trigger JIT + + fnPtr = fullCtor.MethodHandle.GetFunctionPointer(); // replace before returning to caller + return InvocationHelper; + } + } + catch + { + } + + SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.CtorHelper.CreateFactory | Info | {0}..ctor({1}, {2}) not found. Less efficient fallback method will be used.", typeof(TInstance).Name, typeof(TValue).Name, typeof(TIgnored).Name); + return null; // factory not found or an exception occurred + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index b55e9e899d..516b0fd415 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -21,7 +21,7 @@ namespace Microsoft.Data.SqlTypes /// this class provides ways to do that. We must review and update this implementation any time the /// implementation of the corresponding types in System.Data.Common change. /// - internal static class SqlTypeWorkarounds + internal static partial class SqlTypeWorkarounds { #region Work around inability to access SqlXml.CreateSqlXmlReader private static readonly XmlReaderSettings s_defaultXmlReaderSettings = new() { ConformanceLevel = ConformanceLevel.Fragment }; @@ -93,325 +93,5 @@ internal static DateTime SqlDateTimeToDateTime(int daypart, int timepart) private static Exception ThrowOverflowException() => throw SQL.DateTimeOverflow(); #endregion - - #region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation - private static readonly Func s_sqlMoneyfactory = CtorHelper.CreateFactory(); // binds to SqlMoney..ctor(long, int) if it exists - - /// - /// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists - /// only to distinguish this constructor from the constructor that takes a long. - /// Used only internally. - /// - internal static SqlMoney SqlMoneyCtor(long value, int ignored) - { - SqlMoney val; - if (s_sqlMoneyfactory is not null) - { - val = s_sqlMoneyfactory(value); - } - else - { - val = new SqlMoney(((decimal)value) / 10000); - } - - return val; - } - - internal static long SqlMoneyToSqlInternalRepresentation(SqlMoney money) - { - return SqlMoneyHelper.s_sqlMoneyToLong(ref money); - } - - private static class SqlMoneyHelper - { - internal delegate long SqlMoneyToLongDelegate(ref SqlMoney @this); - internal static readonly SqlMoneyToLongDelegate s_sqlMoneyToLong = GetSqlMoneyToLong(); - - internal static SqlMoneyToLongDelegate GetSqlMoneyToLong() - { - SqlMoneyToLongDelegate del = null; - try - { - del = GetFastSqlMoneyToLong(); - } - catch - { - // If an exception occurs for any reason, swallow & use the fallback code path. - } - - return del ?? FallbackSqlMoneyToLong; - } - - private static SqlMoneyToLongDelegate GetFastSqlMoneyToLong() - { - MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation", - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, - null, CallingConventions.Any, new Type[] { }, null); - - if (toSqlInternalRepresentation is not null && toSqlInternalRepresentation.ReturnType == typeof(long)) - { - // On Full Framework, invoking the MethodInfo first before wrapping - // a delegate around it will produce better codegen. We don't need - // to inspect the return value; we just need to call the method. - - _ = toSqlInternalRepresentation.Invoke(new SqlMoney(0), new object[0]); - - // Now create the delegate. This is an open delegate, meaning the - // "this" parameter will be provided as arg0 on each call. - - var del = (SqlMoneyToLongDelegate)toSqlInternalRepresentation.CreateDelegate(typeof(SqlMoneyToLongDelegate), target: null); - - // Now we can cache the delegate and invoke it over and over again. - // Note: the first parameter to the delegate is provided *byref*. - - return del; - } - - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.GetFastSqlMoneyToLong | Info | SqlMoney.ToSqlInternalRepresentation() not found. Less efficient fallback method will be used."); - return null; // missing the expected method - cannot use fast path - } - - // Used in case we can't use a [Serializable]-like mechanism. - private static long FallbackSqlMoneyToLong(ref SqlMoney value) - { - if (value.IsNull) - { - return default; - } - else - { - decimal data = value.ToDecimal(); - return (long)(data * 10000); - } - } - } - #endregion - - #region Work around inability to access SqlDecimal._data1/2/3/4 - internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4) - { - SqlDecimalHelper.s_decompose(d, out data1, out data2, out data3, out data4); - } - - private static class SqlDecimalHelper - { - internal delegate void Decomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4); - internal static readonly Decomposer s_decompose = GetDecomposer(); - - private static Decomposer GetDecomposer() - { - Decomposer decomposer = null; - try - { - decomposer = GetFastDecomposer(); - } - catch - { - // If an exception occurs for any reason, swallow & use the fallback code path. - } - - return decomposer ?? FallbackDecomposer; - } - - private static Decomposer GetFastDecomposer() - { - // This takes advantage of the fact that for [Serializable] types, the member fields are implicitly - // part of the type's serialization contract. This includes the fields' names and types. By default, - // [Serializable]-compliant serializers will read all the member fields and shove the data into a - // SerializationInfo dictionary. We mimic this behavior in a manner consistent with the [Serializable] - // pattern, but much more efficiently. - // - // In order to make sure we're staying compliant, we need to gate our checks to fulfill some core - // assumptions. Importantly, the type must be [Serializable] but cannot be ISerializable, as the - // presence of the interface means that the type wants to be responsible for its own serialization, - // and that member fields are not guaranteed to be part of the serialization contract. Additionally, - // we need to check for [OnSerializing] and [OnDeserializing] methods, because we cannot account - // for any logic which might be present within them. - - if (!typeof(SqlDecimal).IsSerializable) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used."); - return null; // type is not serializable - cannot use fast path assumptions - } - - if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal))) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used."); - return null; // type contains custom logic - cannot use fast path assumptions - } - - foreach (MethodInfo method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute))) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used."); - return null; // type contains custom logic - cannot use fast path assumptions - } - } - - // GetSerializableMembers filters out [NonSerialized] fields for us automatically. - - FieldInfo fiData1 = null, fiData2 = null, fiData3 = null, fiData4 = null; - foreach (MemberInfo candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal))) - { - if (candidate is FieldInfo fi && fi.FieldType == typeof(uint)) - { - if (fi.Name == "m_data1") - { fiData1 = fi; } - else if (fi.Name == "m_data2") - { fiData2 = fi; } - else if (fi.Name == "m_data3") - { fiData3 = fi; } - else if (fi.Name == "m_data4") - { fiData4 = fi; } - } - } - - if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null) - { - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used."); - return null; // missing one of the expected member fields - cannot use fast path assumptions - } - - Type refToUInt32 = typeof(uint).MakeByRefType(); - DynamicMethod dm = new( - name: "sqldecimal-decomposer", - returnType: typeof(void), - parameterTypes: new[] { typeof(SqlDecimal), refToUInt32, refToUInt32, refToUInt32, refToUInt32 }, - restrictedSkipVisibility: true); // perf: JITs method at delegate creation time - - ILGenerator ilGen = dm.GetILGenerator(); - ilGen.Emit(OpCodes.Ldarg_1); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData1); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_2); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData2); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_3); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData3); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ldarg_S, (byte)4); // eval stack := [UInt32&] - ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal] - ilGen.Emit(OpCodes.Ldfld, fiData4); // eval stack := [UInt32&] [UInt32] - ilGen.Emit(OpCodes.Stind_I4); // eval stack := - ilGen.Emit(OpCodes.Ret); - - return (Decomposer)dm.CreateDelegate(typeof(Decomposer), null /* target */); - } - - // Used in case we can't use a [Serializable]-like mechanism. - private static void FallbackDecomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4) - { - if (value.IsNull) - { - data1 = default; - data2 = default; - data3 = default; - data4 = default; - } - else - { - int[] data = value.Data; // allocation - data4 = (uint)data[3]; // write in reverse to avoid multiple bounds checks - data3 = (uint)data[2]; - data2 = (uint)data[1]; - data1 = (uint)data[0]; - } - } - } - #endregion - - #region Work around inability to access SqlBinary.ctor(byte[], bool) - private static readonly Func s_sqlBinaryfactory = CtorHelper.CreateFactory(); // binds to SqlBinary..ctor(byte[], bool) if it exists - - internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored) - { - SqlBinary val; - if (s_sqlBinaryfactory is not null) - { - val = s_sqlBinaryfactory(value); - } - else - { - val = new SqlBinary(value); - } - - return val; - } - #endregion - - #region Work around inability to access SqlGuid.ctor(byte[], bool) - private static readonly Func s_sqlGuidfactory = CtorHelper.CreateFactory(); // binds to SqlGuid..ctor(byte[], bool) if it exists - - internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored) - { - SqlGuid val; - if (s_sqlGuidfactory is not null) - { - val = s_sqlGuidfactory(value); - } - else - { - val = new SqlGuid(value); - } - - return val; - } - #endregion - - private static class CtorHelper - { - // Returns null if .ctor(TValue, TIgnored) cannot be found. - // Caller should have fallback logic in place in case the API doesn't exist. - internal unsafe static Func CreateFactory() where TInstance : struct - { - try - { - ConstructorInfo fullCtor = typeof(TInstance).GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding, - null, new[] { typeof(TValue), typeof(TIgnored) }, null); - if (fullCtor is not null) - { - // Need to use fnptr rather than delegate since MulticastDelegate expects to point to a MethodInfo, - // not a ConstructorInfo. The convention for invoking struct ctors is that the caller zeros memory, - // then passes a ref to the zeroed memory as the implicit arg0 "this". We don't need to worry - // about keeping this pointer alive; the fact that we're instantiated over TInstance will do it - // for us. - // - // On Full Framework, creating a delegate to InvocationHelper before invoking it for the first time - // will cause the delegate to point to the pre-JIT stub, which has an expensive preamble. Instead, - // we invoke InvocationHelper manually with a captured no-op fnptr. We'll then replace it with the - // real fnptr before creating a new delegate (pointing to the real codegen, not the stub) and - // returning that new delegate to our caller. - - static void DummyNoOp(ref TInstance @this, TValue value, TIgnored ignored) - { } - - IntPtr fnPtr; - TInstance InvocationHelper(TValue value) - { - TInstance retVal = default; // ensure zero-inited - ((delegate* managed)fnPtr)(ref retVal, value, default); - return retVal; - } - - fnPtr = (IntPtr)(delegate* managed)(&DummyNoOp); - InvocationHelper(default); // no-op to trigger JIT - - fnPtr = fullCtor.MethodHandle.GetFunctionPointer(); // replace before returning to caller - return InvocationHelper; - } - } - catch - { - } - - SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.CtorHelper.CreateFactory | Info | {0}..ctor({1}, {2}) not found. Less efficient fallback method will be used.", typeof(TInstance).Name, typeof(TValue).Name, typeof(TIgnored).Name); - return null; // factory not found or an exception occurred - } - } } } From 4a49bd90cf89929123470630cfa6322a6cc32b33 Mon Sep 17 00:00:00 2001 From: David Engel Date: Wed, 29 Jun 2022 17:26:43 -0700 Subject: [PATCH 5/7] Cleanup --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 2 -- .../src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs | 3 --- tools/props/Versions.props | 5 ----- tools/specs/Microsoft.Data.SqlClient.nuspec | 2 -- 4 files changed, 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 3b23705075..97103c9056 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -960,8 +960,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs index 516b0fd415..853be887dc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs @@ -6,10 +6,7 @@ using System.Data.SqlTypes; using System.Diagnostics; using System.IO; -using System.Reflection; -using System.Reflection.Emit; using System.Runtime.CompilerServices; -using System.Runtime.Serialization; using System.Xml; using Microsoft.Data.SqlClient; diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 26740ed4fb..f8ee837453 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -53,11 +53,6 @@ 4.3.0 - - - 4.7.0 - 4.7.0 - [1.24.0,2.0.0) diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index f51407864d..b4412f3e75 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -75,8 +75,6 @@ When using NuGet 3.x this package requires at least version 3.4. - - From 5d71d6e0483a000ba58f668fba817f8e1dbc1ac8 Mon Sep 17 00:00:00 2001 From: David Engel Date: Wed, 29 Jun 2022 17:27:47 -0700 Subject: [PATCH 6/7] Cleanup --- .../Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs | 6 ------ .../src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs | 4 ---- 2 files changed, 10 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs index 99f8e46787..43ed8bbee4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs @@ -2,14 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Data.SqlTypes; -using System.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Xml; -using Microsoft.Data.SqlClient; namespace Microsoft.Data.SqlTypes { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs index 7c2e97d1cd..2efd77177e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs @@ -4,13 +4,9 @@ using System; using System.Data.SqlTypes; -using System.Diagnostics; -using System.IO; using System.Reflection; using System.Reflection.Emit; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; -using System.Xml; using Microsoft.Data.SqlClient; namespace Microsoft.Data.SqlTypes From 0c64ab768acaf6882dbe118544625e16af79388e Mon Sep 17 00:00:00 2001 From: David Engel Date: Tue, 19 Jul 2022 09:08:04 -0700 Subject: [PATCH 7/7] Add comment --- .../src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs index 2efd77177e..941e3325b6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs @@ -36,6 +36,7 @@ internal static SqlMoney SqlMoneyCtor(long value, int ignored) } else { + // SqlMoney is a long internally. Dividing by 10,000 gives us the decimal representation val = new SqlMoney(((decimal)value) / 10000); }