Skip to content

Commit f1a7448

Browse files
authored
Merge pull request #1130 from microsoft/fix387
Add helper APIs for variable-length inline arrays
2 parents 9059d51 + d80cf0f commit f1a7448

File tree

8 files changed

+243
-1
lines changed

8 files changed

+243
-1
lines changed

src/Microsoft.Windows.CsWin32/FastSyntaxFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ internal static ForStatementSyntax ForStatement(VariableDeclarationSyntax? decla
102102

103103
internal static VariableDeclarationSyntax VariableDeclaration(TypeSyntax type) => SyntaxFactory.VariableDeclaration(type.WithTrailingTrivia(TriviaList(Space)));
104104

105+
internal static SizeOfExpressionSyntax SizeOfExpression(TypeSyntax type) => SyntaxFactory.SizeOfExpression(Token(SyntaxKind.SizeOfKeyword), Token(SyntaxKind.OpenParenToken), type, Token(SyntaxKind.CloseParenToken));
106+
105107
internal static MemberAccessExpressionSyntax MemberAccessExpression(SyntaxKind kind, ExpressionSyntax expression, SimpleNameSyntax name) => SyntaxFactory.MemberAccessExpression(kind, expression, Token(GetMemberAccessExpressionOperatorTokenKind(kind)), name);
106108

107109
internal static ConditionalAccessExpressionSyntax ConditionalAccessExpression(ExpressionSyntax expression, SimpleNameSyntax name) => SyntaxFactory.ConditionalAccessExpression(expression, Token(SyntaxKind.QuestionToken), MemberBindingExpression(name));

src/Microsoft.Windows.CsWin32/Generator.Features.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public partial class Generator
88
private readonly bool canUseSpan;
99
private readonly bool canCallCreateSpan;
1010
private readonly bool canUseUnsafeAsRef;
11+
private readonly bool canUseUnsafeAdd;
1112
private readonly bool canUseUnsafeNullRef;
1213
private readonly bool canUseUnmanagedCallersOnlyAttribute;
1314
private readonly bool canUseSetLastPInvokeError;

src/Microsoft.Windows.CsWin32/Generator.Struct.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle
2828

2929
// If the last field has the [FlexibleArray] attribute, we must disable marshaling since the struct
3030
// is only ever valid when accessed via a pointer since the struct acts as a header of an arbitrarily-sized array.
31+
FieldDefinitionHandle flexibleArrayFieldHandle = default;
32+
MethodDeclarationSyntax? sizeOfMethod = null;
3133
if (typeDef.GetFields().LastOrDefault() is FieldDefinitionHandle { IsNil: false } lastFieldHandle)
3234
{
3335
FieldDefinition lastField = this.Reader.GetFieldDefinition(lastFieldHandle);
3436
if (MetadataUtilities.FindAttribute(this.Reader, lastField.GetCustomAttributes(), InteropDecorationNamespace, FlexibleArrayAttribute) is not null)
3537
{
38+
flexibleArrayFieldHandle = lastFieldHandle;
3639
context = context with { AllowMarshaling = false };
3740
}
3841
}
@@ -80,6 +83,37 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle
8083
.WithArgumentList(BracketedArgumentList(SingletonSeparatedList(Argument(size)))))
8184
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword), Token(SyntaxKind.FixedKeyword));
8285
}
86+
else if (fieldDefHandle == flexibleArrayFieldHandle)
87+
{
88+
CustomAttributeHandleCollection fieldAttributes = fieldDef.GetCustomAttributes();
89+
var fieldTypeInfo = (ArrayTypeHandleInfo)fieldDef.DecodeSignature(SignatureHandleProvider.Instance, null);
90+
TypeSyntax fieldType = fieldTypeInfo.ElementType.ToTypeSyntax(typeSettings, GeneratingElement.StructMember, fieldAttributes).Type;
91+
92+
if (fieldType is PointerTypeSyntax or FunctionPointerTypeSyntax)
93+
{
94+
// These types are not allowed as generic type arguments (https://github.com/dotnet/runtime/issues/13627)
95+
// so we have to generate a special nested struct dedicated to this type instead of using the generic type.
96+
StructDeclarationSyntax helperStruct = this.DeclareVariableLengthInlineArrayHelper(context, fieldType);
97+
additionalMembers = additionalMembers.Add(helperStruct);
98+
99+
field = FieldDeclaration(
100+
VariableDeclaration(IdentifierName(helperStruct.Identifier.ValueText)))
101+
.AddDeclarationVariables(fieldDeclarator)
102+
.AddModifiers(TokenWithSpace(this.Visibility));
103+
}
104+
else
105+
{
106+
this.RequestVariableLengthInlineArrayHelper(context);
107+
field = FieldDeclaration(
108+
VariableDeclaration(
109+
GenericName($"global::Windows.Win32.VariableLengthInlineArray")
110+
.WithTypeArgumentList(TypeArgumentList().AddArguments(fieldType))))
111+
.AddDeclarationVariables(fieldDeclarator)
112+
.AddModifiers(TokenWithSpace(this.Visibility));
113+
}
114+
115+
sizeOfMethod = this.DeclareSizeOfMethod(name, fieldType, typeSettings);
116+
}
83117
else
84118
{
85119
CustomAttributeHandleCollection fieldAttributes = fieldDef.GetCustomAttributes();
@@ -334,6 +368,12 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle
334368
}
335369
}
336370

