diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4bbac3b8..ea169edf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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
diff --git a/azure-pipelines/integration-test.yml b/azure-pipelines/integration-test.yml
index 965e247c..491f3533 100644
--- a/azure-pipelines/integration-test.yml
+++ b/azure-pipelines/integration-test.yml
@@ -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:
diff --git a/docfx/docs/composition.md b/docfx/docs/composition.md
index 634467a9..4d9a4b12 100644
--- a/docfx/docs/composition.md
+++ b/docfx/docs/composition.md
@@ -23,6 +23,12 @@ C# 14 or later in every project that consumes the generated extension members. T
default when targeting .NET 10 or later; otherwise set `14` (or
`Preview` / `Latest` / `LatestMajor`) in the project file.
+Composition also supports `true`.
+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:
@@ -30,7 +36,7 @@ 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 ` with members declared directly on it. |
-| **Extender** | `extensionReceiver: ""` set | A `partial class ` whose members live inside an `extension() { … }` block. |
+| **Extender** | `extensionReceiver: ""` set | A `partial class ` whose exposed members live inside an `extension() { … }` 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.
diff --git a/integration-tests/sdk-roslyn5/app/NativeMethods.txt b/integration-tests/sdk-roslyn5/app/NativeMethods.txt
index a7653a3c..b65747bb 100644
--- a/integration-tests/sdk-roslyn5/app/NativeMethods.txt
+++ b/integration-tests/sdk-roslyn5/app/NativeMethods.txt
@@ -1 +1,4 @@
GetForegroundWindow
+GetCurrentThreadId
+GetWindowThreadProcessId
+SetLastError
diff --git a/integration-tests/sdk-roslyn5/app/Program.cs b/integration-tests/sdk-roslyn5/app/Program.cs
index 3e9d3f76..5367e2f0 100644
--- a/integration-tests/sdk-roslyn5/app/Program.cs
+++ b/integration-tests/sdk-roslyn5/app/Program.cs
@@ -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
@@ -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);
}
}
diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs
index 1197c9ef..273fe0ce 100644
--- a/src/Microsoft.Windows.CsWin32/Generator.cs
+++ b/src/Microsoft.Windows.CsWin32/Generator.cs
@@ -1450,7 +1450,7 @@ private IReadOnlyList FindTypeSymbolsIfAlreadyAvailable(string fullyQua
/// Wraps a list of members in an extension (Receiver) { ... } block when 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).
///
/// The members that would otherwise be added directly to the host static class.
- /// 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.
+ /// 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.
private SyntaxList WrapAsExtensionMembers(SyntaxList members)
{
#if ROSLYN5
@@ -1467,10 +1467,51 @@ private SyntaxList WrapAsExtensionMembers(SyntaxList imports = default;
+ SyntaxList 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 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(extensionBlock);
+ ExtensionBlockDeclarationSyntax extensionBlock = ExtensionBlock(receiverType, extensionMembers);
+ SyntaxList result = SyntaxFactory.SingletonList(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