Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docfx/docs/3rdPartyMetadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ But the general transformation from metadata to C# code may be applied to other
Constructing metadata is outside the scope of this document.
However you may find [the win32metadata architecture](https://github.com/microsoft/win32metadata/blob/main/docs/architecture.md) document instructive.

Metadata can keep native primitive and pointer signatures while supplying logical enum and ownership information through annotations.
See [metadata annotations and compatibility](metadata-annotations.md) for projection rules, unsupported shapes, and the pinned consumer compatibility harness.

## Hooking metadata into CsWin32

Metadata is fed into CsWin32 through MSBuild items.
Expand Down
1 change: 1 addition & 0 deletions docfx/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
- Generates xml documentation based on and links back to learn.microsoft.com
- Ships no bulky assemblies alongside your application.
- [Layered composition](composition.md): multiple assemblies can extend a single shared `PInvoke` static class so callers reach every native API through one symbol.
- [Metadata annotations](metadata-annotations.md): project logical enums and contextual ownership while preserving native storage.

![Animation demonstrating p/invoke code generation](../images/demo.gif)
190 changes: 190 additions & 0 deletions docfx/docs/metadata-annotations.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docfx/docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ items:
- href: composition.md
- href: ArchSpecificAPIs.md
- href: 3rdPartyMetadata.md
- href: metadata-annotations.md
5 changes: 5 additions & 0 deletions src/Microsoft.Windows.CsWin32/ArrayTypeHandleInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs
inputs = inputs with { AllowMarshaling = false };
}

if (customAttributes is { } attributes)
{
attributes.Generator.ValidateAssociatedEnumElementType(this.ElementType, attributes.Collection);
}

TypeSyntaxAndMarshaling element = this.ElementType.ToTypeSyntax(inputs, forElement, customAttributes);
if (inputs.AllowMarshaling || inputs.IsField)
{
Expand Down
489 changes: 489 additions & 0 deletions src/Microsoft.Windows.CsWin32/Generator.AssociatedEnum.cs

Large diffs are not rendered by default.

31 changes: 23 additions & 8 deletions src/Microsoft.Windows.CsWin32/Generator.Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ public partial class Generator
/// <inheritdoc/>
public bool TryGetEnumName(string enumValueName, [NotNullWhen(true)] out string? declaringEnum) => this.MetadataIndex.TryGetEnumName(this.Reader, enumValueName, out declaringEnum);

private EnumDeclarationSyntax DeclareEnum(TypeDefinition typeDef)
private EnumDeclarationSyntax DeclareEnum(TypeDefinitionHandle typeHandle, TypeDefinition typeDef)
{
bool flagsEnum = this.FindAttribute(typeDef.GetCustomAttributes(), nameof(System), nameof(FlagsAttribute)) is not null;
PrimitiveTypeCode storage = this.GetProjectedEnumUnderlyingType(typeHandle);
bool changedStorage = storage != GetDeclaredEnumUnderlyingType(new(this, this.Reader, typeHandle));

var enumValues = new List<SyntaxNodeOrToken>();
TypeSyntax? enumBaseType = null;
TypeSyntax enumBaseType = PrimitiveTypeHandleInfo.ToTypeSyntax(storage, this.enumTypeSettings.PreferNativeInt);
foreach (FieldDefinitionHandle fieldDefHandle in typeDef.GetFields())
{
AddEnumValue(fieldDefHandle);
Expand All @@ -32,11 +34,6 @@ private EnumDeclarationSyntax DeclareEnum(TypeDefinition typeDef)
}
}

if (enumBaseType is null)
{
throw new NotSupportedException("Unknown enum type.");
}

string? name = this.Reader.GetString(typeDef.Name);
EnumDeclarationSyntax result = EnumDeclaration(Identifier(name), SeparatedList<EnumMemberDeclarationSyntax>(enumValues))
.WithModifiers([TokenWithSpace(this.Visibility)]);
Expand Down Expand Up @@ -64,12 +61,30 @@ void AddEnumValue(FieldDefinitionHandle fieldDefHandle)
ConstantHandle valueHandle = fieldDef.GetDefaultValue();
if (valueHandle.IsNil)
{
enumBaseType = fieldDef.DecodeSignature(this.SignatureHandleProvider, null).ToTypeSyntax(this.enumTypeSettings, GeneratingElement.EnumValue, null).Type;
return;
}

bool enumBaseTypeIsSigned = enumBaseType is PredefinedTypeSyntax { Keyword: { RawKind: (int)SyntaxKind.LongKeyword or (int)SyntaxKind.IntKeyword or (int)SyntaxKind.ShortKeyword or (int)SyntaxKind.SByteKeyword } };
ExpressionSyntax enumValue = flagsEnum ? ToHexExpressionSyntax(this.Reader, valueHandle, enumBaseTypeIsSigned) : ToExpressionSyntax(this.Reader, valueHandle);
if (changedStorage)
{
Constant constant = this.Reader.GetConstant(valueHandle);
decimal value = Convert.ToDecimal(this.Reader.GetBlobReader(constant.Value).ReadConstant(constant.TypeCode), CultureInfo.InvariantCulture);
(decimal minimum, decimal maximum) = storage switch
{
PrimitiveTypeCode.SByte or PrimitiveTypeCode.Byte => (sbyte.MinValue, byte.MaxValue),
PrimitiveTypeCode.Int16 or PrimitiveTypeCode.UInt16 => (short.MinValue, ushort.MaxValue),
PrimitiveTypeCode.Int32 or PrimitiveTypeCode.UInt32 => (int.MinValue, uint.MaxValue),
_ => (long.MinValue, (decimal)ulong.MaxValue),
};
if (value < minimum || value > maximum)
{
throw new GenerationFailedException($"AssociatedEnum member {enumValueName}={value} does not fit native {storage} storage without truncation.");
}

enumValue = UncheckedExpression(CastExpression(enumBaseType, enumValue));
}

EnumMemberDeclarationSyntax enumMember = EnumMemberDeclaration(SafeIdentifier(enumValueName), EqualsValueClause(enumValue));
enumValues.Add(enumMember);
enumValues.Add(TokenWithLineFeed(SyntaxKind.CommaToken));
Expand Down
48 changes: 3 additions & 45 deletions src/Microsoft.Windows.CsWin32/Generator.Extern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,26 +220,6 @@ private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle)
CustomAttributeHandleCollection? returnTypeAttributes = this.GetReturnTypeCustomAttributes(methodDefinition);
TypeSyntaxAndMarshaling returnType = signature.ReturnType.ToTypeSyntax(typeSettings with { IsReturnValue = true }, GeneratingElement.ExternMethod, returnTypeAttributes?.QualifyWith(this), ParameterAttributes.Out);