371+
// Add a SizeOf method, if there is a FlexibleArray field.
372+
if (sizeOfMethod is not null)
373+
{
374+
members.Add(sizeOfMethod);
375+
}
376+
337377
// Add the additional members, taking care to not introduce redundant declarations.
338378
members.AddRange(additionalMembers.Where(c => c is not StructDeclarationSyntax cs || !members.OfType<StructDeclarationSyntax>().Any(m => m.Identifier.ValueText == cs.Identifier.ValueText)));
339379

@@ -370,6 +410,95 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle
370410
return result;
371411
}
372412

413+
private StructDeclarationSyntax DeclareVariableLengthInlineArrayHelper(Context context, TypeSyntax fieldType)
414+
{
415+
IdentifierNameSyntax firstElementFieldName = IdentifierName("e0");
416+
List<MemberDeclarationSyntax> members = new();
417+
418+
// internal unsafe T e0;
419+
members.Add(FieldDeclaration(VariableDeclaration(fieldType).AddVariables(VariableDeclarator(firstElementFieldName.Identifier)))
420+
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword)));
421+
422+
if (this.canUseUnsafeAdd)
423+
{
424+
////[MethodImpl(MethodImplOptions.AggressiveInlining)]
425+
////get { fixed (int** p = &e0) return *(p + index); }
426+
IdentifierNameSyntax pLocal = IdentifierName("p");
427+
AccessorDeclarationSyntax getter = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
428+
.WithBody(Block().AddStatements(
429+
FixedStatement(
430+
VariableDeclaration(PointerType(fieldType)).AddVariables(
431+
VariableDeclarator(pLocal.Identifier).WithInitializer(EqualsValueClause(PrefixUnaryExpression(SyntaxKind.AddressOfExpression, firstElementFieldName)))),
432+
ReturnStatement(PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, ParenthesizedExpression(BinaryExpression(SyntaxKind.AddExpression, pLocal, IdentifierName("index"))))))))
433+
.AddAttributeLists(AttributeList().AddAttributes(MethodImpl(MethodImplOptions.AggressiveInlining)));
434+
435+
////[MethodImpl(MethodImplOptions.AggressiveInlining)]
436+
////set { fixed (int** p = &e0) *(p + index) = value; }
437+
AccessorDeclarationSyntax setter = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
438+
.WithBody(Block().AddStatements(
439+
FixedStatement(
440+
VariableDeclaration(PointerType(fieldType)).AddVariables(
441+
VariableDeclarator(pLocal.Identifier).WithInitializer(EqualsValueClause(PrefixUnaryExpression(SyntaxKind.AddressOfExpression, firstElementFieldName)))),
442+
ExpressionStatement(AssignmentExpression(
443+
SyntaxKind.SimpleAssignmentExpression,
444+
PrefixUnaryExpression(SyntaxKind.PointerIndirectionExpression, ParenthesizedExpression(BinaryExpression(SyntaxKind.AddExpression, pLocal, IdentifierName("index")))),
445+
IdentifierName("value"))))))
446+
.AddAttributeLists(AttributeList().AddAttributes(MethodImpl(MethodImplOptions.AggressiveInlining)));
447+
448+
////internal unsafe T this[int index]
449+
members.Add(IndexerDeclaration(fieldType.WithTrailingTrivia(Space))
450+
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword))
451+
.AddParameterListParameters(Parameter(Identifier("index")).WithType(PredefinedType(TokenWithSpace(SyntaxKind.IntKeyword))))
452+
.AddAccessorListAccessors(getter, setter));
453+
}
454+
455+
// internal partial struct VariableLengthInlineArrayHelper
456+
return StructDeclaration(Identifier("VariableLengthInlineArrayHelper"))
457+
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.PartialKeyword))
458+
.AddMembers(members.ToArray());
459+
}
460+
461+
private MethodDeclarationSyntax DeclareSizeOfMethod(TypeSyntax structType, TypeSyntax elementType, TypeSyntaxSettings typeSettings)
462+
{
463+
PredefinedTypeSyntax intType = PredefinedType(TokenWithSpace(SyntaxKind.IntKeyword));
464+
IdentifierNameSyntax countName = IdentifierName("count");
465+
IdentifierNameSyntax localName = IdentifierName("v");
466+
List<StatementSyntax> statements = new();
467+
468+
// int v = sizeof(OUTER_STRUCT);
469+
statements.Add(LocalDeclarationStatement(VariableDeclaration(intType).AddVariables(
470+
VariableDeclarator(localName.Identifier).WithInitializer(EqualsValueClause(SizeOfExpression(structType))))));
471+
472+
// if (count > 1)
473+
// v += checked((count - 1) * sizeof(ELEMENT_TYPE));
474+
// else if (count < 0)
475+
// throw new ArgumentOutOfRangeException(nameof(count));
476+
statements.Add(IfStatement(
477+
BinaryExpression(SyntaxKind.GreaterThanExpression, countName, LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(1))),
478+
ExpressionStatement(AssignmentExpression(
479+
SyntaxKind.AddAssignmentExpression,
480+
localName,
481+
CheckedExpression(BinaryExpression(
482+
SyntaxKind.MultiplyExpression,
483+
ParenthesizedExpression(BinaryExpression(SyntaxKind.SubtractExpression, countName, LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(1)))),
484+
SizeOfExpression(elementType))))),
485+
ElseClause(IfStatement(
486+
BinaryExpression(SyntaxKind.LessThanExpression, countName, LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(0))),
487+
ThrowStatement(ObjectCreationExpression(IdentifierName(nameof(ArgumentOutOfRangeException))))).WithCloseParenToken(TokenWithLineFeed(SyntaxKind.CloseParenToken)))).WithCloseParenToken(TokenWithLineFeed(SyntaxKind.CloseParenToken)));
488+
489+
// return v;
490+
statements.Add(ReturnStatement(localName));
491+
492+
// internal static unsafe int SizeOf(int count)
493+
MethodDeclarationSyntax sizeOfMethod = MethodDeclaration(intType, Identifier("SizeOf"))
494+
.AddParameterListParameters(Parameter(countName.Identifier).WithType(intType))
495+
.WithBody(Block().AddStatements(statements.ToArray()))
496+
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.UnsafeKeyword))
497+
.WithLeadingTrivia(ParseLeadingTrivia("/// <summary>Computes the amount of memory that must be allocated to store this struct, including the specified number of elements in the variable length inline array at the end.</summary>\n"));
498+
499+
return sizeOfMethod;
500+
}
501+
373502
private (TypeSyntax FieldType, SyntaxList<MemberDeclarationSyntax> AdditionalMembers, AttributeSyntax? MarshalAsAttribute) ReinterpretFieldType(FieldDefinition fieldDef, TypeSyntax originalType, CustomAttributeHandleCollection customAttributes, Context context)
374503
{
375504
TypeSyntaxSettings typeSettings = context.Filter(this.fieldTypeSettings);
@@ -397,4 +526,23 @@ private StructDeclarationSyntax DeclareStruct(TypeDefinitionHandle typeDefHandle
397526

398527
return (originalType, default(SyntaxList<MemberDeclarationSyntax>), marshalAs);
399528
}
529+
530+
private void RequestVariableLengthInlineArrayHelper(Context context)
531+
{
532+
if (this.IsWin32Sdk)
533+
{
534+
if (!this.IsTypeAlreadyFullyDeclared($"{this.Namespace}.{this.variableLengthInlineArrayStruct.Identifier.ValueText}"))
535+
{
536+
this.DeclareUnscopedRefAttributeIfNecessary();
537+
this.volatileCode.GenerateSpecialType("VariableLengthInlineArray", () => this.volatileCode.AddSpecialType("VariableLengthInlineArray", this.variableLengthInlineArrayStruct));
538+
}
539+
}
540+
else if (this.SuperGenerator is not null && this.SuperGenerator.TryGetGenerator("Windows.Win32", out Generator? generator))
541+
{
542+
generator.volatileCode.GenerationTransaction(delegate
543+
{
544+
generator.RequestVariableLengthInlineArrayHelper(context);
545+
});
546+
}
547+
}
400548
}

