From f4dea8bd4c474e99ff63e247660901c9bf0b8f32 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Mon, 7 Sep 2026 16:24:02 +0200 Subject: [PATCH 1/3] Extract STJ UnsafeAccessor emitter into a shared SourceGenerators helper Move the System.Text.Json source generator's accessor-emission machinery (UnsafeAccessor-based get/set/field accessors, constructor accessors, generic wrappers, and the reflection fallback) into a new shared helper, Common/src/SourceGenerators/UnsafeAccessorEmitter.cs, so other source generators (e.g. the Microsoft.Extensions.Configuration.Binder generator) can reuse it. The helper works over neutral, primitive-only spec types (UnsafeAccessorMemberSpec, UnsafeAccessorConstructorSpec, UnsafeAccessorParameterSpec) and owns accessor naming using STJ's existing scheme, parameterized by a type-friendly name. STJ's GenerateMemberAccessors and GenerateConstructorAccessor become thin adapters that build the neutral specs and delegate to the helper. This is a pure refactoring with no behavior change: the generated output is byte-identical, as verified by the SourceGeneratedOutputTests baselines for both the netcoreapp (UnsafeAccessor) and net462 (reflection) code paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a0d7ac2-f2a3-4526-93ab-36bf1a23933f --- .../SourceGenerators/UnsafeAccessorEmitter.cs | 406 ++++++++++++++++++ .../gen/JsonSourceGenerator.Emitter.cs | 362 ++-------------- .../System.Text.Json.SourceGeneration.targets | 1 + 3 files changed, 450 insertions(+), 319 deletions(-) create mode 100644 src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs diff --git a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs new file mode 100644 index 00000000000000..555690e9ac38e2 --- /dev/null +++ b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs @@ -0,0 +1,406 @@ +// 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.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, and a + /// ValueTypeSetter<TDeclaringType, TValue> delegate 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()"; + + 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; } + + /// 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; } + } + + /// A single constructor parameter of a . + internal sealed record UnsafeAccessorParameterSpec + { + public required string TypeFQN { get; init; } + public required int Index { 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<int>.__get_MyType_Name. + /// For non-generic types, returns the plain accessor name. + /// + public static string GetQualifiedAccessorName( + ImmutableEquatableArray? declaringTypeParameterNames, + 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}{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}"; + + 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, + IReadOnlyList members) + { + 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 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 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 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 class __GenericAccessors_{typeFriendlyName}<{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!; + + 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 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 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 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. The wrapper has the same signature in both + /// cases: static TypeName __ctor_TypeName(params). + /// + public static void EmitConstructorAccessor(SourceWriter writer, UnsafeAccessorConstructorSpec spec) + { + string typeFQN = spec.TypeFQN; + string wrapperName = GetConstructorAccessorName(spec.TypeFriendlyName); + ImmutableEquatableArray parameters = spec.Parameters; + + // Build the parameter list for the wrapper method. + var wrapperParams = new StringBuilder(); + + foreach (UnsafeAccessorParameterSpec param in parameters) + { + if (wrapperParams.Length > 0) + { + wrapperParams.Append(", "); + } + + wrapperParams.Append($"{param.TypeFQN} p{param.Index}"); + } + + if (spec.CanUseUnsafeAccessor) + { + writer.WriteLine($"[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)]"); + writer.WriteLine($"private static 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})"))}}}"; + + writer.WriteLine($"private static global::System.Reflection.ConstructorInfo? {cacheName};"); + + string invokeArgs = parameters.Count == 0 + ? "null" + : $"new object?[] {{{string.Join(", ", parameters.Select(p => $"p{p.Index}"))}}}"; + + writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!).Invoke({invokeArgs});"); + } + } + + 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 bb0283cc71456a..7582e5699f35bd 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, typeMetadata); + _emitValueTypeSetterDelegate |= GenerateMemberAccessors(writer, typeMetadata); // Generate constructor accessor for inaccessible [JsonConstructor] constructors. GenerateConstructorAccessor(writer, typeMetadata); @@ -829,7 +826,7 @@ private static string FormatNullCast(UnionCaseSpec caseSpec) private void GeneratePropMetadataInitFunc(SourceWriter writer, 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('{'); @@ -999,14 +996,14 @@ 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, declaringTypeFQN, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) + : UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, declaringTypeFQN, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); return $"static obj => {accessorName}({castExpr})"; } // Reflection fallback wrappers are strongly typed; cast in the delegate. - string getterName = GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); + string getterName = UnsafeAccessorEmitter.GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); return $"static obj => {getterName}(({declaringTypeFQN})obj)"; } @@ -1068,16 +1065,16 @@ private static string GetAccessorBasedSetterDelegate( if (property.IsProperty) { - string accessorName = GetQualifiedAccessorName(property, typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); + string accessorName = UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, 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, declaringTypeFQN, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); return $"static (obj, value) => {fieldName}({castExpr}) = value!"; } // Reflection fallback wrapper is strongly typed; cast in the delegate like UnsafeAccessor. - string setterName = GetAccessorName(typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); + string setterName = UnsafeAccessorEmitter.GetAccessorName(typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); string setterCastExpr = typeGenerationSpec.TypeRef.IsValueType ? $"ref {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)" : $"({declaringTypeFQN})obj"; @@ -1085,279 +1082,32 @@ private static string GetAccessorBasedSetterDelegate( return $"static (obj, value) => {setterName}({setterCastExpr}, value!)"; } - private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGenerationSpec typeGenerationSpec) + private static bool GenerateMemberAccessors(SourceWriter writer, TypeGenerationSpec typeGenerationSpec) { 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 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 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 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 class __GenericAccessors_{typeFriendlyName}<{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!; - - 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 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 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 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<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}{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, + DeclaringTypeParameterNames = property.DeclaringTypeParameterNames, + OpenDeclaringTypeFQN = property.OpenDeclaringTypeFQN, + OpenMemberTypeFQN = property.OpenPropertyTypeFQN, + DeclaringTypeParameterConstraintClauses = property.DeclaringTypeParameterConstraintClauses, + }); } - return duplicates; + return UnsafeAccessorEmitter.EmitMemberAccessors(writer, typeGenerationSpec.TypeInfoPropertyName, typeGenerationSpec.TypeRef.IsValueType, 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) - => $"__ctor_{typeSpec.TypeInfoPropertyName}"; - - 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. @@ -1372,49 +1122,23 @@ private static void GenerateConstructorAccessor(SourceWriter writer, TypeGenerat writer.WriteLine(); - string typeFQN = typeSpec.TypeRef.FullyQualifiedName; - string wrapperName = GetConstructorAccessorName(typeSpec); - ImmutableEquatableArray parameters = typeSpec.CtorParamGenSpecs; - - // 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) + parameters.Add(new UnsafeAccessorEmitter.UnsafeAccessorParameterSpec { - wrapperParams.Append(", "); - callArgs.Append(", "); - } - - wrapperParams.Append($"{param.ParameterType.FullyQualifiedName} p{param.ParameterIndex}"); - callArgs.Append($"p{param.ParameterIndex}"); + TypeFQN = param.ParameterType.FullyQualifiedName, + Index = param.ParameterIndex, + }); } - if (typeSpec.CanUseUnsafeAccessorForConstructor) + UnsafeAccessorEmitter.EmitConstructorAccessor(writer, new UnsafeAccessorEmitter.UnsafeAccessorConstructorSpec { - writer.WriteLine($"[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)]"); - writer.WriteLine($"private static 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})"))}}}"; - - writer.WriteLine($"private static global::System.Reflection.ConstructorInfo? {cacheName};"); - - string invokeArgs = parameters.Count == 0 - ? "null" - : $"new object?[] {{{string.Join(", ", parameters.Select(p => $"p{p.ParameterIndex}"))}}}"; - - writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!).Invoke({invokeArgs});"); - } + TypeFriendlyName = typeSpec.TypeInfoPropertyName, + TypeFQN = typeSpec.TypeRef.FullyQualifiedName, + CanUseUnsafeAccessor = typeSpec.CanUseUnsafeAccessorForConstructor, + Parameters = parameters.ToImmutableEquatableArray(), + }); } /// @@ -1440,15 +1164,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.DeclaringType.FullyQualifiedName, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) + : UnsafeAccessorEmitter.GetQualifiedAccessorName(property.DeclaringTypeParameterNames, 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})"; } @@ -1539,7 +1263,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) { @@ -1700,7 +1424,7 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type if (typeGenerationSpec.ConstructorIsInaccessible) { - string accessorName = GetConstructorAccessorName(typeGenerationSpec); + string accessorName = UnsafeAccessorEmitter.GetConstructorAccessorName(typeGenerationSpec.TypeInfoPropertyName); sb.Append($"return {accessorName}("); } else @@ -1711,7 +1435,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.GetConstructorAccessorName(typeGenerationSpec.TypeInfoPropertyName); sb = new($"static args => {accessorName}("); } else @@ -2414,7 +2138,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.GetConstructorAccessorName(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 @@ + From a70824150f776c9566abaecb0bb9ec4eaf2c5858 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 11 Sep 2026 17:37:04 +0200 Subject: [PATCH 2/3] Move reflection-fallback declarations into UnsafeAccessorEmitter The reflection fallback emitted by the shared helper references an InstanceMemberBindingFlags const and a ValueTypeSetter<,> delegate that the consuming generator must declare. Their exact name and signature are a contract with the fallback code, so expose them as InstanceMemberBindingFlagsDeclaration and ValueTypeSetterDelegateDeclaration constants on the helper and have STJ write those instead of hardcoding the text. The constants hold the identical text STJ emitted, so the generated output is byte-identical (output baselines unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a0d7ac2-f2a3-4526-93ab-36bf1a23933f --- .../SourceGenerators/UnsafeAccessorEmitter.cs | 28 +++++++++++++++++-- .../gen/JsonSourceGenerator.Emitter.cs | 11 ++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs index 555690e9ac38e2..62eb4c4a350000 100644 --- a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs +++ b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs @@ -16,9 +16,10 @@ namespace SourceGenerators /// /// /// The reflection fallback emitted for members references an InstanceMemberBindingFlags constant (of type - /// ) that the consuming generator must emit into the same scope, and a - /// ValueTypeSetter<TDeclaringType, TValue> delegate when returns - /// for a value-type setter. + /// ) 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 { @@ -26,6 +27,27 @@ internal static class UnsafeAccessorEmitter private const string UnsafeAccessorKindTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorKind"; private const string EmptyTypeArray = "global::System.Array.Empty()"; + /// + /// 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, diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 7582e5699f35bd..987968bc5acc9b 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1692,18 +1692,11 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec GetLogicForDefaultSerializerOptionsInit(contextSpec.GeneratedOptionsSpec, writer); - writer.WriteLine($""" - - private const global::System.Reflection.BindingFlags InstanceMemberBindingFlags = - global::System.Reflection.BindingFlags.Instance | - global::System.Reflection.BindingFlags.Public | - global::System.Reflection.BindingFlags.NonPublic; - - """); + writer.WriteLine(UnsafeAccessorEmitter.InstanceMemberBindingFlagsDeclaration); if (emitValueTypeSetterDelegate) { - writer.WriteLine("private delegate void ValueTypeSetter(ref TDeclaringType obj, TValue value);"); + writer.WriteLine(UnsafeAccessorEmitter.ValueTypeSetterDelegateDeclaration); writer.WriteLine(); } From 339d4eddc8ecc8797ca974361d0dda044d2cdc2d Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 11 Sep 2026 18:54:38 +0200 Subject: [PATCH 3/3] Make the shared accessor declarations blank-free InstanceMemberBindingFlagsDeclaration now holds only the declaration, not surrounding blank lines, so callers own separation. STJ reproduces its existing indented blank lines via WriteLine of an empty string (output byte-identical, baselines unchanged); other consumers can add clean blanks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a0d7ac2-f2a3-4526-93ab-36bf1a23933f --- .../Common/src/SourceGenerators/UnsafeAccessorEmitter.cs | 2 -- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs index 62eb4c4a350000..3de12186fafeba 100644 --- a/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs +++ b/src/libraries/Common/src/SourceGenerators/UnsafeAccessorEmitter.cs @@ -33,12 +33,10 @@ internal static class UnsafeAccessorEmitter /// 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; - """; /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 987968bc5acc9b..3644a49b35a827 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1692,7 +1692,9 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec GetLogicForDefaultSerializerOptionsInit(contextSpec.GeneratedOptionsSpec, writer); + writer.WriteLine(""); writer.WriteLine(UnsafeAccessorEmitter.InstanceMemberBindingFlagsDeclaration); + writer.WriteLine(""); if (emitValueTypeSetterDelegate) {