diff --git a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs new file mode 100644 index 00000000000000..3d4f6ce9170b2b --- /dev/null +++ b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs @@ -0,0 +1,528 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace SourceGenerators +{ + /// + /// Emits member and constructor accessors for a source generator: [UnsafeAccessor] externs on frameworks that + /// support them, and a reflection-based fallback (cached delegates / / + /// ) downlevel. Callers describe the members to emit with the neutral, + /// primitive-only spec types in this file; the emitter owns accessor naming and the generic wrapper-class machinery. + /// + /// + /// The reflection fallback emitted for members references an InstanceMemberBindingFlags constant (of type + /// ) that the consuming generator must emit into the same scope by + /// writing , and a ValueTypeSetter<TDeclaringType, TValue> + /// delegate (written via ) when + /// returns for a value-type setter. + /// + internal static class UnsafeAccessorEmitter + { + private const string UnsafeAccessorAttributeTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorAttribute"; + private const string UnsafeAccessorKindTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorKind"; + private const string EmptyTypeArray = "global::System.Array.Empty()"; + + // RefKind.RefReadOnlyParameter (value 4) is unavailable in the Roslyn version the netstandard build compiles + // against; reference it by its underlying value so both Roslyn targets build. + private const RefKind RefKindRefReadOnlyParameter = (RefKind)4; + + /// + /// The declaration of the InstanceMemberBindingFlags constant that the reflection fallback (emitted by + /// and ) references. A consuming generator + /// must write this once into the scope containing the emitted accessors. + /// + public const string InstanceMemberBindingFlagsDeclaration = """ + private const global::System.Reflection.BindingFlags InstanceMemberBindingFlags = + global::System.Reflection.BindingFlags.Instance | + global::System.Reflection.BindingFlags.Public | + global::System.Reflection.BindingFlags.NonPublic; + """; + + /// + /// The declaration of the ValueTypeSetter<TDeclaringType, TValue> delegate that the value-type + /// setter reflection fallback references. A consuming generator must write this once when + /// returns . + /// + public const string ValueTypeSetterDelegateDeclaration = "private delegate void ValueTypeSetter(ref TDeclaringType obj, TValue value);"; + + internal enum AccessorMemberKind + { + Property, + Field, + } + + /// + /// Describes a single member (property or field) an accessor may be emitted for. All type names are neutral + /// fully-qualified strings so the spec carries no Roslyn symbols and remains incremental-pipeline safe. + /// + internal sealed record UnsafeAccessorMemberSpec + { + public required AccessorMemberKind Kind { get; init; } + public required string MemberName { get; init; } + public required bool NeedsGetter { get; init; } + public required bool NeedsSetter { get; init; } + public required bool CanUseUnsafeAccessors { get; init; } + public required string DeclaringTypeFQN { get; init; } + public required string MemberTypeFQN { get; init; } + + /// + /// The zero-based position of the declaring type in the containing type's inheritance hierarchy, used to + /// disambiguate the generic wrapper class between members inherited from different generic base types. + /// + public int DeclaringTypeIndex { get; init; } + + /// Type-parameter names of the declaring type when it is generic (.NET 9+ wrapper class), otherwise . + public ImmutableEquatableArray? DeclaringTypeParameterNames { get; init; } + public string? OpenDeclaringTypeFQN { get; init; } + public string? OpenMemberTypeFQN { get; init; } + public string? DeclaringTypeParameterConstraintClauses { get; init; } + + public bool IsProperty => Kind is AccessorMemberKind.Property; + } + + /// Describes a constructor accessor to emit. + internal sealed record UnsafeAccessorConstructorSpec + { + public required string TypeFriendlyName { get; init; } + public required string TypeFQN { get; init; } + public required bool CanUseUnsafeAccessor { get; init; } + public required ImmutableEquatableArray Parameters { get; init; } + + /// Type-parameter names of the type when it is generic (.NET 9+ wrapper class), otherwise . + public ImmutableEquatableArray? DeclaringTypeParameterNames { get; init; } + public string? OpenDeclaringTypeFQN { get; init; } + public string? DeclaringTypeParameterConstraintClauses { get; init; } + } + + /// A single constructor parameter of a . + internal sealed record UnsafeAccessorParameterSpec + { + public required string TypeFQN { get; init; } + public required int Index { get; init; } + public RefKind RefKind { get; init; } + + /// The open (type-parameter-referencing) form of the parameter type, used inside the generic wrapper class; when the declaring type is non-generic or the parameter type contains no type parameters. + public string? OpenTypeFQN { get; init; } + } + + /// + /// Gets the accessor name for a property or field. For UnsafeAccessor this is the extern method name; + /// for reflection fallback this is the strongly typed wrapper method name. + /// Use kind "get"/"set" for property getters/setters, or "field" for field UnsafeAccessor externs. + /// The property index suffix is only appended when needed to disambiguate shadowed members. + /// + public static string GetAccessorName(string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) + => needsDisambiguation + ? $"__{accessorKind}_{typeFriendlyName}_{memberName}_{propertyIndex}" + : $"__{accessorKind}_{typeFriendlyName}_{memberName}"; + + /// + /// For properties on generic types using wrapper-class UnsafeAccessors (.NET 9+), returns the + /// fully qualified accessor reference including the generic wrapper class prefix, e.g. + /// __GenericAccessors_MyType_0<int>.__get_MyType_Name. + /// For non-generic types, returns the plain accessor name. + /// + public static string GetQualifiedAccessorName( + ImmutableEquatableArray? declaringTypeParameterNames, + int declaringTypeIndex, + string declaringTypeFQN, + string typeFriendlyName, + string accessorKind, + string memberName, + int propertyIndex, + bool needsDisambiguation) + { + string accessorName = GetAccessorName(typeFriendlyName, accessorKind, memberName, propertyIndex, needsDisambiguation); + if (declaringTypeParameterNames is null) + { + return accessorName; + } + + int openAngle = declaringTypeFQN.IndexOf('<'); + string typeArgsList = declaringTypeFQN.Substring(openAngle); + return $"__GenericAccessors_{typeFriendlyName}_{declaringTypeIndex}{typeArgsList}.{accessorName}"; + } + + public static string GetReflectionCacheName(string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) + => needsDisambiguation + ? $"s_{accessorKind}_{typeFriendlyName}_{memberName}_{propertyIndex}" + : $"s_{accessorKind}_{typeFriendlyName}_{memberName}"; + + /// + /// Gets the unified constructor accessor name. The wrapper has the same + /// signature for both UnsafeAccessor and reflection fallback: + /// static TypeName __ctor_TypeName(params) + /// + public static string GetConstructorAccessorName(string typeFriendlyName) + => $"__ctor_{typeFriendlyName}"; + + /// + /// For an inaccessible constructor on a generic type using a wrapper-class UnsafeAccessor (.NET 9+), returns the + /// fully qualified accessor reference including the generic wrapper class prefix, e.g. + /// __GenericAccessors_MyType_0<int>.__ctor_MyType. Otherwise returns the plain accessor name. The + /// wrapper is only used for the UnsafeAccessor path; the reflection fallback for a generic type is a flat method. + /// + public static string GetQualifiedConstructorAccessorName( + bool canUseUnsafeAccessor, + ImmutableEquatableArray? declaringTypeParameterNames, + string typeFQN, + string typeFriendlyName) + { + string accessorName = GetConstructorAccessorName(typeFriendlyName); + if (canUseUnsafeAccessor && declaringTypeParameterNames is not null) + { + string typeArgsList = typeFQN.Substring(typeFQN.IndexOf('<')); + return $"__GenericAccessors_{typeFriendlyName}_0{typeArgsList}.{accessorName}"; + } + + return accessorName; + } + + public static string GetConstructorReflectionCacheName(string typeFriendlyName) + => $"s_ctor_{typeFriendlyName}"; + + /// + /// Returns the set of member names that appear more than once in the property list. + /// This occurs when derived types shadow base members via the new keyword. + /// + public static HashSet GetDuplicateMemberNames(IEnumerable memberNames) + { + HashSet seen = new(); + HashSet duplicates = new(); + foreach (string memberName in memberNames) + { + if (!seen.Add(memberName)) + { + duplicates.Add(memberName); + } + } + + return duplicates; + } + + /// + /// Emits the member accessors for the given ordered member list. Members not requiring a getter or setter + /// accessor are skipped. Returns if a value-type setter reflection fallback was emitted, + /// requiring the consuming generator to emit the ValueTypeSetter delegate type. + /// + public static bool EmitMemberAccessors( + SourceWriter writer, + string typeFriendlyName, + bool declaringTypeIsValueType, + bool useUpdatedMemorySafetyRules, + IReadOnlyList members) + { + string safetyModifier = useUpdatedMemorySafetyRules ? "safe " : ""; + HashSet duplicateMemberNames = GetDuplicateMemberNames(members.Select(static m => m.MemberName)); + bool needsAccessors = false; + bool needsValueTypeSetterDelegate = false; + Dictionary>? genericAccessorEntries = null; + + for (int i = 0; i < members.Count; i++) + { + UnsafeAccessorMemberSpec member = members[i]; + bool needsGetterAccessor = member.NeedsGetter; + bool needsSetterAccessor = member.NeedsSetter; + + if (!needsGetterAccessor && !needsSetterAccessor) + { + continue; + } + + if (!needsAccessors) + { + writer.WriteLine(); + needsAccessors = true; + } + + string declaringTypeFQN = member.DeclaringTypeFQN; + string propertyTypeFQN = member.MemberTypeFQN; + bool disambiguate = duplicateMemberNames.Contains(member.MemberName); + + if (member.CanUseUnsafeAccessors) + { + if (member.DeclaringTypeParameterNames is not null) + { + // Generic types need a wrapper class for UnsafeAccessor (.NET 9+). + // Collect the accessor and emit the wrapper class after the loop. + string key = member.DeclaringTypeFQN; + genericAccessorEntries ??= new(); + if (!genericAccessorEntries.TryGetValue(key, out List<(UnsafeAccessorMemberSpec Member, int Index, bool Disambiguate)>? entries)) + { + entries = new(); + genericAccessorEntries[key] = entries; + } + + entries.Add((member, i, disambiguate)); + } + else + { + string refPrefix = declaringTypeIsValueType ? "ref " : ""; + + if (member.IsProperty) + { + if (needsGetterAccessor) + { + string accessorName = GetAccessorName(typeFriendlyName, "get", member.MemberName, i, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{member.MemberName}")]"""); + writer.WriteLine($"private static {safetyModifier}extern {propertyTypeFQN} {accessorName}({refPrefix}{declaringTypeFQN} obj);"); + } + + if (needsSetterAccessor) + { + string accessorName = GetAccessorName(typeFriendlyName, "set", member.MemberName, i, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{member.MemberName}")]"""); + writer.WriteLine($"private static {safetyModifier}extern void {accessorName}({refPrefix}{declaringTypeFQN} obj, {propertyTypeFQN} value);"); + } + } + else + { + // Field: single UnsafeAccessor that returns ref T, used for both get and set. + string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", member.MemberName, i, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{member.MemberName}")]"""); + writer.WriteLine($"private static {safetyModifier}extern ref {propertyTypeFQN} {fieldAccessorName}({refPrefix}{declaringTypeFQN} obj);"); + } + } + } + else if (member.IsProperty) + { + // Reflection fallback for properties: use Delegate.CreateDelegate on the MethodInfo for efficient invocation. + // Wrapper methods are strongly typed to match UnsafeAccessor signatures. + string propertyExpr = $"typeof({declaringTypeFQN}).GetProperty({FormatStringLiteral(member.MemberName)}, InstanceMemberBindingFlags, null, typeof({propertyTypeFQN}), {EmptyTypeArray}, null)!"; + + if (needsGetterAccessor) + { + string cacheName = GetReflectionCacheName(typeFriendlyName, "get", member.MemberName, i, disambiguate); + string wrapperName = GetAccessorName(typeFriendlyName, "get", member.MemberName, i, disambiguate); + + if (declaringTypeIsValueType) + { + // For value types, Delegate.CreateDelegate doesn't work with struct instance getters + // on .NET Framework (the this parameter is passed by-ref internally). + // Cache the MethodInfo and use Invoke instead. + string methodCacheType = "global::System.Reflection.MethodInfo"; + writer.WriteLine($"private static {methodCacheType}? {cacheName};"); + writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}({declaringTypeFQN} obj) => ({propertyTypeFQN})({cacheName} ??= {propertyExpr}.GetGetMethod(true)!).Invoke(obj, null)!;"); + } + else + { + string delegateType = $"global::System.Func<{declaringTypeFQN}, {propertyTypeFQN}>"; + writer.WriteLine($"private static {delegateType}? {cacheName};"); + writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}({declaringTypeFQN} obj) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetGetMethod(true)!))(obj);"); + } + } + + if (needsSetterAccessor) + { + string cacheName = GetReflectionCacheName(typeFriendlyName, "set", member.MemberName, i, disambiguate); + string wrapperName = GetAccessorName(typeFriendlyName, "set", member.MemberName, i, disambiguate); + + if (declaringTypeIsValueType) + { + // For value types, use a ref-parameter delegate to mutate the unboxed value in-place. + needsValueTypeSetterDelegate = true; + string delegateType = $"ValueTypeSetter<{declaringTypeFQN}, {propertyTypeFQN}>"; + writer.WriteLine($"private static {delegateType}? {cacheName};"); + writer.WriteLine($"private static void {wrapperName}(ref {declaringTypeFQN} obj, {propertyTypeFQN} value) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetSetMethod(true)!))(ref obj, value);"); + } + else + { + string delegateType = $"global::System.Action<{declaringTypeFQN}, {propertyTypeFQN}>"; + writer.WriteLine($"private static {delegateType}? {cacheName};"); + writer.WriteLine($"private static void {wrapperName}({declaringTypeFQN} obj, {propertyTypeFQN} value) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetSetMethod(true)!))(obj, value);"); + } + } + } + else + { + // Reflection fallback for fields: cache the FieldInfo and use GetValue/SetValue. + // Fields don't have MethodInfo, so Delegate.CreateDelegate can't be used. + string fieldExpr = $"typeof({declaringTypeFQN}).GetField({FormatStringLiteral(member.MemberName)}, InstanceMemberBindingFlags)!"; + string fieldCacheName = GetReflectionCacheName(typeFriendlyName, "field", member.MemberName, i, disambiguate); + writer.WriteLine($"private static global::System.Reflection.FieldInfo? {fieldCacheName};"); + + if (needsGetterAccessor) + { + string wrapperName = GetAccessorName(typeFriendlyName, "get", member.MemberName, i, disambiguate); + writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}(object obj) => ({propertyTypeFQN})({fieldCacheName} ??= {fieldExpr}).GetValue(obj)!;"); + } + + if (needsSetterAccessor) + { + string wrapperName = GetAccessorName(typeFriendlyName, "set", member.MemberName, i, disambiguate); + writer.WriteLine($"private static void {wrapperName}(object obj, {propertyTypeFQN} value) => ({fieldCacheName} ??= {fieldExpr}).SetValue(obj, value);"); + } + } + } + + // Emit generic wrapper classes for UnsafeAccessors on generic types (.NET 9+). + if (genericAccessorEntries is not null) + { + foreach (KeyValuePair> kvp in genericAccessorEntries) + { + List<(UnsafeAccessorMemberSpec Member, int Index, bool Disambiguate)> entries = kvp.Value; + UnsafeAccessorMemberSpec firstMember = entries[0].Member; + ImmutableEquatableArray typeParams = firstMember.DeclaringTypeParameterNames!; + string openDeclaringTypeFQN = firstMember.OpenDeclaringTypeFQN!; + string refPrefix = declaringTypeIsValueType ? "ref " : ""; + string typeParamList = string.Join(", ", typeParams); + string constraintClauses = firstMember.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; + + writer.WriteLine(); + writer.WriteLine($"private static partial class __GenericAccessors_{typeFriendlyName}_{firstMember.DeclaringTypeIndex}<{typeParamList}>{constraintClauses}"); + writer.WriteLine('{'); + writer.Indentation++; + + foreach ((UnsafeAccessorMemberSpec member, int index, bool disambiguate) in entries) + { + bool needsGetter = member.NeedsGetter; + bool needsSetter = member.NeedsSetter; + string openPropertyTypeFQN = member.OpenMemberTypeFQN ?? member.MemberTypeFQN; + + if (member.IsProperty) + { + if (needsGetter) + { + string accessorName = GetAccessorName(typeFriendlyName, "get", member.MemberName, index, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{member.MemberName}")]"""); + writer.WriteLine($"public static {safetyModifier}extern {openPropertyTypeFQN} {accessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); + } + + if (needsSetter) + { + string accessorName = GetAccessorName(typeFriendlyName, "set", member.MemberName, index, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{member.MemberName}")]"""); + writer.WriteLine($"public static {safetyModifier}extern void {accessorName}({refPrefix}{openDeclaringTypeFQN} obj, {openPropertyTypeFQN} value);"); + } + } + else + { + string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", member.MemberName, index, disambiguate); + writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{member.MemberName}")]"""); + writer.WriteLine($"public static {safetyModifier}extern ref {openPropertyTypeFQN} {fieldAccessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); + } + } + + writer.Indentation--; + writer.WriteLine('}'); + } + } + + return needsValueTypeSetterDelegate; + } + + /// + /// Emits a constructor accessor: a [UnsafeAccessor(Constructor)] extern where supported, otherwise a + /// cached wrapper. On a generic type the UnsafeAccessor extern is + /// emitted inside a partial __GenericAccessors_{TypeFriendlyName}_0 wrapper class (.NET 9+), shared with the + /// member accessors; the reflection fallback for a generic type is a flat method. + /// + public static void EmitConstructorAccessor(SourceWriter writer, bool useUpdatedMemorySafetyRules, UnsafeAccessorConstructorSpec spec) + { + bool useGenericWrapper = spec.CanUseUnsafeAccessor && spec.OpenDeclaringTypeFQN is not null; + string typeFQN = useGenericWrapper ? spec.OpenDeclaringTypeFQN! : spec.TypeFQN; + string wrapperName = GetConstructorAccessorName(spec.TypeFriendlyName); + ImmutableEquatableArray parameters = spec.Parameters; + + if (useGenericWrapper) + { + // The constructor and member accessors share the declaring type's helper. + string typeParamList = string.Join(", ", spec.DeclaringTypeParameterNames!); + string constraintClauses = spec.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; + writer.WriteLine($"private static partial class __GenericAccessors_{spec.TypeFriendlyName}_0<{typeParamList}>{constraintClauses}"); + writer.WriteLine('{'); + writer.Indentation++; + } + + // Build the parameter list for the wrapper method. + var wrapperParams = new StringBuilder(); + var callArgs = new StringBuilder(); + + foreach (UnsafeAccessorParameterSpec param in parameters) + { + if (wrapperParams.Length > 0) + { + wrapperParams.Append(", "); + callArgs.Append(", "); + } + + string parameterTypeFQN = (useGenericWrapper ? param.OpenTypeFQN : null) ?? param.TypeFQN; + string refModifier = param.RefKind switch + { + RefKind.Ref => "ref ", + RefKind.Out => "out ", + // 'in' preserves the readonly by-ref signature without requiring C# 12. + RefKind.In or RefKindRefReadOnlyParameter => "in ", + _ => "", + }; + wrapperParams.Append($"{refModifier}{parameterTypeFQN} p{param.Index}"); + callArgs.Append(param.RefKind is RefKind.Out ? "null" : $"p{param.Index}"); + } + + if (spec.CanUseUnsafeAccessor) + { + string safetyModifier = useUpdatedMemorySafetyRules ? "safe " : ""; + writer.WriteLine($"[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)]"); + writer.WriteLine($"{(useGenericWrapper ? "public" : "private")} static {safetyModifier}extern {typeFQN} {wrapperName}({wrapperParams});"); + } + else + { + // Reflection fallback: cached ConstructorInfo + Invoke. + // Note: ConstructorInfo cannot be wrapped in a delegate, so we cache the ConstructorInfo directly. + string cacheName = GetConstructorReflectionCacheName(spec.TypeFriendlyName); + + string argTypes = parameters.Count == 0 + ? EmptyTypeArray + : $"new global::System.Type[] {{{string.Join(", ", parameters.Select(p => $"typeof({p.TypeFQN}){(p.RefKind is RefKind.None ? "" : ".MakeByRefType()")}"))}}}"; + + writer.WriteLine($"private static global::System.Reflection.ConstructorInfo? {cacheName};"); + + string invokeArgs = parameters.Count == 0 + ? "null" + : $"new object?[] {{{callArgs}}}"; + string constructorInfo = $"{cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!"; + + if (parameters.Any(p => p.RefKind is RefKind.Ref or RefKind.Out)) + { + writer.WriteLine($$""" + private static {{typeFQN}} {{wrapperName}}({{wrapperParams}}) + { + object?[] args = {{invokeArgs}}; + {{typeFQN}} result = ({{typeFQN}})({{constructorInfo}}).Invoke(args); + """); + writer.Indentation++; + + foreach (UnsafeAccessorParameterSpec param in parameters) + { + if (param.RefKind is RefKind.Ref or RefKind.Out) + { + writer.WriteLine($"p{param.Index} = ({param.TypeFQN})args[{param.Index}]!;"); + } + } + + writer.WriteLine("return result;"); + writer.Indentation--; + writer.WriteLine('}'); + } + else + { + writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({constructorInfo}).Invoke({invokeArgs});"); + } + } + + if (useGenericWrapper) + { + writer.Indentation--; + writer.WriteLine('}'); + } + } + + private static string FormatStringLiteral(string? value) => value is null ? "null" : SymbolDisplay.FormatLiteral(value, quote: true); + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 8ec68abadc4318..a508b9c76f5ba4 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -11,7 +11,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using SourceGenerators; -using GenericAccessorEntry = (System.Text.Json.SourceGeneration.PropertyGenerationSpec Property, int Index, bool Disambiguate, bool NeedsGetter, bool NeedsSetter); namespace System.Text.Json.SourceGeneration { @@ -28,8 +27,6 @@ private sealed partial class Emitter private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; - private const string UnsafeAccessorAttributeTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorAttribute"; - private const string UnsafeAccessorKindTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorKind"; private const string JsonEncodedTextTypeRef = "global::System.Text.Json.JsonEncodedText"; private const string JsonNamingPolicyTypeRef = "global::System.Text.Json.JsonNamingPolicy"; private const string JsonSerializerTypeRef = "global::System.Text.Json.JsonSerializer"; @@ -659,7 +656,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene } // Generate UnsafeAccessor methods or reflection cache fields for property accessors. - _emitValueTypeSetterDelegate |= GeneratePropertyAccessors(writer, contextSpec, typeMetadata); + _emitValueTypeSetterDelegate |= GenerateMemberAccessors(writer, contextSpec, typeMetadata); // Generate constructor accessor for inaccessible [JsonConstructor] constructors. GenerateConstructorAccessor(writer, contextSpec, typeMetadata); @@ -829,7 +826,7 @@ private static string FormatNullCast(UnionCaseSpec caseSpec) private void GeneratePropMetadataInitFunc(SourceWriter writer, ContextGenerationSpec contextSpec, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; - HashSet duplicateMemberNames = GetDuplicateMemberNames(properties); + HashSet duplicateMemberNames = UnsafeAccessorEmitter.GetDuplicateMemberNames(properties.Select(static p => p.MemberName)); writer.WriteLine($"private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerOptionsTypeRef} options)"); writer.WriteLine('{'); @@ -1006,13 +1003,13 @@ private static string GetPropertyGetterValue( : $"({declaringTypeFQN})obj"; string accessorName = property.IsProperty - ? GetQualifiedAccessorName(property, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) - : GetQualifiedAccessorName(property, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); + ? UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, declaringTypeFQN, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) + : UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, declaringTypeFQN, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); return $"static obj => {accessorName}({castExpr})"; } - string getterName = GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); + string getterName = UnsafeAccessorEmitter.GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); if (!property.IsProperty) { return $"static obj => {getterName}(obj)"; @@ -1081,15 +1078,15 @@ private static string GetAccessorBasedSetterDelegate( if (property.IsProperty) { - string accessorName = GetQualifiedAccessorName(property, typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); + string accessorName = UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, declaringTypeFQN, typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); return $"static (obj, value) => {accessorName}({castExpr}, value!)"; } - string fieldName = GetQualifiedAccessorName(property, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); + string fieldName = UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, declaringTypeFQN, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); return $"static (obj, value) => {fieldName}({castExpr}) = value!"; } - string setterName = GetAccessorName(typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); + string setterName = UnsafeAccessorEmitter.GetAccessorName(typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); if (!property.IsProperty) { return $"static (obj, value) => {setterName}(obj, value!)"; @@ -1103,290 +1100,33 @@ private static string GetAccessorBasedSetterDelegate( return $"static (obj, value) => {setterName}({setterCastExpr}, value!)"; } - private static bool GeneratePropertyAccessors(SourceWriter writer, ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) + private static bool GenerateMemberAccessors(SourceWriter writer, ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { - string safetyModifier = contextSpec.UseUpdatedMemorySafetyRules ? "safe " : ""; ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; - HashSet duplicateMemberNames = GetDuplicateMemberNames(properties); - bool needsAccessors = false; - bool needsValueTypeSetterDelegate = false; - Dictionary>? genericAccessorEntries = null; + var members = new List(properties.Count); - for (int i = 0; i < properties.Count; i++) - { - PropertyGenerationSpec property = properties[i]; - bool needsGetterAccessor = NeedsAccessorForGetter(property); - bool needsSetterAccessor = NeedsAccessorForSetter(property); - - if (!needsGetterAccessor && !needsSetterAccessor) - { - continue; - } - - if (!needsAccessors) - { - writer.WriteLine(); - needsAccessors = true; - } - - string typeFriendlyName = typeGenerationSpec.TypeInfoPropertyName; - string declaringTypeFQN = property.DeclaringType.FullyQualifiedName; - string propertyTypeFQN = property.PropertyType.FullyQualifiedName; - bool disambiguate = duplicateMemberNames.Contains(property.MemberName); - - if (property.CanUseUnsafeAccessors) - { - if (property.DeclaringTypeParameterNames is not null) - { - // Generic types need a wrapper class for UnsafeAccessor (.NET 9+). - // Collect the accessor and emit the wrapper class after the loop. - string key = property.DeclaringType.FullyQualifiedName; - genericAccessorEntries ??= new(); - if (!genericAccessorEntries.TryGetValue(key, out List? entries)) - { - entries = new(); - genericAccessorEntries[key] = entries; - } - - entries.Add((property, i, disambiguate, needsGetterAccessor, needsSetterAccessor)); - } - else - { - string refPrefix = typeGenerationSpec.TypeRef.IsValueType ? "ref " : ""; - - if (property.IsProperty) - { - if (needsGetterAccessor) - { - string accessorName = GetAccessorName(typeFriendlyName, "get", property.MemberName, i, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{property.MemberName}")]"""); - writer.WriteLine($"private static {safetyModifier}extern {propertyTypeFQN} {accessorName}({refPrefix}{declaringTypeFQN} obj);"); - } - - if (needsSetterAccessor) - { - string accessorName = GetAccessorName(typeFriendlyName, "set", property.MemberName, i, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{property.MemberName}")]"""); - writer.WriteLine($"private static {safetyModifier}extern void {accessorName}({refPrefix}{declaringTypeFQN} obj, {propertyTypeFQN} value);"); - } - } - else - { - // Field: single UnsafeAccessor that returns ref T, used for both get and set. - string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", property.MemberName, i, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{property.MemberName}")]"""); - writer.WriteLine($"private static {safetyModifier}extern ref {propertyTypeFQN} {fieldAccessorName}({refPrefix}{declaringTypeFQN} obj);"); - } - } - } - else if (property.IsProperty) - { - // Reflection fallback for properties: use Delegate.CreateDelegate on the MethodInfo for efficient invocation. - // Wrapper methods are strongly typed to match UnsafeAccessor signatures. - string propertyExpr = $"typeof({declaringTypeFQN}).GetProperty({FormatStringLiteral(property.MemberName)}, InstanceMemberBindingFlags, null, typeof({propertyTypeFQN}), {EmptyTypeArray}, null)!"; - - if (needsGetterAccessor) - { - string cacheName = GetReflectionCacheName(typeFriendlyName, "get", property.MemberName, i, disambiguate); - string wrapperName = GetAccessorName(typeFriendlyName, "get", property.MemberName, i, disambiguate); - - if (typeGenerationSpec.TypeRef.IsValueType) - { - // For value types, Delegate.CreateDelegate doesn't work with struct instance getters - // on .NET Framework (the this parameter is passed by-ref internally). - // Cache the MethodInfo and use Invoke instead. - string methodCacheType = "global::System.Reflection.MethodInfo"; - writer.WriteLine($"private static {methodCacheType}? {cacheName};"); - writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}({declaringTypeFQN} obj) => ({propertyTypeFQN})({cacheName} ??= {propertyExpr}.GetGetMethod(true)!).Invoke(obj, null)!;"); - } - else - { - string delegateType = $"global::System.Func<{declaringTypeFQN}, {propertyTypeFQN}>"; - writer.WriteLine($"private static {delegateType}? {cacheName};"); - writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}({declaringTypeFQN} obj) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetGetMethod(true)!))(obj);"); - } - } - - if (needsSetterAccessor) - { - string cacheName = GetReflectionCacheName(typeFriendlyName, "set", property.MemberName, i, disambiguate); - string wrapperName = GetAccessorName(typeFriendlyName, "set", property.MemberName, i, disambiguate); - - if (typeGenerationSpec.TypeRef.IsValueType) - { - // For value types, use a ref-parameter delegate to mutate the unboxed value in-place. - needsValueTypeSetterDelegate = true; - string delegateType = $"ValueTypeSetter<{declaringTypeFQN}, {propertyTypeFQN}>"; - writer.WriteLine($"private static {delegateType}? {cacheName};"); - writer.WriteLine($"private static void {wrapperName}(ref {declaringTypeFQN} obj, {propertyTypeFQN} value) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetSetMethod(true)!))(ref obj, value);"); - } - else - { - string delegateType = $"global::System.Action<{declaringTypeFQN}, {propertyTypeFQN}>"; - writer.WriteLine($"private static {delegateType}? {cacheName};"); - writer.WriteLine($"private static void {wrapperName}({declaringTypeFQN} obj, {propertyTypeFQN} value) => ({cacheName} ??= ({delegateType})global::System.Delegate.CreateDelegate(typeof({delegateType}), {propertyExpr}.GetSetMethod(true)!))(obj, value);"); - } - } - } - else - { - // Reflection fallback for fields: cache the FieldInfo and use GetValue/SetValue. - // Fields don't have MethodInfo, so Delegate.CreateDelegate can't be used. - string fieldExpr = $"typeof({declaringTypeFQN}).GetField({FormatStringLiteral(property.MemberName)}, InstanceMemberBindingFlags)!"; - string fieldCacheName = GetReflectionCacheName(typeFriendlyName, "field", property.MemberName, i, disambiguate); - writer.WriteLine($"private static global::System.Reflection.FieldInfo? {fieldCacheName};"); - - if (needsGetterAccessor) - { - string wrapperName = GetAccessorName(typeFriendlyName, "get", property.MemberName, i, disambiguate); - writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}(object obj) => ({propertyTypeFQN})({fieldCacheName} ??= {fieldExpr}).GetValue(obj)!;"); - } - - if (needsSetterAccessor) - { - string wrapperName = GetAccessorName(typeFriendlyName, "set", property.MemberName, i, disambiguate); - writer.WriteLine($"private static void {wrapperName}(object obj, {propertyTypeFQN} value) => ({fieldCacheName} ??= {fieldExpr}).SetValue(obj, value);"); - } - } - } - - // Emit generic wrapper classes for UnsafeAccessors on generic types (.NET 9+). - if (genericAccessorEntries is not null) - { - string typeFriendlyName = typeGenerationSpec.TypeInfoPropertyName; - - foreach (KeyValuePair> kvp in genericAccessorEntries) - { - List entries = kvp.Value; - PropertyGenerationSpec firstProperty = entries[0].Property; - ImmutableEquatableArray typeParams = firstProperty.DeclaringTypeParameterNames!; - string openDeclaringTypeFQN = firstProperty.OpenDeclaringTypeFQN!; - string refPrefix = typeGenerationSpec.TypeRef.IsValueType ? "ref " : ""; - string typeParamList = string.Join(", ", typeParams); - string constraintClauses = firstProperty.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; - - writer.WriteLine(); - writer.WriteLine($"private static partial class __GenericAccessors_{typeFriendlyName}_{firstProperty.DeclaringTypeIndex}<{typeParamList}>{constraintClauses}"); - writer.WriteLine('{'); - writer.Indentation++; - - foreach (GenericAccessorEntry entry in entries) - { - PropertyGenerationSpec property = entry.Property; - int index = entry.Index; - bool disambiguate = entry.Disambiguate; - bool needsGetter = entry.NeedsGetter; - bool needsSetter = entry.NeedsSetter; - string openPropertyTypeFQN = property.OpenPropertyTypeFQN ?? property.PropertyType.FullyQualifiedName; - - if (property.IsProperty) - { - if (needsGetter) - { - string accessorName = GetAccessorName(typeFriendlyName, "get", property.MemberName, index, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{property.MemberName}")]"""); - writer.WriteLine($"public static {safetyModifier}extern {openPropertyTypeFQN} {accessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); - } - - if (needsSetter) - { - string accessorName = GetAccessorName(typeFriendlyName, "set", property.MemberName, index, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{property.MemberName}")]"""); - writer.WriteLine($"public static {safetyModifier}extern void {accessorName}({refPrefix}{openDeclaringTypeFQN} obj, {openPropertyTypeFQN} value);"); - } - } - else - { - string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", property.MemberName, index, disambiguate); - writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{property.MemberName}")]"""); - writer.WriteLine($"public static {safetyModifier}extern ref {openPropertyTypeFQN} {fieldAccessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); - } - } - - writer.Indentation--; - writer.WriteLine('}'); - } - } - - return needsValueTypeSetterDelegate; - } - - /// - /// Gets the accessor name for a property or field. For UnsafeAccessor this is the extern method name; - /// for reflection fallback this is the strongly typed wrapper method name. - /// Use kind "get"/"set" for property getters/setters, or "field" for field UnsafeAccessor externs. - /// The property index suffix is only appended when needed to disambiguate shadowed members. - /// - private static string GetAccessorName(string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) - => needsDisambiguation - ? $"__{accessorKind}_{typeFriendlyName}_{memberName}_{propertyIndex}" - : $"__{accessorKind}_{typeFriendlyName}_{memberName}"; - - /// - /// For properties on generic types using wrapper-class UnsafeAccessors (.NET 9+), returns the - /// fully qualified accessor reference including the generic wrapper class prefix, e.g. - /// __GenericAccessors_MyType_0<int>.__get_MyType_Name. - /// For non-generic types, returns the plain accessor name. - /// - private static string GetQualifiedAccessorName(PropertyGenerationSpec property, string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) - { - string accessorName = GetAccessorName(typeFriendlyName, accessorKind, memberName, propertyIndex, needsDisambiguation); - if (property.DeclaringTypeParameterNames is null) - { - return accessorName; - } - - string closedTypeArgs = property.DeclaringType.FullyQualifiedName; - int openAngle = closedTypeArgs.IndexOf('<'); - string typeArgsList = closedTypeArgs.Substring(openAngle); - return $"__GenericAccessors_{typeFriendlyName}_{property.DeclaringTypeIndex}{typeArgsList}.{accessorName}"; - } - - private static string GetReflectionCacheName(string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) - => needsDisambiguation - ? $"s_{accessorKind}_{typeFriendlyName}_{memberName}_{propertyIndex}" - : $"s_{accessorKind}_{typeFriendlyName}_{memberName}"; - - /// - /// Returns the set of member names that appear more than once in the property list. - /// This occurs when derived types shadow base members via the new keyword. - /// - private static HashSet GetDuplicateMemberNames(ImmutableEquatableArray properties) - { - HashSet seen = new(); - HashSet duplicates = new(); foreach (PropertyGenerationSpec property in properties) { - if (!seen.Add(property.MemberName)) + members.Add(new UnsafeAccessorEmitter.UnsafeAccessorMemberSpec { - duplicates.Add(property.MemberName); - } + Kind = property.IsProperty ? UnsafeAccessorEmitter.AccessorMemberKind.Property : UnsafeAccessorEmitter.AccessorMemberKind.Field, + MemberName = property.MemberName, + NeedsGetter = NeedsAccessorForGetter(property), + NeedsSetter = NeedsAccessorForSetter(property), + CanUseUnsafeAccessors = property.CanUseUnsafeAccessors, + DeclaringTypeFQN = property.DeclaringType.FullyQualifiedName, + MemberTypeFQN = property.PropertyType.FullyQualifiedName, + DeclaringTypeIndex = property.DeclaringTypeIndex, + DeclaringTypeParameterNames = property.DeclaringTypeParameterNames, + OpenDeclaringTypeFQN = property.OpenDeclaringTypeFQN, + OpenMemberTypeFQN = property.OpenPropertyTypeFQN, + DeclaringTypeParameterConstraintClauses = property.DeclaringTypeParameterConstraintClauses, + }); } - return duplicates; + return UnsafeAccessorEmitter.EmitMemberAccessors(writer, typeGenerationSpec.TypeInfoPropertyName, typeGenerationSpec.TypeRef.IsValueType, contextSpec.UseUpdatedMemorySafetyRules, members); } - /// - /// Gets the unified constructor accessor name. The wrapper has the same - /// signature for both UnsafeAccessor and reflection fallback: - /// static TypeName __ctor_TypeName(params) - /// - private static string GetConstructorAccessorName(TypeGenerationSpec typeSpec, bool qualified = true) - { - string accessorName = $"__ctor_{typeSpec.TypeInfoPropertyName}"; - if (qualified && typeSpec.CanUseUnsafeAccessorForConstructor && typeSpec.OpenDeclaringTypeFQN is not null) - { - string typeFQN = typeSpec.TypeRef.FullyQualifiedName; - string typeArgsList = typeFQN.Substring(typeFQN.IndexOf('<')); - return $"__GenericAccessors_{typeSpec.TypeInfoPropertyName}_0{typeArgsList}.{accessorName}"; - } - - return accessorName; - } - - private static string GetConstructorReflectionCacheName(TypeGenerationSpec typeSpec) - => $"s_ctor_{typeSpec.TypeInfoPropertyName}"; - /// /// Generates the constructor accessor for inaccessible constructors. /// For UnsafeAccessor: emits a [UnsafeAccessor(Constructor)] extern method. @@ -1401,102 +1141,28 @@ private static void GenerateConstructorAccessor(SourceWriter writer, ContextGene writer.WriteLine(); - bool useGenericWrapper = typeSpec.CanUseUnsafeAccessorForConstructor && typeSpec.OpenDeclaringTypeFQN is not null; - string typeFQN = useGenericWrapper ? typeSpec.OpenDeclaringTypeFQN! : typeSpec.TypeRef.FullyQualifiedName; - string wrapperName = GetConstructorAccessorName(typeSpec, qualified: false); - ImmutableEquatableArray parameters = typeSpec.CtorParamGenSpecs; - - if (useGenericWrapper) - { - // The constructor and member accessors share the declaring type's helper. - string typeParamList = string.Join(", ", typeSpec.DeclaringTypeParameterNames!); - string constraintClauses = typeSpec.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; - writer.WriteLine($"private static partial class __GenericAccessors_{typeSpec.TypeInfoPropertyName}_0<{typeParamList}>{constraintClauses}"); - writer.WriteLine('{'); - writer.Indentation++; - } - - // Build the parameter list for the wrapper method. - var wrapperParams = new StringBuilder(); - var callArgs = new StringBuilder(); - - foreach (ParameterGenerationSpec param in parameters) + var parameters = new List(typeSpec.CtorParamGenSpecs.Count); + foreach (ParameterGenerationSpec param in typeSpec.CtorParamGenSpecs) { - if (wrapperParams.Length > 0) - { - wrapperParams.Append(", "); - callArgs.Append(", "); - } - - string parameterTypeFQN = (useGenericWrapper ? param.OpenParameterTypeFQN : null) ?? param.ParameterType.FullyQualifiedName; - string refModifier = param.RefKind switch - { - RefKind.Ref => "ref ", - RefKind.Out => "out ", - // 'in' preserves the readonly by-ref signature without requiring C# 12. - RefKind.In or RefKindRefReadOnlyParameter => "in ", - _ => "", - }; - wrapperParams.Append($"{refModifier}{parameterTypeFQN} p{param.ParameterIndex}"); - callArgs.Append(param.RefKind is RefKind.Out ? "null" : $"p{param.ParameterIndex}"); - } - - if (typeSpec.CanUseUnsafeAccessorForConstructor) - { - string safetyModifier = contextSpec.UseUpdatedMemorySafetyRules ? "safe " : ""; - writer.WriteLine($"[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)]"); - writer.WriteLine($"{(useGenericWrapper ? "public" : "private")} static {safetyModifier}extern {typeFQN} {wrapperName}({wrapperParams});"); - } - else - { - // Reflection fallback: cached ConstructorInfo + Invoke. - // Note: ConstructorInfo cannot be wrapped in a delegate, so we cache the ConstructorInfo directly. - string cacheName = GetConstructorReflectionCacheName(typeSpec); - - string argTypes = parameters.Count == 0 - ? EmptyTypeArray - : $"new global::System.Type[] {{{string.Join(", ", parameters.Select(p => $"typeof({p.ParameterType.FullyQualifiedName}){(p.RefKind is RefKind.None ? "" : ".MakeByRefType()")}"))}}}"; - - writer.WriteLine($"private static global::System.Reflection.ConstructorInfo? {cacheName};"); - - string invokeArgs = parameters.Count == 0 - ? "null" - : $"new object?[] {{{callArgs}}}"; - string constructorInfo = $"{cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!"; - - if (parameters.Any(p => p.RefKind is RefKind.Ref or RefKind.Out)) + parameters.Add(new UnsafeAccessorEmitter.UnsafeAccessorParameterSpec { - writer.WriteLine($$""" - private static {{typeFQN}} {{wrapperName}}({{wrapperParams}}) - { - object?[] args = {{invokeArgs}}; - {{typeFQN}} result = ({{typeFQN}})({{constructorInfo}}).Invoke(args); - """); - writer.Indentation++; - - foreach (ParameterGenerationSpec param in parameters) - { - if (param.RefKind is RefKind.Ref or RefKind.Out) - { - writer.WriteLine($"p{param.ParameterIndex} = ({param.ParameterType.FullyQualifiedName})args[{param.ParameterIndex}]!;"); - } - } - - writer.WriteLine("return result;"); - writer.Indentation--; - writer.WriteLine('}'); - } - else - { - writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({constructorInfo}).Invoke({invokeArgs});"); - } + TypeFQN = param.ParameterType.FullyQualifiedName, + Index = param.ParameterIndex, + RefKind = param.RefKind, + OpenTypeFQN = param.OpenParameterTypeFQN, + }); } - if (useGenericWrapper) + UnsafeAccessorEmitter.EmitConstructorAccessor(writer, contextSpec.UseUpdatedMemorySafetyRules, new UnsafeAccessorEmitter.UnsafeAccessorConstructorSpec { - writer.Indentation--; - writer.WriteLine('}'); - } + TypeFriendlyName = typeSpec.TypeInfoPropertyName, + TypeFQN = typeSpec.TypeRef.FullyQualifiedName, + CanUseUnsafeAccessor = typeSpec.CanUseUnsafeAccessorForConstructor, + Parameters = parameters.ToImmutableEquatableArray(), + DeclaringTypeParameterNames = typeSpec.DeclaringTypeParameterNames, + OpenDeclaringTypeFQN = typeSpec.OpenDeclaringTypeFQN, + DeclaringTypeParameterConstraintClauses = typeSpec.DeclaringTypeParameterConstraintClauses, + }); } /// @@ -1522,15 +1188,15 @@ private static string GetFastPathPropertyValueExpr( if (property.CanUseUnsafeAccessors) { string accessorName = property.IsProperty - ? GetQualifiedAccessorName(property, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) - : GetQualifiedAccessorName(property, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); + ? UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, property.DeclaringType.FullyQualifiedName, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) + : UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, property.DeclaringTypeIndex, property.DeclaringType.FullyQualifiedName, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); return typeGenSpec.TypeRef.IsValueType ? $"{accessorName}(ref value)" : $"{accessorName}({objectExpr})"; } - string getterName = GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); + string getterName = UnsafeAccessorEmitter.GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); return $"{getterName}({objectExpr})"; } @@ -1621,7 +1287,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio GenerateFastPathFuncHeader(writer, typeGenSpec, serializeMethodName); - HashSet duplicateMemberNames = GetDuplicateMemberNames(typeGenSpec.PropertyGenSpecs); + HashSet duplicateMemberNames = UnsafeAccessorEmitter.GetDuplicateMemberNames(typeGenSpec.PropertyGenSpecs.Select(static p => p.MemberName)); if (typeGenSpec.ImplementsIJsonOnSerializing) { @@ -1782,7 +1448,7 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type if (typeGenerationSpec.ConstructorIsInaccessible) { - string accessorName = GetConstructorAccessorName(typeGenerationSpec); + string accessorName = UnsafeAccessorEmitter.GetQualifiedConstructorAccessorName(typeGenerationSpec.CanUseUnsafeAccessorForConstructor, typeGenerationSpec.DeclaringTypeParameterNames, typeGenerationSpec.TypeRef.FullyQualifiedName, typeGenerationSpec.TypeInfoPropertyName); sb.Append($"return {accessorName}("); } else @@ -1793,7 +1459,7 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type else if (typeGenerationSpec.ConstructorIsInaccessible) { // Inaccessible constructor: use the unified constructor accessor wrapper. - string accessorName = GetConstructorAccessorName(typeGenerationSpec); + string accessorName = UnsafeAccessorEmitter.GetQualifiedConstructorAccessorName(typeGenerationSpec.CanUseUnsafeAccessorForConstructor, typeGenerationSpec.DeclaringTypeParameterNames, typeGenerationSpec.TypeRef.FullyQualifiedName, typeGenerationSpec.TypeInfoPropertyName); sb = new($"static args => {accessorName}("); } else @@ -2496,7 +2162,7 @@ private static string FormatDefaultConstructorExpr(TypeGenerationSpec typeSpec) { IsValueTuple: true } => $"() => default({typeSpec.TypeRef.FullyQualifiedName})", { ConstructionStrategy: ObjectConstructionStrategy.ParameterlessConstructor, ConstructorIsInaccessible: false } => $"() => new {typeSpec.TypeRef.FullyQualifiedName}()", { ConstructionStrategy: ObjectConstructionStrategy.ParameterlessConstructor, ConstructorIsInaccessible: true } => - $"static () => {GetConstructorAccessorName(typeSpec)}()", + $"static () => {UnsafeAccessorEmitter.GetQualifiedConstructorAccessorName(typeSpec.CanUseUnsafeAccessorForConstructor, typeSpec.DeclaringTypeParameterNames, typeSpec.TypeRef.FullyQualifiedName, typeSpec.TypeInfoPropertyName)}()", _ => "null", }; } diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 830c1c88c2a2b6..ac8db9f9cc6282 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -35,6 +35,7 @@ +