src/Microsoft.Windows.CsWin32/Generator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public partial class Generator : IGenerator, IDisposable
2222
private readonly TypeSyntaxSettings errorMessageTypeSettings;
2323

2424
private readonly ClassDeclarationSyntax comHelperClass;
25+
private readonly StructDeclarationSyntax variableLengthInlineArrayStruct;
2526

2627
private readonly Dictionary<string, IReadOnlyList<ISymbol>> findTypeSymbolIfAlreadyAvailableCache = new(StringComparer.Ordinal);
2728
private readonly Rental<MetadataReader> metadataReader;
@@ -86,7 +87,8 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
8687

8788
this.canUseSpan = this.compilation?.GetTypeByMetadataName(typeof(Span<>).FullName) is not null;
8889
this.canCallCreateSpan = this.compilation?.GetTypeByMetadataName(typeof(MemoryMarshal).FullName)?.GetMembers("CreateSpan").Any() is true;
89-
this.canUseUnsafeAsRef = this.compilation?.GetTypeByMetadataName(typeof(Unsafe).FullName)?.GetMembers("AsRef").Any() is true;
90+
this.canUseUnsafeAsRef = this.compilation?.GetTypeByMetadataName(typeof(Unsafe).FullName)?.GetMembers("Add").Any() is true;
91+
this.canUseUnsafeAdd = this.compilation?.GetTypeByMetadataName(typeof(Unsafe).FullName)?.GetMembers("AsRef").Any() is true;
9092
this.canUseUnsafeNullRef = this.compilation?.GetTypeByMetadataName(typeof(Unsafe).FullName)?.GetMembers("NullRef").Any() is true;
9193
this.canUseUnmanagedCallersOnlyAttribute = this.compilation?.GetTypeByMetadataName("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute") is not null;
9294
this.canUseSetLastPInvokeError = this.compilation?.GetTypeByMetadataName("System.Runtime.InteropServices.Marshal")?.GetMembers("GetLastSystemError").IsEmpty is false;
@@ -110,6 +112,7 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
110112
AddSymbolIf(this.canUseSpan, "canUseSpan");
111113
AddSymbolIf(this.canCallCreateSpan, "canCallCreateSpan");
112114
AddSymbolIf(this.canUseUnsafeAsRef, "canUseUnsafeAsRef");
115+
AddSymbolIf(this.canUseUnsafeAdd, "canUseUnsafeAdd");
113116
AddSymbolIf(this.canUseUnsafeNullRef, "canUseUnsafeNullRef");
114117
AddSymbolIf(compilation?.GetTypeByMetadataName("System.Drawing.Point") is not null, "canUseSystemDrawing");
115118
AddSymbolIf(this.IsFeatureAvailable(Feature.InterfaceStaticMembers), "canUseInterfaceStaticMembers");
@@ -149,6 +152,7 @@ void AddSymbolIf(bool condition, string symbol)
149152
this.methodsAndConstantsClassName = IdentifierName(options.ClassName);
150153

