Skip to content
Merged
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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ jobs:
- name: 🧪 dotnet build sdk-roslyn5 (extensionReceiver)
run: dotnet build
working-directory: ${{ runner.temp }}/integration-tests/sdk-roslyn5
- name: 🧪 dotnet build sdk-roslyn5 (extensionReceiver, build task)
run: dotnet build -t:Rebuild -p:CsWin32RunAsBuildTask=true
working-directory: ${{ runner.temp }}/integration-tests/sdk-roslyn5
- name: 🧪 VS 2022 MSBuild buildtask-sdk-style
run: |
$msbuild = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -version '[17.0,18.0)' -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1
Expand Down
4 changes: 4 additions & 0 deletions azure-pipelines/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ jobs:
displayName: 🧪 dotnet build sdk-roslyn5 (extensionReceiver)
workingDirectory: $(Pipeline.Workspace)\integration-tests\sdk-roslyn5
condition: eq(variables.currentSdk, 'true')
- pwsh: dotnet build -t:Rebuild -p:CsWin32RunAsBuildTask=true
displayName: 🧪 dotnet build sdk-roslyn5 (extensionReceiver, build task)
workingDirectory: $(Pipeline.Workspace)\integration-tests\sdk-roslyn5
condition: eq(variables.currentSdk, 'true')
- task: MSBuild@1
displayName: 🧪 MSBuild buildtask-sdk-style
inputs:
Expand Down
8 changes: 7 additions & 1 deletion docfx/docs/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ C# 14 or later in every project that consumes the generated extension members. T
default when targeting .NET 10 or later; otherwise set `<LangVersion>14</LangVersion>` (or
`Preview` / `Latest` / `LatestMajor`) in the project file.

Composition also supports `<CsWin32RunAsBuildTask>true</CsWin32RunAsBuildTask>`.
When source-generated marshalling is enabled, CsWin32 places `LibraryImport` declarations
in a private nested partial class and emits forwarding methods in the extension block,
because `LibraryImport` cannot generate implementations inside extension blocks.
Callers still use `PInvoke.X()`; source-generated marshalling remains enabled.

## Two roles

Every assembly that runs CsWin32 plays exactly one role:

| Role | `NativeMethods.json` shape | What the generator emits |
|---|---|---|
| **Owner** | `extensionReceiver` *not* set | A plain `partial class <className>` with members declared directly on it. |
| **Extender** | `extensionReceiver: "<OwnerClassName>"` set | A `partial class <className>` whose members live inside an `extension(<OwnerClassName>) { … }` block. |
| **Extender** | `extensionReceiver: "<OwnerClassName>"` set | A `partial class <className>` whose exposed members live inside an `extension(<OwnerClassName>) { … }` block. |

In a layered stack, the lowest assembly is the owner; every higher assembly is an extender. Each
extender must use a `className` that differs from the owner's and from every other extender's.
Expand Down
3 changes: 3 additions & 0 deletions integration-tests/sdk-roslyn5/app/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
GetForegroundWindow
GetCurrentThreadId
GetWindowThreadProcessId
SetLastError
6 changes: 5 additions & 1 deletion integration-tests/sdk-roslyn5/app/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// This test verifies that the Roslyn 5.0 analyzer leg is loaded and the
// extensionReceiver feature works. The base project generates PInvoke with
// GetTickCount. This app project uses extensionReceiver to attach
// GetForegroundWindow as an extension member of the base PInvoke class.
// native APIs as extension members of the base PInvoke class, using either
// analyzer or build-task generation.
//
// If the Roslyn 4.11 leg were loaded instead, PInvoke013 would reject
// extensionReceiver, GetForegroundWindow would land on AppPInvokes, and
Expand All @@ -18,5 +19,8 @@ static void Main(string[] args)

