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
32 changes: 28 additions & 4 deletions docs/design/libraries/LibraryImportGenerator/Pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,31 @@ The P/Invoke source generator is responsible for finding all methods marked with
1. [Generate the corresponding P/Invoke](#pinvoke)
1. Add the generated source to the compilation.

The pipeline uses the Roslyn [Syntax APIs](https://learn.microsoft.com/dotnet/api/microsoft.codeanalysis.csharp.syntax) to create the generated code. This imposes some structure for the marshalling generators and allows for easier inspection or modification (if desired) of the generated code.
## Text emission

The interop generators share their text-emission infrastructure in
`src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration`.
Input analysis extracts type names, attributes, containing declarations, and marshalling
information into value models. Emission operates on these models, not on syntax trees.
`ContainingSyntaxContext` stores syntax-independent `DeclarationHeader` values shared
with the JSON source generator. `ContainingTypeUtilities` in
`src/libraries/Common/src/SourceGenerators` supplies containing-type traversal and
declaration formatting; interop retains source-spelled names, while JSON uses symbol-formatted
names and requires every containing type to be partial. Generic-parameter attributes remain
on the original declaration rather than being repeated on generated partial declarations.
`GeneratedParameter` and `GeneratedMethodSignature` describe signatures, and
`IndentedTextWriter` writes statements and scoped blocks with deterministic line endings.

Marshallers write each marshalling stage to a writer. The stub generators compose those
stages while preserving pinning scopes, exception handling, and resource-cleanup order.
Callers that need local functions own the body block, call `GenerateStubStatements`,
and emit the local function declarations themselves.
The pin stage writes `fixed` headers; the caller supplies the enclosed block. Close all
writer block scopes before obtaining their text with `ToString()`. Literal values must be
escaped with `CodeWriterHelpers.StringLiteral`, rather than interpolated as C# source.

Roslyn syntax APIs remain appropriate for inspecting input declarations and implementing
code fixes. They should not be used to construct, parse, or normalize generated output.

## Symbol and metadata processing

Expand Down Expand Up @@ -58,7 +82,7 @@ The marshalling generators are responsible for generating the code for each [sta

## Stub code generation

Generation of the stub code happens in stages. The marshalling generator for each parameter and return is called to generate code for each stage of the stub. The statements and syntax provided by each marshalling generator for each stage combine to form the full stub implementation.
Generation of the stub code happens in stages. The marshalling generator for each parameter and return is called to generate code for each stage of the stub. The text written by each marshalling generator for each stage combines to form the full stub implementation.

The stub code generator itself will handle some initial setup and variable declarations:
- Assign `out` parameters to `default`
Expand All @@ -74,7 +98,7 @@ The stub code generator itself will handle some initial setup and variable decla
- Call `Generate` on the marshalling generator for every parameter
1. `Pin`: data pinning in preparation for calling the generated P/Invoke
- Call `Generate` on the marshalling generator for every parameter
- Ignore any statements that are not `fixed` statements
- Write only `fixed` headers; the stub code generator supplies the enclosed block
1. `PinnedMarshal`: conversion of managed to native data
- Call `Generate` on the marshalling generator for every parameter
1. `Invoke`: call to the generated P/Invoke
Expand Down Expand Up @@ -181,7 +205,7 @@ Clearing the system error in step 1 is necessary because the native method may n

## P/Invoke

The P/Invoke called by the stub is created based on the user's original declaration of the stub. The signature is generated using the syntax returned by `AsNativeType` and `AsParameter` of the marshalling generators for the return and parameters. Any marshalling attributes on the return and parameters of the managed method - [`MarshalAsAttribute`][MarshalAsAttribute], [`InAttribute`][InAttribute], [`OutAttribute`][OutAttribute] - are dropped.
The P/Invoke called by the stub is created based on the user's original declaration of the stub. The native return type and parameter models supplied by the marshalling generators form a `GeneratedMethodSignature`, which is written directly as source text. Any marshalling attributes on the return and parameters of the managed method - [`MarshalAsAttribute`][MarshalAsAttribute], [`InAttribute`][InAttribute], [`OutAttribute`][OutAttribute] - are dropped.

The fields of the [`DllImportAttribute`][DllImportAttribute] are set based on the fields of `LibraryImportAttribute` as follows:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace SourceGenerators;

/// <summary>Extracts containing declarations during source-generator input analysis.</summary>
public static class ContainingTypeUtilities
{
public static IEnumerable<TypeDeclarationSyntax> EnumerateContainingTypes(TypeDeclarationSyntax? innermostType)
{
for (TypeDeclarationSyntax? current = innermostType; current is not null; current = current.Parent as TypeDeclarationSyntax)
{
yield return current;
}
}

public static ImmutableArray<string> GetModifiers(SyntaxTokenList modifiers)
=> modifiers.Select(static modifier => modifier.Text).ToImmutableArray();

public static DeclarationHeader GetDeclarationHeader(TypeDeclarationSyntax declaration)
{
return new DeclarationHeader(
GetModifiers(declaration.Modifiers),
GetTypeKindKeyword(declaration),
declaration.Identifier.Text,
declaration.Identifier.Text + GetTypeParameters(declaration.TypeParameterList));
}

public static DeclarationHeader GetDeclarationHeader(MethodDeclarationSyntax declaration)
{
return new DeclarationHeader(
GetModifiers(declaration.Modifiers),
"",
declaration.Identifier.Text,
declaration.Identifier.Text + GetTypeParameters(declaration.TypeParameterList));
}

public static bool TryGetContainingTypeDeclarations(
TypeDeclarationSyntax innermostType,
SemanticModel semanticModel,
CancellationToken cancellationToken,
[NotNullWhen(true)] out List<string>? declarations)
{
declarations = null;
foreach (TypeDeclarationSyntax current in EnumerateContainingTypes(innermostType))
{
if (!current.Modifiers.Any(SyntaxKind.PartialKeyword))
{
declarations = null;
return false;
}

INamedTypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(current, cancellationToken);
Debug.Assert(typeSymbol is not null);
var header = new DeclarationHeader(
GetModifiers(current.Modifiers),
GetTypeKindKeyword(current),
current.Identifier.Text,
typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
(declarations ??= new()).Add(header.ToString());
}
Debug.Assert(declarations?.Count > 0);
return true;
}

private static string GetTypeKindKeyword(TypeDeclarationSyntax declaration) => declaration.Kind() switch
{
SyntaxKind.ClassDeclaration => "class",
SyntaxKind.InterfaceDeclaration => "interface",
SyntaxKind.StructDeclaration => "struct",
SyntaxKind.RecordDeclaration => "record",
SyntaxKind.RecordStructDeclaration => "record struct",
_ => throw new InvalidOperationException("Unexpected containing declaration kind."),
};

private static string? GetTypeParameters(TypeParameterListSyntax? typeParameters)
{
if (typeParameters is null)
{
return null;
}

// Attributes on generic parameters are merged across partial declarations, so do not repeat them.
return "<" + string.Join(", ", typeParameters.Parameters.Select(static parameter =>
(parameter.VarianceKeyword.RawKind == 0 ? "" : parameter.VarianceKeyword.Text + " ")
+ parameter.Identifier.Text)) + ">";
Comment thread
Copilot marked this conversation as resolved.
}
}
59 changes: 59 additions & 0 deletions src/libraries/Common/src/SourceGenerators/DeclarationHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Linq;

namespace SourceGenerators;

/// <summary>Cacheable declaration text independent of Roslyn syntax nodes and symbols.</summary>
public readonly struct DeclarationHeader : IEquatable<DeclarationHeader>
{
public DeclarationHeader(ImmutableArray<string> modifiers, string keyword, string identifier, string? name = null)
{
Modifiers = modifiers;
Keyword = keyword;
Identifier = identifier;
Name = name ?? identifier;
}

public ImmutableArray<string> Modifiers { get; init; }
public string Keyword { get; init; }
public string Identifier { get; init; }

/// <summary>The declaration name, including any generic parameters but not their attributes.</summary>
public string Name { get; init; }

public bool Equals(DeclarationHeader other)
{
return Keyword == other.Keyword
&& Identifier == other.Identifier
&& Name == other.Name
&& Modifiers.SequenceEqual(other.Modifiers);
}

public override bool Equals(object? obj) => obj is DeclarationHeader other && Equals(other);

public override int GetHashCode()
{
unchecked
{
int hash = Keyword.GetHashCode();
hash = hash * 31 + Identifier.GetHashCode();
hash = hash * 31 + Name.GetHashCode();
foreach (string modifier in Modifiers)
{
hash = hash * 31 + modifier.GetHashCode();
}
return hash;
}
}

public override string ToString()
{
string modifiers = Modifiers.IsEmpty ? "" : string.Join(" ", Modifiers) + " ";
string keyword = Keyword.Length == 0 ? "" : Keyword + " ";
return modifiers + keyword + Name;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
using System;
using System.Collections.Generic;
Expand Down
Loading
Loading