bool customMarshaling = this.options.AllowMarshaling && this.useSourceGenerators;

// Search for any enum substitutions.
TypeSyntax? returnTypeEnumName = customMarshaling ? null : this.FindAssociatedEnum(returnTypeAttributes);
TypeSyntax?[]? parameterEnumType = null;
foreach (ParameterHandle parameterHandle in methodDefinition.GetParameters())
{
Parameter parameter = this.Reader.GetParameter(parameterHandle);
if (parameter.SequenceNumber == 0)
{
continue;
}

if (this.FindAssociatedEnum(parameter.GetCustomAttributes()) is IdentifierNameSyntax parameterEnumName && !customMarshaling)
{
parameterEnumType ??= new TypeSyntax?[signature.ParameterTypes.Length];
parameterEnumType[parameter.SequenceNumber - 1] = parameterEnumName;
}
}

bool setLastError = (import.Attributes & MethodImportAttributes.SetLastError) == MethodImportAttributes.SetLastError;
bool setLastErrorViaMarshaling = setLastError && (this.Options.AllowMarshaling || !this.canUseSetLastPInvokeError);
bool setLastErrorManually = setLastError && !setLastErrorViaMarshaling;
Expand Down Expand Up @@ -293,48 +273,26 @@ AttributeListSyntax CreateDllImportAttributeList()
}