// From this project via extensionReceiver (C# 14 extension member)
HWND hwnd = PInvoke.GetForegroundWindow();
uint threadId = PInvoke.GetCurrentThreadId();
PInvoke.GetWindowThreadProcessId(hwnd, out uint processId);
PInvoke.SetLastError(0);
}
}
47 changes: 44 additions & 3 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,7 @@ private IReadOnlyList<ISymbol> FindTypeSymbolsIfAlreadyAvailable(string fullyQua
/// Wraps a list of members in an <c>extension (Receiver) { ... }</c> block when <see cref="GeneratorOptions.ExtensionReceiver"/> is configured; otherwise returns the input unchanged. Under the Roslyn 4 leg the wrap is a no-op (the feature is gated to Roslyn 5 + C# 14).
/// </summary>
/// <param name="members">The members that would otherwise be added directly to the host static class.</param>
/// <returns>A new member list (length 1) containing an extension block, or the original list when no extension receiver is configured, the input is empty, or the analyzer is the Roslyn 4 leg.</returns>
/// <returns>An extension block and, when needed, a nested partial class for source-generated imports; otherwise the original list when no extension receiver is configured, the input is empty, or the analyzer is the Roslyn 4 leg.</returns>
private SyntaxList<MemberDeclarationSyntax> WrapAsExtensionMembers(SyntaxList<MemberDeclarationSyntax> members)
{
#if ROSLYN5
Expand All @@ -1467,10 +1467,51 @@ private SyntaxList<MemberDeclarationSyntax> WrapAsExtensionMembers(SyntaxList<Me
return members;
}

string importsClassName = this.options.ClassName + "Imports";
SyntaxList<MemberDeclarationSyntax> imports = default;
SyntaxList<MemberDeclarationSyntax> extensionMembers = default;
foreach (MemberDeclarationSyntax member in members)
{
if (member is MethodDeclarationSyntax method && method.Modifiers.Any(SyntaxKind.PartialKeyword))
{
// LibraryImport cannot generate implementations inside an extension block.
// Keep the import in a partial type and expose a forwarding extension member instead.
imports = imports.Add(method);
InvocationExpressionSyntax invocation = InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ParseName($"global::{this.Namespace}.{this.options.ClassName}.{importsClassName}"),
IdentifierName(method.Identifier)),
[.. method.ParameterList.Parameters.Select(parameter => Argument(IdentifierName(parameter.Identifier))
.WithRefKindKeyword(parameter.Modifiers.FirstOrDefault(modifier => modifier.IsKind(SyntaxKind.RefKeyword) || modifier.IsKind(SyntaxKind.OutKeyword) || modifier.IsKind(SyntaxKind.InKeyword))))]);
SyntaxList<AttributeListSyntax> attributes = [.. method.AttributeLists
.Select(list => list.WithAttributes([.. list.Attributes.Where(attribute => attribute.Name.ToString() is not ("LibraryImport" or "DefaultDllImportSearchPaths"))]))
.Where(list => list.Attributes.Count > 0)];
extensionMembers = extensionMembers.Add(method
.WithAttributeLists(attributes)
.WithModifiers(SyntaxFactory.TokenList(method.Modifiers.Where(modifier => !modifier.IsKind(SyntaxKind.PartialKeyword))))
.WithBody(null)
.WithExpressionBody(ArrowExpressionClause(invocation))
.WithSemicolonToken(SemicolonWithLineFeed)
.WithLeadingTrivia(method.GetLeadingTrivia()));
}
else
{
extensionMembers = extensionMembers.Add(member);
}
}

// Use a global-qualified name so the receiver is unambiguous regardless of `using`s in the consuming file.
TypeSyntax receiverType = ParseName($"global::{this.Namespace}.{this.options.ExtensionReceiver}");
ExtensionBlockDeclarationSyntax extensionBlock = ExtensionBlock(receiverType, members);
return SyntaxFactory.SingletonList<MemberDeclarationSyntax>(extensionBlock);
ExtensionBlockDeclarationSyntax extensionBlock = ExtensionBlock(receiverType, extensionMembers);
SyntaxList<MemberDeclarationSyntax> result = SyntaxFactory.SingletonList<MemberDeclarationSyntax>(extensionBlock);
if (imports.Count > 0)
{
result = result.Add(ClassDeclaration(Identifier(importsClassName), imports)
.AddModifiers(TokenWithSpace(SyntaxKind.PrivateKeyword), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.PartialKeyword)));
}

return result;
#else
return members;
#endif
Expand Down
Loading