Skip to content

Commit

Permalink
Add source generator to emit supporting attribute types (#181)
Browse files Browse the repository at this point in the history
* Add blank dnne-analyzers project

* Add Roslyn dependencies and basic setup

* Add AttributesGenerator type to emit attributes

* Add DNNE.Analyzers.targets file

* Wire up source generator to build setup

* Use .NET 7 SDK in the CI script

* Add ExportAttribute to the generated attributes

* Update unit tests, remove local attributes

* Reference analyzer directly in test project if needed

* Add [AttributeUsage] to generated attributes

* Add [ExcludeFromCodeCoverage] to generated attributes

* Fix pseudo package scenario.
Import new targets file using DNNE.targets

---------

Co-authored-by: Aaron Robinson <arobins@microsoft.com>
  • Loading branch information
Sergio0694 and AaronRobinsonMSFT authored Nov 6, 2023
1 parent 0093cdc commit 4ea06f6
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 50 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
include-prerelease: true
- name: Build Product and Package
run: dotnet build src/create_package.proj -c ${{ matrix.flavor }}
Expand Down Expand Up @@ -51,7 +51,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
include-prerelease: true
- name: Build Product and Package
run: dotnet build src\create_package.proj -c ${{ matrix.flavor }}
Expand All @@ -76,7 +76,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
dotnet-version: '7.0.x'
include-prerelease: true
- name: Build Product and Package
run: dotnet build src/create_package.proj -c ${{ matrix.flavor }}
Expand Down
6 changes: 6 additions & 0 deletions DNNE.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "platform", "platform", "{95
src\platform\platform.c = src\platform\platform.c
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dnne-analyzers", "src\dnne-analyzers\dnne-analyzers.csproj", "{2A4C07AB-EAA1-4B80-8A80-BD2DB6375C35}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -47,6 +49,10 @@ Global
{451402EC-4AE5-425A-9835-9D36A55E8081}.Debug|Any CPU.Build.0 = Debug|Any CPU
{451402EC-4AE5-425A-9835-9D36A55E8081}.Release|Any CPU.ActiveCfg = Release|Any CPU
{451402EC-4AE5-425A-9835-9D36A55E8081}.Release|Any CPU.Build.0 = Release|Any CPU
{2A4C07AB-EAA1-4B80-8A80-BD2DB6375C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A4C07AB-EAA1-4B80-8A80-BD2DB6375C35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A4C07AB-EAA1-4B80-8A80-BD2DB6375C35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A4C07AB-EAA1-4B80-8A80-BD2DB6375C35}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
13 changes: 8 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ struct some_data
};
```

The following attributes can be used to enable the above scenario. They must be defined by the project in order to be used - DNNE provides no assembly to reference. Refer to [`ExportingAssembly`](./test/ExportingAssembly/Dnne.Attributes.cs) for an example.
The following attributes can be used to enable the above scenario. They are automatically generated into projects referencing DNNE, because DNNE provides no assembly to reference. If your build system or IDE does not support source generators (e.g., you're using a version older than Visual Studio 2022, or .NET Framework with `packages.config`), you will have to define these types yourself:

```CSharp
namespace DNNE
Expand All @@ -98,7 +98,8 @@ namespace DNNE
/// - stdint.h
/// - dnne.h
/// </remarks>
internal class C99DeclCodeAttribute : System.Attribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false)]
internal sealed class C99DeclCodeAttribute : System.Attribute
{
public C99DeclCodeAttribute(string code) { }
}
Expand All @@ -109,7 +110,8 @@ namespace DNNE
/// <remarks>
/// The level of indirection should be included in the supplied string.
/// </remarks>
internal class C99TypeAttribute : System.Attribute
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class C99TypeAttribute : System.Attribute
{
public C99TypeAttribute(string code) { }
}
Expand Down Expand Up @@ -196,12 +198,13 @@ In addition to providing declaration code directly, users can also supply `#incl

### Experimental attribute

There are scenarios where updating `UnmanagedCallersOnlyAttribute` may take time. In order to enable independent development and experimentation, the `DNNE.ExportAttribute` is also respected. This type can be modified to suit one's needs and `dnne-gen` updated to respect those changes at source gen time. The user should define the following in their assembly. They can then modify the attribute and `dnne-gen` as needed.
There are scenarios where updating `UnmanagedCallersOnlyAttribute` may take time. In order to enable independent development and experimentation, the `DNNE.ExportAttribute` is also respected. Like other DNNE attributes, this type is also automatically generated into projects referencing the DNNE package. This type can be modified to suit one's needs (by tweaking the generated source in `dnne-analyzers`) and `dnne-gen` updated as needed to respect those changes at source gen time.

``` CSharp
namespace DNNE
{
internal class ExportAttribute : Attribute
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class ExportAttribute : Attribute
{
public ExportAttribute() { }
public string EntryPoint { get; set; }
Expand Down
9 changes: 9 additions & 0 deletions src/dnne-analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

## Release 1.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
2 changes: 2 additions & 0 deletions src/dnne-analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
89 changes: 89 additions & 0 deletions src/dnne-analyzers/AttributesGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Microsoft.CodeAnalysis;

namespace DNNE;

/// <summary>
/// A generator that generates all the necessary DNNE attributes.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed class AttributesGenerator : IIncrementalGenerator
{
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static context =>
{
context.AddSource("DnneAttributes.g.cs", """
// <auto-generated/>
#pragma warning disable
namespace DNNE
{
/// <summary>
/// Defines a C export. Can be used when updating to use <c>UnmanagedCallersOnlyAttribute</c> would take more time.
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class ExportAttribute : global::System.Attribute
{
/// <summary>
/// Creates a new <see cref="ExportAttribute"/> instance.
/// </summary>
public ExportAttribute()
{
}
/// <summary>
/// Gets or sets the entry point to use to produce the C export.
/// </summary>
public string EntryPoint { get; set; }
}
/// <summary>
/// Provides C code to be defined early in the generated C header file.
/// </summary>
/// <remarks>
/// This attribute is respected on an exported method declaration or on a parameter for the method.
/// The following header files will be included prior to the code being defined.
/// <list type="bullet">
/// <item><c>stddef.h</c></item>
/// <item><c>stdint.h</c></item>
/// <item><c>dnne.h</c></item>
/// </list>
/// </remarks>
[global::System.AttributeUsage(global::System.AttributeTargets.Method | global::System.AttributeTargets.Parameter, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class C99DeclCodeAttribute : global::System.Attribute
{
/// <summary>
/// Creates a new <see cref="C99DeclCodeAttribute"/> instance with the specified parameters.
/// </summary>
/// <param name="code">The C code to be defined in the generated C header file.</param>
public C99DeclCodeAttribute(string code)
{
}
}
/// <summary>
/// Defines the C type to be used.
/// </summary>
/// <remarks>
/// The level of indirection should be included in the supplied string.
/// </remarks>
[global::System.AttributeUsage(global::System.AttributeTargets.Parameter | global::System.AttributeTargets.ReturnValue, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class C99TypeAttribute : global::System.Attribute
{
/// <summary>
/// Creates a new <see cref="C99TypeAttribute"/> instance with the specified parameters.
/// </summary>
/// <param name="code">The C type to be used.</param>
public C99TypeAttribute(string code)
{
}
}
}
""");
});
}
}
105 changes: 105 additions & 0 deletions src/dnne-analyzers/DNNE.Analyzers.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<Project>