151154
FetchTemplate("ComHelpers", this, out this.comHelperClass);
155+
FetchTemplate("VariableLengthInlineArray`1", this, out this.variableLengthInlineArrayStruct);
152156
}
153157

154158
internal enum GeneratingElement
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
internal struct VariableLengthInlineArray<T>
2+
where T : unmanaged
3+
{
4+
internal T e0;
5+
6+
#if canUseUnsafeAdd
7+
internal ref T this[int index]
8+
{
9+
[UnscopedRef]
10+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
11+
get => ref Unsafe.Add(ref this.e0, index);
12+
}
13+
#endif
14+
15+
#if canUseSpan
16+
[UnscopedRef]
17+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
18+
internal Span<T> AsSpan(int length)
19+
{
20+
#if canCallCreateSpan
21+
return MemoryMarshal.CreateSpan(ref this.e0, length);
22+
#else
23+
unsafe
24+
{
25+
fixed (void* p = &this.e0)
26+
{
27+
return new Span<T>(p, length);
28+
}
29+
}
30+
#endif
31+
}
32+
#endif
33+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Runtime.InteropServices;
5+
using Windows.Win32.System.Ole;
6+
7+
public class FlexibleArrayTests
8+
{
9+
[Fact]
10+
public unsafe void FlexibleArraySizing()
11+
{
12+
const int count = 3;
13+
PAGESET* pPageSet = (PAGESET*)Marshal.AllocHGlobal(PAGESET.SizeOf(count));
14+
try
15+
{
16+
pPageSet->rgPages[0].nFromPage = 0;
17+
18+
Span<PAGERANGE> pageRange = pPageSet->rgPages.AsSpan(count);
19+
for (int i = 0; i < count; i++)
20+
{
21+
pageRange[i].nFromPage = i * 2;
22+
pageRange[i].nToPage = (i * 2) + 1;
23+
}
24+
}
25+
finally
26+
{
27+
Marshal.FreeHGlobal((IntPtr)pPageSet);
28+
}
29+
}
30+
31+
[Fact]
32+
public void SizeOf_Minimum1Element()
33+
{
34+
Assert.Equal(PAGESET.SizeOf(1), PAGESET.SizeOf(0));
35+
Assert.Equal(Marshal.SizeOf<PAGERANGE>(), PAGESET.SizeOf(2) - PAGESET.SizeOf(1));
36+
}
37+
}

test/GenerationSandbox.Tests/NativeMethods.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ MAKELRESULT
3030
MAKEWPARAM
3131
MAX_PATH
3232
NTSTATUS
33+
PAGESET
3334
PathParseIconLocation
3435
PROCESS_BASIC_INFORMATION
3536
PZZSTR

test/Microsoft.Windows.CsWin32.Tests/StructTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,22 @@ public void StructConstantsAreGeneratedAsConstants()
244244
Assert.NotEmpty(type.Members.OfType<FieldDeclarationSyntax>().Where(f => f.Modifiers.Any(SyntaxKind.ConstKeyword)));
245245
}
246246