MethodDeclarationSyntax exposedMethod;
if (returnTypeEnumName is null && parameterEnumType is null && !setLastErrorManually)
if (!setLastErrorManually)
{
// No need for wrapping the extern method, so just expose it directly.
exposedMethod = externDeclaration.WithModifiers(externDeclaration.Modifiers.Insert(0, TokenWithSpace(this.Visibility)));
}
else
{
string ns = this.GetMethodNamespace(methodDefinition);
NameSyntax nsSyntax = ParseName(ReplaceCommonNamespaceWithAlias(this, ns));
ParameterListSyntax exposedParameterList = this.CreateParameterList(methodDefinition, signature, typeSettings, GeneratingElement.ExternMethod);
static SyntaxToken RefInOutKeyword(ParameterSyntax p) =>
p.Modifiers.Any(SyntaxKind.OutKeyword) ? TokenWithSpace(SyntaxKind.OutKeyword) :
p.Modifiers.Any(SyntaxKind.RefKeyword) ? TokenWithSpace(SyntaxKind.RefKeyword) :
default;
ArgumentListSyntax argumentList = ArgumentList([.. exposedParameterList.Parameters.Select(p => Argument(IdentifierName(p.Identifier.ValueText)).WithRefKindKeyword(RefInOutKeyword(p)))]);
if (parameterEnumType is not null)
{
for (int i = 0; i < parameterEnumType.Length; i++)
{
if (parameterEnumType[i] is TypeSyntax parameterType)
{
NameSyntax qualifiedParameterType = QualifiedName(nsSyntax, (SimpleNameSyntax)parameterType);
exposedParameterList = exposedParameterList.ReplaceNode(exposedParameterList.Parameters[i], exposedParameterList.Parameters[i].WithType(qualifiedParameterType.WithTrailingTrivia(Space)));
this.RequestInteropType(ns, parameterEnumType[i]!.ToString(), this.DefaultContext);
argumentList = argumentList.ReplaceNode(argumentList.Arguments[i], argumentList.Arguments[i].WithExpression(CastExpression(externDeclaration.ParameterList.Parameters[i].Type!.WithTrailingTrivia(default(SyntaxTriviaList)), argumentList.Arguments[i].Expression)));
}
}
}

// We need to specify Entrypoint because our local function will have a different name.
// It must have a unique name because some functions will have the same signature as our exposed method except for the return type.
entrypoint ??= methodName;
IdentifierNameSyntax localExternFunctionName = IdentifierName("LocalExternFunction");
ExpressionSyntax invocation = InvocationExpression(localExternFunctionName, argumentList);

if (returnTypeEnumName is not null)
{
this.RequestInteropType(ns, returnTypeEnumName.ToString(), this.DefaultContext);
returnTypeEnumName = QualifiedName(nsSyntax, (SimpleNameSyntax)returnTypeEnumName);
invocation = CastExpression(returnTypeEnumName, invocation);
}

BlockSyntax body = Block();

IdentifierNameSyntax? retValLocalName = returnType.Type is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword } ? null : IdentifierName("__retVal");
Expand All @@ -352,7 +310,7 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) =>
// var __retVal = LocalExternFunction(...);
body = body.AddStatements(
LocalDeclarationStatement(
VariableDeclaration(returnTypeEnumName ?? returnType.Type, [VariableDeclarator(retValLocalName.Identifier, EqualsValueClause(invocation))])));
VariableDeclaration(returnType.Type, [VariableDeclarator(retValLocalName.Identifier, EqualsValueClause(invocation))])));
}
else
{
Expand Down Expand Up @@ -381,7 +339,7 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) =>
.WithSemicolonToken(SemicolonWithLineFeed);
body = body.AddStatements(externFunction);

exposedMethod = MethodDeclaration(returnTypeEnumName ?? returnType.Type, externDeclaration.Identifier)
exposedMethod = MethodDeclaration(returnType.Type, externDeclaration.Identifier)
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword))
.WithParameterList(exposedParameterList)
.WithBody(body);
Expand Down
11 changes: 9 additions & 2 deletions src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ private ExpressionSyntax GetIntPtrFromTypeDef(ExpressionSyntax typedefValue, Typ

private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverloads(MethodDefinition methodDefinition, MethodDeclarationSyntax externMethodDeclaration, NameSyntax declaringTypeName, FriendlyOverloadOf overloadOf, HashSet<string> helperMethodsAdded, bool avoidWinmdRootAlias)
{
if (overloadOf is FriendlyOverloadOf.ExternMethod or FriendlyOverloadOf.StructMethod
&& this.DeclareStoragePreservingEnumOverload(methodDefinition, externMethodDeclaration, avoidWinmdRootAlias) is MethodDeclarationSyntax enumOverload)
{
yield return enumOverload;
externMethodDeclaration = enumOverload;
}

if (!this.options.FriendlyOverloads.Enabled)
{
yield break;
Expand Down Expand Up @@ -578,7 +585,7 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
IdentifierNameSyntax localName = IdentifierName(externParam.Identifier.ValueText + "Local");

// NOTE: We don't handle scenarios where the parameter is [MemorySize] annotated (e.g. EnumProcessModules) or [NativeArrayInfo] (e.g. ITypeInfo.GetNames)
if (this.RequestSafeHandle(outReleaseMethod, pointedElementInfo) is TypeSyntax safeHandleType)
if (this.RequestSafeHandle(outReleaseMethod, pointedElementInfo, valueAttributes: paramAttributes) is TypeSyntax safeHandleType)
{
// out SafeHandle
parameters[paramIndex] = externParam
Expand Down Expand Up @@ -1404,7 +1411,7 @@ bool TryHandleCountParam(TypeSyntax elementType, bool nullableSource)
}

TypeSyntax? returnSafeHandleType = this.TryGetResourceReleaseMethod(originalSignature.ReturnType, returnTypeAttributes, out string? returnReleaseMethod)
? this.RequestSafeHandle(returnReleaseMethod, originalSignature.ReturnType) : null;
? this.RequestSafeHandle(returnReleaseMethod, originalSignature.ReturnType, valueAttributes: returnTypeAttributes) : null;

IdentifierNameSyntax resultLocal = IdentifierName("__result");

Expand Down
Loading
Loading