Skip to content

Commit ecd70e0

Browse files
authored
Improve projection of MemorySize-d and optional ref/out parameters (#1511)
* Fix optional flexible array paramsHandle struct-typed MemorySized params and Optional out/ref params better * Apply CharSet workaround to Windows.Wdk also. Use unsafe syntax for out/ref params at p/invoke boundary.
1 parent da9906d commit ecd70e0

18 files changed

Lines changed: 577 additions & 82 deletions

File tree

docfx/docs/getting-started.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,65 @@ For example:
160160
}
161161
```
162162

163+
### Optional out/ref parameters
164+
165+
Some parameters in win32 are `[optional, out]` or `[optional, in, out]`. C# does not have an idiomatic way to represent this concept, so for any method that
166+
has such parameters, CsWin32 will generate two versions: one with all `ref` or `out` parameters included, and one with all such parameters omitted. For example:
167+
168+
```c#
169+
// Omitting the optional parameter:
170+
IsTextUnicode(buffer);
171+
172+
// Passing ref for optional parameter:
173+
IS_TEXT_UNICODE_RESULT result = default;
174+
IsTextUnicode(buffer, ref result);
175+
```
176+
177+
### Working with Span-typed and MemorySize-d parameters
178+
179+
In the Win32 APIs there are many functions where one parameter is a buffer (`void*` or `byte*`) and another parameter is the size of that buffer. When generating
180+
for a target framework that supports Spans, there will be overloads of these functions that take a `Span<byte>` which represents both of these parameters,
181+
since a Span refers to a chunk of memory and a length. For example, an API like [IsTextUnicode](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-istextunicode)
182+
has a `void*` parameter whose length is described by the iSize parameter in the native signature. The CsWin32 projection of this method will be:
183+
184+
```c#
185+
BOOL IsTextUnicode(ReadOnlySpan<byte> lpv, Span<IS_TEXT_UNICODE_RESULT> lpiResult)
186+
```
187+
188+
Instead of passing the buffer and length separately, in this projection you pass just one parameter. Span is a flexible type with many things that can
189+
be converted to it safely. You will also see Span parameters for things that may look like a struct but are variable sized. For example, [InitializeAcl](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-initializeacl)
190+
looks like it returns an ACL struct but the parameter is annotated with a `[MemorySize]` attribute in the metadata, indicating it is variable-sized based on another parameter.
191+
Thus, the cswin32 projection of this method will project this parameter as a `Span<byte>` since the size of the parameter is variable:
192+
193+
```c#
194+
// The cswin32 signature:
195+
static BOOL InitializeAcl(Span<byte> pAcl, ACE_REVISION dwAclRevision) { ... }
196+
```
197+
198+
And you would call this by creating a buffer to receive the ACL. Then, after the call you can reinterpret the buffer as an ACL:
199+
```c#
200+
// Make a buffer
201+
Span<byte> buffer = new byte[CalculateAclSize(...)];
202+
InitializeAcl(buffer, ACE_REVISION.ACL_REVISION);
203+
204+
// The beginning of the buffer is an ACL, so cast it to a ref:
205+
ref ACL acl = ref MemoryMarshal.AsRef<ACL>(buffer);
206+
207+
// Or treat it as a Span:
208+
Span<ACL> aclSpan = MemoryMarshal.Cast<byte, ACL>(buffer);
209+
```
210+
211+
CsWin32 will also generate a struct-typed parameter for convenience but this overload will pass `sizeof(T)` for the length parameter to the
212+
underlying Win32 API, so this only makes sense in some overloads such as [SHGetFileInfo](https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shgetfileinfow)
213+
where the parameter has an annotation indicating it's variable-sized, but the size is only ever `sizeof(SHFILEINFOW)`:
214+
215+
```c#
216+
// Span<byte> overload:
217+
static nuint SHGetFileInfo(string pszPath, FILE_FLAGS_AND_ATTRIBUTES dwFileAttributes, Span<byte> psfi, SHGFI_FLAGS uFlags)
218+
// ref SHGETFILEINFOW overload:
219+
static nuint SHGetFileInfo(string pszPath, FILE_FLAGS_AND_ATTRIBUTES dwFileAttributes, ref SHFILEINFOW psfi, SHGFI_FLAGS uFlags)
220+
```
221+
163222
### Newer metadata
164223

165224
To update the metadata used as the source for code generation, you may install a newer `Microsoft.Windows.SDK.Win32Metadata` package:

src/CsWin32Generator/Program.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ private unsafe bool ProcessNativeMethodsFile(SuperGenerator superGenerator, File
552552
}
553553
catch (Exception ex)
554554
{
555-
this.ReportError($"'{name}': {ex.Message}");
555+
this.ReportError($"'{name}': {this.ErrorChainToString(ex)}");
556556
errorCount++;
557557
}
558558
}
@@ -562,11 +562,18 @@ private unsafe bool ProcessNativeMethodsFile(SuperGenerator superGenerator, File
562562
}
563563
catch (Exception ex)
564564
{
565-
this.ReportError($"Failed to process NativeMethods.txt file: {ex.Message}");
565+
this.ReportError($"Failed to process NativeMethods.txt file: {this.ErrorChainToString(ex)}");
566566
return false;
567567
}
568568
}
569569

570+
private string ErrorChainToString(Exception ex)
571+
{
572+
return ex.InnerException is null
573+
? ex.Message
574+
: $"{ex.Message} => {this.ErrorChainToString(ex.InnerException)}";
575+
}
576+
570577
/// <summary>
571578
/// Generates and writes source files to the output directory.
572579
/// </summary>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ static SyntaxToken RefInOutKeyword(ParameterSyntax p) =>
380380
this.volatileCode.AddMemberToModule(moduleName, this.DeclareFriendlyOverloads(methodDefinition, exposedMethod, this.methodsAndConstantsClassName, FriendlyOverloadOf.ExternMethod, this.injectedPInvokeHelperMethods, avoidWinmdRootAlias: false));
381381
this.volatileCode.AddMemberToModule(moduleName, exposedMethod);
382382
}
383-
catch (Exception ex)
383+
catch (Exception ex) when (ex is not GenerationFailedException)
384384
{
385385
throw new GenerationFailedException($"Failed while generating extern method: {methodName}", ex);
386386
}

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,6 @@ private void DeclareUnscopedRefAttributeIfNecessary()
108108

109109
private void DeclareCharSetWorkaroundIfNecessary()
110110
{
111-
// Always generate these in the context of the most common metadata so we don't emit it more than once.
112-
if (!this.IsWin32Sdk)
113-
{
114-
this.MainGenerator.volatileCode.GenerationTransaction(() => this.MainGenerator.DeclareCharSetWorkaroundIfNecessary());
115-
return;
116-
}
117-
118111
const string name = "CharSet_Workaround";
119112
this.volatileCode.GenerateSpecialType(name, delegate
120113
{
@@ -125,6 +118,9 @@ private void DeclareCharSetWorkaroundIfNecessary()
125118
}
126119

127120
MemberDeclarationSyntax templateNamespace = compilationUnit.Members.Single();
121+
122+
// templateNamespace is System.Runtime.InteropServices, nest it within this generator's root namespace
123+
templateNamespace = NamespaceDeclaration(ParseName(this.Namespace)).AddMembers(templateNamespace);
128124
this.volatileCode.AddSpecialType(name, templateNamespace, topLevel: true);
129125
});
130126
}

0 commit comments

Comments
 (0)