247+
[Theory]
248+
[MemberData(nameof(TFMData))]
249+
public void FlexibleArrayMember(string tfm)
250+
{
251+
this.compilation = this.starterCompilations[tfm];
252+
this.GenerateApi("BITMAPINFO");
253+
var type = (StructDeclarationSyntax)Assert.Single(this.FindGeneratedType("BITMAPINFO"));
254+
FieldDeclarationSyntax flexArrayField = Assert.Single(type.Members.OfType<FieldDeclarationSyntax>(), m => m.Declaration.Variables.Any(v => v.Identifier.ValueText == "bmiColors"));
255+
var fieldType = Assert.IsType<GenericNameSyntax>(Assert.IsType<QualifiedNameSyntax>(flexArrayField.Declaration.Type).Right);
256+
Assert.Equal("VariableLengthInlineArray", fieldType.Identifier.ValueText);
257+
Assert.Equal("RGBQUAD", Assert.IsType<QualifiedNameSyntax>(Assert.Single(fieldType.TypeArgumentList.Arguments)).Right.Identifier.ValueText);
258+
259+
// Verify that the SizeOf method was generated.
260+
Assert.Single(this.FindGeneratedMethod("SizeOf"));
261+
}
262+
247263
[Theory]
248264
[CombinatorialData]
249265
public void InterestingStructs(

0 commit comments

Comments
 (0)