// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
namespace Microsoft.Interop
{
public sealed record SignatureContext
{
// We don't need the warnings around not setting the various
// non-nullable fields/properties on this type in the constructor
// since we always use a property initializer.
#pragma warning disable 8618
private SignatureContext()
{
}
#pragma warning restore
public ImmutableArray<TypePositionInfo> ElementTypeInformation { get; init; }
public IEnumerable<TypePositionInfo> ManagedParameters => ElementTypeInformation.Where(tpi => !TypePositionInfo.IsSpecialIndex(tpi.ManagedIndex));
public string StubReturnType { get; init; }
public IEnumerable<GeneratedParameter> StubParameters
{
get
{
foreach (TypePositionInfo typeInfo in ElementTypeInformation)
{
if (!TypePositionInfo.IsSpecialIndex(typeInfo.ManagedIndex))
{
yield return new GeneratedParameter(
typeInfo.ManagedType.FullTypeName,
typeInfo.InstanceIdentifier,
MarshallerHelpers.GetManagedParameterModifiers(typeInfo));
}
}
}
}
public ImmutableArray<string> AdditionalAttributes { get; init; }
public static SignatureContext Create(
IMethodSymbol method,
MarshallingInfoParser marshallingInfoParser,
StubEnvironment env,
CodeEmitOptions options,
Assembly generatorInfoAssembly)
{
return Create(method, marshallingInfoParser, env, options, generatorInfoAssembly, errorHandlingInfo: null);
}
public static SignatureContext Create(
IMethodSymbol method,
MarshallingInfoParser marshallingInfoParser,
StubEnvironment env,
CodeEmitOptions options,
Assembly generatorInfoAssembly,
ErrorHandlingInfo? errorHandlingInfo)
{
ImmutableArray<TypePositionInfo> typeInfos = GenerateTypeInformation(method, marshallingInfoParser, env, errorHandlingInfo);
ImmutableArray<string>.Builder additionalAttrs = ImmutableArray.CreateBuilder<string>();
string generatorName = generatorInfoAssembly.GetName().Name;
string generatorVersion = generatorInfoAssembly.GetName().Version.ToString();
// Define additional attributes for the stub definition.
additionalAttrs.Add($"{TypeNames.GlobalAlias}{TypeNames.System_CodeDom_Compiler_GeneratedCodeAttribute}({CodeWriterHelpers.StringLiteral(generatorName)}, {CodeWriterHelpers.StringLiteral(generatorVersion)})");
if (options.SkipInit && !MethodIsSkipLocalsInit(env, method))
{
additionalAttrs.Add(TypeNames.GlobalAlias + TypeNames.System_Runtime_CompilerServices_SkipLocalsInitAttribute);
}
return new SignatureContext()
{
StubReturnType = method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
ElementTypeInformation = typeInfos,
AdditionalAttributes = additionalAttrs.ToImmutable(),
};
}
private static ImmutableArray<TypePositionInfo> GenerateTypeInformation(
IMethodSymbol method,
MarshallingInfoParser marshallingInfoParser,
StubEnvironment env,
ErrorHandlingInfo? errorHandlingInfo)
{
// When the underlying method is a property accessor, bare attributes on the property declaration
// (e.g. `[MarshalUsing(typeof(X))] string Prop { get; set; }`) land on the property symbol and
// are not otherwise visible to the marshalling pipeline. Fall them through to the accessor's
// value surface only -- the getter's return, or the setter's value parameter (the last
// parameter, after any indexer index parameters). Index parameters on indexer accessors and the
// setter's `void` return are not value surfaces and do not inherit property-level attributes.
// Accessor-level attributes win over property-level ones on a per-type basis. Target-scoped
// attributes (`[return:]`, `[param:]`, `[get:]`, `[set:]`) are routed by Roslyn onto the
// accessor directly and so are already in the accessor's attribute set.
ImmutableArray<AttributeData> associatedPropertyAttributes = method.AssociatedSymbol is IPropertySymbol property
? property.GetAttributes()
: ImmutableArray<AttributeData>.Empty;
// The value parameter on a setter is the last parameter (index parameters precede it on
// indexer setters). Getters have no value parameter -- their value surface is the return.
int valueParameterIndex = method.MethodKind == MethodKind.PropertySet
? method.Parameters.Length - 1
: -1;
ImmutableArray<TypePositionInfo>.Builder typeInfos = ImmutableArray.CreateBuilder<TypePositionInfo>();
for (int i = 0; i < method.Parameters.Length; i++)
{
IParameterSymbol param = method.Parameters[i];
ImmutableArray<AttributeData> paramAttributes = i == valueParameterIndex
? MergeAccessorAndPropertyAttributes(param.GetAttributes(), associatedPropertyAttributes)
: param.GetAttributes();
MarshallingInfo marshallingInfo = marshallingInfoParser.ParseMarshallingInfo(param.Type, paramAttributes);
var typeInfo = TypePositionInfo.CreateForParameter(param, marshallingInfo, env.Compilation);
typeInfo = typeInfo with
{
ManagedIndex = i,
NativeIndex = typeInfos.Count
};
typeInfos.Add(typeInfo);
}
ImmutableArray<AttributeData> returnAttributes = method.MethodKind == MethodKind.PropertyGet
? MergeAccessorAndPropertyAttributes(method.GetReturnTypeAttributes(), associatedPropertyAttributes)
: method.GetReturnTypeAttributes();
TypePositionInfo retTypeInfo = new(ManagedTypeInfo.CreateTypeInfoForTypeSymbol(method.ReturnType), marshallingInfoParser.ParseMarshallingInfo(method.ReturnType, returnAttributes));
retTypeInfo = retTypeInfo with
{
ManagedIndex = TypePositionInfo.ReturnIndex,
NativeIndex = TypePositionInfo.ReturnIndex,
};
typeInfos.Add(retTypeInfo);
if (errorHandlingInfo is not null)
{
ApplyErrorHandlingInfo(typeInfos, errorHandlingInfo);
}
return typeInfos.ToImmutable();
void ApplyErrorHandlingInfo(ImmutableArray<TypePositionInfo>.Builder infos, ErrorHandlingInfo errorInfo)
{
TypePositionInfo CreateErrorInfo(
int nativeIndex)
{
return new TypePositionInfo(errorInfo.ManagedType, errorInfo.MarshallingInfo)
{
InstanceIdentifier = "__error",
RefKind = nativeIndex == TypePositionInfo.ReturnIndex ? RefKind.None : RefKind.Out,
ManagedIndex = TypePositionInfo.ErrorIndex,
NativeIndex = nativeIndex,
IsErrorHandlingPosition = true,
};
}
bool MatchesManagedType(TypePositionInfo info) => info.ManagedType == errorInfo.ManagedType;
switch (errorInfo.Location)
{
case ErrorHandlingLocation.ReturnValue:
int returnIndex = infos.Count - 1;
TypePositionInfo returnInfo = infos[returnIndex];
if (MatchesManagedType(returnInfo))
{
infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
}
else if (returnInfo.ManagedType == SpecialTypeInfo.Void)
{
infos[returnIndex] = returnInfo with { NativeIndex = TypePositionInfo.UnsetIndex };
infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
}
break;
case ErrorHandlingLocation.HiddenReturnValue:
int hiddenReturnIndex = infos.Count - 1;
TypePositionInfo hiddenReturnInfo = infos[hiddenReturnIndex];
if (hiddenReturnInfo.ManagedType == SpecialTypeInfo.Void)
{
infos[hiddenReturnIndex] = hiddenReturnInfo with { NativeIndex = TypePositionInfo.UnsetIndex };
}
else
{
// Match the COM ABI transformation: keep the value in the managed return
// position while moving it to a final out parameter in the native signature.
infos[hiddenReturnIndex] = hiddenReturnInfo with
{
RefKind = RefKind.Out,
NativeIndex = method.Parameters.Length,
};
}
infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
break;
case ErrorHandlingLocation.LastParameter:
TypePositionInfo lastParameter = GetLastManagedParameter(infos);
Debug.Assert(lastParameter is { RefKind: RefKind.Out or RefKind.Ref }
&& MatchesManagedType(lastParameter));
infos.Add(CreateErrorInfo(lastParameter.NativeIndex));
break;
case ErrorHandlingLocation.HiddenLastParameter:
infos.Add(CreateErrorInfo(method.Parameters.Length));
break;
}
static TypePositionInfo GetLastManagedParameter(ImmutableArray<TypePositionInfo>.Builder infos)
{
TypePositionInfo? lastParameter = null;
foreach (TypePositionInfo info in infos)
{
if (!TypePositionInfo.IsSpecialIndex(info.ManagedIndex)
&& (lastParameter is null || info.ManagedIndex > lastParameter.ManagedIndex))
{
lastParameter = info;
}
}
return lastParameter ?? throw new UnreachableException();
}
}
}
private static ImmutableArray<AttributeData> MergeAccessorAndPropertyAttributes(
ImmutableArray<AttributeData> accessorAttributes,
ImmutableArray<AttributeData> associatedPropertyAttributes)
{
if (associatedPropertyAttributes.IsEmpty)
{
return accessorAttributes;
}
// Accessor-level attributes win over property-level ones at the same dedup key
// (attribute type + ElementIndirectionDepth for [MarshalUsing], attribute type alone
// otherwise). [MarshalUsing] is the only AllowMultiple = true attribute that flows
// through this merge: it can repeat on a single value surface with distinct
// ElementIndirectionDepth values to describe marshalling at successive levels of
// indirection (the value itself at depth 0, its elements at depth 1, and so on). The
// public contract on MarshalUsingAttribute.ElementIndirectionDepth states only one
// [MarshalUsing] with a given depth may be provided on a given parameter or return
// value, so dedup keys for [MarshalUsing] include the depth -- an accessor-level
// [MarshalUsing] overrides only the property-level [MarshalUsing] at the matching
// depth, and property-level [MarshalUsing]s at other depths flow through.
//
// To keep this dedup unambiguous, the COM generator additionally rejects accessor-level
// [MarshalUsing] attributes that omit the marshaller type (see
// MarshalUsingOnPropertyAccessorMustSpecifyType in GeneratorDiagnostics). That keeps the
// partial-split case (e.g., marshaller type on the property and count-only on the
// accessor) from silently dropping one side; the user combines the information on a
// single attribute or attaches the count-only [MarshalUsing] to the property.
HashSet<(string?, int)> accessorAttributeKeys = new();
foreach (AttributeData attr in accessorAttributes)
{
accessorAttributeKeys.Add(GetMergeKey(attr));
}
ImmutableArray<AttributeData>.Builder merged = ImmutableArray.CreateBuilder<AttributeData>(accessorAttributes.Length + associatedPropertyAttributes.Length);
merged.AddRange(accessorAttributes);
foreach (AttributeData attr in associatedPropertyAttributes)
{
if (!accessorAttributeKeys.Contains(GetMergeKey(attr)))
{
merged.Add(attr);
}
}
return merged.ToImmutable();
static (string?, int) GetMergeKey(AttributeData attr)
{
string? attributeName = attr.AttributeClass?.ToDisplayString();
int depth = 0;
if (attributeName == TypeNames.MarshalUsingAttribute)
{
foreach (KeyValuePair<string, TypedConstant> named in attr.NamedArguments)
{
if (named.Key == ManualTypeMarshallingHelper.MarshalUsingProperties.ElementIndirectionDepth)
{
depth = (int)named.Value.Value!;
break;
}
}
}
return (attributeName, depth);
}
}
public bool Equals(SignatureContext other)
{
// We don't check if the generator factories are equal since
// the generator factory is deterministically created based on the ElementTypeInformation and Options.
return other is not null
&& ElementTypeInformation.SequenceEqual(other.ElementTypeInformation)
&& StubReturnType == other.StubReturnType
&& AdditionalAttributes.SequenceEqual(other.AdditionalAttributes);
}
public override int GetHashCode()
{
throw new UnreachableException();
}
private static bool MethodIsSkipLocalsInit(StubEnvironment env, IMethodSymbol method)
{
if (env.EnvironmentFlags.HasFlag(EnvironmentFlags.SkipLocalsInit))
{
return true;
}
if (method.GetAttributes().Any(IsSkipLocalsInitAttribute))
{
return true;
}
for (INamedTypeSymbol type = method.ContainingType; type is not null; type = type.ContainingType)
{
if (type.GetAttributes().Any(IsSkipLocalsInitAttribute))
{
return true;
}
}
// We check the module case earlier, so we don't need to do it here.
return false;
static bool IsSkipLocalsInitAttribute(AttributeData a)
=> a.AttributeClass?.ToDisplayString() == TypeNames.System_Runtime_CompilerServices_SkipLocalsInitAttribute;
}
}
}