<!-- Get the analyzer from the DNNE NuGet package -->
<Target Name="_DnneGatherAnalyzers">
<ItemGroup>
<_DnneAnalyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == 'DNNE'" />
</ItemGroup>
</Target>

<!-- Remove the analyzer if using Roslyn < 4.0 (DNNE's generators require Roslyn 4.0) -->
<Target Name="_DnneRemoveAnalyzersForRoslyn3"
Condition="'$(CSharpCoreTargetsPath)' != ''"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
DependsOnTargets="_DnneGatherAnalyzers">

<!--
Use the CSharpCoreTargetsPath property to find the version of the compiler we are using. This is the same mechanism
MSBuild uses to find the compiler. We could check the assembly version for any compiler assembly (since they all have
the same version) but Microsoft.Build.Tasks.CodeAnalysis.dll is where MSBuild loads the compiler tasks from so if
someone is getting creative with msbuild tasks/targets this is the "most correct" assembly to check.
-->
<GetAssemblyIdentity AssemblyFiles="$([System.IO.Path]::Combine(`$([System.IO.Path]::GetDirectoryName($(CSharpCoreTargetsPath)))`,`Microsoft.Build.Tasks.CodeAnalysis.dll`))">
<Output TaskParameter="Assemblies" ItemName="DnneCurrentCompilerAssemblyIdentity"/>
</GetAssemblyIdentity>

<PropertyGroup>

<!-- Transform the resulting item from GetAssemblyIdentity into a property representing its assembly version -->
<DnneCurrentCompilerVersion>@(DnneCurrentCompilerAssemblyIdentity->'%(Version)')</DnneCurrentCompilerVersion>

<!-- The CurrentCompilerVersionIsNotNewEnough property can now be defined based on the Roslyn assembly version -->
<DnneCurrentCompilerVersionIsNotNewEnough Condition="$([MSBuild]::VersionLessThan($(DnneCurrentCompilerVersion), 4.0))">true</DnneCurrentCompilerVersionIsNotNewEnough>
</PropertyGroup>

<!-- If the Roslyn version is < 4.0, disable the source generators -->
<ItemGroup Condition ="'$(DnneCurrentCompilerVersionIsNotNewEnough)' == 'true'">
<Analyzer Remove="@(_DnneAnalyzer)"/>
</ItemGroup>

<!--
If the source generators are disabled, also emit a warning. This would've been produced by MSBuild itself as well, but
emitting this manually lets us customize the message to inform developers as to why exactly the generators have been
disabled, and that DNNE will not work at all unless a more up to date IDE or compiler version are used.
-->
<Warning Condition ="'$(DnneCurrentCompilerVersionIsNotNewEnough)' == 'true'"
Code="DNNECFG0001"
Text="The DNNE source generators have been disabled on the current configuration, as they need Roslyn 4.0 in order to work. DNNE requires the source generators to generate the additional supporting attribute types. To use them, a more up to date IDE (eg. VS 2022 17.0 or greater) or .NET SDK version (.NET 6.0.400 SDK or greater) is needed."/>
</Target>

<!-- Remove the analyzer if Roslyn is missing -->
<Target Name="_DnneRemoveAnalyzersForRosynNotFound"
Condition="'$(CSharpCoreTargetsPath)' == ''"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
DependsOnTargets="_DnneGatherAnalyzers">

<!-- If no Roslyn assembly could be found, just remove the analyzer without emitting a warning -->
<ItemGroup>
<Analyzer Remove="@(_DnneAnalyzer)"/>
</ItemGroup>
</Target>

<!--
Inform the user if packages.config is used (as the analyzers and the source generators
won't work at all). Since packages.config can only be used with legacy-style projects,
the entire package can be skipped if an SDK-style project is used.
-->
<Target Name="_DnneWarnForPackagesConfigUse"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
Condition="'$(UsingMicrosoftNetSDK)' != 'true'">

<!--
Check whether packages are being restored via packages.config, by reading the associated MSBuild property.
This happens when either the project style is using packages.config, or when explicitly requested.
See https://learn.microsoft.com/nuget/reference/msbuild-targets#restoring-packagereference-and-packagesconfig-projects-with-msbuild.
-->
<PropertyGroup>
<DnneIsTargetProjectUsingPackagesConfig Condition ="'$(RestorePackagesConfig)' == 'true' OR '$(RestoreProjectStyle)' == 'PackagesConfig'">true</DnneIsTargetProjectUsingPackagesConfig>
</PropertyGroup>

<!--
If no packages.config properties are set, also try to manually find the packages.config file.
This will be in the @(None) elements, if present. Doing so makes sure this works in builds as
well, since the implicit targets populating the properties above only run when restoring.
Since the packages.config file will always be in the root of the project, if present, we will
match with the full item spec (see https://learn.microsoft.com/nuget/reference/packages-config).
-->
<FindInList ItemSpecToFind="packages.config"
List="@(None)"
MatchFileNameOnly="false"
Condition="'$(DnneIsTargetProjectUsingPackagesConfig)' != 'true'">
<Output TaskParameter="ItemFound" PropertyName="DnnePackagesConfigFile"/>
</FindInList>

<!-- Make sure to update the MSBuild property if the above task did find something -->
<PropertyGroup>
<DnneIsTargetProjectUsingPackagesConfig Condition ="'$(DnnePackagesConfigFile)' == 'packages.config'">true</DnneIsTargetProjectUsingPackagesConfig>
</PropertyGroup>

<!-- Emit a warning in case packages.config is used -->
<Warning Condition ="'$(DnneIsTargetProjectUsingPackagesConfig)' == 'true'"
Code="DNNECFG0002"
Text="The DNNE source generators might not be loaded correctly, as the current project is using the packages.config setup to restore NuGet packages. Source generators require PackageReference to be used (either in a legacy-style or SDK-style .csproj project, both are supported as long as PackageReference is used)."/>
</Target>

</Project>
18 changes: 18 additions & 0 deletions src/dnne-analyzers/dnne-analyzers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>DNNE</RootNamespace>
<LangVersion>11.0</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" Pack="false" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
10 changes: 9 additions & 1 deletion src/dnne-pkg/dnne-pkg.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand Down Expand Up @@ -37,6 +37,9 @@

<!-- Reference the project to trigger build -->
<ItemGroup>
<ProjectReference Include="../dnne-analyzers/dnne-analyzers.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="../dnne-gen/dnne-gen.csproj">
<Properties>TargetFramework=$(TargetFramework)</Properties>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
Expand All @@ -57,6 +60,8 @@
<Net472Dest>$(BuildDest)net472/</Net472Dest>
<ToolsDest>$(PseudoPackageRoot)tools/</ToolsDest>
<PlatformDest>$(ToolsDest)platform/</PlatformDest>
<AnalyzersDest>$(PseudoPackageRoot)analyzers/</AnalyzersDest>
<CSAnalyzersDest>$(AnalyzersDest)dotnet/cs</CSAnalyzersDest>
</PropertyGroup>

<!-- Create the pseudo package directories -->
Expand All @@ -66,16 +71,19 @@
<ItemGroup>
<BuildFiles Include="../msbuild/DNNE.props" />
<BuildFiles Include="../msbuild/DNNE.targets" />
<BuildFiles Include="../dnne-analyzers/DNNE.Analyzers.targets" />
<NetStandard21Task Include="../msbuild/DNNE.BuildTasks/bin/$(Configuration)/netstandard2.1/*" />
<Net472Task Include="../msbuild/DNNE.BuildTasks/bin/$(Configuration)/net472/*" />
<DnneGenFiles Include="../dnne-gen/bin/$(Configuration)/$(TargetFramework)/dnne-gen.dll;../dnne-gen/bin/$(Configuration)/$(TargetFramework)/dnne-gen.runtimeconfig.json" />
<PlatformFiles Include="../platform/*" />
<CSAnalyzers Include="../dnne-analyzers/bin/$(Configuration)/netstandard2.0/dnne-analyzers.dll" />
</ItemGroup>
<Copy SourceFiles="@(BuildFiles)" DestinationFolder="$(BuildDest)" />
<Copy SourceFiles="@(NetStandard21Task)" DestinationFolder="$(NetStandard21Dest)" />
<Copy SourceFiles="@(Net472Task)" DestinationFolder="$(Net472Dest)" />
<Copy SourceFiles="@(DnneGenFiles)" DestinationFolder="$(ToolsDest)" />
<Copy SourceFiles="@(PlatformFiles)" DestinationFolder="$(PlatformDest)" />
<Copy SourceFiles="@(CSAnalyzers)" DestinationFolder="$(CSAnalyzersDest)" />

<!-- Define the NuPkg content -->
<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/msbuild/DNNE.targets
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ DNNE.targets
-->
<Project>
<Import Project="DNNE.Analyzers.targets"/>

<PropertyGroup>
<!-- Let the user define the name of the native binary -->
<DnneNativeExportsBinaryName>$(DnneNativeBinaryName)</DnneNativeExportsBinaryName>
Expand Down
2 changes: 1 addition & 1 deletion test/DNNE.UnitTests/DNNE.UnitTests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
Loading

0 comments on commit 4ea06f6

Please sign in to comment.