diff --git a/.editorconfig b/.editorconfig index 85b0d0be..50dbcc3a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,15 @@ csharp_space_after_cast = true [*.cs] indent_style = space indent_size = 4 + +[*.csproj] +indent_style = space +indent_size = 4 + +[*.props] +indent_style = space +indent_size = 4 + +[*.targets] +indent_style = space +indent_size = 4 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 8630fb6a..00000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,4 +0,0 @@ -template: | - ## Changes since $PREVIOUS_TAG: - - $CHANGES diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..c15b9fe7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build + +on: + push: + branches: + - 'main' + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + global-json-file: ./global.json + + - name: Pack + run: dotnet pack SharpGenTools.sln --configuration Release -p:Packing=true + + - name: Pack SharpGen.Runtime.COM + run: dotnet pack SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln --configuration Release -p:Packing=true + + - name: Publish to NuGet + if: github.event_name == 'push' + run: dotnet nuget push artifacts/**/*.nupkg -k ${{secrets.NUGET_TOKEN}} --skip-duplicate --source https://api.nuget.org/v3/index.json diff --git a/Directory.Build.props b/Directory.Build.props index 2e198172..0e1462fe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,66 +1,60 @@ - - 2.0.0 - - - - - - $(VersionPrefix)-local - - - - - $(ReleaseTag) - - - - - $(VersionPrefix)-ci.$(BuildNumber) - - - - - - 10 - 00240000048000009400000006020000002400005253413100040000010001003dab93dc845fe6b52b20d86918a54f7300fa6959d56e9743c6f721857346811cd6a82d12132856755ab87e014127322421694fb522ad98fc3c6b65b389ab18ee3bbdec5c2ad5a8bef05599a3615c3e6afdade7eb2cf571b5ede7feb026b099fa94ee73f2f8dadcb6b1be62f7c984226eb0508d5ca6c3e394605c5cb0fa0851a2 - $(MSBuildThisFileDirectory)SharpGenTools.snk - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - false - true - - - - false - false - true - - - - jkoritzinsky - (c) 2010-2017 Alexandre Mutel, 2017-2018 Jeremy Koritzinsky - MIT - https://github.com/SharpGenTools/SharpGenTools - SharpGen;CodeGen;CPlusPlus;PInvoke;Native;COM - https://github.com/SharpGenTools/SharpGenTools - - - true - - - true - - - true - snupkg - - - - - + + 2.4.2 + beta + $(VersionPrefix)-$(VersionSuffix) + + + + $(MSBuildThisFileDirectory)NuGet.config + true + + + + $(MSBuildThisFileDirectory) + + + + latest + 00240000048000009400000006020000002400005253413100040000010001003dab93dc845fe6b52b20d86918a54f7300fa6959d56e9743c6f721857346811cd6a82d12132856755ab87e014127322421694fb522ad98fc3c6b65b389ab18ee3bbdec5c2ad5a8bef05599a3615c3e6afdade7eb2cf571b5ede7feb026b099fa94ee73f2f8dadcb6b1be62f7c984226eb0508d5ca6c3e394605c5cb0fa0851a2 + $(MSBuildThisFileDirectory)SharpGenTools.snk + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + false + true + + + + false + false + true + + + + jkoritzinsky + (c) 2010-2017 Alexandre Mutel, 2017-2023 Jeremy Koritzinsky, 2023-2024 Amer Koleci + MIT + https://github.com/SharpGenTools/SharpGenTools + $(MSBuildThisFileDirectory)artifacts/ + SharpGen;CodeGen;CPlusPlus;PInvoke;Native;COM + https://github.com/SharpGenTools/SharpGenTools + + + true + + + true + + + true + snupkg + + + + $(NoWarn);NETSDK1212 + diff --git a/Directory.Build.targets b/Directory.Build.targets index ac0efe94..10038fbd 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,7 +1,8 @@ - + + + + $(DefineConstants);SIGNED_BUILD + - - $(DefineConstants);SIGNED_BUILD - \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..24bfa93a --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,38 @@ + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 77d88787..4db1370a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/Packages.props b/Packages.props deleted file mode 100644 index 47d138fe..00000000 --- a/Packages.props +++ /dev/null @@ -1,53 +0,0 @@ - - - - - runtime; build; native; contentfiles; analyzers - all - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 40f42d62..d7d5c364 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # SharpGenTools -[![Build Status](https://dev.azure.com/SharpGenTools/SharpGenTools/_apis/build/status/SharpGenTools?branchName=master)](https://dev.azure.com/SharpGenTools/SharpGenTools/_build/latest?definitionId=1&branchName=master) [![MyGet Pre Release](https://img.shields.io/myget/sharpgentools/vpre/SharpGenTools.Sdk.svg)](https://www.myget.org/feed/Packages/sharpgentools) [![NuGet](https://img.shields.io/nuget/v/SharpGenTools.Sdk.svg)](https://www.nuget.org/packages/SharpGenTools.Sdk) [![Docs](https://readthedocs.org/projects/sharpgentools/badge/?version=latest)](https://sharpgentools.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/SharpGenTools/SharpGenTools/branch/master/graph/badge.svg)](https://codecov.io/gh/SharpGenTools/SharpGenTools) [![CodeFactor](https://www.codefactor.io/repository/github/sharpgentools/sharpgentools/badge)](https://www.codefactor.io/repository/github/sharpgentools/sharpgentools) +[![Build status](https://github.com/SharpGenTools/SharpGenTools/workflows/Build/badge.svg)](https://github.com/SharpGenTools/SharpGenTools/actions) +[![NuGet](https://img.shields.io/nuget/v/SharpGenTools.Sdk.svg)](https://www.nuget.org/packages/SharpGenTools.Sdk) [![Docs](https://readthedocs.org/projects/sharpgentools/badge/?version=latest)](https://sharpgentools.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/SharpGenTools/SharpGenTools/branch/main/graph/badge.svg)](https://codecov.io/gh/SharpGenTools/SharpGenTools) [![CodeFactor](https://www.codefactor.io/repository/github/sharpgentools/sharpgentools/badge)](https://www.codefactor.io/repository/github/sharpgentools/sharpgentools) Accurate and high performance C++ interop code generator for C#. @@ -31,25 +32,13 @@ Accurate and high performance C++ interop code generator for C#. * SDK-style (CPS) MSBuild projects * .NET environment, at least one of the following: - * .NET SDK (5 or newer) - * .NET Core SDK (2.1 or newer) - * Visual Studio 2017.3 with desktop .NET workload, .NET Framework 4.7.2 SDK or newer + * .NET SDK (7 or newer) + * .NET Core SDK (3.1 or newer) + * Visual Studio 2019 with desktop .NET workload, .NET Framework 4.7.2 SDK or newer * Make any mapping files a `SharpGenMapping` item in your `.csproj`. ### To Build -* .NET SDK: 5.0 or newer. +* .NET SDK: 7.0 or newer. * CMake: 3.0 or newer. -* SDK tests require x64 Windows, VS2019 with x86 and x64 C++ compilers, recent PowerShell version. - -## Nightly (CI) builds -Add SharpGenTools MyGet feed to your NuGet.config: - -```xml - - - - - - -``` \ No newline at end of file +* SDK tests require x64 Windows, VS2022 with x86 and x64 C++ compilers, recent PowerShell version. \ No newline at end of file diff --git a/SdkTests/Directory.Build.props b/SdkTests/Directory.Build.props index e54af4a2..cd917c52 100644 --- a/SdkTests/Directory.Build.props +++ b/SdkTests/Directory.Build.props @@ -6,8 +6,6 @@ true - - diff --git a/SdkTests/Directory.Build.targets b/SdkTests/Directory.Build.targets index 2ff7612c..7b5a698a 100644 --- a/SdkTests/Directory.Build.targets +++ b/SdkTests/Directory.Build.targets @@ -13,6 +13,4 @@ /> - - diff --git a/SdkTests/Interface/Mapping.xml b/SdkTests/Interface/Mapping.xml index 15719ba0..0c37d61f 100644 --- a/SdkTests/Interface/Mapping.xml +++ b/SdkTests/Interface/Mapping.xml @@ -29,7 +29,7 @@ - + @@ -39,10 +39,10 @@ - + - + diff --git a/SdkTests/Managed.props b/SdkTests/Managed.props index c55d73c3..5175ab23 100644 --- a/SdkTests/Managed.props +++ b/SdkTests/Managed.props @@ -12,12 +12,12 @@ - net5.0 + net6.0 x86 - netcoreapp2.1;net5.0 + netcoreapp3.1;net6.0 @@ -59,7 +59,7 @@ + Condition="'$(TargetFramework)' != 'net472' and '$(TargetFramework)' != 'netcoreapp3.1' and '$(TargetFramework)' != 'net6.0'"/> diff --git a/SharpGen.Generator/SharpGen.Generator.csproj b/SharpGen.Generator/SharpGen.Generator.csproj index 01238138..c45d9cdc 100644 --- a/SharpGen.Generator/SharpGen.Generator.csproj +++ b/SharpGen.Generator/SharpGen.Generator.csproj @@ -1,33 +1,28 @@ - + - + + netstandard2.0 + true + latest + false + enable + true + true + SHARPGEN_ROSLYN + - - net472;net5.0 - true - latest - false - enable - true - true - SHARPGEN_ROSLYN - + + + + - - - - - - - - - - - StatementSyntaxList.cs - - - SyntaxListBase.cs - - + + + StatementSyntaxList.cs + + + SyntaxListBase.cs + + \ No newline at end of file diff --git a/SharpGen.Generator/SharpGenModuleGenerator.GenerateModule.cs b/SharpGen.Generator/SharpGenModuleGenerator.GenerateModule.cs index dcc0bcc2..120ccc24 100644 --- a/SharpGen.Generator/SharpGenModuleGenerator.GenerateModule.cs +++ b/SharpGen.Generator/SharpGenModuleGenerator.GenerateModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -19,9 +20,10 @@ private static void GenerateModule(GeneratorExecutionContext context) List guidJobs = new(); List vtblJobs = new(); + List preserveInterfaceJobs = new(); void HandleGuid(ITypeSymbol symbol, Guid parsedGuid) => - guidJobs.Add(new GuidJob(symbol, Utilities.GetGuidParameters(parsedGuid))); + guidJobs.Add(new GuidJob(symbol, parsedGuid, Utilities.GetGuidParameters(parsedGuid))); void HandleVtbl(ITypeSymbol symbol, ITypeSymbol vtblTypeSymbol) { @@ -51,6 +53,9 @@ bool TypePredicate(INamedTypeSymbol x) => if (symbol.GetVtblAttribute() is { } vtblAttribute) HandleVtbl(symbol, vtblAttribute); + + if (symbol.HasBaseClass(CallbackBaseClassName)) + preserveInterfaceJobs.Add(new LinkerPreserveInterfaceJob(symbol)); } if (context.CancellationToken.IsCancellationRequested) @@ -58,16 +63,23 @@ bool TypePredicate(INamedTypeSymbol x) => StatementSyntaxList body = new(); - body.AddRange( - guidJobs, - job => ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - StorageField(ParseName(job.Type.ToDisplayString()), IdentifierName("Guid")), - ObjectCreationExpression(ParseTypeName("System.Guid")).WithArgumentList(job.Guid) + StatementSyntax GuidTransform(GuidJob job) => + context.Compilation.IsSymbolAccessibleWithin(job.Type, context.Compilation.Assembly) + ? ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + StorageField(ParseName(job.Type.ToDisplayString()), IdentifierName("Guid")), + ObjectCreationExpression(ParseTypeName("System.Guid"), job.GuidSyntax, default) + ) ) - ) - ); + : Block() + .WithLeadingTrivia( + Comment( + $"// Type {job.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} is inaccessible, but has GUID {job.Guid}" + ) + ); + + body.AddRange(guidJobs, GuidTransform); if (context.CancellationToken.IsCancellationRequested) return; @@ -173,6 +185,48 @@ localVtbl is not null } } + foreach (var preserveInterfaceJob in preserveInterfaceJobs) + { + var accessibility = preserveInterfaceJob.Type.DeclaredAccessibility; + if (Utilities.IsAnyOfFollowing(accessibility, Accessibility.Private, Accessibility.ProtectedOrInternal, Accessibility.ProtectedOrFriend, Accessibility.ProtectedAndInternal, Accessibility.NotApplicable, Accessibility.Protected)) + { + context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor + ( + "SG0000", + "Privately accessible classes that inherit from `CallbackBase` cannot be protected from Assembly Trimming.", + "Class {0} inheriting from `CallbackBase` should be marked with an accessibility modifier that makes it accessible from other classes (e.g. `internal`). A private accessibility modifier prevents SharpGenTools from protecting your callbacks against IL Linker/Assembly Trimmer.", + "SharpGenTools", + DiagnosticSeverity.Warning, + true, + null, + null, WellKnownDiagnosticTags.Build + ), null, preserveInterfaceJob.Type.ToDisplayString())); + } + else + { + body.Add(ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("SharpGen"), + IdentifierName("Runtime")), + IdentifierName("Trimming")), + IdentifierName("TrimmingHelpers")), + GenericName( + Identifier("PreserveMe")) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + IdentifierName(preserveInterfaceJob.Type.ToDisplayString())))))))); + } + } + if (body.Count == 0) return; @@ -184,31 +238,37 @@ localVtbl is not null .WithModifiers(staticModifier) .AddModifiers(Token(SyntaxKind.UnsafeKeyword)) .WithMembers( - SingletonList( - MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), "Initialize") - .WithModifiers(staticModifier) - .AddAttributeLists(ModuleInitializerAttributeList) - .WithBody(body.ToBlock()) + List( + new MemberDeclarationSyntax[] + { + SuppressWarningsStatement, + MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), "Initialize") + .WithModifiers(staticModifier) + .AddAttributeLists(ModuleInitializerAttributeList) + .WithBody(body.ToBlock()) + } ) ); context.AddSource( - "SharpGen.g.cs", + "SharpGen.Module.g.cs", SourceText.From(GenerateCompilationUnit(clazz).ToString(), Encoding.UTF8) ); } - private sealed record GuidJob(ITypeSymbol Type, ArgumentListSyntax Guid) + private sealed record GuidJob(ITypeSymbol Type, Guid Guid, ArgumentListSyntax GuidSyntax) { public readonly ITypeSymbol Type = Type ?? throw new ArgumentNullException(nameof(Type)); - public readonly ArgumentListSyntax Guid = Guid ?? throw new ArgumentNullException(nameof(Guid)); + public readonly ArgumentListSyntax GuidSyntax = GuidSyntax ?? throw new ArgumentNullException(nameof(GuidSyntax)); } + private sealed record LinkerPreserveInterfaceJob(ITypeSymbol Type); + private sealed class VtblJob { public readonly ITypeSymbol InterfaceType; public readonly ITypeSymbol VtblType; - public INamedTypeSymbol[] CallbackInterfaces { get; init; } + public INamedTypeSymbol[]? CallbackInterfaces { get; init; } public VtblJob(ITypeSymbol interfaceType, ITypeSymbol vtblType) { diff --git a/SharpGen.Generator/SharpGenModuleGenerator.GenerateUtilities.cs b/SharpGen.Generator/SharpGenModuleGenerator.GenerateUtilities.cs index d51371ff..c5afd455 100644 --- a/SharpGen.Generator/SharpGenModuleGenerator.GenerateUtilities.cs +++ b/SharpGen.Generator/SharpGenModuleGenerator.GenerateUtilities.cs @@ -45,43 +45,4 @@ public sealed partial class SharpGenModuleGenerator ) ) ); - - private static void GenerateUtilities(GeneratorExecutionContext context) - { - List attributes = new(1); - - if (context.Compilation.GetTypeByMetadataName(ModuleInitializerAttributeName) is not - { IsReferenceType: true, IsGenericType: false }) - attributes.Add(ModuleInitializerAttribute); - - if (attributes.Count == 0) - return; - - context.AddSource( - "SourceGeneratorUtilities.g.cs", - SourceText.From(GenerateCompilationUnit(attributes).ToString(), Encoding.UTF8) - ); - } - - private static NamespaceDeclarationSyntax ModuleInitializerAttribute => - NamespaceDeclaration( - QualifiedName( - QualifiedName(IdentifierName("System"), IdentifierName("Runtime")), - IdentifierName("CompilerServices") - ) - ) - .AddMembers( - ClassDeclaration("ModuleInitializerAttribute") - .AddAttributeLists( - AttributeList(SingletonSeparatedList(MethodAttributeUsage)), - AttributeList(SingletonSeparatedList(DebugConditionalAttribute)) - ) - .AddModifiers(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.SealedKeyword)) - .AddBaseListTypes(SimpleBaseType(Attribute)) - .AddMembers( - ConstructorDeclaration(Identifier("ModuleInitializerAttribute")) - .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) - .WithBody(Block()) - ) - ); } \ No newline at end of file diff --git a/SharpGen.Generator/SharpGenModuleGenerator.cs b/SharpGen.Generator/SharpGenModuleGenerator.cs index eec840d9..0070d33b 100644 --- a/SharpGen.Generator/SharpGenModuleGenerator.cs +++ b/SharpGen.Generator/SharpGenModuleGenerator.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -12,10 +13,30 @@ namespace SharpGen.Generator; public sealed partial class SharpGenModuleGenerator : ISourceGenerator { private const string CallbackableInterfaceName = "SharpGen.Runtime.ICallbackable"; + private const string CallbackBaseClassName = "SharpGen.Runtime.CallbackBase"; private const string ModuleInitializerAttributeName = "System.Runtime.CompilerServices.ModuleInitializerAttribute"; - private static readonly AttributeListSyntax ModuleInitializerAttributeList = AttributeList( - SingletonSeparatedList(Attribute(ParseName(ModuleInitializerAttributeName))) + private static readonly AttributeListSyntax[] ModuleInitializerAttributeList = new[] + { + // Module Initializer. + AttributeList( + SingletonSeparatedList(Attribute(ParseName(ModuleInitializerAttributeName))) + ) + }; + + private static readonly GlobalStatementSyntax SuppressWarningsStatement = GlobalStatement + ( + ExpressionStatement(IdentifierName( + Identifier( + TriviaList + ( + Trivia(IfDirectiveTrivia(IdentifierName("NET6_0_OR_GREATER"), true, false, false)), + DisabledText(@"[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(""ReflectionAnalysis"", ""IL2111"")] +[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(""ReflectionAnalysis"", ""IL2110"")] +"), + Trivia(EndIfDirectiveTrivia(true)) + ), + "", TriviaList()))).WithSemicolonToken(MissingToken(SyntaxKind.SemicolonToken)) ); private static readonly NameSyntax TypeDataStorage = ParseName("SharpGen.Runtime.TypeDataStorage"); @@ -42,8 +63,6 @@ public void Execute(GeneratorExecutionContext context) if (context.CancellationToken.IsCancellationRequested) return; - GenerateUtilities(context); - if (context.CancellationToken.IsCancellationRequested) return; diff --git a/SharpGen.Generator/Utilities.cs b/SharpGen.Generator/Utilities.cs index 47b5b843..9755dc14 100644 --- a/SharpGen.Generator/Utilities.cs +++ b/SharpGen.Generator/Utilities.cs @@ -150,4 +150,39 @@ static ArgumentSyntax Literal2(short value) => static ArgumentSyntax Literal4(int value) => LiteralArgument(Literal("0x" + value.ToString("X8"), value)); } + + /// + /// Returns true if the given symbol contains a base class with a given type name. + /// + /// The class symbol. + /// Full name of the type, e.g. \"SharpGen.Runtime.CallbackBase\" + /// + public static bool HasBaseClass(this ITypeSymbol symbol, string fullTypeName) + { + var baseClass = symbol.BaseType; + + while (baseClass != null) + { + if (baseClass.ToDisplayString() == fullTypeName) + return true; + + baseClass = baseClass.BaseType; + } + + return false; + } + + /// + /// Checks if an enum is any of the following given param values. + /// + public static bool IsAnyOfFollowing(T value, params T[] values) where T : Enum + { + foreach (var val in values) + { + if (value.Equals(val)) + return true; + } + + return false; + } } \ No newline at end of file diff --git a/SharpGen.Platform/IncludeDirectoryResolver.cs b/SharpGen.Platform/IncludeDirectoryResolver.cs index b07113f6..4feabcf0 100644 --- a/SharpGen.Platform/IncludeDirectoryResolver.cs +++ b/SharpGen.Platform/IncludeDirectoryResolver.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Microsoft.Win32; using SharpGen.Config; using SharpGen.Logging; @@ -75,7 +76,11 @@ public IReadOnlyList IncludePaths // Is Using registry? if (path.StartsWith("=")) { +#if NET6_0_OR_GREATER + if (OperatingSystem.IsWindows()) +#else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif { var registryPath = path.Substring(1); var indexOfSubPath = directory.Path.IndexOf(";"); @@ -119,6 +124,7 @@ public Item(string path, IncludeDirRule rule) public IncludeDirRule Rule { get; } } + [SupportedOSPlatform("windows")] private (string path, bool success) ResolveRegistryDirectory(string registryPath) { string path = null; diff --git a/SharpGen.Platform/SharpGen.Platform.csproj b/SharpGen.Platform/SharpGen.Platform.csproj index 26844435..db9a2364 100644 --- a/SharpGen.Platform/SharpGen.Platform.csproj +++ b/SharpGen.Platform/SharpGen.Platform.csproj @@ -1,53 +1,53 @@ - + - + + Library + netstandard2.0;net8.0 + true + true + - - Library - net472;net5.0 - true - + + + + + - - - - + + + <_Parameter1>SharpGen.UnitTests, PublicKey=$(SharpGenPublicKey) + + + <_Parameter1>SharpGenTools.Sdk, PublicKey=$(SharpGenPublicKey) + + - - - <_Parameter1>SharpGen.UnitTests, PublicKey=$(SharpGenPublicKey) - - - <_Parameter1>SharpGenTools.Sdk, PublicKey=$(SharpGenPublicKey) - - + + + - - - + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - - + \ No newline at end of file diff --git a/SharpGen.Runtime.COM/NuGet.config b/SharpGen.Runtime.COM/NuGet.config deleted file mode 100644 index 36c68fbb..00000000 --- a/SharpGen.Runtime.COM/NuGet.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/Program.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/Program.cs new file mode 100644 index 00000000..2b1bd9b3 --- /dev/null +++ b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/Program.cs @@ -0,0 +1,6 @@ +// See https://aka.ms/new-console-template for more information + +// I'm a dummy project for testing trimmability, since the analyzer outside of publish time isn't fully perfect yet +// test my trimming with `dotnet publish -r win-x64` + +Console.WriteLine("Hello SharpGen.Runtime.COM"); \ No newline at end of file diff --git a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/SharpGen.Runtime.COM.Trim.Dummy.csproj b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/SharpGen.Runtime.COM.Trim.Dummy.csproj new file mode 100644 index 00000000..4b012e62 --- /dev/null +++ b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.Trim.Dummy/SharpGen.Runtime.COM.Trim.Dummy.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + false + + + + + + + + + + + + diff --git a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj deleted file mode 100644 index ce070309..00000000 --- a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - net5.0;netstandard2.0;net45;netstandard1.3 - SharpGen.Runtime - C# COM Interop classes for use with SharpGenTools generated libraries - true - - true - $(CoreCompileDependsOn);SharpGenSetRoslynGeneratedPath - - - - true - false - - - - - - - - - - - $(IntermediateOutputPath)Generated - - - - - - true - - - - - - false - - - - - - - - diff --git a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln index fcf62b36..3f37b139 100644 --- a/SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln +++ b/SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln @@ -1,9 +1,11 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Runtime.COM", "SharpGen.Runtime.COM.csproj", "{1E2AA0D7-49F8-4E21-966D-60899298214C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Runtime.COM", "SharpGen.Runtime.COM\SharpGen.Runtime.COM.csproj", "{1E2AA0D7-49F8-4E21-966D-60899298214C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGen.Runtime.COM.Trim.Dummy", "SharpGen.Runtime.COM.Trim.Dummy\SharpGen.Runtime.COM.Trim.Dummy.csproj", "{E4721BB6-1428-47B8-A6C9-A0B622D50FFB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {1E2AA0D7-49F8-4E21-966D-60899298214C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E2AA0D7-49F8-4E21-966D-60899298214C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E2AA0D7-49F8-4E21-966D-60899298214C}.Release|Any CPU.Build.0 = Release|Any CPU + {E4721BB6-1428-47B8-A6C9-A0B622D50FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4721BB6-1428-47B8-A6C9-A0B622D50FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4721BB6-1428-47B8-A6C9-A0B622D50FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4721BB6-1428-47B8-A6C9-A0B622D50FFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SharpGen.Runtime.COM/.gitignore b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/.gitignore similarity index 100% rename from SharpGen.Runtime.COM/.gitignore rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/.gitignore diff --git a/SharpGen.Runtime.COM/ComActivationHelpers.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComActivationHelpers.cs similarity index 100% rename from SharpGen.Runtime.COM/ComActivationHelpers.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComActivationHelpers.cs diff --git a/SharpGen.Runtime.COM/ComContext.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComContext.cs similarity index 100% rename from SharpGen.Runtime.COM/ComContext.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComContext.cs diff --git a/SharpGen.Runtime.COM/ComUtilities.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComUtilities.cs similarity index 100% rename from SharpGen.Runtime.COM/ComUtilities.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/ComUtilities.cs diff --git a/SharpGen.Runtime.COM/Mapping.xml b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Mapping.xml similarity index 99% rename from SharpGen.Runtime.COM/Mapping.xml rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Mapping.xml index ed0f1206..14329eee 100644 --- a/SharpGen.Runtime.COM/Mapping.xml +++ b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Mapping.xml @@ -126,7 +126,7 @@ - + diff --git a/SharpGen.Runtime.COM/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj new file mode 100644 index 00000000..9a201955 --- /dev/null +++ b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj @@ -0,0 +1,57 @@ + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0;net462;net471 + SharpGen.Runtime + C# COM Interop classes for use with SharpGenTools generated libraries + true + + true + true + true + $(CoreCompileDependsOn);SharpGenSetRoslynGeneratedPath + + + + true + false + + + + + + + + + + + $(IntermediateOutputPath)Generated + + + + + + true + + + + + + false + + + + + + + + diff --git a/SharpGen.Runtime.COM/Win32/ComObjectEnumerator.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComObjectEnumerator.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/ComObjectEnumerator.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComObjectEnumerator.cs diff --git a/SharpGen.Runtime.COM/Win32/ComStreamProxy.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComStreamProxy.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/ComStreamProxy.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComStreamProxy.cs diff --git a/SharpGen.Runtime.COM/Win32/ComStringEnumerator.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComStringEnumerator.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/ComStringEnumerator.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ComStringEnumerator.cs diff --git a/SharpGen.Runtime.COM/Win32/ErrorCodeHelper.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ErrorCodeHelper.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/ErrorCodeHelper.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ErrorCodeHelper.cs diff --git a/SharpGen.Runtime.COM/Win32/ExceptionInfo.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ExceptionInfo.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/ExceptionInfo.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/ExceptionInfo.cs diff --git a/SharpGen.Runtime.COM/Win32/IEnumString.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IEnumString.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/IEnumString.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IEnumString.cs diff --git a/SharpGen.Runtime.COM/Win32/IEnumUnknown.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IEnumUnknown.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/IEnumUnknown.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IEnumUnknown.cs diff --git a/SharpGen.Runtime.COM/Win32/IPropertyBag2.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IPropertyBag2.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/IPropertyBag2.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IPropertyBag2.cs diff --git a/SharpGen.Runtime.COM/Win32/IPropertyStore.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IPropertyStore.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/IPropertyStore.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/IPropertyStore.cs diff --git a/SharpGen.Runtime.COM/Win32/PropertyKey.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/PropertyKey.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/PropertyKey.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/PropertyKey.cs diff --git a/SharpGen.Runtime.COM/Win32/Variant.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/Variant.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/Variant.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/Variant.cs diff --git a/SharpGen.Runtime.COM/Win32/VariantElementType.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantElementType.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/VariantElementType.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantElementType.cs diff --git a/SharpGen.Runtime.COM/Win32/VariantFullType.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantFullType.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/VariantFullType.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantFullType.cs diff --git a/SharpGen.Runtime.COM/Win32/VariantType.cs b/SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantType.cs similarity index 100% rename from SharpGen.Runtime.COM/Win32/VariantType.cs rename to SharpGen.Runtime.COM/SharpGen.Runtime.COM/Win32/VariantType.cs diff --git a/SharpGen.Runtime.Trim.Dummy.CallbackTest/CallbackHandlers.cs b/SharpGen.Runtime.Trim.Dummy.CallbackTest/CallbackHandlers.cs new file mode 100644 index 00000000..8b567562 --- /dev/null +++ b/SharpGen.Runtime.Trim.Dummy.CallbackTest/CallbackHandlers.cs @@ -0,0 +1,55 @@ +namespace SharpGen.Runtime.Trim.Dummy.CallbackTest; + +// Hi, if you remove CallbackBase from me, I will be trimmed away! +// Otherwise my interfaces are preserved +public class PublicCallbackHandler : CallbackBase, IHello +{ + public void SayHello() => Console.WriteLine("We're no strangers to love"); +} + +public class PublicCallbackHandlerEx : PublicCallbackHandler, IGoodbye +{ + public void SayGoodbye() => Console.WriteLine("Never gonna"); +} + +// I shouldn't be trimmed away either. +internal class InternalCallbackHandler : CallbackBase, IHello +{ + public void SayHello() => Console.WriteLine("You know the rules, and so do I"); +} + +internal class NestedCallbackHandlers +{ + // I should throw a compilation warning! [and not be preserved, because I can't] + private class PrivateNestedCallbackHandler : CallbackBase, IHello + { + public void SayHello() => Console.WriteLine("A full commitment's what I'm thinking of"); + } + + // I should be preserved + internal class NestedCallbackHandler : CallbackBase, IHello + { + public void SayHello() => Console.WriteLine("You wouldn't get this from any other guy"); + } + + // I can't should be preserved + protected class ProtectedCallbackHandler : CallbackBase, IHello + { + public void SayHello() => Console.WriteLine("You wouldn't get this from any other guy"); + } +} + +public interface IHello +{ + void SayHello(); +} + +public interface IGoodbye +{ + void SayGoodbye(); +} + + + +// To prevent SharpGen.Runtime.Trim.Dummy.CallbackTest from being trimmed away from Dummy; +public class UnrelatedClass { } \ No newline at end of file diff --git a/SharpGen.Runtime.Trim.Dummy.CallbackTest/SharpGen.Runtime.Trim.Dummy.CallbackTest.csproj b/SharpGen.Runtime.Trim.Dummy.CallbackTest/SharpGen.Runtime.Trim.Dummy.CallbackTest.csproj new file mode 100644 index 00000000..bc27816a --- /dev/null +++ b/SharpGen.Runtime.Trim.Dummy.CallbackTest/SharpGen.Runtime.Trim.Dummy.CallbackTest.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + true + true + true + false + + + + + + + + diff --git a/SharpGen.Runtime.Trim.Dummy/Program.cs b/SharpGen.Runtime.Trim.Dummy/Program.cs new file mode 100644 index 00000000..1bd94517 --- /dev/null +++ b/SharpGen.Runtime.Trim.Dummy/Program.cs @@ -0,0 +1,12 @@ +// See https://aka.ms/new-console-template for more information + +// I'm a dummy project for testing trimmability, since the analyzer outside of publish time isn't fully perfect yet +// test my trimming with `dotnet publish -r win-x64` + +using SharpGen.Runtime.Trim.Dummy.CallbackTest; + +Console.WriteLine("Hello SharpGen.Runtime"); + +// The purpose of this is to prevent `SharpGen.Runtime.Trim.Dummy.CallbackTest` from being linked away. +// as we need to test trimming on the callback. +var unrelated = new UnrelatedClass(); \ No newline at end of file diff --git a/SharpGen.Runtime.Trim.Dummy/SharpGen.Runtime.Trim.Dummy.csproj b/SharpGen.Runtime.Trim.Dummy/SharpGen.Runtime.Trim.Dummy.csproj new file mode 100644 index 00000000..12049858 --- /dev/null +++ b/SharpGen.Runtime.Trim.Dummy/SharpGen.Runtime.Trim.Dummy.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + true + false + + + + + + + + + + + + + diff --git a/SharpGen.Runtime/COM/ComObject.cs b/SharpGen.Runtime/COM/ComObject.cs index f4849bc7..b1ce6bd7 100644 --- a/SharpGen.Runtime/COM/ComObject.cs +++ b/SharpGen.Runtime/COM/ComObject.cs @@ -18,7 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable + using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -31,11 +34,14 @@ namespace SharpGen.Runtime; [Guid("00000000-0000-0000-C000-000000000046")] public class ComObject : CppObject, IUnknown { - public ComObject(IntPtr nativePtr): base(nativePtr) + public ComObject(IntPtr nativePtr) : base(nativePtr) { } - public static explicit operator ComObject(IntPtr nativePtr) => nativePtr == IntPtr.Zero ? null : new ComObject(nativePtr); + public static explicit operator ComObject?(IntPtr nativePtr) + { + return nativePtr == IntPtr.Zero ? null : new ComObject(nativePtr); + } /// HRESULT IUnknown::QueryInterface([In] const GUID& riid, [Out] void** ppvObject) /// IUnknown::QueryInterface @@ -43,7 +49,7 @@ public unsafe Result QueryInterface(Guid riid, out IntPtr ppvObject) { Result __result__; fixed (void* ppvObject_ = &ppvObject) - __result__ = ((delegate* unmanaged[Stdcall] )this[0U])(NativePointer, &riid, ppvObject_); + __result__ = ((delegate* unmanaged[Stdcall]) this[0U])(NativePointer, &riid, ppvObject_); return __result__; } @@ -52,7 +58,7 @@ public unsafe Result QueryInterface(Guid riid, out IntPtr ppvObject) public unsafe uint AddRef() { uint __result__; - __result__ = ((delegate* unmanaged[Stdcall] )this[1U])(NativePointer); + __result__ = ((delegate* unmanaged[Stdcall]) this[1U])(NativePointer); return __result__; } @@ -61,7 +67,7 @@ public unsafe uint AddRef() public unsafe uint Release() { uint __result__; - __result__ = ((delegate* unmanaged[Stdcall] )this[2U])(NativePointer); + __result__ = ((delegate* unmanaged[Stdcall]) this[2U])(NativePointer); return __result__; } @@ -69,7 +75,7 @@ public unsafe uint Release() /// Initializes a new instance of the class from a IUnknown object. /// /// Reference to a IUnknown object -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [SupportedOSPlatform("windows")] #endif public ComObject(object iunknownObject) : base(Marshal.GetIUnknownForObject(iunknownObject)) @@ -105,10 +111,14 @@ public virtual IntPtr QueryInterfaceOrNull(Guid guid) /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface - public virtual T QueryInterface() where T : ComObject + public virtual T QueryInterface< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>() where T : ComObject { QueryInterface(typeof(T).GetTypeInfo().GUID, out var parentPtr).CheckError(); - return MarshallingHelpers.FromPointer(parentPtr); + return MarshallingHelpers.FromPointer(parentPtr)!; } /// @@ -120,10 +130,14 @@ public virtual T QueryInterface() where T : ComObject /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [SupportedOSPlatform("windows")] #endif - public static T As(object comObject) where T : ComObject => As(Marshal.GetIUnknownForObject(comObject)); + public static T As< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(object comObject) where T : ComObject => As(Marshal.GetIUnknownForObject(comObject)); /// /// Queries a managed object for a particular COM interface support (This method is a shortcut to ) @@ -134,7 +148,11 @@ public virtual T QueryInterface() where T : ComObject /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface - public static T As(IntPtr iunknownPtr) where T : ComObject + public static T As< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(IntPtr iunknownPtr) where T : ComObject { using var tempObject = new ComObject(iunknownPtr); return tempObject.QueryInterface(); @@ -149,10 +167,14 @@ public static T As(IntPtr iunknownPtr) where T : ComObject /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [SupportedOSPlatform("windows")] #endif - public static T QueryInterface(object comObject) where T : ComObject => + public static T QueryInterface< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(object comObject) where T : ComObject => As(Marshal.GetIUnknownForObject(comObject)); /// @@ -164,7 +186,11 @@ public static T QueryInterface(object comObject) where T : ComObject => /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface - public static T QueryInterfaceOrNull(IntPtr comPointer) where T : ComObject + public static T? QueryInterfaceOrNull< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(IntPtr comPointer) where T : ComObject { using var tempObject = new ComObject(comPointer); return tempObject.QueryInterfaceOrNull(); @@ -178,16 +204,24 @@ public static T QueryInterfaceOrNull(IntPtr comPointer) where T : ComObject /// ms682521 /// IUnknown::QueryInterface /// IUnknown::QueryInterface - public virtual T QueryInterfaceOrNull() where T : ComObject => - MarshallingHelpers.FromPointer(QueryInterfaceOrNull(typeof(T).GetTypeInfo().GUID)); + public virtual T? QueryInterfaceOrNull< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>() where T : ComObject + { + return MarshallingHelpers.FromPointer(QueryInterfaceOrNull(typeof(T).GetTypeInfo().GUID)); + } /// /// Query Interface for a particular interface support and attach to the given instance. /// /// /// - protected void QueryInterfaceFrom(T fromObject) where T : ComObject => + protected void QueryInterfaceFrom(T fromObject) where T : ComObject + { NativePointer = fromObject.QueryInterfaceOrNull(GetType().GetTypeInfo().GUID); + } protected override void DisposeCore(IntPtr nativePointer, bool disposing) { diff --git a/SharpGen.Runtime/COM/ComObjectVtbl.cs b/SharpGen.Runtime/COM/ComObjectVtbl.cs index 6a16153c..f986f34b 100644 --- a/SharpGen.Runtime/COM/ComObjectVtbl.cs +++ b/SharpGen.Runtime/COM/ComObjectVtbl.cs @@ -26,12 +26,12 @@ namespace SharpGen.Runtime; public static unsafe class ComObjectVtbl { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER public static readonly IntPtr[] Vtbl = { - (IntPtr) (delegate* unmanaged[Stdcall]) (&QueryInterfaceImpl), - (IntPtr) (delegate* unmanaged[Stdcall]) (&AddRefImpl), - (IntPtr) (delegate* unmanaged[Stdcall]) (&ReleaseImpl) + (IntPtr) (delegate* unmanaged) (&QueryInterfaceImpl), + (IntPtr) (delegate* unmanaged) (&AddRefImpl), + (IntPtr) (delegate* unmanaged) (&ReleaseImpl) }; #else private static readonly QueryInterfaceDelegate QueryInterfaceDelegateCache = QueryInterfaceImpl; @@ -45,11 +45,11 @@ public static unsafe class ComObjectVtbl }; #endif -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int QueryInterfaceDelegate(IntPtr thisObject, Guid* guid, void* output); #else - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + [UnmanagedCallersOnly] #endif private static int QueryInterfaceImpl(IntPtr thisObject, Guid* guid, void* output) { @@ -78,20 +78,20 @@ private static int QueryInterfaceImpl(IntPtr thisObject, Guid* guid, void* outpu #endif } -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint AddRefDelegate(IntPtr thisObject); #else - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + [UnmanagedCallersOnly] #endif private static uint AddRefImpl(IntPtr thisObject) => MarshallingHelpers.AddRef(CppObjectShadow.ToCallback(thisObject)); -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint ReleaseDelegate(IntPtr thisObject); #else - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + [UnmanagedCallersOnly] #endif private static uint ReleaseImpl(IntPtr thisObject) => MarshallingHelpers.Release(CppObjectShadow.ToCallback(thisObject)); diff --git a/SharpGen.Runtime/COM/InspectableVtbl.cs b/SharpGen.Runtime/COM/InspectableVtbl.cs index 6f82f9e5..b94ab70a 100644 --- a/SharpGen.Runtime/COM/InspectableVtbl.cs +++ b/SharpGen.Runtime/COM/InspectableVtbl.cs @@ -27,12 +27,12 @@ namespace SharpGen.Runtime; public static unsafe class InspectableVtbl { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER public static readonly IntPtr[] Vtbl = { - (IntPtr) (delegate *unmanaged[Stdcall]) (&GetIids), - (IntPtr) (delegate *unmanaged[Stdcall]) (&GetRuntimeClassName), - (IntPtr) (delegate *unmanaged[Stdcall]) (&GetTrustLevel) + (IntPtr) (delegate *unmanaged) (&GetIids), + (IntPtr) (delegate *unmanaged) (&GetRuntimeClassName), + (IntPtr) (delegate *unmanaged) (&GetTrustLevel) }; #else private static readonly GetIidsDelegate GetIidsDelegateCache = GetIids; @@ -52,11 +52,11 @@ public static unsafe class InspectableVtbl /// /* [size_is][size_is][out] */ __RPC__deref_out_ecount_full_opt(*iidCount) IID **iids /// ) /// -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetIidsDelegate(IntPtr thisPtr, int* iidCount, IntPtr** iids); #else - [UnmanagedCallersOnly(CallConvs = new[]{typeof(CallConvStdcall)})] + [UnmanagedCallersOnly] #endif private static int GetIids(IntPtr thisPtr, int* iidCount, IntPtr** iids) { @@ -85,11 +85,11 @@ private static int GetIids(IntPtr thisPtr, int* iidCount, IntPtr** iids) /// /// HRESULT STDMETHODCALLTYPE GetRuntimeClassName([out] __RPC__deref_out_opt HSTRING *className) /// -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetRuntimeClassNameDelegate(IntPtr thisPtr, IntPtr* className); #else - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + [UnmanagedCallersOnly] #endif private static int GetRuntimeClassName(IntPtr thisPtr, IntPtr* className) { @@ -118,11 +118,11 @@ private static int GetRuntimeClassName(IntPtr thisPtr, IntPtr* className) /// /// HRESULT STDMETHODCALLTYPE GetTrustLevel(/* [out] */ __RPC__out TrustLevel *trustLevel); /// -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetTrustLevelDelegate(IntPtr thisPtr, int* trustLevel); #else - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + [UnmanagedCallersOnly] #endif private static int GetTrustLevel(IntPtr thisPtr, int* trustLevel) { diff --git a/SharpGen.Runtime/CallbackBase.Reflection.cs b/SharpGen.Runtime/CallbackBase.Reflection.cs index 8e8f8158..4d3f75f0 100644 --- a/SharpGen.Runtime/CallbackBase.Reflection.cs +++ b/SharpGen.Runtime/CallbackBase.Reflection.cs @@ -1,111 +1,223 @@ +#nullable enable + using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using SharpGen.Runtime.Trimming; namespace SharpGen.Runtime; public abstract partial class CallbackBase { - private static readonly Dictionary> TypeToShadowTypes = new(); - - private static Guid[] BuildGuidList(Type type) + private readonly struct ImmediateShadowInterfaceInfo { - List guids = new(); +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + public readonly TypeInfo Type; + public readonly List ImplementedInterfaces; - // Associate all shadows with their interfaces. - foreach (var item in GetUninheritedShadowedInterfaces(type)) + public ImmediateShadowInterfaceInfo( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TypeInfo type) { - var itemType = item.Type; - if (!ExcludeFromTypeListAttribute.Has(itemType)) - guids.Add(itemType.GUID); + Type = type; + ImplementedInterfaces = new(6); - // Associate also inherited interface to this shadow - foreach (var inheritInterface in item.ImplementedInterfaces) + foreach (var implementedInterface in type.ImplementedInterfaces) { - if (!ExcludeFromTypeListAttribute.Has(inheritInterface)) - guids.Add(inheritInterface.GUID); + var interfaceInfo = implementedInterface.GetTypeInfo(); + + // If there is no Vtbl attribute then this isn't a native interface. + if (!VtblAttribute.Has(interfaceInfo)) + continue; + + ImplementedInterfaces.Add(interfaceInfo); } } + } -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1 || NET472 - HashSet guidSet = new(guids.Count); -#else - HashSet guidSet = new(); + // Cache reflection on interface inheritance + private class CallbackTypeInfo + { +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif + private readonly TypeInfo type; + private ImmediateShadowInterfaceInfo[]? _vtbls; + private TypeInfo[]? _shadows; + private Guid[]? _guids; + + public CallbackTypeInfo( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + Type type) : this(type.GetTypeInfo()) + { + } - return guids.Where(guid => guidSet.Add(guid)).ToArray(); - } + private CallbackTypeInfo( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TypeInfo type) + { + this.type = type ?? throw new ArgumentNullException(nameof(type)); + } + /// + /// Gets a list of implemented interfaces that aren't inherited by any other [Vtbl] interfaces. + /// + /// The interface list. + public ImmediateShadowInterfaceInfo[] Vtbls + { + get + { + lock (this) + if (_vtbls is { } vtbls) + return vtbls; - /// - /// Gets a list of interfaces implemented by that aren't inherited by any other shadowed interfaces. - /// - /// The type for which to get the list. - /// The interface list. - private static List GetUninheritedShadowedInterfaces(Type type) - { - List list; - var typeToShadowTypes = TypeToShadowTypes; + var list = BuildVtblList(); + + lock (this) + _vtbls = list; - // Cache reflection on interface inheritance - lock (typeToShadowTypes) - if (typeToShadowTypes.TryGetValue(type, out list)) return list; + } + } - list = BuildUninheritedShadowedInterfacesList(type); + public TypeInfo[] Shadows + { + get + { + lock (this) + if (_shadows is { } shadows) + return shadows; - lock (typeToShadowTypes) - typeToShadowTypes[type] = list; + var list = BuildShadowList(); - return list; - } + lock (this) + _shadows = list; - private static List BuildUninheritedShadowedInterfacesList(Type type) - { - HashSet removeQueue = new(); - List result = new(); + return list; + } + } - foreach (var implementedInterface in type.GetTypeInfo().ImplementedInterfaces) + public Guid[] Guids { - var item = implementedInterface.GetTypeInfo(); + get + { + lock (this) + if (_guids is { } guids) + return guids; - // Only process interfaces that are have vtbl - if (!VtblAttribute.Has(item)) - continue; + var list = BuildGuidList(); - ImmediateShadowInterfaceInfo interfaceInfo = new(item); - result.Add(interfaceInfo); + lock (this) + _guids = list; - // Keep only final interfaces and not intermediate. - foreach (var @interface in interfaceInfo.ImplementedInterfaces) - removeQueue.Add(@interface); + return list; + } } - result.RemoveAll(item => removeQueue.Contains(item.Type)); - return result; - } + private ImmediateShadowInterfaceInfo[] BuildVtblList() + { + HashSet removeQueue = new(); + List result = new(); - private readonly struct ImmediateShadowInterfaceInfo - { - public readonly TypeInfo Type; - public readonly List ImplementedInterfaces; + foreach (var implementedInterface in type.ImplementedInterfaces) + { + var item = implementedInterface.GetTypeInfoWithNestedPreservedInterfaces(); + + // Only process interfaces that have vtbl + if (!VtblAttribute.Has(item)) + continue; + + ImmediateShadowInterfaceInfo interfaceInfo = new(item); + result.Add(interfaceInfo); + + // Keep only final interfaces and not intermediate. + foreach (var @interface in interfaceInfo.ImplementedInterfaces) + removeQueue.Add(@interface); + } + + result.RemoveAll(item => removeQueue.Contains(item.Type)); + return result.ToArray(); + } - public ImmediateShadowInterfaceInfo(TypeInfo type) + private Guid[] BuildGuidList() { - Type = type; - ImplementedInterfaces = new(6); + List guids = new(); + + // Associate all shadows with their interfaces. + foreach (var item in Vtbls) + { + var itemType = item.Type; + if (!ExcludeFromTypeListAttribute.Has(itemType)) + guids.Add(itemType.GUID); + + // Associate also inherited interface to this shadow + foreach (var inheritInterface in item.ImplementedInterfaces) + { + if (!ExcludeFromTypeListAttribute.Has(inheritInterface)) + guids.Add(inheritInterface.GUID); + } + } + +#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1 || NET472 + HashSet guidSet = new(guids.Count); +#else + HashSet guidSet = new(); +#endif + + return guids.Where(guid => guidSet.Add(guid)).ToArray(); + } + + private TypeInfo[] BuildShadowList() + { + List shadows = new(), result; foreach (var implementedInterface in type.ImplementedInterfaces) { - var interfaceInfo = implementedInterface.GetTypeInfo(); + var attribute = ShadowAttribute.Get(implementedInterface); - // If there is no Vtbl attribute then this isn't a native interface. - if (!VtblAttribute.Has(interfaceInfo)) + // Only process interfaces that have Shadow attribute + if (attribute is null) continue; - ImplementedInterfaces.Add(interfaceInfo); + shadows.Add(attribute.Type.GetTypeInfo()); } + + var count = shadows.Count; + result = new List(count); + + // Retain only shadows which have no other subtypes (none of the other shadows are children) + for (var i = 0; i < count; i++) + { + var item = shadows[i]; + + var any = false; + for (var j = 0; j < count; j++) + { + if (i == j) + continue; + + if (item.IsAssignableFrom(shadows[j])) + { + any = true; + break; + } + } + + if (!any) + result.Add(item); + } + + return result.ToArray(); } } } \ No newline at end of file diff --git a/SharpGen.Runtime/CallbackBase.ReflectionCache.cs b/SharpGen.Runtime/CallbackBase.ReflectionCache.cs new file mode 100644 index 00000000..63fe9a00 --- /dev/null +++ b/SharpGen.Runtime/CallbackBase.ReflectionCache.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using SharpGen.Runtime.Trimming; + +namespace SharpGen.Runtime; + +public abstract partial class CallbackBase +{ + private static readonly Dictionary TypeReflectionCache = new(); + + private CallbackTypeInfo GetTypeInfo() + { + CallbackTypeInfo? info; + var type = this.GetTypeWithNestedPreservedInterfaces(); + var cache = TypeReflectionCache; + + lock (cache) + { + if (cache.TryGetValue(type, out info)) + return info; + } + + info = new CallbackTypeInfo(type); + + lock (cache) + cache[type] = info; + + return info; + } +} \ No newline at end of file diff --git a/SharpGen.Runtime/CallbackBase.ReflectionImpl.cs b/SharpGen.Runtime/CallbackBase.ReflectionImpl.cs new file mode 100644 index 00000000..139c39e5 --- /dev/null +++ b/SharpGen.Runtime/CallbackBase.ReflectionImpl.cs @@ -0,0 +1,83 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace SharpGen.Runtime; + +public abstract unsafe partial class CallbackBase +{ + protected virtual Guid[] BuildGuidList() => GetTypeInfo().Guids; + + private GCHandle CreateShadow( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + TypeInfo type) + { + var shadow = (CppObjectShadow) Activator.CreateInstance(type.AsType())!; + + // Initialize the shadow with the callback + shadow.Initialize(ThisHandle); + + return GCHandle.Alloc(shadow, GCHandleType.Normal); + } + +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062", Justification = $"{nameof(ShadowAttribute.Type)} is already marked `DynamicallyAccessedMemberTypes.PublicConstructors` and the existing check via `Debug.Assert(holder.GetTypeInfo().GetConstructor(Type.EmptyTypes)` will ensure correctness.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Same as above.")] +#endif + protected virtual void InitializeCallableWrappers(IDictionary ccw) + { + // Associate all shadows with their interfaces. + var typeInfo = GetTypeInfo(); + + var interfaces = typeInfo.Vtbls; + if (interfaces.Length == 0) + return; + + // Lazy solution: a single shadow for the whole hierarchy. + // There are limitations to this approach in multi-inheritance scenarios, + // when there are multiple shadows inheriting one, and they are in separate vtbl trees. + // Then ToShadow methods. + var shadowTypes = typeInfo.Shadows; + + var shadowHandle = shadowTypes.Length switch + { + 0 => ThisHandle, + 1 => CreateShadow(shadowTypes[0]), + _ => CreateMultiInheritanceShadow(shadowTypes.Select(CreateShadow).ToArray()) + }; + + foreach (var item in interfaces) + { + Debug.Assert(VtblAttribute.Has(item.Type)); + + var success = TypeDataStorage.GetTargetVtbl(item.Type, out var vtbl); + Debug.Assert(success); + + var wrapper = CreateCallableWrapper(vtbl, shadowHandle); + + ccw[item.Type.GUID] = wrapper; + + // Associate also inherited interface to this shadow + foreach (var inheritInterface in item.ImplementedInterfaces) + { + var guid = inheritInterface.GUID; + + // If we have the same GUID as an already added interface, + // then there's already an accurate shadow for it, so we have nothing to do. + if (ccw.ContainsKey(guid)) + continue; + + // Use same CCW as derived + ccw[guid] = wrapper; + } + } + } +} \ No newline at end of file diff --git a/SharpGen.Runtime/CallbackBase.cs b/SharpGen.Runtime/CallbackBase.cs index ae3b7151..0be15dff 100644 --- a/SharpGen.Runtime/CallbackBase.cs +++ b/SharpGen.Runtime/CallbackBase.cs @@ -18,10 +18,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -35,10 +37,10 @@ namespace SharpGen.Runtime; /// public abstract unsafe partial class CallbackBase : DisposeBase, ICallbackable { - private Dictionary _ccw; + private Dictionary? _ccw; private IntPtr _guidPtr; - private IntPtr[] _guids; -#if NET5_0_OR_GREATER + private IntPtr[]? _guids; +#if NET6_0_OR_GREATER private uint _refCount = 1; #else private int _refCount = 1; @@ -78,7 +80,7 @@ protected virtual void DisposeCore(bool disposing) public uint AddRef() { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER return Interlocked.Increment(ref _refCount); #else return (uint)Interlocked.Increment(ref _refCount); @@ -87,7 +89,7 @@ public uint AddRef() public uint Release() { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER var newRefCount = Interlocked.Decrement(ref _refCount); #else var newRefCount = Interlocked.Decrement(ref _refCount); @@ -110,7 +112,7 @@ public ReadOnlySpan Guids if (_guids is { } guids) return guids; - var guidList = BuildGuidList(GetType()); + var guidList = BuildGuidList(); var guidCount = guidList.Length; var guidPtr = Marshal.AllocHGlobal(sizeof(Guid) * guidCount); var pGuid = (Guid*) guidPtr; @@ -134,7 +136,8 @@ public IntPtr Find(Guid guidType) if (_ccw is not { } guidToShadow) { guidToShadow = _ccw = new(4); - InitializeCallableWrapperStorage(); + Debug.Assert(!_thisHandle.IsAllocated); + InitializeCallableWrappers(_ccw); } return guidToShadow.TryGetValue(guidType, out var shadow) ? shadow : IntPtr.Zero; @@ -142,60 +145,34 @@ public IntPtr Find(Guid guidType) public IntPtr Find() where TCallback : ICallbackable => Find(TypeDataStorage.GetGuid()); - private void InitializeCallableWrapperStorage() + protected GCHandle ThisHandle => _thisHandle switch { - var ccw = _ccw; - Debug.Assert(ccw is not null); - Debug.Assert(ccw.Count == 0); - Debug.Assert(!_thisHandle.IsAllocated); - - // Associate all shadows with their interfaces. - var interfaces = GetUninheritedShadowedInterfaces(GetType()); - if (interfaces.Count == 0) - return; + { IsAllocated: true } handle => handle, + _ => _thisHandle = GCHandle.Alloc(this, GCHandleType.WeakTrackResurrection) + }; - var thisHandle = _thisHandle = GCHandle.Alloc(this, GCHandleType.WeakTrackResurrection); + protected static IntPtr CreateCallableWrapper(void* vtbl, GCHandle callback) + { + Debug.Assert(vtbl != (void*) 0); + Debug.Assert(callback.IsAllocated); + return CppObjectCallableWrapper.Create(vtbl, callback); + } - foreach (var item in interfaces) + protected static GCHandle CreateMultiInheritanceShadow(params GCHandle[] shadows) + { + if (shadows == null) throw new ArgumentNullException(nameof(shadows)); + return shadows.Length switch { - Debug.Assert(VtblAttribute.Has(item.Type)); - - GCHandle shadowHandle; - if (ShadowAttribute.Get(item.Type) is { Type: { } shadowType }) - { - var shadow = (CppObjectShadow) Activator.CreateInstance(shadowType); - - // Initialize the shadow with the callback - shadow.Initialize(thisHandle); - - shadowHandle = GCHandle.Alloc(shadow, GCHandleType.Normal); - } - else - { - shadowHandle = thisHandle; - } - - var success = TypeDataStorage.GetVtbl(item.Type.GUID, out var vtbl); - Debug.Assert(success); - - var wrapper = CppObjectShadow.CreateCallableWrapper(shadowHandle, vtbl); - - ccw[item.Type.GUID] = wrapper; - - // Associate also inherited interface to this shadow - foreach (var inheritInterface in item.ImplementedInterfaces) - { - var guid = inheritInterface.GUID; - - // If we have the same GUID as an already added interface, - // then there's already an accurate shadow for it, so we have nothing to do. - if (ccw.ContainsKey(guid)) - continue; - - // Use same CCW as derived - ccw[guid] = wrapper; - } - } + 0 => throw new ArgumentException( + "Multi-inheritance shadow cannot be constructed for empty shadow collection.", + nameof(shadows) + ), + 1 => throw new ArgumentException( + "Multi-inheritance shadow cannot be constructed for a single shadow.", + nameof(shadows) + ), + _ => GCHandle.Alloc(new CppObjectMultiShadow(shadows), GCHandleType.Normal) + }; } private void DisposeCallableWrappers(bool disposing) @@ -209,7 +186,7 @@ private void DisposeCallableWrappers(bool disposing) #endif foreach (var comObjectCallbackNative in shadows) if (freed.Add(comObjectCallbackNative)) - CppObjectShadow.FreeCallableWrapper(comObjectCallbackNative, disposing); + CppObjectCallableWrapper.Free(comObjectCallbackNative, disposing); } if (Interlocked.Exchange(ref _guidPtr, default) is var pointer) @@ -218,4 +195,37 @@ private void DisposeCallableWrappers(bool disposing) if (_thisHandle.IsAllocated) _thisHandle.Free(); } + + internal IReadOnlyCollection Shadows + { + get + { + Debug.Assert(_ccw is not null); + + HashSet shadows = new(ReferenceEqualityComparer.Instance); + + foreach (CppObjectCallableWrapper* ccw in _ccw!.Values) + { + var handle = ccw->Shadow; + if (!handle.IsAllocated) + continue; + + switch (handle.Target) + { + case CppObjectShadow shadow: + shadows.Add(shadow); + break; + case CppObjectMultiShadow multiShadow: + multiShadow.AddShadowsToSet(shadows); + break; + } + } + +#if NET45 + return shadows.ToArray(); +#else + return shadows; +#endif + } + } } \ No newline at end of file diff --git a/SharpGen.Runtime/CppObjectCallableWrapper.cs b/SharpGen.Runtime/CppObjectCallableWrapper.cs new file mode 100644 index 00000000..b465c0a4 --- /dev/null +++ b/SharpGen.Runtime/CppObjectCallableWrapper.cs @@ -0,0 +1,46 @@ +#nullable enable + +using System; +using System.Runtime.InteropServices; + +namespace SharpGen.Runtime; + +internal unsafe ref struct CppObjectCallableWrapper +{ + internal static readonly int Size = IntPtr.Size * 2; + + // ReSharper disable once NotAccessedField.Local + private void* _vtbl; + private IntPtr _shadow; + + public readonly GCHandle Shadow => GCHandle.FromIntPtr(_shadow); + + public static IntPtr Create(void* vtbl, GCHandle callback) + { + // Allocate ptr to vtbl + ptr to callback together + var nativePointer = Marshal.AllocHGlobal(Size); + ref var native = ref *(CppObjectCallableWrapper*) nativePointer; + + native._vtbl = vtbl; + native._shadow = GCHandle.ToIntPtr(callback); + + return nativePointer; + } + + public static void Free(IntPtr pointer, bool disposing) + { + // Free the callback + if (((CppObjectCallableWrapper*) pointer)->Shadow is { IsAllocated: true, Target: CppObjectShadow shadow } handle) + { + // Callback is a CppObjectShadow subtype. Dispose it if needed. + MemoryHelpers.Dispose(shadow, disposing); + + // Free GCHandle if it points to a shadow, not the CallbackBase. Why? + // Same GCHandle is reused in multiple CCWs to lower the handle table pressure. + handle.Free(); + } + + // Free instance + Marshal.FreeHGlobal(pointer); + } +} \ No newline at end of file diff --git a/SharpGen.Runtime/CppObjectMultiShadow.cs b/SharpGen.Runtime/CppObjectMultiShadow.cs new file mode 100644 index 00000000..777810e5 --- /dev/null +++ b/SharpGen.Runtime/CppObjectMultiShadow.cs @@ -0,0 +1,81 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace SharpGen.Runtime; + +internal sealed class CppObjectMultiShadow +{ + private readonly GCHandle[] _shadows; + + public CppObjectMultiShadow(GCHandle[] shadows) + { + _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); + +#if DEBUG + foreach (var handle in _shadows) + { + Debug.Assert(handle.IsAllocated); + Debug.Assert(handle.Target is CppObjectShadow or CppObjectMultiShadow); + } +#endif + } + + public T? ToShadow() where T : CppObjectShadow + { + foreach (var handle in _shadows) + { + switch (handle.Target) + { + case T shadow: + return shadow; + case CppObjectMultiShadow multiShadow when multiShadow.ToShadow() is { } shadow: + return shadow; + } + } + + return null; + } + + public bool ToCallback([NotNullWhen(true)] out T? value) where T : ICallbackable + { + foreach (var handle in _shadows) + { + switch (handle.Target) + { + case CppObjectShadow shadow: + value = shadow.ToCallback(); + return true; + case CppObjectMultiShadow multiShadow when multiShadow.ToShadow() is { } shadow: + value = shadow.ToCallback(); + return true; + } + } + + value = default; + return false; + } + + internal void AddShadowsToSet(HashSet shadows) + { + foreach (var handle in _shadows) + { + if (!handle.IsAllocated) + continue; + + switch (handle.Target) + { + case CppObjectShadow shadow: + shadows.Add(shadow); + break; + case CppObjectMultiShadow multiShadow: + multiShadow.AddShadowsToSet(shadows); + break; + } + } + } +} \ No newline at end of file diff --git a/SharpGen.Runtime/CppObjectShadow.cs b/SharpGen.Runtime/CppObjectShadow.cs index ac327ac7..ea2be973 100644 --- a/SharpGen.Runtime/CppObjectShadow.cs +++ b/SharpGen.Runtime/CppObjectShadow.cs @@ -18,8 +18,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable + using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SharpGen.Runtime; @@ -36,8 +41,7 @@ public abstract unsafe class CppObjectShadow static CppObjectShadow() { - Debug.Assert(Marshal.SizeOf(typeof(CppObjectNative)) == CppObjectNative.Size); - Debug.Assert(sizeof(CppObjectNative) == CppObjectNative.Size); + Debug.Assert(sizeof(CppObjectCallableWrapper) == CppObjectCallableWrapper.Size); } protected CppObjectShadow() @@ -54,63 +58,44 @@ internal void Initialize(GCHandle callbackHandle) this.callbackHandle = callbackHandle; } - internal static IntPtr CreateCallableWrapper(GCHandle callback, void* vtbl) - { - // Allocate ptr to vtbl + ptr to callback together - var nativePointer = Marshal.AllocHGlobal(CppObjectNative.Size); - ref var native = ref *(CppObjectNative*) nativePointer; - - Debug.Assert(callback.IsAllocated); - - native.VtblPointer = vtbl; - native.Shadow = callback; - - return nativePointer; - } - - internal static void FreeCallableWrapper(IntPtr pointer, bool disposing) - { - // Free the callback - if (((CppObjectNative*) pointer)->Shadow is { IsAllocated: true, Target: CppObjectShadow shadow } handle) - { - // Callback is a CppObjectShadow subtype. Dispose it if needed. - MemoryHelpers.Dispose(shadow, disposing); - - // Free GCHandle if it points to a shadow, not the CallbackBase. Why? - // Same GCHandle is reused in multiple CCWs to lower the handle table pressure. - handle.Free(); - } - - // Free instance - Marshal.FreeHGlobal(pointer); - } - - public static T ToShadow(IntPtr thisPtr) where T : CppObjectShadow + public static T ToAnyShadow(IntPtr thisPtr) where T : CppObjectShadow { Debug.Assert(thisPtr != IntPtr.Zero); - var handle = ((CppObjectNative*) thisPtr)->Shadow; + var handle = ((CppObjectCallableWrapper*) thisPtr)->Shadow; Debug.Assert(handle.IsAllocated); return handle.Target switch { T shadow => shadow, + CppObjectMultiShadow multiShadow => multiShadow.ToShadow() ?? throw new Exception($"Shadow {typeof(T).FullName} not found in the inheritance graph"), + CallbackBase callback => callback.Shadows.OfType().FirstOrDefault() ?? throw new Exception($"Shadow {typeof(T).FullName} not found in the inheritance graph"), null => throw new Exception($"Shadow {typeof(T).FullName} is dead"), + ICallbackable value => throw new Exception( + $"Shadow is of an unexpected {nameof(ICallbackable)} type {value.GetType().FullName}, expected {typeof(T).FullName}" + ), { } value => throw new Exception( $"Shadow is of an unexpected type {value.GetType().FullName}, expected {typeof(T).FullName}" ) }; } + public static IEnumerable ToAllShadows(IntPtr thisPtr) where T : CppObjectShadow => + ToCallback(thisPtr).Shadows.OfType(); + + public IEnumerable ToAllShadows() where T : CppObjectShadow => ToCallback().Shadows.OfType(); + public static T ToCallback(IntPtr thisPtr) where T : ICallbackable { Debug.Assert(thisPtr != IntPtr.Zero); - var handle = ((CppObjectNative*) thisPtr)->Shadow; + var handle = ((CppObjectCallableWrapper*) thisPtr)->Shadow; Debug.Assert(handle.IsAllocated); return handle.Target switch { T value => value, CppObjectShadow shadow => shadow.ToCallback(), + CppObjectMultiShadow multiShadow when multiShadow.ToCallback(out T? callback) => callback, + CppObjectMultiShadow => throw new Exception($"Shadow {typeof(T).FullName} is missing the callback in the whole inheritance graph"), null => throw new Exception($"Shadow {typeof(T).FullName} is dead"), { } value => throw new Exception( $"Shadow is of an unexpected type {value.GetType().FullName}, expected {typeof(T).FullName}" @@ -131,19 +116,4 @@ public T ToCallback() where T : ICallbackable ) }; } - - private ref struct CppObjectNative - { - internal static readonly int Size = IntPtr.Size * 2; - - // ReSharper disable once NotAccessedField.Local - public void* VtblPointer; - private IntPtr _shadowPointer; - - public GCHandle Shadow - { - readonly get => GCHandle.FromIntPtr(_shadowPointer); - set => _shadowPointer = GCHandle.ToIntPtr(value); - } - } } \ No newline at end of file diff --git a/SharpGen.Runtime/Diagnostics/ObjectTracker.cs b/SharpGen.Runtime/Diagnostics/ObjectTracker.cs index 8d04d44a..fd4444b2 100644 --- a/SharpGen.Runtime/Diagnostics/ObjectTracker.cs +++ b/SharpGen.Runtime/Diagnostics/ObjectTracker.cs @@ -167,7 +167,7 @@ internal static void MigrateNativePointer(CppObject cppObject, IntPtr oldNativeP /// /// Reports all COM object that are active and not yet disposed. /// - private static List> FindActiveObjects() + public static List> FindActiveObjects() { List> activeObjects; var objectReferences = ObjectReferences; diff --git a/SharpGen.Runtime/InterfaceArray.cs b/SharpGen.Runtime/InterfaceArray.cs index 74c5c4f7..cad61622 100644 --- a/SharpGen.Runtime/InterfaceArray.cs +++ b/SharpGen.Runtime/InterfaceArray.cs @@ -33,7 +33,11 @@ namespace SharpGen.Runtime; [DebuggerTypeProxy(typeof(InterfaceArray<>.InterfaceArrayDebugView))] [DebuggerDisplay("Count={" + nameof(Length) + "}")] [SuppressMessage("ReSharper", "ConvertToAutoProperty")] -public unsafe struct InterfaceArray : IReadOnlyList, IEnlightenedDisposable, IDisposable +public unsafe struct InterfaceArray< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif +T> : IReadOnlyList, IEnlightenedDisposable, IDisposable where T : CppObject { // .NET Native has issues with <...> in property backing fields in structs diff --git a/SharpGen.Runtime/Mapping.xml b/SharpGen.Runtime/Mapping.xml index 9c2933dd..754b58fc 100644 --- a/SharpGen.Runtime/Mapping.xml +++ b/SharpGen.Runtime/Mapping.xml @@ -1,48 +1,50 @@ - - - - - - - + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + \ No newline at end of file diff --git a/SharpGen.Runtime/MarshallingHelpers.cs b/SharpGen.Runtime/MarshallingHelpers.cs index a3aa2b93..708401d9 100644 --- a/SharpGen.Runtime/MarshallingHelpers.cs +++ b/SharpGen.Runtime/MarshallingHelpers.cs @@ -1,4 +1,7 @@ +#nullable enable + using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SharpGen.Runtime; @@ -11,8 +14,14 @@ public static partial class MarshallingHelpers /// The CppObject class that will be returned /// The native pointer to a C++ object. /// An instance of T bound to the native pointer - public static T FromPointer(IntPtr cppObjectPtr) where T : CppObject => - cppObjectPtr == IntPtr.Zero ? null : (T) Activator.CreateInstance(typeof(T), cppObjectPtr); + public static T? FromPointer(IntPtr cppObjectPtr, Func factory) where T : CppObject + { + if (cppObjectPtr == IntPtr.Zero) + return default; + + T? result = factory(cppObjectPtr); + return result; + } /// /// Instantiate a CppObject from a native pointer. @@ -20,25 +29,60 @@ public static T FromPointer(IntPtr cppObjectPtr) where T : CppObject => /// The CppObject class that will be returned /// The native pointer to a C++ object. /// An instance of T bound to the native pointer - public static T FromPointer(UIntPtr cppObjectPtr) where T : CppObject => - cppObjectPtr == UIntPtr.Zero ? null : (T) Activator.CreateInstance(typeof(T), cppObjectPtr); + public static T? FromPointer< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(IntPtr cppObjectPtr) where T : CppObject + { + if (cppObjectPtr == IntPtr.Zero) + return default; + + object? result = Activator.CreateInstance(typeof(T), cppObjectPtr); + if (result is null) + return default; + + return (T) result; + } + + /// + /// Instantiate a CppObject from a native pointer. + /// + /// The CppObject class that will be returned + /// The native pointer to a C++ object. + /// An instance of T bound to the native pointer + public static T? FromPointer< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(UIntPtr cppObjectPtr) where T : CppObject + { + if (cppObjectPtr == UIntPtr.Zero) + return default; + + object? result = Activator.CreateInstance(typeof(T), cppObjectPtr); + if (result is null) + return default; + + return (T) result; + } [MethodImpl(Utilities.MethodAggressiveOptimization)] public static uint AddRef(TCallback callback) where TCallback : ICallbackable => callback switch { - null => throw new NullReferenceException(), ComObject cpp => cpp.AddRef(), CallbackBase managed => managed.AddRef(), + _ => throw new NotImplementedException(), }; [MethodImpl(Utilities.MethodAggressiveOptimization)] public static uint Release(TCallback callback) where TCallback : ICallbackable => callback switch { - null => throw new NullReferenceException(), ComObject cpp => cpp.Release(), CallbackBase managed => managed.Release(), + _ => throw new NotImplementedException(), }; /// @@ -47,12 +91,12 @@ public static uint Release(TCallback callback) where TCallback : ICal /// The type of the callback. /// The callback. /// A pointer to the unmanaged C++ object of the callback - public static IntPtr ToCallbackPtr(ICallbackable callback) where TCallback : ICallbackable => + public static nint ToCallbackPtr(ICallbackable callback) where TCallback : ICallbackable => callback switch { - null => IntPtr.Zero, CppObject cpp => cpp.NativePointer, - CallbackBase managed => managed.Find() + CallbackBase managed => managed.Find(), + _ => 0, }; /// @@ -63,7 +107,7 @@ public static IntPtr ToCallbackPtr(ICallbackable callback) where TCal /// A pointer to the unmanaged C++ object of the callback /// This method is meant as a fast-path for codegen to use to reduce the number of casts. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static IntPtr ToCallbackPtr(CppObject obj) where TCallback : ICallbackable + public static IntPtr ToCallbackPtr(CppObject? obj) where TCallback : ICallbackable => obj?.NativePointer ?? IntPtr.Zero; /// @@ -73,5 +117,5 @@ public static IntPtr ToCallbackPtr(CppObject obj) where TCallback : I /// A pointer to the unmanaged C++ object of the callback /// This method is meant as a fast-path for codegen to use to reduce the number of casts. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static IntPtr ToCallbackPtr(CppObject obj) => obj?.NativePointer ?? IntPtr.Zero; + public static IntPtr ToCallbackPtr(CppObject? obj) => obj?.NativePointer ?? IntPtr.Zero; } \ No newline at end of file diff --git a/SharpGen.Runtime/MemoryHelpers.Alloc.cs b/SharpGen.Runtime/MemoryHelpers.Alloc.cs index 3850b93b..2addecf7 100644 --- a/SharpGen.Runtime/MemoryHelpers.Alloc.cs +++ b/SharpGen.Runtime/MemoryHelpers.Alloc.cs @@ -3,10 +3,11 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; namespace SharpGen.Runtime; -public static partial class MemoryHelpers +public static unsafe partial class MemoryHelpers { /// /// Allocate an aligned memory buffer. @@ -17,14 +18,20 @@ public static partial class MemoryHelpers /// /// To free this buffer, call . /// - public static unsafe void* AllocateMemory(nuint sizeInBytes, uint align = 16) + public static void* AllocateMemory(nuint sizeInBytes, uint alignment = 16) { - nuint mask = align - 1u; +#if NET6_0_OR_GREATER + var ptr = NativeMemory.AlignedAlloc(sizeInBytes, alignment); + Debug.Assert(IsMemoryAligned(ptr, alignment)); + return ptr; +#else + nuint mask = alignment - 1u; var memPtr = Marshal.AllocHGlobal((nint) (sizeInBytes + mask) + sizeof(void*)); var ptr = (nuint) ((byte*) memPtr + sizeof(void*) + mask) & ~mask; - Debug.Assert(IsMemoryAligned(ptr, align)); + Debug.Assert(IsMemoryAligned(ptr, alignment)); ((IntPtr*) ptr)[-1] = memPtr; return (void*) ptr; +#endif } /// @@ -38,9 +45,75 @@ public static partial class MemoryHelpers /// [Obsolete("Use void*(nuint, uint) overload instead")] [EditorBrowsable(EditorBrowsableState.Advanced)] - public static unsafe IntPtr AllocateMemory(int sizeInBytes, int align = 16) => + public static IntPtr AllocateMemory(int sizeInBytes, int align = 16) => new(AllocateMemory((nuint) sizeInBytes, (uint) align)); + /// Allocates a chunk of unmanaged memory. + /// The count of elements contained in the allocation. + /// The size, in bytes, of the elements in the allocation. + /// true if the allocated memory should be zeroed; otherwise, false. + /// The address to an allocated chunk of memory that is at least bytes in length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AllocateArray(nuint count, nuint size, bool zero = false) + { +#if NET6_0_OR_GREATER + void* result = NativeMemory.Alloc(count, size); + +#else + void* result = (void*)Marshal.AllocHGlobal(checked((int) (count * size))); +#endif + + if (result == null) + { + ThrowOutOfMemoryException(count, size); + } + + if(zero) + { +#if NET6_0_OR_GREATER + NativeMemory.Clear(result, count * size); +#else + ClearMemory(result, count * size); +#endif + } + + return result; + } + + /// Allocates a chunk of unmanaged memory. + /// The type used to compute the size, in bytes, of the elements in the allocation. + /// The count of elements contained in the allocation. + /// true if the allocated memory should be zeroed; otherwise, false. + /// The address to an allocated chunk of memory that is at least sizeof() bytes in length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AllocateArray(nuint count, bool zero = false) + where T : unmanaged + { + +#if NET6_0_OR_GREATER + T* result = (T*)NativeMemory.Alloc(count, SizeOf()); + +#else + T* result = (T*) Marshal.AllocHGlobal(checked((int) (count * SizeOf()))); +#endif + + if (result == null) + { + ThrowOutOfMemoryException(count, SizeOf()); + } + + if (zero) + { +#if NET6_0_OR_GREATER + NativeMemory.Clear(result, count * SizeOf()); +#else + ClearMemory(result, count * SizeOf()); +#endif + } + + return result; + } + /// /// Free an aligned memory buffer. /// @@ -48,11 +121,15 @@ public static unsafe IntPtr AllocateMemory(int sizeInBytes, int align = 16) => /// /// The buffer must have been allocated with . /// - public static unsafe void FreeMemory(void* alignedBuffer) + public static void FreeMemory(void* alignedBuffer) { if (alignedBuffer == default) return; +#if NET6_0_OR_GREATER + NativeMemory.AlignedFree(alignedBuffer); +#else Marshal.FreeHGlobal(((IntPtr*) alignedBuffer)[-1]); +#endif } /// @@ -63,7 +140,7 @@ public static unsafe void FreeMemory(void* alignedBuffer) /// The buffer must have been allocated with . /// [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void FreeMemory(UIntPtr alignedBuffer) => FreeMemory(alignedBuffer.ToPointer()); + public static void FreeMemory(UIntPtr alignedBuffer) => FreeMemory(alignedBuffer.ToPointer()); /// /// Free an aligned memory buffer. @@ -73,5 +150,17 @@ public static unsafe void FreeMemory(void* alignedBuffer) /// The buffer must have been allocated with . /// [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void FreeMemory(IntPtr alignedBuffer) => FreeMemory(alignedBuffer.ToPointer()); + public static void FreeMemory(IntPtr alignedBuffer) => FreeMemory(alignedBuffer.ToPointer()); + + [DoesNotReturn] + private static void ThrowOutOfMemoryException(ulong size) + { + throw new OutOfMemoryException($"The allocation of '{size}' bytes failed"); + } + + [DoesNotReturn] + public static void ThrowOutOfMemoryException(ulong count, ulong size) + { + throw new OutOfMemoryException($"The allocation of '{count}x{size}' bytes failed"); + } } \ No newline at end of file diff --git a/SharpGen.Runtime/MemoryHelpers.cs b/SharpGen.Runtime/MemoryHelpers.cs index 9ec69d65..57544eac 100644 --- a/SharpGen.Runtime/MemoryHelpers.cs +++ b/SharpGen.Runtime/MemoryHelpers.cs @@ -18,16 +18,257 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable + using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SharpGen.Runtime; /// /// Utility class. /// -public static partial class MemoryHelpers +public static unsafe partial class MemoryHelpers { +#pragma warning disable CS8500 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint SizeOf() => unchecked((uint) sizeof(T)); +#pragma warning restore CS8500 + + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(o))] + public static T? As(this object? o) + where T : class? + { + Debug.Assert(o is null or T); + return Unsafe.As(o); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref TTo As(ref TFrom source) + => ref Unsafe.As(ref source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span As(this Span span) + where TFrom : unmanaged + where TTo : unmanaged + { + Debug.Assert(SizeOf() == SizeOf()); + +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.As(ref MemoryMarshal.GetReference(span)), span.Length); +#else + return new(Unsafe.AsPointer(ref Unsafe.As(ref MemoryMarshal.GetReference(span))), span.Length); +#endif + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan As(this ReadOnlySpan span) + where TFrom : unmanaged + where TTo : unmanaged + { + Debug.Assert(SizeOf() == SizeOf()); + + +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref MemoryMarshal.GetReference(span)), span.Length); +#else + return new(Unsafe.AsPointer(ref Unsafe.As(ref MemoryMarshal.GetReference(span))), span.Length); +#endif + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* AsPointer(in T source) + where T : unmanaged => (T*) Unsafe.AsPointer(ref Unsafe.AsRef(in source)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref readonly TTo AsReadonly(in TFrom source) + => ref Unsafe.As(ref Unsafe.AsRef(in source)); + + /// Reinterprets the given native integer as a reference. + /// The type of the reference. + /// The native integer to reinterpret. + /// A reference to a value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T AsRef(nint source) => ref Unsafe.AsRef((void*) source); + + /// Reinterprets the given native unsigned integer as a reference. + /// The type of the reference. + /// The native unsigned integer to reinterpret. + /// A reference to a value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T AsRef(nuint source) => ref Unsafe.AsRef((void*) source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T AsRef(in T source) => ref Unsafe.AsRef(in source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref TTo AsRef(in TFrom source) => ref Unsafe.As(ref Unsafe.AsRef(in source)); + + /// Reinterprets the readonly span as a writeable span. + /// The type of items in + /// The readonly span to reinterpret. + /// A writeable span that points to the same items as . + public static Span AsSpan(this ReadOnlySpan span) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.AsRef(in MemoryMarshal.GetReference(span)), span.Length); +#else + return new(Unsafe.AsPointer(ref Unsafe.AsRef(in MemoryMarshal.GetReference(span))), span.Length); +#endif + } + + /// + public static Span Cast(this Span span) + where TFrom : struct + where TTo : struct => MemoryMarshal.Cast(span); + + /// + public static ReadOnlySpan Cast(this ReadOnlySpan span) + where TFrom : struct + where TTo : struct => MemoryMarshal.Cast(span); + + // + public static void CopyBlock(ref TDestination destination, in TSource source, uint byteCount) => Unsafe.CopyBlock(ref Unsafe.As(ref destination), ref Unsafe.As(ref Unsafe.AsRef(in source)), byteCount); + + /// + public static void CopyBlockUnaligned(ref TDestination destination, in TSource source, uint byteCount) => Unsafe.CopyBlockUnaligned(ref Unsafe.As(ref destination), ref Unsafe.As(ref Unsafe.AsRef(in source)), byteCount); + + /// + public static Span CreateSpan(scoped ref T reference, int length) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref reference, length); +#else + return new(Unsafe.AsPointer(ref reference), length); +#endif + } + + /// + public static ReadOnlySpan CreateReadOnlySpan(scoped in T reference, int length) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in reference), length); +#else + return new(Unsafe.AsPointer(ref Unsafe.AsRef(in reference)), length); +#endif + } + + /// Returns a pointer to the element of the span at index zero. + /// The type of items in . + /// The span from which the pointer is retrieved. + /// A pointer to the item at index zero of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* GetPointerUnsafe(this Span span) + where T : unmanaged => (T*) Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)); + + /// Returns a pointer to the element of the span at index zero. + /// The type of items in . + /// The span from which the pointer is retrieved. + /// A pointer to the item at index zero of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* GetPointerUnsafe(this ReadOnlySpan span) + where T : unmanaged => (T*) Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this T[] array) => ref GetArrayDataReference(array); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this T[] array, int index) => ref Unsafe.Add(ref GetArrayDataReference(array), index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this T[] array, nuint index) => ref Unsafe.Add(ref GetArrayDataReference(array), index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this Span span) => ref MemoryMarshal.GetReference(span); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this Span span, int index) => ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetReferenceUnsafe(this Span span, nuint index) => ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref readonly T GetReferenceUnsafe(this ReadOnlySpan span) => ref MemoryMarshal.GetReference(span); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref readonly T GetReferenceUnsafe(this ReadOnlySpan span, int index) => ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref readonly T GetReferenceUnsafe(this ReadOnlySpan span, nuint index) => ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); + + /// Determines if a given reference to a value of type is not a null reference. + /// The type of the reference + /// The reference to check. + /// true if is not a null reference; otherwise, false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNotNullRef(in T source) => !Unsafe.IsNullRef(ref Unsafe.AsRef(in source)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNullRef(in T source) => Unsafe.IsNullRef(ref Unsafe.AsRef(in source)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T NullRef() => ref Unsafe.NullRef(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ReadUnaligned(void* source) + where T : unmanaged => Unsafe.ReadUnaligned(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ReadUnaligned(void* source, nuint offset) + where T : unmanaged => Unsafe.ReadUnaligned((void*) ((nuint) source + offset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUnaligned(void* source, T value) + where T : unmanaged => Unsafe.WriteUnaligned(source, value); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUnaligned(void* source, nuint offset, T value) + where T : unmanaged => Unsafe.WriteUnaligned((void*) ((nuint) source + offset), value); + + /// + /// Returns a reference to the 0th element of . If the array is empty, returns a reference + /// to where the 0th element would have been stored. Such a reference may be used for pinning but must never be dereferenced. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetArrayDataReference(T[] array) + { +#if NET6_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + return ref MemoryMarshal.GetReference(array.AsSpan()); +#endif + } + /// /// Native memcpy. /// @@ -35,7 +276,7 @@ public static partial class MemoryHelpers /// The source memory location. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(IntPtr dest, IntPtr src, int sizeInBytesToCopy) => + public static void CopyMemory(IntPtr dest, IntPtr src, int sizeInBytesToCopy) => Unsafe.CopyBlockUnaligned((void*) dest, (void*) src, (uint) sizeInBytesToCopy); /// @@ -45,7 +286,7 @@ public static unsafe void CopyMemory(IntPtr dest, IntPtr src, int sizeInBytesToC /// The source memory location. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(IntPtr dest, IntPtr src, uint sizeInBytesToCopy) => + public static void CopyMemory(IntPtr dest, IntPtr src, uint sizeInBytesToCopy) => Unsafe.CopyBlockUnaligned((void*) dest, (void*) src, sizeInBytesToCopy); /// @@ -54,9 +295,18 @@ public static unsafe void CopyMemory(IntPtr dest, IntPtr src, uint sizeInBytesTo /// The destination memory location. /// The source memory location. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(IntPtr dest, ReadOnlySpan src) where T : struct => + public static void CopyMemory(IntPtr dest, ReadOnlySpan src) where T : struct => src.CopyTo(new Span((void*) dest, src.Length)); + /// + /// Native memcpy. + /// + /// The destination memory location. + /// The source memory location. + [MethodImpl(Utilities.MethodAggressiveOptimization)] + public static void CopyMemory(IntPtr dest, Span src) where T : struct => + src.CopyTo(new Span(dest.ToPointer(), src.Length)); + /// /// Native memcpy. /// @@ -64,7 +314,7 @@ public static unsafe void CopyMemory(IntPtr dest, ReadOnlySpan src) where /// The source memory location. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(void* dest, void* src, int sizeInBytesToCopy) => + public static void CopyMemory(void* dest, void* src, int sizeInBytesToCopy) => Unsafe.CopyBlockUnaligned(dest, src, (uint) sizeInBytesToCopy); /// @@ -74,7 +324,7 @@ public static unsafe void CopyMemory(void* dest, void* src, int sizeInBytesToCop /// The source memory location. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(void* dest, void* src, uint sizeInBytesToCopy) => + public static void CopyMemory(void* dest, void* src, uint sizeInBytesToCopy) => Unsafe.CopyBlockUnaligned(dest, src, sizeInBytesToCopy); /// @@ -83,7 +333,7 @@ public static unsafe void CopyMemory(void* dest, void* src, uint sizeInBytesToCo /// The destination memory location. /// The source memory location. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void CopyMemory(void* dest, ReadOnlySpan src) where T : struct => + public static void CopyMemory(void* dest, ReadOnlySpan src) where T : struct => src.CopyTo(new Span(dest, src.Length)); /// @@ -93,7 +343,7 @@ public static unsafe void CopyMemory(void* dest, ReadOnlySpan src) where T /// The value to initialize the block to. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void ClearMemory(IntPtr dest, byte value, int sizeInBytesToClear) => + public static void ClearMemory(IntPtr dest, byte value, int sizeInBytesToClear) => Unsafe.InitBlockUnaligned(ref *(byte*) dest, value, (uint) sizeInBytesToClear); /// @@ -103,7 +353,7 @@ public static unsafe void ClearMemory(IntPtr dest, byte value, int sizeInBytesTo /// The value to initialize the block to. /// The byte count. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe void ClearMemory(IntPtr dest, byte value, uint sizeInBytesToClear) => + public static void ClearMemory(IntPtr dest, byte value, uint sizeInBytesToClear) => Unsafe.InitBlockUnaligned(ref *(byte*) dest, value, sizeInBytesToClear); /// @@ -124,6 +374,16 @@ public static void ClearMemory(IntPtr dest, int sizeInBytesToClear) => public static void ClearMemory(IntPtr dest, uint sizeInBytesToClear) => ClearMemory(dest, 0, sizeInBytesToClear); + /// + /// Clears the memory. + /// + /// The address of the start of the memory block to initialize. + /// The value to initialize the block to. + /// The byte count. + [MethodImpl(Utilities.MethodAggressiveOptimization)] + public static void ClearMemory(void* dest, nuint sizeInBytes) => + Unsafe.InitBlockUnaligned(dest, 0, (uint) sizeInBytes); + /// /// Reads the specified array T[] data from a memory location. /// @@ -143,7 +403,7 @@ public static IntPtr Read(IntPtr source, T[] data, int offset, int count) whe /// The target data pointer. /// The byte count to read from the memory location. /// source pointer + sizeInBytes - public static unsafe IntPtr Read(IntPtr source, void* data, int sizeInBytes) + public static IntPtr Read(IntPtr source, void* data, int sizeInBytes) { Unsafe.CopyBlockUnaligned(data, (void*) source, (uint) sizeInBytes); return source + sizeInBytes; @@ -170,7 +430,7 @@ public static unsafe IntPtr Write(IntPtr destination, void* data, int sizeInByte /// The data span to write to. /// The number of T element to read from the memory location. /// source pointer + sizeof(T) * count - public static unsafe IntPtr Read(IntPtr source, ReadOnlySpan data, int count) where T : unmanaged + public static IntPtr Read(IntPtr source, ReadOnlySpan data, int count) where T : unmanaged { fixed (void* dataPtr = data) return Read(source, dataPtr, count * sizeof(T)); @@ -184,7 +444,7 @@ public static unsafe IntPtr Read(IntPtr source, ReadOnlySpan data, int cou /// The span of T data to write. /// The number of T element to write to the memory location. /// destination pointer + sizeof(T) * count - public static unsafe IntPtr Write(IntPtr destination, Span data, int count) where T : unmanaged + public static IntPtr Write(IntPtr destination, Span data, int count) where T : unmanaged { fixed (void* dataPtr = data) return Write(destination, dataPtr, count * sizeof(T)); @@ -197,7 +457,7 @@ public static unsafe IntPtr Write(IntPtr destination, Span data, int count /// Memory location to read from. /// The T to read to. /// source pointer + sizeof(T) - public static unsafe IntPtr Read(IntPtr source, ref T data) where T : unmanaged + public static IntPtr Read(IntPtr source, ref T data) where T : unmanaged { fixed (void* dataPtr = &data) return Read(source, dataPtr, sizeof(T)); @@ -209,7 +469,7 @@ public static unsafe IntPtr Read(IntPtr source, ref T data) where T : unmanag /// Type of a data to read. /// Memory location to read from. /// The T value read from the pointer. - public static unsafe T Read(IntPtr source) where T : unmanaged + public static T Read(IntPtr source) where T : unmanaged { T data = default; Read(source, &data, sizeof(T)); @@ -223,7 +483,7 @@ public static unsafe T Read(IntPtr source) where T : unmanaged /// Memory location to write to. /// The data structure to write. /// destination pointer + sizeof(T) - public static unsafe IntPtr Write(IntPtr destination, ref T data) where T : unmanaged + public static IntPtr Write(IntPtr destination, ref T data) where T : unmanaged { fixed (void* dataPtr = &data) return Write(destination, dataPtr, sizeof(T)); @@ -236,7 +496,7 @@ public static unsafe IntPtr Write(IntPtr destination, ref T data) where T : u /// Memory location to write to. /// The data structure to write. /// destination pointer + sizeof(T) - public static unsafe IntPtr Write(IntPtr destination, T data) where T : unmanaged => + public static IntPtr Write(IntPtr destination, T data) where T : unmanaged => Write(destination, &data, sizeof(T)); /// @@ -246,8 +506,8 @@ public static unsafe IntPtr Write(IntPtr destination, T data) where T : unman /// The align. /// true if the specified memory pointer is aligned in memory; otherwise, false. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static bool IsMemoryAligned(nint memoryPtr, uint align = 16) => - (memoryPtr & (align - 1)) == 0; + public static bool IsMemoryAligned(nint memoryPtr, uint alignment = 16) => + (memoryPtr & (alignment - 1)) == 0; /// /// Determines whether the specified memory pointer is aligned in memory. @@ -256,8 +516,8 @@ public static bool IsMemoryAligned(nint memoryPtr, uint align = 16) => /// The align. /// true if the specified memory pointer is aligned in memory; otherwise, false. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static bool IsMemoryAligned(nuint memoryPtr, uint align = 16) => - (memoryPtr & (align - 1)) == 0; + public static bool IsMemoryAligned(nuint memoryPtr, uint alignment = 16) => + (memoryPtr & (alignment - 1)) == 0; /// /// Determines whether the specified memory pointer is aligned in memory. @@ -266,8 +526,8 @@ public static bool IsMemoryAligned(nuint memoryPtr, uint align = 16) => /// The align. /// true if the specified memory pointer is aligned in memory; otherwise, false. [MethodImpl(Utilities.MethodAggressiveOptimization)] - public static unsafe bool IsMemoryAligned(void* memoryPtr, uint align = 16) => - IsMemoryAligned((nuint) memoryPtr, align); + public static bool IsMemoryAligned(void* memoryPtr, uint alignment = 16) => + IsMemoryAligned((nuint) memoryPtr, alignment); #nullable enable diff --git a/SharpGen.Runtime/NativeLong.cs b/SharpGen.Runtime/NativeLong.cs index 50be12c5..8ad204ae 100644 --- a/SharpGen.Runtime/NativeLong.cs +++ b/SharpGen.Runtime/NativeLong.cs @@ -5,7 +5,7 @@ namespace SharpGen.Runtime; [StructLayout(LayoutKind.Explicit)] public readonly struct NativeLong : IEquatable, IComparable, IComparable -#if NET5_0 +#if NET6_0_OR_GREATER , IFormattable #endif { @@ -66,8 +66,8 @@ public override string ToString() => public string ToString(string format) => UseInt ? _intValue.ToString(format) : _pointerValue.ToString(format); -#if NET5_0 - public string ToString(IFormatProvider formatProvider) => +#if NET6_0_OR_GREATER + public string ToString(IFormatProvider formatProvider) => UseInt ? _intValue.ToString(formatProvider) : _pointerValue.ToString(formatProvider); /// diff --git a/SharpGen.Runtime/NativeULong.cs b/SharpGen.Runtime/NativeULong.cs index 2386949b..60e41af6 100644 --- a/SharpGen.Runtime/NativeULong.cs +++ b/SharpGen.Runtime/NativeULong.cs @@ -5,7 +5,7 @@ namespace SharpGen.Runtime; [StructLayout(LayoutKind.Explicit)] public readonly struct NativeULong : IEquatable, IComparable, IComparable -#if NET5_0 +#if NET6_0_OR_GREATER , IFormattable #endif { @@ -13,7 +13,7 @@ namespace SharpGen.Runtime; private readonly nuint _pointerValue; [FieldOffset(0)] private readonly uint _intValue; - + private static readonly bool UseInt = NativeLong.UseInt; public NativeULong(uint value) @@ -59,27 +59,33 @@ public NativeULong(UIntPtr value) } /// - public override string ToString() => + public override string ToString() => UseInt ? _intValue.ToString() : _pointerValue.ToString(); -#if NET5_0 - public string ToString(string format) => - UseInt ? _intValue.ToString(format) : _pointerValue.ToString(format); +#if NET6_0_OR_GREATER + public string ToString(string format) + { + return UseInt ? _intValue.ToString(format) : _pointerValue.ToString(format); + } - public string ToString(IFormatProvider formatProvider) => - UseInt ? _intValue.ToString(formatProvider) : _pointerValue.ToString(formatProvider); + public string ToString(IFormatProvider formatProvider) + { + return UseInt ? _intValue.ToString(formatProvider) : _pointerValue.ToString(formatProvider); + } - /// - public string ToString(string format, IFormatProvider formatProvider) => - UseInt ? _intValue.ToString(format, formatProvider) : _pointerValue.ToString(format, formatProvider); + /// + public string ToString(string format, IFormatProvider formatProvider) + { + return UseInt ? _intValue.ToString(format, formatProvider) : _pointerValue.ToString(format, formatProvider); + } #endif - + /// - public override int GetHashCode() => + public override int GetHashCode() => UseInt ? _intValue.GetHashCode() : _pointerValue.GetHashCode(); /// - public bool Equals(NativeULong other) => + public bool Equals(NativeULong other) => UseInt ? _intValue == other._intValue : _pointerValue.Equals(other._pointerValue); /// @@ -172,7 +178,7 @@ public override bool Equals(object obj) => /// /// The value. /// The result of the conversion. - public static explicit operator uint(NativeULong value) => + public static explicit operator uint(NativeULong value) => UseInt ? value._intValue : (uint) value._pointerValue; /// @@ -180,7 +186,7 @@ public static explicit operator uint(NativeULong value) => /// /// The value. /// The result of the conversion. - public static implicit operator ulong(NativeULong value) => + public static implicit operator ulong(NativeULong value) => UseInt ? value._intValue : (ulong) value._pointerValue; /// @@ -188,7 +194,7 @@ public static implicit operator ulong(NativeULong value) => /// /// The value. /// The result of the conversion. - public static implicit operator UIntPtr(NativeULong value) => + public static implicit operator UIntPtr(NativeULong value) => UseInt ? (UIntPtr) value._intValue : (UIntPtr) value._pointerValue; /// @@ -211,7 +217,7 @@ public static implicit operator UIntPtr(NativeULong value) => /// The value. /// The result of the conversion. public static explicit operator NativeULong(UIntPtr value) => new NativeULong(value); - + /// public int CompareTo(NativeULong other) { @@ -219,7 +225,7 @@ public int CompareTo(NativeULong other) return _intValue.CompareTo(other._intValue); #if !NET5_0 - return ((ulong)_pointerValue).CompareTo((ulong) other._pointerValue); + return ((ulong) _pointerValue).CompareTo((ulong) other._pointerValue); #else return _pointerValue.CompareTo(other._pointerValue); #endif diff --git a/SharpGen.Runtime/PointerSize.cs b/SharpGen.Runtime/PointerSize.cs index 6f341390..5fd0b9ae 100644 --- a/SharpGen.Runtime/PointerSize.cs +++ b/SharpGen.Runtime/PointerSize.cs @@ -19,63 +19,80 @@ // THE SOFTWARE. using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +#nullable enable + namespace SharpGen.Runtime; /// /// The maximum number of bytes to which a pointer can point. Use for a count that must span the full range of a pointer. /// Equivalent to the native type size_t. /// -public readonly struct PointerSize : IEquatable, IFormattable +public readonly struct PointerSize : IEquatable, +#if NET8_0_OR_GREATER + IComparable, +#endif + IFormattable { - private readonly IntPtr _size; + public readonly IntPtr Value; /// /// An empty pointer size initialized to zero. /// public static readonly PointerSize Zero = new(0); - public PointerSize(IntPtr size) => _size = size; - private unsafe PointerSize(void* size) => _size = new IntPtr(size); - public PointerSize(int size) => _size = new IntPtr(size); - public PointerSize(long size) => _size = new IntPtr(size); + public PointerSize(IntPtr value) => Value = value; + private unsafe PointerSize(void* value) => Value = new IntPtr(value); + public PointerSize(int value) => Value = new IntPtr(value); + public PointerSize(long value) => Value = new IntPtr(value); public override string ToString() => ToString(null, null); - public string ToString(string format, IFormatProvider formatProvider) => string.Format( + public string ToString(string? format, IFormatProvider? formatProvider) => string.Format( formatProvider ?? CultureInfo.CurrentCulture, string.IsNullOrEmpty(format) ? "{0}" : "{0:" + format + "}", - _size + Value ); - public string ToString(string format) => ToString(format, null); + public string ToString(string? format) => ToString(format, null); + + public override int GetHashCode() => Value.GetHashCode(); - public override int GetHashCode() => _size.GetHashCode(); + public bool Equals(PointerSize other) => Value.Equals(other.Value); - public bool Equals(PointerSize other) => _size.Equals(other._size); + public override bool Equals([NotNullWhen(true)] object? obj) => obj is PointerSize value && Equals(value); - public override bool Equals(object value) +#if NET8_0_OR_GREATER + public int CompareTo(object? obj) { - if (ReferenceEquals(null, value)) return false; - return value is PointerSize size && Equals(size); + if (obj is PointerSize other) + { + return CompareTo(other); + } + + return (obj is null) ? 1 : throw new ArgumentException("obj is not an instance of PointerSize."); } - public static PointerSize operator +(PointerSize left, PointerSize right) => new(left._size.ToInt64() + right._size.ToInt64()); + public int CompareTo(PointerSize other) => Value.CompareTo(other.Value); +#endif + + public static PointerSize operator +(PointerSize left, PointerSize right) => new(left.Value.ToInt64() + right.Value.ToInt64()); public static PointerSize operator +(PointerSize value) => value; - public static PointerSize operator -(PointerSize left, PointerSize right) => new(left._size.ToInt64() - right._size.ToInt64()); - public static PointerSize operator -(PointerSize value) => new(-value._size.ToInt64()); - public static PointerSize operator *(int scale, PointerSize value) => new(scale*value._size.ToInt64()); - public static PointerSize operator *(PointerSize value, int scale) => new(scale*value._size.ToInt64()); - public static PointerSize operator /(PointerSize value, int scale) => new(value._size.ToInt64()/scale); + public static PointerSize operator -(PointerSize left, PointerSize right) => new(left.Value.ToInt64() - right.Value.ToInt64()); + public static PointerSize operator -(PointerSize value) => new(-value.Value.ToInt64()); + public static PointerSize operator *(int scale, PointerSize value) => new(scale * value.Value.ToInt64()); + public static PointerSize operator *(PointerSize value, int scale) => new(scale * value.Value.ToInt64()); + public static PointerSize operator /(PointerSize value, int scale) => new(value.Value.ToInt64() / scale); public static bool operator ==(PointerSize left, PointerSize right) => left.Equals(right); public static bool operator !=(PointerSize left, PointerSize right) => !left.Equals(right); - public static implicit operator int(PointerSize value) => value._size.ToInt32(); - public static implicit operator long(PointerSize value) => value._size.ToInt64(); + public static implicit operator int(PointerSize value) => value.Value.ToInt32(); + public static implicit operator long(PointerSize value) => value.Value.ToInt64(); public static implicit operator PointerSize(int value) => new(value); public static implicit operator PointerSize(long value) => new(value); public static implicit operator PointerSize(IntPtr value) => new(value); - public static implicit operator IntPtr(PointerSize value) => value._size; + public static implicit operator IntPtr(PointerSize value) => value.Value; public static unsafe implicit operator PointerSize(void* value) => new(value); - public static unsafe implicit operator void*(PointerSize value) => (void*) value._size; + public static unsafe implicit operator void*(PointerSize value) => (void*) value.Value; } \ No newline at end of file diff --git a/SharpGen.Runtime/PointerUSize.cs b/SharpGen.Runtime/PointerUSize.cs new file mode 100644 index 00000000..12036e88 --- /dev/null +++ b/SharpGen.Runtime/PointerUSize.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2010-2014 SharpDX - Alexandre Mutel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +#nullable enable + +namespace SharpGen.Runtime; + +/// +/// The maximum number of bytes to which a pointer can point. Use for a count that must span the full range of a pointer. +/// Equivalent to the native type size_t. +/// +public readonly struct PointerUSize : IEquatable, +#if NET8_0_OR_GREATER + IComparable, +#endif + IFormattable +{ + public readonly UIntPtr Value; + + /// + /// An empty pointer size initialized to zero. + /// + public static readonly PointerUSize Zero = new(0); + + public PointerUSize(UIntPtr value) => Value = value; + private unsafe PointerUSize(void* value) => Value = new UIntPtr(value); + public PointerUSize(uint value) => Value = new UIntPtr(value); + public PointerUSize(ulong value) => Value = new UIntPtr(value); + + public override string ToString() => ToString(null, null); + + public string ToString(string? format, IFormatProvider? formatProvider) => string.Format( + formatProvider ?? CultureInfo.CurrentCulture, + string.IsNullOrEmpty(format) ? "{0}" : "{0:" + format + "}", + Value + ); + + public string ToString(string? format) => ToString(format, null); + + public override int GetHashCode() => Value.GetHashCode(); + + public bool Equals(PointerUSize other) => Value.Equals(other.Value); + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is PointerUSize value && Equals(value); + + +#if NET8_0_OR_GREATER + public int CompareTo(object? obj) + { + if (obj is PointerUSize other) + { + return CompareTo(other); + } + + return (obj is null) ? 1 : throw new ArgumentException("obj is not an instance of PointerUSize."); + } + + public int CompareTo(PointerUSize other) => Value.CompareTo(other.Value); +#endif + + public static PointerUSize operator +(PointerUSize left, PointerUSize right) => new(left.Value.ToUInt64() + right.Value.ToUInt64()); + public static PointerUSize operator -(PointerUSize left, PointerUSize right) => new(left.Value.ToUInt64() - right.Value.ToUInt64()); + public static PointerUSize operator *(uint scale, PointerUSize value) => new(scale*value.Value.ToUInt64()); + public static PointerUSize operator *(PointerUSize value, uint scale) => new(scale*value.Value.ToUInt64()); + public static PointerUSize operator /(PointerUSize value, uint scale) => new(value.Value.ToUInt64()/scale); + public static bool operator ==(PointerUSize left, PointerUSize right) => left.Equals(right); + public static bool operator !=(PointerUSize left, PointerUSize right) => !left.Equals(right); + public static implicit operator uint(PointerUSize value) => value.Value.ToUInt32(); + public static implicit operator ulong(PointerUSize value) => value.Value.ToUInt64(); + public static implicit operator PointerUSize(uint value) => new(value); + public static implicit operator PointerUSize(ulong value) => new(value); + public static implicit operator PointerUSize(UIntPtr value) => new(value); + public static implicit operator UIntPtr(PointerUSize value) => value.Value; + public static unsafe implicit operator PointerUSize(void* value) => new(value); + public static unsafe implicit operator void*(PointerUSize value) => (void*) value.Value; +} \ No newline at end of file diff --git a/SharpGen.Runtime/Result.cs b/SharpGen.Runtime/Result.cs index edb628b4..5c51c740 100644 --- a/SharpGen.Runtime/Result.cs +++ b/SharpGen.Runtime/Result.cs @@ -102,7 +102,8 @@ namespace SharpGen.Runtime; public override bool Equals(object? obj) => obj is Result res && Equals(res); public override int GetHashCode() => Code; public override string ToString() => Code.ToString("X8"); - public string ToString(string format, IFormatProvider formatProvider) => Code.ToString(format, formatProvider); + /// + public string ToString(string? format, IFormatProvider? formatProvider) => Code.ToString(format, formatProvider); public int CompareTo(Result other) => Code.CompareTo(other.Code); diff --git a/SharpGen.Runtime/ShadowAttribute.cs b/SharpGen.Runtime/ShadowAttribute.cs index 8e2f6e08..6e4fe2e7 100644 --- a/SharpGen.Runtime/ShadowAttribute.cs +++ b/SharpGen.Runtime/ShadowAttribute.cs @@ -20,6 +20,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -34,13 +35,20 @@ public sealed class ShadowAttribute : Attribute /// /// Type of the associated shadow /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif public Type Type { get; } /// /// Initializes a new instance of class. /// /// Type of the associated shadow - public ShadowAttribute(Type holder) + public ShadowAttribute( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type holder) { Type = holder ?? throw new ArgumentNullException(nameof(holder)); diff --git a/SharpGen.Runtime/SharpGen.Runtime.csproj b/SharpGen.Runtime/SharpGen.Runtime.csproj index 8909b46c..8c174e33 100644 --- a/SharpGen.Runtime/SharpGen.Runtime.csproj +++ b/SharpGen.Runtime/SharpGen.Runtime.csproj @@ -1,43 +1,44 @@  - - + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + Support classes for code generated by SharpGen. + true + false + true + true + true + preview + true + - - - net5.0;netcoreapp3.0;netstandard2.1;netcoreapp2.1;netstandard2.0;net471;net46;net45;netstandard1.3 - Support classes for code generated by SharpGen. - true - false - true - true - preview - + + + + + + - - - - - - + + + + - - - - + + + + + + + <_Parameter1>SharpGen.Runtime.COM, PublicKey=$(SharpGenPublicKey) + + - - - diff --git a/SharpGen.Runtime/SharpGen.Runtime.props b/SharpGen.Runtime/SharpGen.Runtime.props index 88e4c793..9a54ae7a 100644 --- a/SharpGen.Runtime/SharpGen.Runtime.props +++ b/SharpGen.Runtime/SharpGen.Runtime.props @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/SharpGen.Runtime/Shim/FormattableString.cs b/SharpGen.Runtime/Shim/FormattableString.cs deleted file mode 100644 index 8a91e031..00000000 --- a/SharpGen.Runtime/Shim/FormattableString.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -/*============================================================ -** -** -** -** Purpose: implementation of the FormattableString -** class. -** -===========================================================*/ - -namespace System; - -/// -/// A composite format string along with the arguments to be formatted. An instance of this -/// type may result from the use of the C# or VB language primitive "interpolated string". -/// -internal abstract class FormattableString : IFormattable -{ - /// - /// The composite format string. - /// - public abstract string Format { get; } - - /// - /// Returns an object array that contains zero or more objects to format. Clients should not - /// mutate the contents of the array. - /// - public abstract object?[] GetArguments(); - - /// - /// The number of arguments to be formatted. - /// - public abstract int ArgumentCount { get; } - - /// - /// Returns one argument to be formatted from argument position . - /// - public abstract object? GetArgument(int index); - - /// - /// Format to a string using the given culture. - /// - public abstract string ToString(IFormatProvider? formatProvider); - - string IFormattable.ToString(string? ignored, IFormatProvider? formatProvider) - { - return ToString(formatProvider); - } - - /// - /// Format the given object in the invariant culture. This static method may be - /// imported in C# by - /// - /// using static System.FormattableString; - /// . - /// Within the scope - /// of that import directive an interpolated string may be formatted in the - /// invariant culture by writing, for example, - /// - /// Invariant($"{{ lat = {latitude}; lon = {longitude} }}") - /// - /// - public static string Invariant(FormattableString formattable) - { - if (formattable == null) - { - throw new ArgumentNullException(nameof(formattable)); - } - - return formattable.ToString(Globalization.CultureInfo.InvariantCulture); - } - - /// - /// Format the given object in the current culture. This static method may be - /// imported in C# by - /// - /// using static System.FormattableString; - /// . - /// Within the scope - /// of that import directive an interpolated string may be formatted in the - /// current culture by writing, for example, - /// - /// CurrentCulture($"{{ lat = {latitude}; lon = {longitude} }}") - /// - /// - public static string CurrentCulture(FormattableString formattable) - { - if (formattable == null) - { - throw new ArgumentNullException(nameof(formattable)); - } - - return formattable.ToString(Globalization.CultureInfo.CurrentCulture); - } - - public override string ToString() - { - return ToString(Globalization.CultureInfo.CurrentCulture); - } -} \ No newline at end of file diff --git a/SharpGen.Runtime/Shim/FormattableStringFactory.cs b/SharpGen.Runtime/Shim/FormattableStringFactory.cs deleted file mode 100644 index 974fb978..00000000 --- a/SharpGen.Runtime/Shim/FormattableStringFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -/*============================================================ -** -** -** -** Purpose: implementation of the FormattableStringFactory -** class. -** -===========================================================*/ - -namespace System.Runtime.CompilerServices; - -/// -/// A factory type used by compilers to create instances of the type . -/// -internal static class FormattableStringFactory -{ - /// - /// Create a from a composite format string and object - /// array containing zero or more objects to format. - /// - public static FormattableString Create(string format, params object?[] arguments) - { - if (format == null) - { - throw new ArgumentNullException(nameof(format)); - } - - if (arguments == null) - { - throw new ArgumentNullException(nameof(arguments)); - } - - return new ConcreteFormattableString(format, arguments); - } - - private sealed class ConcreteFormattableString : FormattableString - { - private readonly string _format; - private readonly object?[] _arguments; - - internal ConcreteFormattableString(string format, object?[] arguments) - { - _format = format; - _arguments = arguments; - } - - public override string Format => _format; - public override object?[] GetArguments() { return _arguments; } - public override int ArgumentCount => _arguments.Length; - public override object? GetArgument(int index) { return _arguments[index]; } - public override string ToString(IFormatProvider? formatProvider) { return string.Format(formatProvider, _format, _arguments); } - } -} \ No newline at end of file diff --git a/SharpGen.Runtime/Shim/ReferenceEqualityComparer.cs b/SharpGen.Runtime/Shim/ReferenceEqualityComparer.cs new file mode 100644 index 00000000..7215373a --- /dev/null +++ b/SharpGen.Runtime/Shim/ReferenceEqualityComparer.cs @@ -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. + +#nullable enable + +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic; + +/// +/// An that uses reference equality () +/// instead of value equality () when comparing two object instances. +/// +/// +/// The type cannot be instantiated. Instead, use the property +/// to access the singleton instance of this type. +/// +internal sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer +{ + private ReferenceEqualityComparer() { } + + /// + /// Gets the singleton instance. + /// + public static ReferenceEqualityComparer Instance { get; } = new(); + + /// + /// Determines whether two object references refer to the same object instance. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// if both and refer to the same object instance + /// or if both are ; otherwise, . + /// + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + + /// + /// Returns a hash code for the specified object. The returned hash code is based on the object + /// identity, not on the contents of the object. + /// + /// The object for which to retrieve the hash code. + /// A hash code for the identity of . + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public int GetHashCode(object? obj) + { + // Depending on target framework, RuntimeHelpers.GetHashCode might not be annotated + // with the proper nullability attribute. We'll suppress any warning that might + // result. + return RuntimeHelpers.GetHashCode(obj!); + } +} \ No newline at end of file diff --git a/SharpGen.Runtime/Trimming/TrimmingExtensions.cs b/SharpGen.Runtime/Trimming/TrimmingExtensions.cs new file mode 100644 index 00000000..4ab76bc5 --- /dev/null +++ b/SharpGen.Runtime/Trimming/TrimmingExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace SharpGen.Runtime.Trimming +{ + internal static class TrimmingExtensions + { + /* + These extensions are implemented in this way to show intent based on usage [suppressing un-analyzable patterns]. + + Given the analyzer code, seen below + https://github.com/dotnet/linker/blob/a66609adf79440272e6522c29e8a90291030125b/src/ILLink.RoslynAnalyzer/DataFlow/DynamicallyAccessedMembersBinder.cs#L341 + + Nested interfaces should already be preserved when `DynamicallyAccessedMemberTypes.Interfaces` is specified. + */ + +#if NET6_0_OR_GREATER + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2068", Justification = "Nested interfaces are already preserved.")] +#endif + public static TypeInfo GetTypeInfoWithNestedPreservedInterfaces(this Type type) + { + return type.GetTypeInfo(); + } + +#if NET6_0_OR_GREATER + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2073", Justification = "Nested interfaces are already preserved.")] +#endif + public static Type GetTypeWithNestedPreservedInterfaces(this object obj) + { + return obj.GetType(); + } + } +} diff --git a/SharpGen.Runtime/Trimming/TrimmingHelpers.cs b/SharpGen.Runtime/Trimming/TrimmingHelpers.cs new file mode 100644 index 00000000..57b1f66b --- /dev/null +++ b/SharpGen.Runtime/Trimming/TrimmingHelpers.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SharpGen.Runtime.Trimming +{ + /// + /// Helper class used for preserving metadata of given types. + /// + public static class TrimmingHelpers + { + /// + /// Dummy method used for telling the IL Linker to preserve the type in its entirety. + /// + /// Type to preserve all implementation for. + public static void PreserveMe< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +# endif + T>() {} + } +} diff --git a/SharpGen.Runtime/TypeDataRegistrationHelper.cs b/SharpGen.Runtime/TypeDataRegistrationHelper.cs index 30b62176..8f56332c 100644 --- a/SharpGen.Runtime/TypeDataRegistrationHelper.cs +++ b/SharpGen.Runtime/TypeDataRegistrationHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; namespace SharpGen.Runtime; @@ -19,7 +20,7 @@ private void InitializeIfNeeded() _size = 0; } - public void Add() where T : ICallbackable => Add(TypeDataStorage.Storage.SourceVtbl); + public void Add() where T : ICallbackable => Add(TypeDataStorage.GetSourceVtbl()); public void Add(IntPtr[] vtbl) { @@ -31,7 +32,16 @@ public void Add(IntPtr[] vtbl) _size += (uint) vtbl.Length; } - public void Register() where T : ICallbackable + public void Register() where T : ICallbackable => TypeDataStorage.Register(RegisterImpl()); + + internal IntPtr Register(TypeInfo type) + { + var vtbl = RegisterImpl(); + TypeDataStorage.Register(type.GUID, vtbl); + return new IntPtr(vtbl); + } + + private void** RegisterImpl() { InitializeIfNeeded(); @@ -46,8 +56,9 @@ public void Register() where T : ICallbackable offset += length; } - TypeDataStorage.Register(nativePointer); pointers.Clear(); _size = 0; + + return nativePointer; } } \ No newline at end of file diff --git a/SharpGen.Runtime/TypeDataStorage.cs b/SharpGen.Runtime/TypeDataStorage.cs index 836afc0b..19d5996c 100644 --- a/SharpGen.Runtime/TypeDataStorage.cs +++ b/SharpGen.Runtime/TypeDataStorage.cs @@ -1,9 +1,16 @@ +// #define FORCE_REFLECTION_ONLY + +#nullable enable + using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using SharpGen.Runtime.Trimming; namespace SharpGen.Runtime; @@ -32,6 +39,7 @@ static TypeDataStorage() [MethodImpl(Utilities.MethodAggressiveOptimization)] internal static Guid GetGuid() where T : ICallbackable { +#if !FORCE_REFLECTION_ONLY ref var guid = ref Storage.Guid; if (guid != default) @@ -41,29 +49,145 @@ internal static Guid GetGuid() where T : ICallbackable return guid = typeof(T).GetTypeInfo().GUID; #else return guid = typeof(T).GUID; +#endif +#else +#if NETSTANDARD1_3 + return typeof(T).GetTypeInfo().GUID; +#else + return typeof(T).GUID; +#endif #endif } internal static void Register(void* vtbl) where T : ICallbackable { - Debug.Assert(Storage.TargetVtbl is null); - Storage.TargetVtbl = vtbl; - vtblByGuid[GetGuid()] = new IntPtr(vtbl); +#if !FORCE_REFLECTION_ONLY + Register(GetGuid(), vtbl); +#endif } - internal static bool GetVtbl(Guid guid, out void* pointer) + internal static void Register(Guid guid, void* vtbl) => vtblByGuid[guid] = new IntPtr(vtbl); + + internal static IntPtr[]? GetSourceVtbl() where T : ICallbackable { - var result = vtblByGuid.TryGetValue(guid, out var ptr); - pointer = ptr.ToPointer(); - return result; +#if !FORCE_REFLECTION_ONLY + if (Storage.SourceVtbl is { } storedVtbl) + return storedVtbl; +#endif + + return GetSourceVtblFromReflection(typeof(T)); } + private static IntPtr[]? GetSourceVtblFromReflection( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif + Type type) + { + const string vtbl = "Vtbl"; + + var vtblAttribute = VtblAttribute.Get(type); + Debug.Assert(vtblAttribute is not null, $"Type {type.FullName} has no Vtbl attribute"); + + var vtblType = vtblAttribute!.Type; + +#if NETSTANDARD1_3 + static bool Predicate(MemberInfo x) => x.Name == vtbl; + + if (vtblType.GetRuntimeFields().FirstOrDefault(Predicate)?.GetValue(null) is IntPtr[] vtblFieldValue) + { + return vtblFieldValue; + } + + if (vtblType.GetRuntimeProperties().FirstOrDefault(Predicate)?.GetValue(null) is IntPtr[] vtblPropertyValue) + { + return vtblPropertyValue; + } +#else + const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Static; + + if (vtblType.GetField(vtbl, flags)?.GetValue(null) is IntPtr[] vtblFieldValue) + { + return vtblFieldValue; + } + + if (vtblType.GetProperty(vtbl, flags)?.GetValue(null) is IntPtr[] vtblPropertyValue) + { + return vtblPropertyValue; + } +#endif + + Debug.Fail($"Type {type.FullName} has no Vtbl field or property"); + + return null; + } + + internal static bool GetTargetVtbl( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TypeInfo type, out void* pointer) + { +#if !FORCE_REFLECTION_ONLY + if (vtblByGuid.TryGetValue(type.GUID, out var ptr)) + { + pointer = ptr.ToPointer(); + return true; + } +#endif + + if (GetSourceVtblFromReflection(type.AsType()) is { } sourceVtbl) + { + pointer = RegisterFromReflection(type, sourceVtbl).ToPointer(); + return true; + } + + pointer = default; + return false; + } + + private static IntPtr RegisterFromReflection( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + TypeInfo type, IntPtr[] sourceVtbl) + { + var callbackable = typeof(ICallbackable).GetTypeInfo(); + + TypeDataRegistrationHelper helper = new(); + List items = new(); + + foreach (var iface in type.ImplementedInterfaces) + { + var typeInfo = iface.GetTypeInfoWithNestedPreservedInterfaces(); + if (callbackable == typeInfo || !callbackable.IsAssignableFrom(typeInfo)) + continue; + + var iSourceVtbl = GetSourceVtblFromReflection(iface); + if (iSourceVtbl is null) + throw new Exception($"Failed to reflect Vtbl out of an {nameof(ICallbackable)} interface '{iface.FullName}'."); + + items.Add(new RegisterInheritanceItem(typeInfo, typeInfo.ImplementedInterfaces.Count(), iSourceVtbl)); + } + + // TODO: properly sort items by inheritance + // TODO: verify single inheritance + items.Sort(static (x, y) => Comparer.Default.Compare(x.InterfaceCount, y.InterfaceCount)); + + foreach (var (_, _, iSourceVtbl) in items) + helper.Add(iSourceVtbl); + + helper.Add(sourceVtbl); + return helper.Register(type); + } + + private record struct RegisterInheritanceItem(TypeInfo Type, int InterfaceCount, IntPtr[] SourceVtbl); + [SuppressMessage("ReSharper", "StaticMemberInGenericType")] [SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible")] public static class Storage where T : ICallbackable { public static Guid Guid; - public static IntPtr[] SourceVtbl; - public static void* TargetVtbl; + public static IntPtr[]? SourceVtbl; } } \ No newline at end of file diff --git a/SharpGen.Runtime/VtblAttribute.cs b/SharpGen.Runtime/VtblAttribute.cs index 7ea97689..3e74d58d 100644 --- a/SharpGen.Runtime/VtblAttribute.cs +++ b/SharpGen.Runtime/VtblAttribute.cs @@ -1,5 +1,7 @@ -using System; +#nullable enable +using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -14,13 +16,20 @@ public sealed class VtblAttribute : Attribute /// /// Type of the associated virtual method table /// +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif public Type Type { get; } /// /// Initializes a new instance of class. /// /// Type of the associated virtual method table - public VtblAttribute(Type holder) + public VtblAttribute( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif + Type holder) { Type = holder ?? throw new ArgumentNullException(nameof(holder)); @@ -28,9 +37,9 @@ public VtblAttribute(Type holder) } [MethodImpl(Utilities.MethodAggressiveOptimization)] - internal static VtblAttribute Get(Type type) => Get(type.GetTypeInfo()); + internal static VtblAttribute? Get(Type type) => Get(type.GetTypeInfo()); [MethodImpl(Utilities.MethodAggressiveOptimization)] - internal static VtblAttribute Get(TypeInfo type) => type.GetCustomAttribute(); + internal static VtblAttribute? Get(TypeInfo type) => type.GetCustomAttribute(); internal static bool Has(Type type) => Get(type.GetTypeInfo()) != null; internal static bool Has(TypeInfo type) => Get(type) != null; } \ No newline at end of file diff --git a/SharpGen.UnitTests/Mapping/Interface.cs b/SharpGen.UnitTests/Mapping/Interface.cs index 2df09b72..4d5dbb35 100644 --- a/SharpGen.UnitTests/Mapping/Interface.cs +++ b/SharpGen.UnitTests/Mapping/Interface.cs @@ -323,7 +323,7 @@ public void VoidInBufferParameter() }, new DefineExtensionRule { - Struct = "SharpGen.Runtime.PointerSize", + Struct = "SharpGen.Runtime.PointerUSize", SizeOf = 8, IsNativePrimitive = true } @@ -331,7 +331,7 @@ public void VoidInBufferParameter() Bindings = { new BindRule("HRESULT", "SharpGen.Runtime.Result"), - new BindRule("SIZE_T", "SharpGen.Runtime.PointerSize"), + new BindRule("SIZE_T", "SharpGen.Runtime.PointerUSize"), new BindRule("UINT32", "System.UInt32"), new BindRule("void", "System.Void") } diff --git a/SharpGen.UnitTests/RoslynGeneratorTests.cs b/SharpGen.UnitTests/RoslynGeneratorTests.cs new file mode 100644 index 00000000..59924b26 --- /dev/null +++ b/SharpGen.UnitTests/RoslynGeneratorTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using Microsoft.CodeAnalysis.CSharp; +using SharpGen.Generator; +using SharpGen.Model; +using SharpGen.Transform; +using Xunit; +using Xunit.Abstractions; + +namespace SharpGen.UnitTests; + +public class RoslynGeneratorTests : TestBase +{ + public RoslynGeneratorTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + [Fact] + public void IncludeXmlDocIsNotMangled() + { + CsAssembly assembly = new(); + CsNamespace @namespace = new("Test"); + @namespace.Add( + new CsStruct(null, "PAIR") + { + CppElementName = "PAIR" + } + ); + assembly.Add(@namespace); + + XmlDocument docs = new(); + docs.LoadXml(@"Test"); + + AddIocServices(CreateExternalDocCommentsReader(docs)); + + Assert.Collection( + GenerateLines(assembly), + x => Assert.Equal(@"// ", x), + x => Assert.Equal(@"namespace Test", x), + x => Assert.Equal(@"{", x), + x => Assert.Equal(@"/// ", x), + x => Assert.Equal(@"/// PAIR", x), + x => Assert.Equal(@"/// PAIR", x), + x => Assert.Equal(@"[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]", x), + x => Assert.Equal(@"public partial struct PAIR", x), + x => Assert.Equal(@"{", x), + x => Assert.Equal(@"}", x), + x => Assert.Equal(@"}", x) + ); + } + + private static Action CreateExternalDocCommentsReader(XmlDocument docs) + { + return container => + container.AddService(new ExternalDocCommentsReader(new Dictionary { ["doc.xml"] = docs })); + } + + private Action CreateDefaultGenerators() + { + return container => container.AddService(new DefaultGenerators(Ioc)); + } + + private string[] GenerateLines(CsAssembly assembly) + { + AddIocServices(CreateDefaultGenerators()); + + var root = new RoslynGenerator().Run(assembly, Ioc).GetCompilationUnitRoot(); + return root.ToFullString() + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } +} \ No newline at end of file diff --git a/SharpGen.UnitTests/Runtime/ICallback.cs b/SharpGen.UnitTests/Runtime/ICallback.cs index ff597331..b69a9b59 100644 --- a/SharpGen.UnitTests/Runtime/ICallback.cs +++ b/SharpGen.UnitTests/Runtime/ICallback.cs @@ -2,59 +2,58 @@ using System.Runtime.InteropServices; using SharpGen.Runtime; -namespace SharpGen.UnitTests.Runtime +namespace SharpGen.UnitTests.Runtime; + +[Vtbl(typeof(CallbackVtbl))] +interface ICallback : ICallbackable { - [Vtbl(typeof(CallbackVtbl))] - interface ICallback : ICallbackable - { - int Increment(int param); - } + int Increment(int param); +} - class CallbackImpl : CallbackBase, ICallback - { - public int Increment(int param) => param + 1; - } +class CallbackImpl : CallbackBase, ICallback +{ + public int Increment(int param) => param + 1; +} + +public static class CallbackVtbl +{ + private static readonly IncrementDelegate Increment = IncrementImpl; - public static class CallbackVtbl + public static readonly IntPtr[] Vtbl = { - private static readonly IncrementDelegate Increment = IncrementImpl; + Marshal.GetFunctionPointerForDelegate(Increment) + }; - public static readonly IntPtr[] Vtbl = - { - Marshal.GetFunctionPointerForDelegate(Increment) - }; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int IncrementDelegate(IntPtr thisObj, int param); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate int IncrementDelegate(IntPtr thisObj, int param); + private static int IncrementImpl(IntPtr thisObj, int param) => + CppObjectShadow.ToCallback(thisObj).Increment(param); +} - private static int IncrementImpl(IntPtr thisObj, int param) => - CppObjectShadow.ToCallback(thisObj).Increment(param); - } +[Vtbl(typeof(Callback2Vtbl))] +interface ICallback2: ICallback +{ + int Decrement(int param); +} - [Vtbl(typeof(Callback2Vtbl))] - interface ICallback2: ICallback - { - int Decrement(int param); - } +public static class Callback2Vtbl +{ + private static readonly DecrementDelegate Decrement = DecrementImpl; - public static class Callback2Vtbl + public static IntPtr[] Vtbl { get; } = { - private static readonly DecrementDelegate Decrement = DecrementImpl; - - public static IntPtr[] Fill() => new[] - { - Marshal.GetFunctionPointerForDelegate(Decrement) - }; + Marshal.GetFunctionPointerForDelegate(Decrement) + }; - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate int DecrementDelegate(IntPtr thisObj, int param); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int DecrementDelegate(IntPtr thisObj, int param); - private static int DecrementImpl(IntPtr thisObj, int param) => - CppObjectShadow.ToCallback(thisObj).Increment(param); - } - - class Callback2Impl : CallbackImpl, ICallback, ICallback2 - { - public int Decrement(int param) => param - 1; - } + private static int DecrementImpl(IntPtr thisObj, int param) => + CppObjectShadow.ToCallback(thisObj).Increment(param); } + +class Callback2Impl : CallbackImpl, ICallback, ICallback2 +{ + public int Decrement(int param) => param - 1; +} \ No newline at end of file diff --git a/SharpGen.UnitTests/Runtime/TestTypeRegistry.cs b/SharpGen.UnitTests/Runtime/TestTypeRegistry.cs index e0038e78..b797020c 100644 --- a/SharpGen.UnitTests/Runtime/TestTypeRegistry.cs +++ b/SharpGen.UnitTests/Runtime/TestTypeRegistry.cs @@ -9,7 +9,7 @@ internal static class TestTypeRegistry internal static void Initialize() { TypeDataStorage.Storage.SourceVtbl = CallbackVtbl.Vtbl; - TypeDataStorage.Storage.SourceVtbl = Callback2Vtbl.Fill(); + TypeDataStorage.Storage.SourceVtbl = Callback2Vtbl.Vtbl; TypeDataRegistrationHelper helper = new(); { helper.Add(); diff --git a/SharpGen.UnitTests/Runtime/VtblTests.cs b/SharpGen.UnitTests/Runtime/VtblTests.cs index 4bca413e..7d13839c 100644 --- a/SharpGen.UnitTests/Runtime/VtblTests.cs +++ b/SharpGen.UnitTests/Runtime/VtblTests.cs @@ -3,21 +3,20 @@ using SharpGen.Runtime; using Xunit; -namespace SharpGen.UnitTests.Runtime +namespace SharpGen.UnitTests.Runtime; + +public class VtblTests { - public class VtblTests + [Fact] + public void CanRoundTripCallThroughNativeVtblToManagedObject() { - [Fact] - public void CanRoundTripCallThroughNativeVtblToManagedObject() - { - using var callback = new CallbackImpl(); + using var callback = new CallbackImpl(); - var callbackPtr = MarshallingHelpers.ToCallbackPtr(callback); - Assert.NotEqual(IntPtr.Zero, callbackPtr); + var callbackPtr = MarshallingHelpers.ToCallbackPtr(callback); + Assert.NotEqual(IntPtr.Zero, callbackPtr); - var methodPtr = Marshal.ReadIntPtr(Marshal.ReadIntPtr(callbackPtr)); - var delegateObject = Marshal.GetDelegateForFunctionPointer(methodPtr); - Assert.Equal(3, delegateObject(callbackPtr, 2)); - } + var methodPtr = Marshal.ReadIntPtr(Marshal.ReadIntPtr(callbackPtr)); + var delegateObject = Marshal.GetDelegateForFunctionPointer(methodPtr); + Assert.Equal(3, delegateObject(callbackPtr, 2)); } -} +} \ No newline at end of file diff --git a/SharpGen.UnitTests/SharpGen.UnitTests.csproj b/SharpGen.UnitTests/SharpGen.UnitTests.csproj index 920321b6..339c6f12 100644 --- a/SharpGen.UnitTests/SharpGen.UnitTests.csproj +++ b/SharpGen.UnitTests/SharpGen.UnitTests.csproj @@ -1,27 +1,23 @@  + + net9.0 + false + true + - + + + + - - net5.0 - false - true - - - - - - - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/SharpGen.UnitTests/TestBase.cs b/SharpGen.UnitTests/TestBase.cs index f6f5dca7..81115d85 100644 --- a/SharpGen.UnitTests/TestBase.cs +++ b/SharpGen.UnitTests/TestBase.cs @@ -24,6 +24,11 @@ protected TestBase(ITestOutputHelper outputHelper) Ioc.ConfigureServices(serviceContainer); } + protected void AddIocServices(Action action) + { + action(serviceContainer); + } + protected IDisposable LoggerEnvironment(LoggerAssertHandler handler) { return new LoggerTestEnvironment(loggerImpl, handler); diff --git a/SharpGen/Config/BindRule.cs b/SharpGen/Config/BindRule.cs index 45185256..60c8faa3 100644 --- a/SharpGen/Config/BindRule.cs +++ b/SharpGen/Config/BindRule.cs @@ -18,6 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable + +using System.ComponentModel; using System.Globalization; using System.Xml.Serialization; @@ -30,35 +33,20 @@ namespace SharpGen.Config; [XmlType("bind")] public class BindRule : ConfigBaseRule { - /// - /// Initializes a new instance of the class. - /// public BindRule() { } - /// - /// Initializes a new instance of the class. - /// - /// The cpp type - /// The C# type - public BindRule(string @from, string to) - { - From = from; - To = to; - } - - /// - /// Initializes a new instance of the class. - /// - /// The c++ type + /// The C++ type /// The C# public type /// The C# marshal type - public BindRule(string @from, string to, string marshal) + /// The source of the definition + public BindRule(string from, string to, string? marshal = null, string? source = null) { From = from; To = to; Marshal = marshal; + Source = source; } /// @@ -79,12 +67,32 @@ public BindRule(string @from, string to, string marshal) /// Gets or sets the C# the marshal type /// [XmlAttribute("marshal")] - public string Marshal { get; set; } + public string? Marshal { get; set; } - /// - [ExcludeFromCodeCoverage] - public override string ToString() + /// The source of the definition + [XmlAttribute("source")] + [DefaultValue(null)] + public string? Source { get; set; } + + /// + /// Provides an ability to override an existing mapping + /// + [XmlIgnore] + public bool? Override { get; set; } + + [XmlAttribute("override")] + public bool _Override_ { - return string.Format(CultureInfo.InvariantCulture, "{0} from:{1} to:{2} marshal:{3}", base.ToString(), From, To, Marshal); + get => Override.Value; + set => Override = value; } + + public bool ShouldSerialize_Override_() => Override != null; + + /// + [ExcludeFromCodeCoverage] + public override string ToString() => string.Format( + CultureInfo.InvariantCulture, + "{0} from:{1} to:{2} marshal:{3} source:{4} override:{5}", base.ToString(), From, To, Marshal, Source, Override + ); } \ No newline at end of file diff --git a/SharpGen/Config/ConfigFile.cs b/SharpGen/Config/ConfigFile.cs index 6c34f476..cc6a5f8d 100644 --- a/SharpGen/Config/ConfigFile.cs +++ b/SharpGen/Config/ConfigFile.cs @@ -297,7 +297,7 @@ public string ExpandString(string str, bool expandDynamicVariable, Logger logger var localResult = GetVariable(name, logger) ?? Environment.GetEnvironmentVariable(name); if (localResult is not null) return localResult; - logger.Error(LoggingCodes.UnkownVariable, "Unable to substitute config/environment variable $({0}). Variable is not defined", name); + logger.Error(LoggingCodes.UnknownVariable, "Unable to substitute config/environment variable $({0}). Variable is not defined", name); return string.Empty; } ); @@ -314,7 +314,7 @@ public string ExpandString(string str, bool expandDynamicVariable, Logger logger if (GetRoot().DynamicVariables.TryGetValue(name, out var localResult)) return localResult.Trim('"'); - logger.Error(LoggingCodes.UnkownDynamicVariable, + logger.Error(LoggingCodes.UnknownDynamicVariable, "Unable to substitute dynamic variable #({0}). Variable is not defined", name); return string.Empty; } diff --git a/SharpGen/Config/DefineExtensionRule.cs b/SharpGen/Config/DefineExtensionRule.cs index ed8cd520..9cbadb91 100644 --- a/SharpGen/Config/DefineExtensionRule.cs +++ b/SharpGen/Config/DefineExtensionRule.cs @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +using System.ComponentModel; using System.Globalization; using System.Xml.Serialization; @@ -39,41 +40,77 @@ public class DefineExtensionRule : ExtensionBaseRule [XmlAttribute("underlying")] public string UnderlyingType { get; set; } - [XmlAttribute("shadow")] + [XmlAttribute("shadow"), DefaultValue(null)] public string ShadowName { get; set; } [XmlAttribute("vtbl")] public string VtblName { get; set; } - [XmlIgnore] - public int? SizeOf { get; set; } + [XmlIgnore] public int? SizeOf { get; set; } + [XmlAttribute("sizeof")] - public int _SizeOf_ { get { return SizeOf.Value; } set { SizeOf = value; } } public bool ShouldSerialize_SizeOf_() { return SizeOf != null; } - - [XmlIgnore] - public int? Align { get; set; } + public int _SizeOf_ + { + get => SizeOf.Value; + set => SizeOf = value; + } + + public bool ShouldSerialize_SizeOf_() => SizeOf != null; + + [XmlIgnore] public int? Align { get; set; } + [XmlAttribute("align")] - public int _Align_ { get { return Align.Value; } set { Align = value; } } public bool ShouldSerialize_Align_() { return Align != null; } + public int _Align_ + { + get => Align.Value; + set => Align = value; + } + + public bool ShouldSerialize_Align_() => Align != null; + + [XmlIgnore] public bool? HasCustomMarshal { get; set; } - [XmlIgnore] - public bool? HasCustomMarshal { get; set; } [XmlAttribute("marshal")] - public bool _HasCustomMarshal_ { get { return HasCustomMarshal.Value; } set { HasCustomMarshal = value; } } public bool ShouldSerialize_HasCustomMarshal_() { return HasCustomMarshal != null; } + public bool _HasCustomMarshal_ + { + get => HasCustomMarshal.Value; + set => HasCustomMarshal = value; + } + + public bool ShouldSerialize_HasCustomMarshal_() => HasCustomMarshal != null; + + [XmlIgnore] public bool? IsStaticMarshal { get; set; } - [XmlIgnore] - public bool? IsStaticMarshal { get; set; } [XmlAttribute("static-marshal")] - public bool _IsStaticMarshal_ { get { return IsStaticMarshal.Value; } set { IsStaticMarshal = value; } } public bool ShouldSerialize_IsStaticMarshal_() { return IsStaticMarshal != null; } + public bool _IsStaticMarshal_ + { + get => IsStaticMarshal.Value; + set => IsStaticMarshal = value; + } + + public bool ShouldSerialize_IsStaticMarshal_() => IsStaticMarshal != null; + + [XmlIgnore] public bool? HasCustomNew { get; set; } - [XmlIgnore] - public bool? HasCustomNew { get; set; } [XmlAttribute("custom-new")] - public bool _HasCustomNew_ { get { return HasCustomNew.Value; } set { HasCustomNew = value; } } public bool ShouldSerialize_HasCustomNew_() { return HasCustomNew != null; } + public bool _HasCustomNew_ + { + get => HasCustomNew.Value; + set => HasCustomNew = value; + } + + public bool ShouldSerialize_HasCustomNew_() => HasCustomNew != null; + + [XmlIgnore] public bool? IsNativePrimitive { get; set; } - [XmlIgnore] - public bool? IsNativePrimitive { get; set; } [XmlAttribute("primitive")] - public bool _IsNativePrimitive_ { get => IsNativePrimitive.Value; set => IsNativePrimitive = value; } public bool ShouldSerialize_IsNativePrimitive_() => IsNativePrimitive != null; + public bool _IsNativePrimitive_ + { + get => IsNativePrimitive.Value; + set => IsNativePrimitive = value; + } + + public bool ShouldSerialize_IsNativePrimitive_() => IsNativePrimitive != null; [XmlIgnore] public bool? IsCallbackInterface { get; set; } diff --git a/SharpGen/Config/MappingRule.cs b/SharpGen/Config/MappingRule.cs index 70a54e89..f3522a92 100644 --- a/SharpGen/Config/MappingRule.cs +++ b/SharpGen/Config/MappingRule.cs @@ -171,21 +171,7 @@ public int _StructPack_ /// Mapping name /// [XmlAttribute("name")] - public string MappingNameFinal - { - get => MappingName; - set - { - MappingName = value; - IsFinalMappingName = true; - } - } - - /// - /// True if the MappingName doesn't need any further rename processing - /// - [XmlIgnore] - public bool? IsFinalMappingName { get; set; } = false; + public string MappingNameFinal { get; set; } /// /// True if a struct should used a native value type marshalling @@ -468,6 +454,32 @@ public bool _Hidden_ set => Hidden = value; } + /// + /// Provides an ability to prevent default vtbl method implementation generation in an interface + /// + [XmlIgnore] + public bool? ManagedPartial { get; set; } + + [XmlAttribute("managed-partial")] + public bool _ManagedPartial_ + { + get => ManagedPartial.Value; + set => ManagedPartial = value; + } + + /// + /// Provides an ability to prevent default CppObject method implementation generation + /// + [XmlIgnore] + public bool? NativePartial { get; set; } + + [XmlAttribute("native-partial")] + public bool _NativePartial_ + { + get => NativePartial.Value; + set => NativePartial = value; + } + /// /// Best-effort flag for retaining pointers in generated bindings. /// @@ -516,9 +528,9 @@ public StringMarshalType _StringMarshal_ public bool ShouldSerialize_StructPack_() => StructPack != null; - public bool ShouldSerializeMappingName() => IsFinalMappingName.HasValue && !IsFinalMappingName.Value; + public bool ShouldSerializeMappingName() => MappingName != null; - public bool ShouldSerializeMappingNameFinal() => !IsFinalMappingName.HasValue || IsFinalMappingName.Value; + public bool ShouldSerializeMappingNameFinal() => MappingNameFinal != null; public bool ShouldSerialize_StructHasNativeValueType_() => StructHasNativeValueType != null; @@ -540,7 +552,7 @@ public StringMarshalType _StringMarshal_ public bool ShouldSerialize_IsDualCallbackInterface_() => IsDualCallbackInterface != null; - public bool ShouldSerialize_AutoGenerateShadow_() => AutoGenerateShadow != null; + public bool ShouldSerialize_AutoGenerateShadow_() => false; public bool ShouldSerialize_AutoGenerateVtbl_() => AutoGenerateVtbl != null; @@ -558,6 +570,10 @@ public StringMarshalType _StringMarshal_ public bool ShouldSerialize_Hidden_() => Hidden != null; + public bool ShouldSerialize_ManagedPartial_() => ManagedPartial != null; + + public bool ShouldSerialize_NativePartial_() => NativePartial != null; + public bool ShouldSerialize_KeepPointers_() => KeepPointers != null; public bool ShouldSerialize_StringMarshal_() => StringMarshal != null; diff --git a/SharpGen/Config/SdkRule.cs b/SharpGen/Config/SdkRule.cs index b8f252f1..b2232683 100644 --- a/SharpGen/Config/SdkRule.cs +++ b/SharpGen/Config/SdkRule.cs @@ -1,58 +1,57 @@ using System; using System.Xml.Serialization; -namespace SharpGen.Config +namespace SharpGen.Config; + +public enum SdkLib { - public enum SdkLib + StdLib = 1, + WindowsSdk +} + +public static class SdkLibExtensions +{ + public static string Name(this SdkLib lib) => lib switch { - StdLib = 1, - WindowsSdk - } + SdkLib.StdLib => "standard library", + SdkLib.WindowsSdk => "Windows SDK", + _ => lib.ToString() + }; +} - public static class SdkLibExtensions +public class SdkRule +{ + public SdkRule() { - public static string Name(this SdkLib lib) => lib switch - { - SdkLib.StdLib => "standard library", - SdkLib.WindowsSdk => "Windows SDK", - _ => lib.ToString() - }; } - public class SdkRule + public SdkRule(SdkLib name, string version) { - public SdkRule() - { - } - - public SdkRule(SdkLib name, string version) - { - Name = name; - Version = version; - } + Name = name; + Version = version; + } - [XmlIgnore] - public SdkLib? Name { get; private set; } + [XmlIgnore] + public SdkLib? Name { get; private set; } - [XmlAttribute("name")] - public string _Name_ + [XmlAttribute("name")] + public string _Name_ + { + get => Name.ToString(); + set { - get => Name.ToString(); - set - { - if (Enum.TryParse(value, out SdkLib name)) - Name = name; - else - Name = null; - } + if (Enum.TryParse(value, out SdkLib name)) + Name = name; + else + Name = null; } + } - public bool ShouldSerialize_Name_() => Name is { } name && Enum.IsDefined(typeof(SdkLib), name); + public bool ShouldSerialize_Name_() => Name is { } name && Enum.IsDefined(typeof(SdkLib), name); - [XmlAttribute("version")] - public string Version { get; set; } + [XmlAttribute("version")] + public string Version { get; set; } - [XmlAttribute("components")] - public string Components { get; set; } - } -} + [XmlAttribute("components")] + public string Components { get; set; } +} \ No newline at end of file diff --git a/SharpGen/Generator/CallableCodeGenerator.cs b/SharpGen/Generator/CallableCodeGenerator.cs index de2fdf4a..5cdfae42 100644 --- a/SharpGen/Generator/CallableCodeGenerator.cs +++ b/SharpGen/Generator/CallableCodeGenerator.cs @@ -39,6 +39,13 @@ public override MemberDeclarationSyntax GenerateCode(CsCallable csElement) .WithModifiers(TokenList()); } + if (csElement is CsMethod { NativePartial: true }) + { + return methodDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + var statements = NewStatementList; foreach (var param in csElement.Parameters) diff --git a/SharpGen/Generator/GeneratorHelpers.cs b/SharpGen/Generator/GeneratorHelpers.cs index 569b6b39..a02daaf1 100644 --- a/SharpGen/Generator/GeneratorHelpers.cs +++ b/SharpGen/Generator/GeneratorHelpers.cs @@ -237,6 +237,6 @@ public static ExpressionSyntax PlatformSpecificExpression(GlobalNamespaceProv : nonWindowsExpression(); } - public static readonly IdentifierNameSyntax PreprocessorNameSyntax = IdentifierName("NET5_0_OR_GREATER"); + public static readonly IdentifierNameSyntax PreprocessorNameSyntax = IdentifierName("NET6_0_OR_GREATER"); public static readonly PrefixUnaryExpressionSyntax NotPreprocessorNameSyntax = PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, PreprocessorNameSyntax); } \ No newline at end of file diff --git a/SharpGen/Generator/InterfaceCodeGenerator.cs b/SharpGen/Generator/InterfaceCodeGenerator.cs index b74457d4..ed388c98 100644 --- a/SharpGen/Generator/InterfaceCodeGenerator.cs +++ b/SharpGen/Generator/InterfaceCodeGenerator.cs @@ -310,16 +310,29 @@ StatementSyntax GetDisposeInvocation(CsProperty x, params ArgumentSyntax[] args) var resultList = NewMemberList; - if (csElement.IsCallback && csElement.AutoGenerateVtbl) + if (csElement.IsCallback) { - resultList.Add(csElement, Generators.Vtbl); - var shadowAttribute = Attribute( - GlobalNamespace.GetTypeNameSyntax(WellKnownName.VtblAttribute), - AttributeArgumentList( - SingletonSeparatedList(AttributeArgument(TypeOfExpression(ParseTypeName(csElement.VtblName)))) - ) - ); - attributes = attributes?.AddAttributes(shadowAttribute) ?? AttributeList(SingletonSeparatedList(shadowAttribute)); + if (csElement.AutoGenerateVtbl) + { + resultList.Add(csElement, Generators.Vtbl); + var attribute = Attribute( + GlobalNamespace.GetTypeNameSyntax(WellKnownName.VtblAttribute), + AttributeArgumentList( + SingletonSeparatedList(AttributeArgument(TypeOfExpression(ParseTypeName(csElement.VtblName)))) + ) + ); + attributes = attributes?.AddAttributes(attribute) ?? AttributeList(SingletonSeparatedList(attribute)); + } + if (csElement.AutoGenerateShadow) + { + var attribute = Attribute( + GlobalNamespace.GetTypeNameSyntax(WellKnownName.ShadowAttribute), + AttributeArgumentList( + SingletonSeparatedList(AttributeArgument(TypeOfExpression(ParseTypeName(csElement.ShadowName)))) + ) + ); + attributes = attributes?.AddAttributes(attribute) ?? AttributeList(SingletonSeparatedList(attribute)); + } } var attributeList = attributes != null ? SingletonList(attributes) : default; diff --git a/SharpGen/Generator/PropertyCodeGenerator.cs b/SharpGen/Generator/PropertyCodeGenerator.cs index 865dcddf..87612197 100644 --- a/SharpGen/Generator/PropertyCodeGenerator.cs +++ b/SharpGen/Generator/PropertyCodeGenerator.cs @@ -138,7 +138,7 @@ public override IEnumerable GenerateCode(CsProperty csE { var paramByRef = GetMarshaller(csElement.Setter.Parameters[0]) .GenerateManagedArgument(csElement.Setter.Parameters[0]) - .RefOrOutKeyword.Kind() == SyntaxKind.RefKeyword; + .RefOrOutKeyword.IsKind(SyntaxKind.RefKeyword); accessors.Add(AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause( diff --git a/SharpGen/Generator/RoslynGenerator.cs b/SharpGen/Generator/RoslynGenerator.cs index b51d8af8..cf717768 100644 --- a/SharpGen/Generator/RoslynGenerator.cs +++ b/SharpGen/Generator/RoslynGenerator.cs @@ -18,93 +18,48 @@ public sealed class RoslynGenerator SingletonSeparatedList(Attribute(ParseName("System.Runtime.CompilerServices.ModuleInitializerAttribute"))) ); - public void Run(CsAssembly csAssembly, string generatedCodeFolder, Ioc ioc) + public SyntaxTree Run(CsAssembly csAssembly, Ioc ioc) { - if (string.IsNullOrEmpty(generatedCodeFolder)) - throw new ArgumentException("Value cannot be null or empty.", nameof(generatedCodeFolder)); - var logger = ioc.Logger; var generators = ioc.Generators; - var directoryToCreate = new HashSet(StringComparer.CurrentCulture); - - // Remove the generated directory before creating it - if (!directoryToCreate.Contains(generatedCodeFolder)) - { - directoryToCreate.Add(generatedCodeFolder); - if (Directory.Exists(generatedCodeFolder)) - { - foreach (var oldGeneratedFile in Directory.EnumerateFiles(generatedCodeFolder, "*.cs", SearchOption.AllDirectories)) - { - try - { - File.Delete(oldGeneratedFile); - } - catch - { - // ignored - } - } - } - } - - if (!Directory.Exists(generatedCodeFolder)) - Directory.CreateDirectory(generatedCodeFolder); - - logger.Message("Process Assembly => {0}", generatedCodeFolder); - - List trees = new() - { - CreateTree("Enumerations", ns => ns.Enums, generators.Enum), - CreateTree("Structures", ns => ns.Structs, generators.Struct), - CreateTree("Functions", ns => ns.Classes, generators.Group), - CreateTree("Interfaces", ns => ns.Interfaces, generators.Interface) - }; + logger.Message("Generating Roslyn syntax tree..."); var resultConstants = csAssembly.Namespaces .SelectMany(x => x.EnumerateDescendants(withAdditionalItems: false)) .ToArray(); - if (resultConstants.Length > 0) - trees.Add( - CSharpSyntaxTree.Create( - CompilationUnit( - default, - default, - default, - SingletonList( - ClassDeclaration("ModuleDataInitializer") - .WithModifiers(ModuleInitModifiers) - .AddMembers(GenerateResultDescriptor(resultConstants, ioc)) - ) - ).NormalizeWhitespace(elasticTrivia: true), - path: FormatFilePath("ModuleData") - ) - ); - SyntaxTree CreateTree(string fileName, Func> membersFunc, - IMemberCodeGenerator generator) where T : CsBase + var moduleInitializer = resultConstants.Length > 0 + ? new[] + { + ClassDeclaration("ModuleDataInitializer") + .WithModifiers(ModuleInitModifiers) + .AddMembers(GenerateResultDescriptor(resultConstants, ioc)) + } + : Enumerable.Empty(); + + MemberDeclarationSyntax NamespaceSelector(CsNamespace ns) { - MemberDeclarationSyntax NamespaceSelector(CsNamespace ns) - { - MemberSyntaxList list = new(ioc); - list.AddRange(membersFunc(ns).OrderBy(element => element.Name), generator); - return NamespaceDeclaration(ParseName(ns.Name), default, default, List(list)) - .WithLeadingTrivia(Comment(AutoGeneratedCommentText)); - } + MemberSyntaxList list = new(ioc); + list.AddRange(ns.Enums.OrderBy(element => element.Name), generators.Enum); + list.AddRange(ns.Structs.OrderBy(element => element.Name), generators.Struct); + list.AddRange(ns.Classes.OrderBy(element => element.Name), generators.Group); + list.AddRange(ns.Interfaces.OrderBy(element => element.Name), generators.Interface); + return NamespaceDeclaration(ParseName(ns.Name), default, default, List(list)) + .WithLeadingTrivia(Comment(AutoGeneratedCommentText)); + } - return CSharpSyntaxTree.Create( + return CSharpSyntaxTree.Create( + RoslynSyntaxNormalizer.Normalize( CompilationUnit( default, default, default, - List(csAssembly.Namespaces.Select(NamespaceSelector)) - ).NormalizeWhitespace(elasticTrivia: true), - path: FormatFilePath(fileName) - ); - } - - string FormatFilePath(string fileName) => Path.Combine(generatedCodeFolder, $"{fileName}.cs"); - - foreach (var tree in trees) - File.WriteAllText(tree.FilePath, tree.GetCompilationUnitRoot().ToFullString()); + List(csAssembly.Namespaces.Select(NamespaceSelector)).AddRange(moduleInitializer) + ), + " ", + "\r\n", + true + ) + ); } private MethodDeclarationSyntax GenerateResultDescriptor(CsResultConstant[] descriptors, Ioc ioc) diff --git a/SharpGen/Generator/RoslynSyntaxNormalizer.cs b/SharpGen/Generator/RoslynSyntaxNormalizer.cs new file mode 100644 index 00000000..85ffd379 --- /dev/null +++ b/SharpGen/Generator/RoslynSyntaxNormalizer.cs @@ -0,0 +1,1205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace SharpGen.Generator; + +internal sealed class RoslynSyntaxNormalizer : CSharpSyntaxRewriter +{ + private readonly TextSpan _consideredSpan; + private readonly int _initialDepth; + private readonly string _indentWhitespace; + private readonly bool _useElasticTrivia; + private readonly SyntaxTrivia _eolTrivia; + + private bool _isInStructuredTrivia; + + private SyntaxToken _previousToken; + + private bool _afterLineBreak; + private bool _afterIndentation; + private bool _inSingleLineInterpolation; + + // CONSIDER: if we become concerned about space, we shouldn't actually need any + // of the values between indentations[0] and indentations[initialDepth] (exclusive). + private ImmutableArray.Builder? _indentations; + + private RoslynSyntaxNormalizer(TextSpan consideredSpan, int initialDepth, string indentWhitespace, + string eolWhitespace, bool useElasticTrivia) + : base(visitIntoStructuredTrivia: true) + { + _consideredSpan = consideredSpan; + _initialDepth = initialDepth; + _indentWhitespace = indentWhitespace; + _useElasticTrivia = useElasticTrivia; + _eolTrivia = useElasticTrivia + ? SyntaxFactory.ElasticEndOfLine(eolWhitespace) + : SyntaxFactory.EndOfLine(eolWhitespace); + _afterLineBreak = true; + } + + internal static TNode Normalize(TNode node, string indentWhitespace, string eolWhitespace, + bool useElasticTrivia = false) + where TNode : SyntaxNode + { + var normalizer = new RoslynSyntaxNormalizer(node.FullSpan, GetDeclarationDepth(node), indentWhitespace, + eolWhitespace, useElasticTrivia); + return (TNode) normalizer.Visit(node); + } + + public override SyntaxToken VisitToken(SyntaxToken token) + { + if (token.IsKind(SyntaxKind.None) || (token.IsMissing && token.FullSpan.Length == 0)) + { + return token; + } + + try + { + var tk = token; + + var depth = GetDeclarationDepth(token); + + tk = tk.WithLeadingTrivia(RewriteTrivia( + token.LeadingTrivia, + depth, + isTrailing: false, + indentAfterLineBreak: NeedsIndentAfterLineBreak(token), + mustHaveSeparator: false, + lineBreaksAfter: 0)); + + var nextToken = this.GetNextRelevantToken(token); + + _afterLineBreak = IsLineBreak(token); + _afterIndentation = false; + + var lineBreaksAfter = LineBreaksAfter(token, nextToken); + var needsSeparatorAfter = NeedsSeparator(token, nextToken); + tk = tk.WithTrailingTrivia(RewriteTrivia( + token.TrailingTrivia, + depth, + isTrailing: true, + indentAfterLineBreak: false, + mustHaveSeparator: needsSeparatorAfter, + lineBreaksAfter: lineBreaksAfter)); + + return tk; + } + finally + { + // to help debugging + _previousToken = token; + } + } + + private SyntaxToken GetNextRelevantToken(SyntaxToken token) + { + // get next token, skipping zero width tokens except for end-of-directive tokens + var nextToken = token; + + do + { + nextToken = nextToken.GetNextToken(true, true); + } while (nextToken != default && nextToken.Span.Length == 0 && !nextToken.IsKind(SyntaxKind.EndOfDirectiveToken)); + + return _consideredSpan.Contains(nextToken.FullSpan) ? nextToken : default; + } + + private SyntaxTrivia GetIndentation(int count) + { + count = Math.Max(count - _initialDepth, 0); + + int capacity = count + 1; + if (_indentations == null) + { + _indentations = ImmutableArray.CreateBuilder(capacity); + } + + // grow indentation collection if necessary + for (int i = _indentations.Count; i <= count; i++) + { + string text = i == 0 + ? "" + : _indentations[i - 1] + _indentWhitespace; + _indentations.Add( + _useElasticTrivia ? SyntaxFactory.ElasticWhitespace(text) : SyntaxFactory.Whitespace(text)); + } + + return _indentations[count]; + } + + private static bool NeedsIndentAfterLineBreak(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.EndOfFileToken); + } + + private int LineBreaksAfter(SyntaxToken currentToken, SyntaxToken nextToken) + { + if (_inSingleLineInterpolation) + { + return 0; + } + + if (currentToken.IsKind(SyntaxKind.EndOfDirectiveToken)) + { + return 1; + } + + if (nextToken.IsKind(SyntaxKind.None)) + { + return 0; + } + + // none of the following tests currently have meaning for structured trivia + if (_isInStructuredTrivia) + { + return 0; + } + + if (nextToken.IsKind(SyntaxKind.CloseBraceToken) && + IsAccessorListWithoutAccessorsWithBlockBody(currentToken.Parent?.Parent)) + { + return 0; + } + + switch (currentToken.Kind()) + { + case SyntaxKind.None: + return 0; + + case SyntaxKind.OpenBraceToken: + return LineBreaksAfterOpenBrace(currentToken, nextToken); + + case SyntaxKind.FinallyKeyword: + return 1; + + case SyntaxKind.CloseBraceToken: + return LineBreaksAfterCloseBrace(currentToken, nextToken); + + case SyntaxKind.CloseParenToken: + if (currentToken.Parent is PositionalPatternClauseSyntax) + { + //don't break inside a recursive pattern + return 0; + } + + // Note: the `where` case handles constraints on method declarations + // and also `where` clauses (consistently with other LINQ cases below) + return (currentToken.Parent is StatementSyntax && nextToken.Parent != currentToken.Parent) + || nextToken.IsKind(SyntaxKind.OpenBraceToken) + || nextToken.IsKind(SyntaxKind.WhereKeyword) + ? 1 + : 0; + + case SyntaxKind.CloseBracketToken: + if (currentToken.Parent is AttributeListSyntax && currentToken.Parent.Parent is not ParameterSyntax) + { + return 1; + } + + break; + + case SyntaxKind.SemicolonToken: + return LineBreaksAfterSemicolon(currentToken, nextToken); + + case SyntaxKind.CommaToken: + return currentToken.Parent is EnumDeclarationSyntax or SwitchExpressionSyntax ? 1 : 0; + case SyntaxKind.ElseKeyword: + return !nextToken.IsKind(SyntaxKind.IfKeyword) ? 1 : 0; + case SyntaxKind.ColonToken: + if (currentToken.Parent is LabeledStatementSyntax or SwitchLabelSyntax) + { + return 1; + } + + break; + case SyntaxKind.SwitchKeyword when currentToken.Parent is SwitchExpressionSyntax: + return 1; + } + + if ((nextToken.IsKind(SyntaxKind.FromKeyword) && nextToken.Parent.IsKind(SyntaxKind.FromClause)) || + (nextToken.IsKind(SyntaxKind.LetKeyword) && nextToken.Parent.IsKind(SyntaxKind.LetClause)) || + (nextToken.IsKind(SyntaxKind.WhereKeyword) && nextToken.Parent.IsKind(SyntaxKind.WhereClause)) || + (nextToken.IsKind(SyntaxKind.JoinKeyword) && nextToken.Parent.IsKind(SyntaxKind.JoinClause)) || + (nextToken.IsKind(SyntaxKind.JoinKeyword) && nextToken.Parent.IsKind(SyntaxKind.JoinIntoClause)) || + (nextToken.IsKind(SyntaxKind.OrderByKeyword) && nextToken.Parent.IsKind(SyntaxKind.OrderByClause)) || + (nextToken.IsKind(SyntaxKind.SelectKeyword) && nextToken.Parent.IsKind(SyntaxKind.SelectClause)) || + (nextToken.IsKind(SyntaxKind.GroupKeyword) && nextToken.Parent.IsKind(SyntaxKind.GroupClause))) + { + return 1; + } + + return nextToken.Kind() switch + { + SyntaxKind.OpenBraceToken => LineBreaksBeforeOpenBrace(nextToken), + SyntaxKind.CloseBraceToken => LineBreaksBeforeCloseBrace(nextToken), + SyntaxKind.ElseKeyword => 1, + SyntaxKind.FinallyKeyword => 1, + SyntaxKind.OpenBracketToken => nextToken.Parent is AttributeListSyntax && + nextToken.Parent.Parent is not ParameterSyntax + ? 1 + : 0, + SyntaxKind.WhereKeyword => currentToken.Parent is TypeParameterListSyntax ? 1 : 0, + _ => 0 + }; + } + + private static bool IsAccessorListWithoutAccessorsWithBlockBody(SyntaxNode? node) + => node is AccessorListSyntax accessorList && + accessorList.Accessors.All(a => a.Body == null); + + private static bool IsAccessorListFollowedByInitializer(SyntaxNode? node) + => node is AccessorListSyntax accessorList && + node.Parent is PropertyDeclarationSyntax { Initializer: { } }; + + private static int LineBreaksBeforeOpenBrace(SyntaxToken openBraceToken) + { + Debug.Assert(openBraceToken.IsKind(SyntaxKind.OpenBraceToken)); + if (openBraceToken.Parent.IsKind(SyntaxKind.Interpolation) || + openBraceToken.Parent is InitializerExpressionSyntax or PropertyPatternClauseSyntax || + IsAccessorListWithoutAccessorsWithBlockBody(openBraceToken.Parent)) + { + return 0; + } + + return 1; + } + + private static int LineBreaksBeforeCloseBrace(SyntaxToken closeBraceToken) + { + Debug.Assert(closeBraceToken.IsKind(SyntaxKind.CloseBraceToken)); + if (closeBraceToken.Parent.IsKind(SyntaxKind.Interpolation) || + closeBraceToken.Parent is InitializerExpressionSyntax or PropertyPatternClauseSyntax) + { + return 0; + } + + return 1; + } + + private static int LineBreaksAfterOpenBrace(SyntaxToken currentToken, SyntaxToken nextToken) + { + if (currentToken.Parent is InitializerExpressionSyntax or PropertyPatternClauseSyntax || + currentToken.Parent.IsKind(SyntaxKind.Interpolation) || + IsAccessorListWithoutAccessorsWithBlockBody(currentToken.Parent)) + { + return 0; + } + + return 1; + } + + private static int LineBreaksAfterCloseBrace(SyntaxToken currentToken, SyntaxToken nextToken) + { + if (currentToken.Parent is InitializerExpressionSyntax or SwitchExpressionSyntax + or PropertyPatternClauseSyntax || + currentToken.Parent.IsKind(SyntaxKind.Interpolation) || + currentToken.Parent?.Parent is AnonymousFunctionExpressionSyntax || + IsAccessorListFollowedByInitializer(currentToken.Parent)) + { + return 0; + } + + var kind = nextToken.Kind(); + switch (kind) + { + case SyntaxKind.EndOfFileToken: + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CatchKeyword: + case SyntaxKind.FinallyKeyword: + case SyntaxKind.ElseKeyword: + return 1; + default: + if (kind == SyntaxKind.WhileKeyword && + nextToken.Parent.IsKind(SyntaxKind.DoStatement)) + { + return 1; + } + + return 2; + } + } + + private static int LineBreaksAfterSemicolon(SyntaxToken currentToken, SyntaxToken nextToken) + { + if (currentToken.Parent.IsKind(SyntaxKind.ForStatement)) + { + return 0; + } + + if (nextToken.IsKind(SyntaxKind.CloseBraceToken)) + { + return 1; + } + + if (currentToken.Parent.IsKind(SyntaxKind.UsingDirective)) + { + return nextToken.Parent.IsKind(SyntaxKind.UsingDirective) ? 1 : 2; + } + + if (currentToken.Parent.IsKind(SyntaxKind.ExternAliasDirective)) + { + return nextToken.Parent.IsKind(SyntaxKind.ExternAliasDirective) ? 1 : 2; + } + + if (currentToken.Parent is AccessorDeclarationSyntax && + IsAccessorListWithoutAccessorsWithBlockBody(currentToken.Parent.Parent)) + { + return 0; + } + + return 1; + } + + private static bool NeedsSeparatorForPropertyPattern(SyntaxToken token, SyntaxToken next) + { + PropertyPatternClauseSyntax? propPattern; + if (token.Parent.IsKind(SyntaxKind.PropertyPatternClause)) + { + propPattern = (PropertyPatternClauseSyntax) token.Parent; + } + else if (next.Parent.IsKind(SyntaxKind.PropertyPatternClause)) + { + propPattern = (PropertyPatternClauseSyntax) next.Parent; + } + else + { + return false; + } + + var tokenIsOpenBrace = token.IsKind(SyntaxKind.OpenBraceToken); + var nextIsOpenBrace = next.IsKind(SyntaxKind.OpenBraceToken); + var tokenIsCloseBrace = token.IsKind(SyntaxKind.CloseBraceToken); + var nextIsCloseBrace = next.IsKind(SyntaxKind.CloseBraceToken); + + //inner + if (tokenIsOpenBrace) + { + return true; + } + + if (nextIsCloseBrace) + { + return true; + } + + if (propPattern.Parent is RecursivePatternSyntax rps) + { + //outer + if (nextIsOpenBrace) + { + if (rps.Type != null || rps.PositionalPatternClause != null) + { + return true; + } + + return false; + } + + if (tokenIsCloseBrace) + { + if (rps.Designation is null) + { + return false; + } + + return true; + } + } + + return false; + } + + private static bool NeedsSeparatorForPositionalPattern(SyntaxToken token, SyntaxToken next) + { + PositionalPatternClauseSyntax? posPattern; + if (token.Parent.IsKind(SyntaxKind.PositionalPatternClause)) + { + posPattern = (PositionalPatternClauseSyntax) token.Parent; + } + else if (next.Parent.IsKind(SyntaxKind.PositionalPatternClause)) + { + posPattern = (PositionalPatternClauseSyntax) next.Parent; + } + else + { + return false; + } + + var tokenIsOpenParen = token.IsKind(SyntaxKind.OpenParenToken); + var nextIsOpenParen = next.IsKind(SyntaxKind.OpenParenToken); + var tokenIsCloseParen = token.IsKind(SyntaxKind.CloseParenToken); + var nextIsCloseParen = next.IsKind(SyntaxKind.CloseParenToken); + + //inner + if (tokenIsOpenParen) + { + return false; + } + + if (nextIsCloseParen) + { + return false; + } + + if (posPattern.Parent is RecursivePatternSyntax rps) + { + //outer + if (nextIsOpenParen) + { + if (rps.Type != null) + { + return true; + } + + return false; + } + + if (tokenIsCloseParen) + { + if (rps.PropertyPatternClause is not null) + { + return false; + } + + if (rps.Designation is null) + { + return false; + } + + return true; + } + } + + return false; + } + + private static bool NeedsSeparator(SyntaxToken token, SyntaxToken next) + { + if (token.Parent == null || next.Parent == null) + { + return false; + } + + if (IsAccessorListWithoutAccessorsWithBlockBody(next.Parent) || + IsAccessorListWithoutAccessorsWithBlockBody(next.Parent.Parent)) + { + // when the accessors are formatted inline, the separator is needed + // unless there is a semicolon. For example: "{ get; set; }" + return !next.IsKind(SyntaxKind.SemicolonToken); + } + + if (IsXmlTextToken(token.Kind()) || IsXmlTextToken(next.Kind())) + { + return false; + } + + if (next.IsKind(SyntaxKind.EndOfDirectiveToken)) + { + // In a directive, there's often no token between the directive keyword and + // the end-of-directive, so we may need a separator. + return IsKeyword(token.Kind()) && next.LeadingTrivia.Span.Length > 0; + } + + if ((token.Parent is AssignmentExpressionSyntax && AssignmentTokenNeedsSeparator(token.Kind())) || + (next.Parent is AssignmentExpressionSyntax && AssignmentTokenNeedsSeparator(next.Kind())) || + (token.Parent is BinaryExpressionSyntax && BinaryTokenNeedsSeparator(token.Kind())) || + (next.Parent is BinaryExpressionSyntax && BinaryTokenNeedsSeparator(next.Kind()))) + { + return true; + } + + if (token.IsKind(SyntaxKind.GreaterThanToken) && token.Parent.IsKind(SyntaxKind.TypeArgumentList)) + { + if (!SyntaxFacts.IsPunctuation(next.Kind())) + { + return true; + } + } + + if (token.IsKind(SyntaxKind.GreaterThanToken) && token.Parent.IsKind(SyntaxKind.FunctionPointerParameterList)) + { + return true; + } + + if (token.IsKind(SyntaxKind.CommaToken) && + !next.IsKind(SyntaxKind.CommaToken) && + !token.Parent.IsKind(SyntaxKind.EnumDeclaration)) + { + return true; + } + + if (token.IsKind(SyntaxKind.SemicolonToken) + && !(next.IsKind(SyntaxKind.SemicolonToken) || next.IsKind(SyntaxKind.CloseParenToken))) + { + return true; + } + + if (next.IsKind(SyntaxKind.SwitchKeyword) && next.Parent is SwitchExpressionSyntax) + { + return true; + } + + if (token.IsKind(SyntaxKind.QuestionToken) + && (token.Parent.IsKind(SyntaxKind.ConditionalExpression) || token.Parent is TypeSyntax) + && !token.Parent.Parent.IsKind(SyntaxKind.TypeArgumentList)) + { + return true; + } + + if (token.IsKind(SyntaxKind.ColonToken)) + { + return !token.Parent.IsKind(SyntaxKind.InterpolationFormatClause) && + !token.Parent.IsKind(SyntaxKind.XmlPrefix); + } + + if (next.IsKind(SyntaxKind.ColonToken)) + { + if (next.Parent.IsKind(SyntaxKind.BaseList) || + next.Parent.IsKind(SyntaxKind.TypeParameterConstraintClause) || + next.Parent is ConstructorInitializerSyntax) + { + return true; + } + } + + if (token.IsKind(SyntaxKind.CloseBracketToken) && IsWord(next.Kind())) + { + return true; + } + + // We don't want to add extra space after cast, we want space only after tuple + if (token.IsKind(SyntaxKind.CloseParenToken) && IsWord(next.Kind()) && + token.Parent.IsKind(SyntaxKind.TupleType) == true) + { + return true; + } + + if ((next.IsKind(SyntaxKind.QuestionToken) || next.IsKind(SyntaxKind.ColonToken)) + && next.Parent.IsKind(SyntaxKind.ConditionalExpression)) + { + return true; + } + + if (token.IsKind(SyntaxKind.EqualsToken)) + { + return !token.Parent.IsKind(SyntaxKind.XmlTextAttribute); + } + + if (next.IsKind(SyntaxKind.EqualsToken)) + { + return !next.Parent.IsKind(SyntaxKind.XmlTextAttribute); + } + + // Rules for function pointer below are taken from: + // https://github.com/dotnet/roslyn/blob/1cca63b5d8ea170f8d8e88e1574aa3ebe354c23b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SpacingFormattingRule.cs#L321-L413 + if (token.Parent.IsKind(SyntaxKind.FunctionPointerType)) + { + // No spacing between delegate and * + if (next.IsKind(SyntaxKind.AsteriskToken) && token.IsKind(SyntaxKind.DelegateKeyword)) + { + return false; + } + + // Force a space between * and the calling convention + if (token.IsKind(SyntaxKind.AsteriskToken) && + next.Parent.IsKind(SyntaxKind.FunctionPointerCallingConvention)) + { + switch (next.Kind()) + { + case SyntaxKind.IdentifierToken: + case SyntaxKind.ManagedKeyword: + case SyntaxKind.UnmanagedKeyword: + return true; + } + } + } + + if (next.Parent.IsKind(SyntaxKind.FunctionPointerParameterList) && next.IsKind(SyntaxKind.LessThanToken)) + { + switch (token.Kind()) + { + // No spacing between the * and < tokens if there is no calling convention + case SyntaxKind.AsteriskToken: + // No spacing between the calling convention and opening angle bracket of function pointer types: + // delegate* managed< + case SyntaxKind.ManagedKeyword: + case SyntaxKind.UnmanagedKeyword: + // No spacing between the calling convention specifier and the opening angle + // delegate* unmanaged[Cdecl]< + case SyntaxKind.CloseBracketToken + when token.Parent.IsKind(SyntaxKind.FunctionPointerUnmanagedCallingConventionList): + return false; + } + } + + // No space between unmanaged and the [ + // delegate* unmanaged[ + if (token.Parent.IsKind(SyntaxKind.FunctionPointerCallingConvention) && + next.Parent.IsKind(SyntaxKind.FunctionPointerUnmanagedCallingConventionList) && + next.IsKind(SyntaxKind.OpenBracketToken)) + { + return false; + } + + // Function pointer calling convention adjustments + if (next.Parent.IsKind(SyntaxKind.FunctionPointerUnmanagedCallingConventionList) && + token.Parent.IsKind(SyntaxKind.FunctionPointerUnmanagedCallingConventionList)) + { + if (next.IsKind(SyntaxKind.IdentifierToken)) + { + if (token.IsKind(SyntaxKind.OpenBracketToken)) + { + return false; + } + // Space after the , + // unmanaged[Cdecl, Thiscall + + if (token.IsKind(SyntaxKind.CommaToken)) + { + return true; + } + } + + // No space between identifier and comma + // unmanaged[Cdecl, + if (next.IsKind(SyntaxKind.CommaToken)) + { + return false; + } + + // No space before the ] + // unmanaged[Cdecl] + if (next.IsKind(SyntaxKind.CloseBracketToken)) + { + return false; + } + } + + // No space after the < in function pointer parameter lists + // delegate* in function pointer parameter lists + // delegate* + if (next.IsKind(SyntaxKind.GreaterThanToken) && next.Parent.IsKind(SyntaxKind.FunctionPointerParameterList)) + { + return false; + } + + if (token.IsKind(SyntaxKind.EqualsGreaterThanToken) || next.IsKind(SyntaxKind.EqualsGreaterThanToken)) + { + return true; + } + + // Can happen in directives (e.g. #line 1 "file") + if (IsLiteral(token.Kind()) && IsLiteral(next.Kind())) + { + return true; + } + + // No space before an asterisk that's part of a PointerTypeSyntax. + if (next.IsKind(SyntaxKind.AsteriskToken) && next.Parent is PointerTypeSyntax) + { + return false; + } + + // The last asterisk of a pointer declaration should be followed by a space. + if (token.IsKind(SyntaxKind.AsteriskToken) && token.Parent is PointerTypeSyntax && + (next.IsKind(SyntaxKind.IdentifierToken) || next.Parent.IsKind(SyntaxKind.IndexerDeclaration))) + { + return true; + } + + if (IsKeyword(token.Kind())) + { + if (!next.IsKind(SyntaxKind.ColonToken) && + !next.IsKind(SyntaxKind.DotToken) && + !next.IsKind(SyntaxKind.QuestionToken) && + !next.IsKind(SyntaxKind.SemicolonToken) && + !next.IsKind(SyntaxKind.OpenBracketToken) && + (!next.IsKind(SyntaxKind.OpenParenToken) || KeywordNeedsSeparatorBeforeOpenParen(token.Kind()) || + next.Parent.IsKind(SyntaxKind.TupleType)) && + !next.IsKind(SyntaxKind.CloseParenToken) && + !next.IsKind(SyntaxKind.CloseBraceToken) && + !next.IsKind(SyntaxKind.ColonColonToken) && + !next.IsKind(SyntaxKind.GreaterThanToken) && + !next.IsKind(SyntaxKind.CommaToken)) + { + return true; + } + } + + if (IsWord(token.Kind()) && IsWord(next.Kind())) + { + return true; + } + + if (token.Span.Length > 1 && next.Span.Length > 1) + { + var tokenLastChar = token.Text.Last(); + var nextFirstChar = next.Text.First(); + if (tokenLastChar == nextFirstChar && TokenCharacterCanBeDoubled(tokenLastChar)) + { + return true; + } + } + + if (token.Parent is RelationalPatternSyntax) + { + //>, >=, <, <= + return true; + } + + switch (next.Kind()) + { + case SyntaxKind.AndKeyword: + case SyntaxKind.OrKeyword: + return true; + } + + switch (token.Kind()) + { + case SyntaxKind.AndKeyword: + case SyntaxKind.OrKeyword: + case SyntaxKind.NotKeyword: + return true; + } + + if (NeedsSeparatorForPropertyPattern(token, next)) + { + return true; + } + + if (NeedsSeparatorForPositionalPattern(token, next)) + { + return true; + } + + return false; + } + + private static bool IsLiteral(SyntaxKind kind) + { + return kind switch + { + SyntaxKind.IdentifierToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.StringLiteralToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.CharacterLiteralToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.NumericLiteralToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.XmlTextLiteralToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.XmlTextLiteralNewLineToken => + //case SyntaxKind.Unknown: + true, + SyntaxKind.XmlEntityLiteralToken => + //case SyntaxKind.Unknown: + true, + _ => false + }; + } + + public override SyntaxNode? VisitXmlTextAttribute(XmlTextAttributeSyntax node) + { + var attribute = base.VisitXmlTextAttribute(node); + return attribute is null or { HasTrailingTrivia: true } ? attribute : attribute.WithTrailingTrivia(GetSpace()); + } + + private static bool KeywordNeedsSeparatorBeforeOpenParen(SyntaxKind kind) + { + return kind switch + { + SyntaxKind.TypeOfKeyword => false, + SyntaxKind.DefaultKeyword => false, + SyntaxKind.NewKeyword => false, + SyntaxKind.BaseKeyword => false, + SyntaxKind.ThisKeyword => false, + SyntaxKind.CheckedKeyword => false, + SyntaxKind.UncheckedKeyword => false, + SyntaxKind.SizeOfKeyword => false, + SyntaxKind.ArgListKeyword => false, + _ => true + }; + } + + private static bool IsXmlTextToken(SyntaxKind kind) + { + return kind switch + { + SyntaxKind.XmlTextLiteralNewLineToken => true, + SyntaxKind.XmlTextLiteralToken => true, + _ => false + }; + } + + private static bool BinaryTokenNeedsSeparator(SyntaxKind kind) + { + return kind switch + { + SyntaxKind.DotToken => false, + SyntaxKind.MinusGreaterThanToken => false, + _ => SyntaxFacts.GetBinaryExpression(kind) != SyntaxKind.None + }; + } + + private static bool AssignmentTokenNeedsSeparator(SyntaxKind kind) + { + return SyntaxFacts.GetAssignmentExpression(kind) != SyntaxKind.None; + } + + private SyntaxTriviaList RewriteTrivia( + SyntaxTriviaList triviaList, + int depth, + bool isTrailing, + bool indentAfterLineBreak, + bool mustHaveSeparator, + int lineBreaksAfter) + { + var currentTriviaList = ImmutableArray.CreateBuilder(triviaList.Count); + foreach (var trivia in triviaList) + { + if (trivia.IsKind(SyntaxKind.WhitespaceTrivia) || + trivia.IsKind(SyntaxKind.EndOfLineTrivia) || + trivia.FullSpan.Length == 0) + { + continue; + } + + var needsSeparator = + (currentTriviaList.Count > 0 && NeedsSeparatorBetween(currentTriviaList.Last())) || + (currentTriviaList.Count == 0 && isTrailing); + + var needsLineBreak = NeedsLineBreakBefore(trivia, isTrailing) + || (currentTriviaList.Count > 0 && + NeedsLineBreakBetween(currentTriviaList.Last(), trivia, isTrailing)); + + if (needsLineBreak && !_afterLineBreak) + { + currentTriviaList.Add(GetEndOfLine()); + _afterLineBreak = true; + _afterIndentation = false; + } + + if (_afterLineBreak) + { + if (!_afterIndentation && NeedsIndentAfterLineBreak(trivia)) + { + currentTriviaList.Add(this.GetIndentation(GetDeclarationDepth(trivia))); + _afterIndentation = true; + } + } + else if (needsSeparator) + { + currentTriviaList.Add(GetSpace()); + _afterLineBreak = false; + _afterIndentation = false; + } + + if (trivia.HasStructure) + { + var tr = this.VisitStructuredTrivia(trivia); + currentTriviaList.Add(tr); + } + else if (trivia.IsKind(SyntaxKind.DocumentationCommentExteriorTrivia)) + { + // recreate exterior to remove any leading whitespace + currentTriviaList.Add(s_trimmedDocCommentExterior); + } + else + { + currentTriviaList.Add(trivia); + } + + if (NeedsLineBreakAfter(trivia, isTrailing) + && (currentTriviaList.Count == 0 || !EndsInLineBreak(currentTriviaList.Last()))) + { + currentTriviaList.Add(GetEndOfLine()); + _afterLineBreak = true; + _afterIndentation = false; + } + } + + if (lineBreaksAfter > 0) + { + if (currentTriviaList.Count > 0 + && EndsInLineBreak(currentTriviaList.Last())) + { + lineBreaksAfter--; + } + + for (int i = 0; i < lineBreaksAfter; i++) + { + currentTriviaList.Add(GetEndOfLine()); + _afterLineBreak = true; + _afterIndentation = false; + } + } + else if (indentAfterLineBreak && _afterLineBreak && !_afterIndentation) + { + currentTriviaList.Add(this.GetIndentation(depth)); + _afterIndentation = true; + } + else if (mustHaveSeparator) + { + currentTriviaList.Add(GetSpace()); + _afterLineBreak = false; + _afterIndentation = false; + } + + return currentTriviaList.Count switch + { + 0 => default, + 1 => SyntaxFactory.TriviaList(currentTriviaList.First()), + _ => SyntaxFactory.TriviaList(currentTriviaList) + }; + } + + private static readonly SyntaxTrivia + s_trimmedDocCommentExterior = SyntaxFactory.DocumentationCommentExterior("///"); + + private SyntaxTrivia GetSpace() + { + return _useElasticTrivia ? SyntaxFactory.ElasticSpace : SyntaxFactory.Space; + } + + private SyntaxTrivia GetEndOfLine() + { + return _eolTrivia; + } + + private SyntaxTrivia VisitStructuredTrivia(SyntaxTrivia trivia) + { + bool oldIsInStructuredTrivia = _isInStructuredTrivia; + _isInStructuredTrivia = true; + + SyntaxToken oldPreviousToken = _previousToken; + _previousToken = default; + + SyntaxTrivia result = VisitTrivia(trivia); + + _isInStructuredTrivia = oldIsInStructuredTrivia; + _previousToken = oldPreviousToken; + + return result; + } + + private static bool NeedsSeparatorBetween(SyntaxTrivia trivia) + { + return trivia.Kind() switch + { + SyntaxKind.None => false, + SyntaxKind.WhitespaceTrivia => false, + SyntaxKind.DocumentationCommentExteriorTrivia => false, + _ => !SyntaxFacts.IsPreprocessorDirective(trivia.Kind()) + }; + } + + private static bool NeedsLineBreakBetween(SyntaxTrivia trivia, SyntaxTrivia next, bool isTrailingTrivia) + { + return NeedsLineBreakAfter(trivia, isTrailingTrivia) + || NeedsLineBreakBefore(next, isTrailingTrivia); + } + + private static bool NeedsLineBreakBefore(SyntaxTrivia trivia, bool isTrailingTrivia) + { + var kind = trivia.Kind(); + return kind switch + { + SyntaxKind.DocumentationCommentExteriorTrivia => !isTrailingTrivia, + _ => SyntaxFacts.IsPreprocessorDirective(kind) + }; + } + + private static bool NeedsLineBreakAfter(SyntaxTrivia trivia, bool isTrailingTrivia) + { + var kind = trivia.Kind(); + return kind switch + { + SyntaxKind.SingleLineCommentTrivia => true, + SyntaxKind.MultiLineCommentTrivia => !isTrailingTrivia, + _ => SyntaxFacts.IsPreprocessorDirective(kind) + }; + } + + private static bool NeedsIndentAfterLineBreak(SyntaxTrivia trivia) + { + return trivia.Kind() switch + { + SyntaxKind.SingleLineCommentTrivia => true, + SyntaxKind.MultiLineCommentTrivia => true, + SyntaxKind.DocumentationCommentExteriorTrivia => true, + SyntaxKind.SingleLineDocumentationCommentTrivia => true, + SyntaxKind.MultiLineDocumentationCommentTrivia => true, + _ => false + }; + } + + private static bool IsLineBreak(SyntaxToken token) + { + return token.IsKind(SyntaxKind.XmlTextLiteralNewLineToken); + } + + private static bool EndsInLineBreak(SyntaxTrivia trivia) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + return true; + } + + if (trivia.IsKind(SyntaxKind.PreprocessingMessageTrivia) || trivia.IsKind(SyntaxKind.DisabledTextTrivia)) + { + var text = trivia.ToFullString(); + return text.Length > 0 && SyntaxFacts.IsNewLine(text.Last()); + } + + if (trivia.HasStructure) + { + var node = trivia.GetStructure()!; + var trailing = node.GetTrailingTrivia(); + if (trailing.Count > 0) + { + return EndsInLineBreak(trailing.Last()); + } + + return IsLineBreak(node.GetLastToken()); + } + + return false; + } + + private static bool IsWord(SyntaxKind kind) + { + return kind == SyntaxKind.IdentifierToken || IsKeyword(kind); + } + + private static bool IsKeyword(SyntaxKind kind) + { + return SyntaxFacts.IsKeywordKind(kind) || SyntaxFacts.IsPreprocessorKeyword(kind); + } + + private static bool TokenCharacterCanBeDoubled(char c) + { + return c switch + { + '+' => true, + '-' => true, + '<' => true, + ':' => true, + '?' => true, + '=' => true, + '"' => true, + _ => false + }; + } + + private static int GetDeclarationDepth(SyntaxToken token) + { + return GetDeclarationDepth(token.Parent); + } + + private static int GetDeclarationDepth(SyntaxTrivia trivia) + { + if (SyntaxFacts.IsPreprocessorDirective(trivia.Kind())) + { + return 0; + } + + return GetDeclarationDepth(trivia.Token); + } + + private static int GetDeclarationDepth(SyntaxNode? node) + { + if (node != null) + { + if (node.IsStructuredTrivia) + { + var tr = ((StructuredTriviaSyntax) node).ParentTrivia; + return GetDeclarationDepth(tr); + } + + if (node.Parent != null) + { + if (node.Parent.IsKind(SyntaxKind.CompilationUnit)) + { + return 0; + } + + int parentDepth = GetDeclarationDepth(node.Parent); + + if (node.Parent.Kind() is SyntaxKind.GlobalStatement) + { + return parentDepth; + } + + if (node.IsKind(SyntaxKind.IfStatement) && node.Parent.IsKind(SyntaxKind.ElseClause)) + { + return parentDepth; + } + + if (node.Parent is BlockSyntax || node is StatementSyntax and not BlockSyntax) + { + // all nested statements are indented one level + return parentDepth + 1; + } + + if (node is MemberDeclarationSyntax or AccessorDeclarationSyntax or TypeParameterConstraintClauseSyntax + or SwitchSectionSyntax or SwitchExpressionArmSyntax or UsingDirectiveSyntax + or ExternAliasDirectiveSyntax or QueryExpressionSyntax or QueryContinuationSyntax) + { + return parentDepth + 1; + } + + return parentDepth; + } + } + + return 0; + } + + public override SyntaxNode? VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node) + { + if (node.StringStartToken.IsKind(SyntaxKind.InterpolatedStringStartToken)) + { + //Just for non verbatim strings we want to make sure that the formatting of interpolations does not emit line breaks. + //See: https://github.com/dotnet/roslyn/issues/50742 + // + //The flag _inSingleLineInterpolation is set to true while visiting InterpolatedStringExpressionSyntax and checked in LineBreaksAfter + //to suppress adding newlines. + var old = _inSingleLineInterpolation; + _inSingleLineInterpolation = true; + try + { + return base.VisitInterpolatedStringExpression(node); + } + finally + { + _inSingleLineInterpolation = old; + } + } + + return base.VisitInterpolatedStringExpression(node); + } +} \ No newline at end of file diff --git a/SharpGen/Generator/ShadowCallbackGenerator.cs b/SharpGen/Generator/ShadowCallbackGenerator.cs index cd3073a2..eee1ae37 100644 --- a/SharpGen/Generator/ShadowCallbackGenerator.cs +++ b/SharpGen/Generator/ShadowCallbackGenerator.cs @@ -119,6 +119,44 @@ private MethodDeclarationSyntax GenerateShadowCallback(CsCallable csElement, Pla { var interopReturnType = sig.ReturnTypeSyntax; + var methodDeclaration = MethodDeclaration( + interopReturnType, + VtblGenerator.GetMethodImplName(csElement, platform) + ) + .WithModifiers( + TokenList( + Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.UnsafeKeyword) + ) + ) + .WithParameterList(GetNativeParameterList(csElement, sig)) + .WithAttributeLists( + csElement is CsMethod { IsFunctionPointerInVtbl: true } + ? SingletonList( + AttributeList( + SingletonSeparatedList( + Attribute( + UnmanagedCallersOnlyAttributeName, + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument(FnPtrCallConvs(sig.CallingConvention)) + .WithNameEquals(NameEquals("CallConvs")) + ) + ) + ) + ) + ).WithTrailingEndIfDirective() + ) + : default + ); + + if (csElement is CsMethod { ManagedPartial: true }) + { + return methodDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + var statements = NewStatementList; statements.Add(csElement, platform, Generators.ReverseCallableProlog); @@ -265,37 +303,7 @@ private MethodDeclarationSyntax GenerateShadowCallback(CsCallable csElement, Pla ) ); - return MethodDeclaration( - interopReturnType, - VtblGenerator.GetMethodImplName(csElement, platform) - ) - .WithModifiers( - TokenList( - Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword), - Token(SyntaxKind.UnsafeKeyword) - ) - ) - .WithParameterList(GetNativeParameterList(csElement, sig)) - .WithBody(fullBody.ToBlock()) - .WithAttributeLists( - csElement is CsMethod { IsFunctionPointerInVtbl: true } - ? SingletonList( - AttributeList( - SingletonSeparatedList( - Attribute( - UnmanagedCallersOnlyAttributeName, - AttributeArgumentList( - SingletonSeparatedList( - AttributeArgument(FnPtrCallConvs(sig.CallingConvention)) - .WithNameEquals(NameEquals("CallConvs")) - ) - ) - ) - ) - ).WithTrailingEndIfDirective() - ) - : default - ); + return methodDeclaration.WithBody(fullBody.ToBlock()); static ExpressionSyntax FnPtrCallConvs(CallingConvention callingConvention) => ImplicitArrayCreationExpression( diff --git a/SharpGen/Logging/LoggingCodes.cs b/SharpGen/Logging/LoggingCodes.cs index 06ff6166..c1f5f766 100644 --- a/SharpGen/Logging/LoggingCodes.cs +++ b/SharpGen/Logging/LoggingCodes.cs @@ -18,9 +18,9 @@ public static class LoggingCodes public const string RegistryKeyNotFound = "SG0007"; - public const string UnkownVariable = "SG0008"; + public const string UnknownVariable = "SG0008"; - public const string UnkownDynamicVariable = "SG0009"; + public const string UnknownDynamicVariable = "SG0009"; public const string InvalidMethodReturnType = "SG0010"; diff --git a/SharpGen/Model/CppElement.Extensions.cs b/SharpGen/Model/CppElement.Extensions.cs index ca88c439..64fcd564 100644 --- a/SharpGen/Model/CppElement.Extensions.cs +++ b/SharpGen/Model/CppElement.Extensions.cs @@ -151,7 +151,9 @@ private static void ProcessRule(CppElement element, MappingRule newRule, Regex p if (newRule.ParameterUsedAsReturnType != null) tag.ParameterUsedAsReturnType = newRule.ParameterUsedAsReturnType; if (newRule.Relation != null) tag.Relation = newRule.Relation; - if (newRule.Hidden != null) tag.Hidden = newRule.Hidden; + if (newRule.Hidden is { } hidden) tag.Hidden = hidden; + if (newRule.ManagedPartial is { } managedPartial) tag.ManagedPartial = managedPartial; + if (newRule.NativePartial is { } nativePartial) tag.NativePartial = nativePartial; if (newRule.KeepPointers != null) tag.KeepPointers = newRule.KeepPointers; if (newRule.StringMarshal is { } stringMarshal) tag.StringMarshal = stringMarshal; } diff --git a/SharpGen/Model/CsBase.cs b/SharpGen/Model/CsBase.cs index 137560b5..c147b2d4 100644 --- a/SharpGen/Model/CsBase.cs +++ b/SharpGen/Model/CsBase.cs @@ -195,16 +195,14 @@ public virtual string QualifiedName /// The C++ element. public CppElement CppElement { get; } - /// - /// Gets the name of the C++ element. - /// - /// The name of the C++ element. public string CppElementName { get => string.IsNullOrEmpty(_cppElementName) ? CppElement?.Name : _cppElementName; set => _cppElementName = value; } + public string CppElementFullName => CppElement?.FullName ?? CppElementName; + /// /// Gets or sets the doc id. /// @@ -230,7 +228,7 @@ public virtual void FillDocItems(IList docItems, IDocumentationLinker ma { } - public virtual string DocUnmanagedName => CppElementName; + public virtual string DocUnmanagedName => CppElementFullName; public virtual string DocUnmanagedShortName => CppElementName; diff --git a/SharpGen/Model/CsInterface.cs b/SharpGen/Model/CsInterface.cs index 34c6219b..66acf7e4 100644 --- a/SharpGen/Model/CsInterface.cs +++ b/SharpGen/Model/CsInterface.cs @@ -123,7 +123,11 @@ private static string FindGuid(CppInterface cppInterface) public string ShadowName { get => shadowName ?? DefaultShadowFullName; - set => shadowName = value; + set + { + shadowName = value; + AutoGenerateShadow = true; + } } public string VtblName @@ -135,7 +139,7 @@ public string VtblName private string DefaultShadowFullName => $"{QualifiedName}Shadow"; private string DefaultVtblFullName => $"{QualifiedName}Vtbl"; - public bool AutoGenerateShadow { get; } = true; + public bool AutoGenerateShadow { get; private set; } public bool AutoGenerateVtbl { get; } = true; public bool StaticShadowVtbl { get; } = true; public bool AutoDisposePersistentProperties { get; } = true; diff --git a/SharpGen/Model/CsMethod.cs b/SharpGen/Model/CsMethod.cs index 2a2a1b0d..eabb25e0 100644 --- a/SharpGen/Model/CsMethod.cs +++ b/SharpGen/Model/CsMethod.cs @@ -42,6 +42,8 @@ public CsMethod(Ioc ioc, CppMethod cppMethod, string name) : base(ioc, cppMethod AllowProperty = tag.Property ?? AllowProperty; IsPersistent = tag.Persist ?? IsPersistent; Hidden = tag.Hidden ?? Hidden; + ManagedPartial = tag.ManagedPartial ?? ManagedPartial; + NativePartial = tag.NativePartial ?? NativePartial; CustomVtbl = tag.CustomVtbl ?? CustomVtbl; IsKeepImplementPublic = tag.IsKeepImplementPublic ?? IsKeepImplementPublic; @@ -53,6 +55,8 @@ public CsMethod(Ioc ioc, CppMethod cppMethod, string name) : base(ioc, cppMethod } public bool Hidden { get; set; } + public bool ManagedPartial { get; } + public bool NativePartial { get; } private bool IsKeepImplementPublic { get; } public bool? AllowProperty { get; } public bool CustomVtbl { get; } diff --git a/SharpGen/Parser/SdkResolver.cs b/SharpGen/Parser/SdkResolver.cs index 189c3dd3..1e1ea113 100644 --- a/SharpGen/Parser/SdkResolver.cs +++ b/SharpGen/Parser/SdkResolver.cs @@ -44,28 +44,68 @@ private IEnumerable ResolveStdLib(string? version) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var vsInstallDir = GetVSInstallPath(); - - version ??= File.ReadAllText( - Path.Combine(vsInstallDir, "VC", "Auxiliary", "Build", "Microsoft.VCToolsVersion.default.txt") - ); + foreach (var vsInstallDir in GetVSInstallPath()) + { + string actualVersion; + if (version is not null) + { + actualVersion = version; + } + else + { + var defaultVersion = Path.Combine( + vsInstallDir, "VC", "Auxiliary", "Build", "Microsoft.VCToolsVersion.default.txt" + ); + + if (!File.Exists(defaultVersion)) + continue; + + actualVersion = File.ReadAllText(defaultVersion); + } + + var path = Path.Combine( + vsInstallDir, "VC", "Tools", "MSVC", actualVersion.Trim(), "include" + ); + + if (!Directory.Exists(path)) + continue; - yield return new IncludeDirRule( - Path.Combine(vsInstallDir, "VC", "Tools", "MSVC", version.Trim(), "include") - ); + yield return new IncludeDirRule(path); + yield break; + } } else { - yield return new IncludeDirRule(Path.Combine("/usr", "include", "c++", version)); + if (version is not null) + { + var path = Path.Combine("/usr", "include", "c++", version); + if (Directory.Exists(path)) + yield return new IncludeDirRule(path); + } + else + { + DirectoryInfo cpp = new(Path.Combine("/usr", "include", "c++")); + + if (!cpp.Exists) + yield break; + + // TODO: pick latest if not specified + var path = cpp.EnumerateDirectories().FirstOrDefault()?.FullName; + if (path is not null) + yield return new IncludeDirRule(path); + } } } - private string? GetVSInstallPath() + private IReadOnlyCollection GetVSInstallPath() { var vsPathOverride = Environment.GetEnvironmentVariable("SHARPGEN_VS_OVERRIDE"); if (!string.IsNullOrEmpty(vsPathOverride)) - return vsPathOverride; + { + return new[] {vsPathOverride}; + } + List paths = new(); try { var query = new SetupConfiguration(); @@ -85,7 +125,7 @@ private IEnumerable ResolveStdLib(string? version) continue; if (instance2.GetPackages().Any(Predicate)) - return instance2.GetInstallationPath(); + paths.Add(instance2.GetInstallationPath()); } while (fetched > 0); static bool Predicate(ISetupPackageReference pkg) => @@ -99,9 +139,10 @@ static bool Predicate(ISetupPackageReference pkg) => ); } - Logger.Fatal("Unable to find a Visual Studio installation that has the Visual C++ Toolchain installed."); + if (paths.Count == 0) + Logger.Fatal("Unable to find a Visual Studio installation that has the Visual C++ Toolchain installed."); - return null; + return paths; } private IEnumerable ResolveWindowsSdk(string version, string? components) diff --git a/SharpGen/SharpGen.csproj b/SharpGen/SharpGen.csproj index 74864ea9..e23c62f5 100644 --- a/SharpGen/SharpGen.csproj +++ b/SharpGen/SharpGen.csproj @@ -1,26 +1,18 @@  - + + Library + netstandard2.0 + - - Library - netstandard2.0 - + + + - - - - - - - - - - - - - <_Parameter1>SharpGen.UnitTests, PublicKey=$(SharpGenPublicKey) - - + + + <_Parameter1>SharpGen.UnitTests, PublicKey=$(SharpGenPublicKey) + + \ No newline at end of file diff --git a/SharpGen/Transform/ExternalDocCommentsReader.cs b/SharpGen/Transform/ExternalDocCommentsReader.cs index 0d1464e2..ae835048 100644 --- a/SharpGen/Transform/ExternalDocCommentsReader.cs +++ b/SharpGen/Transform/ExternalDocCommentsReader.cs @@ -42,6 +42,6 @@ public string GetDocumentWithExternalComments(CsBase element) private static string GetExternalDocCommentId(CsBase element) { - return element.CppElementName ?? element.QualifiedName; + return element.CppElementFullName ?? element.QualifiedName; } } \ No newline at end of file diff --git a/SharpGen/Transform/InterfaceTransform.cs b/SharpGen/Transform/InterfaceTransform.cs index 7da6a9e4..d67bd5a6 100644 --- a/SharpGen/Transform/InterfaceTransform.cs +++ b/SharpGen/Transform/InterfaceTransform.cs @@ -61,7 +61,6 @@ public InterfaceTransform(NamingRulesManager namingRules, CppObjectType = new CsInterface(null, globalNamespace.GetTypeName(WellKnownName.CppObject)); DefaultCallbackable = new CsInterface(null, globalNamespace.GetTypeName(WellKnownName.ICallbackable)) { - ShadowName = globalNamespace.GetTypeName(WellKnownName.CppObjectShadow), VtblName = globalNamespace.GetTypeName(WellKnownName.CppObjectVtbl) }; } @@ -107,7 +106,7 @@ public override CsInterface Prepare(CppInterface cppInterface) TypeRegistry.BindType(cppInterface.Name, cSharpInterface, source: cppInterface.ParentInclude?.Name); - if (cppInterface.Rule.AutoGenerateShadow == true) + if (cppInterface.Rule.AutoGenerateShadow is { }) Logger.Message("Interface [{0}] has redundant shadow generation flag", cppInterface.FullName); if (cppInterface.Rule.AutoGenerateVtbl == true) @@ -288,10 +287,7 @@ private CsInterface CreateNativeCallbackType(CsInterface interfaceType) MethodTransform.CreateNativeInteropSignatures(interopSignatureTransform, newCsMethod); - var keepImplementPublic = interfaceType.AutoGenerateShadow || - method.IsPublicVisibilityForced(interfaceType); - - if (!keepImplementPublic) + if (!method.IsPublicVisibilityForced(interfaceType)) { newCsMethod.Visibility = Visibility.Internal; newCsMethod.SuffixName("_"); diff --git a/SharpGen/Transform/PrimitiveTypeCode.cs b/SharpGen/Transform/PrimitiveTypeCode.cs index 15a7eb06..b7209dbc 100644 --- a/SharpGen/Transform/PrimitiveTypeCode.cs +++ b/SharpGen/Transform/PrimitiveTypeCode.cs @@ -18,5 +18,7 @@ public enum PrimitiveTypeCode : byte Decimal, String, IntPtr, - UIntPtr + UIntPtr, + NInt, + NUint, } \ No newline at end of file diff --git a/SharpGen/Transform/TransformManager.cs b/SharpGen/Transform/TransformManager.cs index ef5bc60f..1f5c9746 100644 --- a/SharpGen/Transform/TransformManager.cs +++ b/SharpGen/Transform/TransformManager.cs @@ -179,7 +179,8 @@ private void RegisterBindings(ConfigFile file) string.IsNullOrEmpty(bindingRule.Marshal) ? null : TypeRegistry.ImportType(bindingRule.Marshal), - file.Id + bindingRule.Source ?? file.Id, + bindingRule.Override ?? false ); } } @@ -631,49 +632,6 @@ private void HandleConstantRule(CppElementFinder elementFinder, ConstantRule con public (IEnumerable bindings, IEnumerable defines) GenerateTypeBindingsForConsumers() { - return (from record in TypeRegistry.GetTypeBindings() - select new BindRule(record.CppType, record.CSharpType.QualifiedName, record.MarshalType?.QualifiedName), - GenerateDefinesForMappedTypes()); - } - - private IEnumerable GenerateDefinesForMappedTypes() - { - foreach (var (_, CSharpType, _) in TypeRegistry.GetTypeBindings()) - { - switch (CSharpType) - { - case CsEnum csEnum: - CsFundamentalType tempQualifier = csEnum.UnderlyingType; - yield return new DefineExtensionRule - { - Enum = csEnum.QualifiedName, - SizeOf = checked((int) csEnum.Size), - UnderlyingType = tempQualifier?.Name - }; - break; - case CsStruct csStruct: - yield return new DefineExtensionRule - { - Struct = csStruct.QualifiedName, - SizeOf = checked((int) csStruct.Size), - Align = csStruct.Align, - HasCustomMarshal = csStruct.HasCustomMarshal, - HasCustomNew = csStruct.HasCustomNew, - IsStaticMarshal = csStruct.IsStaticMarshal, - IsNativePrimitive = csStruct.IsNativePrimitive - }; - break; - case CsInterface csInterface: - yield return new DefineExtensionRule - { - Interface = csInterface.QualifiedName, - NativeImplementation = csInterface.NativeImplementation?.QualifiedName, - ShadowName = csInterface.ShadowName, - VtblName = csInterface.VtblName, - IsCallbackInterface = csInterface.IsCallback - }; - break; - } - } + return TypeRegistry.GetTypeBindings(); } } \ No newline at end of file diff --git a/SharpGen/Transform/TransformServiceExtensions.cs b/SharpGen/Transform/TransformServiceExtensions.cs index 857d5717..7cd37ccc 100644 --- a/SharpGen/Transform/TransformServiceExtensions.cs +++ b/SharpGen/Transform/TransformServiceExtensions.cs @@ -50,7 +50,7 @@ public static IEnumerable GetDocItems(this IDocumentationLinker aggregat } else { - docItems.Add($""); + docItems.Add($""); } if (element.CppElementName != null) diff --git a/SharpGen/Transform/TypeRegistry.Primitives.cs b/SharpGen/Transform/TypeRegistry.Primitives.cs index 9ecd2bbe..d406dc08 100644 --- a/SharpGen/Transform/TypeRegistry.Primitives.cs +++ b/SharpGen/Transform/TypeRegistry.Primitives.cs @@ -78,6 +78,13 @@ public partial class TypeRegistry typeof(UIntPtr), new PrimitiveTypeIdentity(PrimitiveTypeCode.UIntPtr), "System.UIntPtr" ); + public static readonly CsFundamentalType NInt = new( + typeof(nint), new PrimitiveTypeIdentity(PrimitiveTypeCode.NInt), "nint" + ); + public static readonly CsFundamentalType NUInt = new( + typeof(nuint), new PrimitiveTypeIdentity(PrimitiveTypeCode.NUint), "nuint" + ); + private static readonly Dictionary PrimitiveTypeEntriesByIdentity = new() { @@ -100,6 +107,8 @@ public partial class TypeRegistry [String.PrimitiveTypeIdentity.Value] = String, [IntPtr.PrimitiveTypeIdentity.Value] = IntPtr, [UIntPtr.PrimitiveTypeIdentity.Value] = UIntPtr, + [NInt.PrimitiveTypeIdentity.Value] = NInt, + [NUInt.PrimitiveTypeIdentity.Value] = NUInt, // ReSharper restore PossibleInvalidOperationException }; @@ -125,8 +134,8 @@ public partial class TypeRegistry ["UIntPtr"] = UIntPtr, ["System.IntPtr"] = IntPtr, ["System.UIntPtr"] = UIntPtr, - ["nint"] = IntPtr, - ["nuint"] = UIntPtr, + ["nint"] = NInt, + ["nuint"] = NUInt, }; private static readonly Dictionary PrimitiveRuntimeTypesByCode = new() @@ -148,5 +157,7 @@ public partial class TypeRegistry [PrimitiveTypeCode.String] = typeof(string), [PrimitiveTypeCode.IntPtr] = typeof(IntPtr), [PrimitiveTypeCode.UIntPtr] = typeof(UIntPtr), + [PrimitiveTypeCode.NInt] = typeof(nint), + [PrimitiveTypeCode.NUint] = typeof(nuint), }; } \ No newline at end of file diff --git a/SharpGen/Transform/TypeRegistry.cs b/SharpGen/Transform/TypeRegistry.cs index 7b4ea342..eceac4a5 100644 --- a/SharpGen/Transform/TypeRegistry.cs +++ b/SharpGen/Transform/TypeRegistry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SharpGen.Config; using SharpGen.Logging; using SharpGen.Model; @@ -187,7 +188,7 @@ private static CsFundamentalType FindPrimitiveTypeImpl(PrimitiveTypeIdentity ide /// Name of the CPP. /// The C# type. /// The C# marshal type - public void BindType(string cppName, CsTypeBase type, CsTypeBase marshalType = null, string source = null) + public void BindType(string cppName, CsTypeBase type, CsTypeBase marshalType = null, string source = null, bool @override = false) { if (cppName == null) throw new ArgumentNullException(nameof(cppName)); @@ -197,24 +198,42 @@ public void BindType(string cppName, CsTypeBase type, CsTypeBase marshalType = n if (_mapCppNameToCSharpType.TryGetValue(cppName, out var old)) { - var logLevel = type == old.CSharpType && marshalType == old.MarshalType + var match = type == old.CSharpType && marshalType == old.MarshalType; + LogLevel logLevel; + string message; + + if (@override) + { + message = "Remapping C++ element [{0}]{5} to C# type [{1}/{2}] previously mapped to [{3}/{4}]{6}."; + logLevel = LogLevel.Info; + } + else + { + message = "Mapping C++ element [{0}]{5} to C# type [{1}/{2}] when already mapped to [{3}/{4}]{6}. First binding takes priority."; + logLevel = match ? LogLevel.Info : LogLevel.Warning; + } Logger.LogRawMessage( - logLevel, - LoggingCodes.DuplicateBinding, - "Mapping C++ element [{0}]{5} to C# type [{1}/{2}] when already mapped to [{3}/{4}]{6}. First binding takes priority.", - null, + logLevel, LoggingCodes.DuplicateBinding, message, null, cppName, type.CppElementName, type.QualifiedName, old.CSharpType.CppElementName, old.CSharpType.QualifiedName, AtLocation(source), AtLocation(old.Source) ); + if (@override) + BindImpl(); + static string AtLocation(string location) => location != null ? $" at [{location}]" : string.Empty; } else { - _mapCppNameToCSharpType.Add(cppName, new BoundType(type, marshalType, source)); + BindImpl(); + } + + void BindImpl() + { + _mapCppNameToCSharpType[cppName] = new BoundType(type, marshalType, source); DocLinker.AddOrUpdateDocLink(cppName, type.QualifiedName); } } @@ -237,10 +256,59 @@ public bool FindBoundType(string cppName, out BoundType boundType) return false; } - public IEnumerable<(string CppType, CsTypeBase CSharpType, CsTypeBase MarshalType)> GetTypeBindings() + public (IEnumerable Bindings, IEnumerable Defines) GetTypeBindings() { - return from record in _mapCppNameToCSharpType - select (record.Key, record.Value.CSharpType, record.Value.MarshalType); + List bindRules = new(); + List defineRules = new(); + foreach (var entry in _mapCppNameToCSharpType) + { + var boundType = entry.Value; + var csType = boundType.CSharpType; + var marshalType = boundType.MarshalType; + bindRules.Add(new BindRule(entry.Key, csType.QualifiedName, marshalType?.QualifiedName, boundType.Source)); + + switch (csType) + { + case CsEnum { UnderlyingType: var tempQualifier } csEnum: + defineRules.Add( + new DefineExtensionRule + { + Enum = csEnum.QualifiedName, + SizeOf = checked((int) csEnum.Size), + UnderlyingType = tempQualifier?.Name + } + ); + break; + case CsStruct csStruct: + defineRules.Add( + new DefineExtensionRule + { + Struct = csStruct.QualifiedName, + SizeOf = checked((int) csStruct.Size), + Align = csStruct.Align, + HasCustomMarshal = csStruct.HasCustomMarshal, + HasCustomNew = csStruct.HasCustomNew, + IsStaticMarshal = csStruct.IsStaticMarshal, + IsNativePrimitive = csStruct.IsNativePrimitive + } + ); + break; + case CsInterface csInterface: + defineRules.Add( + new DefineExtensionRule + { + Interface = csInterface.QualifiedName, + NativeImplementation = csInterface.NativeImplementation?.QualifiedName, + ShadowName = csInterface.AutoGenerateShadow ? csInterface.ShadowName : null, + VtblName = csInterface.VtblName, + IsCallbackInterface = csInterface.IsCallback + } + ); + break; + } + } + + return (bindRules, defineRules); } #nullable enable diff --git a/SharpGenTools.Sdk/CallerArgumentExpressionAttribute.cs b/SharpGenTools.Sdk/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..a0f26543 --- /dev/null +++ b/SharpGenTools.Sdk/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +public sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } +} \ No newline at end of file diff --git a/SharpGenTools.Sdk/Extensibility/ExtensibilityDriver.cs b/SharpGenTools.Sdk/Extensibility/ExtensibilityDriver.cs index 3063f70c..810ce0b5 100644 --- a/SharpGenTools.Sdk/Extensibility/ExtensibilityDriver.cs +++ b/SharpGenTools.Sdk/Extensibility/ExtensibilityDriver.cs @@ -54,7 +54,7 @@ private void ResolveExtensibilityPoints(LoggerBase logger, out ImmutableArray(); - void ErrorHandler(object o, ExtensionLoadFailureEventArgs e) + void ErrorHandler(object? o, ExtensionLoadFailureEventArgs e) { var analyzerReference = o as ExtensionReference; Debug.Assert(analyzerReference != null); diff --git a/SharpGenTools.Sdk/Extensibility/ExtensionFileReference.cs b/SharpGenTools.Sdk/Extensibility/ExtensionFileReference.cs index 5577f2ae..48fcc008 100644 --- a/SharpGenTools.Sdk/Extensibility/ExtensionFileReference.cs +++ b/SharpGenTools.Sdk/Extensibility/ExtensionFileReference.cs @@ -97,7 +97,9 @@ public bool Equals(ExtensionReference? other) } public override int GetHashCode() - => HashCode.Combine(RuntimeHelpers.GetHashCode(AssemblyLoader), FullPath.GetHashCode()); + { + return HashCode.Combine(RuntimeHelpers.GetHashCode(AssemblyLoader), FullPath.GetHashCode()); + } public override ImmutableArray GetDocumentationProviders() { diff --git a/SharpGenTools.Sdk/Internal/CacheFile.cs b/SharpGenTools.Sdk/Internal/CacheFile.cs new file mode 100644 index 00000000..44c9f427 --- /dev/null +++ b/SharpGenTools.Sdk/Internal/CacheFile.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Cryptography; + +namespace SharpGenTools.Sdk.Internal; + +public ref struct CacheFile +{ + public enum CacheState + { + Hit, + Miss, + Absent + } + + private bool? _isWriteNeeded = null; + private CacheState _state = CacheState.Absent; + private bool _hasWritten = false; + private StreamWriter? _writer = null; + private FileInfo? _file = null; + + public CacheFile() + { + Stream = new MemoryStream(); + } + + public CacheFile(FileInfo file) : this() + { + File = file; + } + + public FileInfo File + { + get => _file ?? throw new InvalidOperationException(); + set + { + _file = value ?? throw new ArgumentNullException(nameof(value)); + Utilities.RequireAbsolutePath(value.FullName, nameof(value)); + } + } + + private MemoryStream Stream { get; } + + public bool IsWriteNeeded + { + get + { + return _isWriteNeeded ??= ComputeIsWriteNeeded(); + } + } + + public CacheState State + { + get + { + _isWriteNeeded ??= ComputeIsWriteNeeded(); + return _state; + } + } + + private bool ComputeIsWriteNeeded() + { + Debug.Assert(_isWriteNeeded is null); + Debug.Assert(_writer is not null); + Debug.Assert(!_hasWritten); + + _writer.Dispose(); + + File.Refresh(); + + if (!File.Exists) + { + _state = CacheState.Absent; + return true; + } + + ReadOnlySpan cachedHash = System.IO.File.ReadAllBytes(File.FullName); + + Stream.Position = 0; + var success = Stream.TryGetBuffer(out var buffer); + Debug.Assert(success); + + if (buffer.AsSpan().SequenceEqual(cachedHash)) + { + _state = CacheState.Hit; + return false; + } + + _state = CacheState.Miss; + return true; + } + + public StreamWriter StreamWriter => + _writer is null + ? _writer = new StreamWriter(Stream, SharpGenTask.DefaultEncoding, 1024, true) + : throw new InvalidOperationException(); + + public void Write() + { + Debug.Assert(_isWriteNeeded == true); + Debug.Assert(!_hasWritten); + + Stream.Position = 0; + + if (File.DirectoryName is { } directory) + Directory.CreateDirectory(directory); + + using var file = File.Open(FileMode.Create, FileAccess.Write); + Stream.CopyTo(file); + _hasWritten = true; + } + + public byte[] ComputeHash(HashAlgorithm hash) + { + Debug.Assert(_writer is not null); + + _writer.Dispose(); + + Stream.Position = 0; + + return hash.ComputeHash(Stream); + } + + public void Dispose() + { + if (_writer is { } writer) + writer.Dispose(); + Stream.Dispose(); + } +} \ No newline at end of file diff --git a/SharpGenTools.Sdk/Internal/Utilities.cs b/SharpGenTools.Sdk/Internal/Utilities.cs index cbccb5e2..6c7592f5 100644 --- a/SharpGenTools.Sdk/Internal/Utilities.cs +++ b/SharpGenTools.Sdk/Internal/Utilities.cs @@ -91,4 +91,30 @@ internal static Stream OpenRead(string fullPath) throw new IOException(e.Message, e); } } + + private static string FixFilePath(string path) + { + return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/'); + } + + internal static string EnsureTrailingSlash(string fileSpec) + { + fileSpec = FixFilePath(fileSpec); + if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1])) + { + fileSpec += Path.DirectorySeparatorChar; + } + + return fileSpec; + } + + /// + /// Indicates if the given character is a slash. + /// + /// + /// true, if slash + private static bool IsSlash(char c) + { + return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar); + } } \ No newline at end of file diff --git a/SharpGenTools.Sdk/Sdk.props b/SharpGenTools.Sdk/Sdk.props index 3ca2414d..861b8526 100644 --- a/SharpGenTools.Sdk/Sdk.props +++ b/SharpGenTools.Sdk/Sdk.props @@ -1,7 +1,8 @@  - true + $(BeforePack);SharpGenGenerateConsumerBindMappingFile;SharpGenGenerateConsumerProps + SharpGenGenerateConsumerBindMappingFile;SharpGenGenerateConsumerProps @@ -18,12 +19,11 @@ - - - - + + + + - \ No newline at end of file diff --git a/SharpGenTools.Sdk/Sdk.targets b/SharpGenTools.Sdk/Sdk.targets index 3993bf44..6ea4f7fc 100644 --- a/SharpGenTools.Sdk/Sdk.targets +++ b/SharpGenTools.Sdk/Sdk.targets @@ -13,34 +13,33 @@ - + - + - $([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'SharpGen')) + $([MSBuild]::NormalizeDirectory('$(BaseIntermediateOutputPath)')) - $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', 'SharpGen')) + $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)')) - + - $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(SharpGenIntermediateDir)')) + $([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(SharpGenIntermediateOutputDirectory)')) - + - $([MSBuild]::EnsureTrailingSlash('$(SharpGenIntermediateDir)')) + $([MSBuild]::EnsureTrailingSlash('$(SharpGenIntermediateOutputDirectory)')) - $([MSBuild]::NormalizeDirectory('$(SharpGenIntermediateDir)', 'Generated')) $(AssemblyName) $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', '..', 'tools')) @@ -68,8 +67,8 @@ - net5.0 - net472 + net8.0 + netstandard2.0 @@ -85,17 +84,6 @@ - - - - - - - - - - - @@ -108,11 +96,13 @@ - - + - - $(TargetsForTfmSpecificContentInPackage);GenerateConsumerBindMappingFile;GenerateTfmSpecificConsumerProps - - - - - build/$(TargetFramework);buildMultiTargeting/$(TargetFramework) - true - - - build/$(TargetFramework);buildMultiTargeting/$(TargetFramework) - true - - - + + + + + - + Condition="'$(SharpGenGenerateConsumerBindMapping)' != 'false'"> - - - - - + + + build/$(PackageId).props;buildMultiTargeting/$(PackageId).props + true + - - - - - - - - - - - - - - - - - - - + - - - - <_SharpGenMappingNonexistent Include="@(SharpGenMapping)" Condition="!Exists('%(Identity)')" /> <_SharpGenConsumerMappingNonexistent Include="@(SharpGenConsumerMapping)" Condition="!Exists('%(Identity)')" /> @@ -296,33 +188,43 @@ <_SharpGenConsumerMappingNonexistent Remove="@(_SharpGenConsumerMappingNonexistent)" /> - + + + + + + + + + build + true + + - + - + - + diff --git a/SharpGenTools.Sdk/SharpGenTask.InputsCache.cs b/SharpGenTools.Sdk/SharpGenTask.InputsCache.cs new file mode 100644 index 00000000..5856f6ad --- /dev/null +++ b/SharpGenTools.Sdk/SharpGenTask.InputsCache.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using SharpGenTools.Sdk.Internal; + +namespace SharpGenTools.Sdk; + +public sealed partial class SharpGenTask +{ + private const string InputsCacheMarkerFiles = "# Files"; + private const string InputsCacheMarkerEnvironmentVariables = "# Environment variables"; + private readonly List inputsCacheListFiles = new(); + private readonly List inputsCacheListEnvironmentVariables = new(); + private readonly HashSet inputsCacheSetFiles = new(); + private readonly HashSet inputsCacheSetEnvironmentVariables = new(); + private readonly List inputsCacheMetadataFiles = new(); + private readonly List inputsCacheMetadataEnvironmentVariables = new(); + private bool inputsCacheImmutable; + private static readonly Func ComputeInputsCacheFileMetadataFunc = ComputeInputsCacheFileMetadata; + private static readonly Func ComputeInputsCacheEnvironmentVariableMetadataFunc = ComputeInputsCacheEnvironmentVariableMetadata; + + private void GenerateInputsCache() + { + inputsCacheImmutable = true; + + using CacheFile cacheFile = new(new FileInfo(InputsCache)); + + { + using var writer = cacheFile.StreamWriter; + + writer.WriteLine(InputsCacheMarkerFiles); + for (int i = 0, count = inputsCacheListFiles.Count; i < count; i++) + { + writer.Write(inputsCacheMetadataFiles[i]); + writer.Write(' '); + writer.WriteLine(inputsCacheListFiles[i]); + } + + writer.WriteLine(InputsCacheMarkerEnvironmentVariables); + for (int i = 0, count = inputsCacheListEnvironmentVariables.Count; i < count; i++) + { + writer.Write(inputsCacheListEnvironmentVariables[i]); + writer.Write(' '); + writer.WriteLine(inputsCacheMetadataEnvironmentVariables[i]); + } + } + + SharpGenLogger.Message( + cacheFile.State switch + { + CacheFile.CacheState.Hit => "Input file cache is already up-to-date.", + CacheFile.CacheState.Miss => "Input file cache is out-of-date.", + CacheFile.CacheState.Absent => "Input file cache doesn't exist.", + _ => throw new ArgumentOutOfRangeException() + } + ); + + if (cacheFile.IsWriteNeeded) + cacheFile.Write(); + } + + private static string ComputeInputsCacheFileMetadata(string path) + { + FileInfo file = new(path); + Debug.Assert(file.Exists); + + return $"{file.CreationTimeUtc.Ticks} {file.LastWriteTimeUtc.Ticks} {file.Length}"; + } + + private static string ComputeInputsCacheEnvironmentVariableMetadata(string name) => + Environment.GetEnvironmentVariable(name) is { } value ? $"true {value}" : "false"; + + private void AddInputsCacheItem(string item, List itemList, HashSet itemSet, + List metadataList, Func metadataFunc) + { + Debug.Assert(!inputsCacheImmutable); + + if (string.IsNullOrWhiteSpace(item)) + return; + + var metadata = metadataFunc(item); + if (itemSet.Add(item)) + { + itemList.Add(item); + metadataList.Add(metadata); + } + else + { + bool Predicate(string x) => string.Equals(x, item); + + var index = itemList.FindIndex(Predicate); + Debug.Assert(index != -1); + Debug.Assert(metadataList[index] == metadata); + } + } + + private void AddInputsCacheFile(string path) => AddInputsCacheItem( + Utilities.FixFilePath(path, Utilities.EmptyFilePathBehavior.Ignore), + inputsCacheListFiles, inputsCacheSetFiles, inputsCacheMetadataFiles, + ComputeInputsCacheFileMetadataFunc + ); + + private void AddInputsCacheEnvironmentVariable(string name) => AddInputsCacheItem( + name, inputsCacheListEnvironmentVariables, inputsCacheSetEnvironmentVariables, + inputsCacheMetadataEnvironmentVariables, ComputeInputsCacheEnvironmentVariableMetadataFunc + ); + + private void AddInputsCacheFiles(IEnumerable paths) + { + foreach (var path in paths) + AddInputsCacheFile(path); + } + + private bool IsInputsCacheValid() + { + if (!File.Exists(InputsCache)) + return false; + + bool files = false, env = false; + + foreach (var line in File.ReadLines(InputsCache, DefaultEncoding)) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (string.Equals(line, InputsCacheMarkerFiles)) + { + files = true; + env = false; + } + else if (string.Equals(line, InputsCacheMarkerEnvironmentVariables)) + { + files = false; + env = true; + } + else if (files) + { + var items = line.Split(SpaceSeparator, 4); + long creation = long.Parse(items[0]), + modified = long.Parse(items[1]), + size = long.Parse(items[2]); + FileInfo file = new(items[3]); + + if (!file.Exists) + return false; + + if (file.Length != size) + return false; + + if (file.CreationTimeUtc.Ticks != creation) + return false; + + if (file.LastWriteTimeUtc.Ticks != modified) + return false; + } + else if (env) + { + var items = line.Split(SpaceSeparator, 3); + + if (!bool.TryParse(items[1], out var nonNull)) + return false; + + var value = Environment.GetEnvironmentVariable(items[0]); + + if (value is not null != nonNull) + return false; + + if (nonNull && value != items[2]) + return false; + } + else + { + // Unsupported block, either some future SharpGen version wrote this file, + // or the file data is simply corrupt. Either way, invalidate the cache. + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/SharpGenTools.Sdk/SharpGenTask.PropertyCache.cs b/SharpGenTools.Sdk/SharpGenTask.PropertyCache.cs new file mode 100644 index 00000000..040d6ebb --- /dev/null +++ b/SharpGenTools.Sdk/SharpGenTask.PropertyCache.cs @@ -0,0 +1,256 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Microsoft.Build.Framework; +using SharpGenTools.Sdk.Internal; + +namespace SharpGenTools.Sdk; + +public sealed partial class SharpGenTask +{ + private const int CacheFormatSignature = ('S' << 0) | ('G' << 8) | ('P' << 16) | ('C' << 24); + private const int CacheFormatVersion = 1; + + private static HashAlgorithm CreateSettingsHash() => SHA256.Create(); + + private bool GeneratePropertyCache() + { + List parts = new(4) + { + "SharpGen" + }; + + if (!string.Equals(PlatformName, "AnyCPU", StringComparison.InvariantCultureIgnoreCase)) + { + parts.Add(PlatformName!); + } + + if (!string.IsNullOrWhiteSpace(RuntimeIdentifier)) + { + parts.Add(RuntimeIdentifier); + } + + CacheFile cacheFile = new(); + try + { + { + using var writer = cacheFile.StreamWriter; + HashSettings(writer); + } + + var currentHash = Base64UrlEncode(cacheFile.ComputeHash(CreateSettingsHash())); + + parts.Add(currentHash); + + var profileName = string.Join("-", parts); + + workerLock = new Mutex(false, @"Global\" + profileName); + var cacheInvalid = false; + + try + { + while (!(workerLockAcquired = workerLock.WaitOne(TimeSpan.FromSeconds(5))) && !AbortExecution) + { + SharpGenLogger.Message($"Waiting for the worker to finish job {profileName}…"); + } + } + catch (AbandonedMutexException) + { + SharpGenLogger.Message($"The worker failed to complete job {profileName}."); + workerLockAcquired = true; + cacheInvalid = true; + } + + ProfilePath = Path.Combine(IntermediateOutputDirectory, profileName); + + if (!workerLockAcquired) + { + SharpGenLogger.Message($"Aborting the wait for job {profileName} to complete."); + Debug.Assert(AbortExecution); + return true; + } + + cacheFile.File = new FileInfo(PropertyCache); + Utilities.RequireAbsolutePath(PropertyCache, nameof(PropertyCache)); + + if (File.Exists(DirtyMarkerFile)) + { + SharpGenLogger.Message("Dirty marker exists, requesting full regeneration."); + cacheInvalid = true; + } + else if (!cacheInvalid) + { + cacheInvalid = cacheFile.IsWriteNeeded; + SharpGenLogger.Message( + cacheFile.State switch + { + CacheFile.CacheState.Hit => "Properties match cached value.", + CacheFile.CacheState.Miss => "Properties mismatch, writing a new property cache file.", + CacheFile.CacheState.Absent => "Properties cache doesn't exist.", + _ => throw new ArgumentOutOfRangeException() + } + ); + } + + if (cacheFile.IsWriteNeeded) + cacheFile.Write(); + + return cacheInvalid; + } + finally + { + cacheFile.Dispose(); + } + } + + private void HashSettings(StreamWriter writer) + { + writer.Write(CacheFormatSignature.ToString("X8")); + writer.Write('.'); + writer.Write(CacheFormatVersion); + writer.WriteLine(); + + WriteStringArray(CastXmlArguments); + WriteString(CastXmlExecutable); + WriteStringArray(ConfigFiles); + WriteString(ConsumerBindMappingConfigId); + WriteBool(DocumentationFailuresAsErrors); + WriteStringArray(ExtensionAssemblies); + WriteStringArray(ExternalDocumentation); + WriteTaskItems(GlobalNamespaceOverrides); + WriteStringArray(Macros); + WriteString(IntermediateOutputDirectory); + WriteString(PlatformName); + WriteString(RuntimeIdentifier); + WriteStringArray(Platforms); + WriteStringArray(SilenceMissingDocumentationErrorIdentifierPatterns); + + void WriteString(string? s, [CallerArgumentExpression("s")] string? name = null) + { + writer.Write(name); + writer.Write(": "); + writer.WriteLine(s ?? ""); + } + + void WriteBool(bool v, [CallerArgumentExpression("v")] string? name = null) + { + writer.Write(name); + writer.Write(": "); + writer.WriteLine(v); + } + + void WriteStringArray(IReadOnlyList? items, [CallerArgumentExpression("items")] string? name = null) + { + writer.Write(name); + writer.Write(":"); + + if (items is null) + { + writer.WriteLine(" "); + return; + } + + writer.WriteLine(); + + for (int i = 0, length = items.Count; i < length; i++) + { + writer.Write("* "); + writer.WriteLine(items[i]); + } + } + + void WriteTaskItem(ITaskItem? item, [CallerArgumentExpression("item")] string? name = null) + { + writer.Write(name); + writer.Write(": "); + + if (item is null) + { + writer.WriteLine(""); + return; + } + + writer.WriteLine(item.ItemSpec); + + foreach (DictionaryEntry entry in item.CloneCustomMetadata()) + { + writer.Write(name); + writer.Write("["); + writer.Write(entry.Key.ToString() ?? ""); + writer.Write("]: "); + writer.WriteLine(entry.Value?.ToString() ?? ""); + } + } + + void WriteTaskItems(IReadOnlyList? items, [CallerArgumentExpression("items")] string? name = null) + { + writer.Write(name); + writer.Write(":"); + + if (items is null) + { + writer.WriteLine(" "); + return; + } + + writer.WriteLine(); + + for (int i = 0, length = items.Count; i < length; i++) + { + WriteTaskItem(items[i], $"{name}[{i}]"); + } + } + } + + private static string Base64UrlEncode(byte[] input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + // Special-case empty input + var count = input.Length; + if (count == 0) + return string.Empty; + + var numWholeOrPartialInputBlocks = checked(count + 2) / 3; + var buffer = new char[checked(numWholeOrPartialInputBlocks * 4)]; + var numBase64Chars = Base64UrlEncode(input, buffer, count); + + return new string(buffer, 0, numBase64Chars); + } + + private static int Base64UrlEncode(byte[] input, char[] output, int count) + { + // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + + // Start with default Base64 encoding. + var numBase64Chars = Convert.ToBase64CharArray(input, 0, count, output, 0); + + // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. + for (var i = 0; i < numBase64Chars; i++) + { + switch (output[i]) + { + case '+': + output[i] = '-'; + break; + case '/': + output[i] = '_'; + break; + case '=': + // We've reached a padding character; truncate the remainder. + return i; + } + } + + return numBase64Chars; + } +} \ No newline at end of file diff --git a/SharpGenTools.Sdk/SharpGenTask.cs b/SharpGenTools.Sdk/SharpGenTask.cs new file mode 100644 index 00000000..37c73d86 --- /dev/null +++ b/SharpGenTools.Sdk/SharpGenTask.cs @@ -0,0 +1,560 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.CodeAnalysis.CSharp; +using SharpGen; +using SharpGen.Config; +using SharpGen.CppModel; +using SharpGen.Generator; +using SharpGen.Logging; +using SharpGen.Model; +using SharpGen.Parser; +using SharpGen.Platform; +using SharpGen.Platform.Documentation; +using SharpGen.Transform; +using SharpGenTools.Sdk.Documentation; +using SharpGenTools.Sdk.Extensibility; +using SharpGenTools.Sdk.Internal; +using Logger = SharpGen.Logging.Logger; +using SdkResolver = SharpGen.Parser.SdkResolver; + +namespace SharpGenTools.Sdk; + +public sealed partial class SharpGenTask : Task, ICancelableTask +{ + // Default encoding used by MSBuild ReadLinesFromFile task + internal static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); + private static readonly char[] SpaceSeparator = { ' ' }; + private readonly IocServiceContainer serviceContainer = new(); + private readonly Ioc ioc = new(); + private string? profilePath; + private volatile bool isCancellationRequested; + private Mutex? workerLock; + private bool workerLockAcquired, regenerationStarted; + + // ReSharper disable MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global + [Required] public string[]? CastXmlArguments { get; set; } + [Required] public string? CastXmlExecutable { get; set; } + [Required] public string[]? ConfigFiles { get; set; } + [Required] public string? ConsumerBindMappingConfigId { get; set; } + [Required] public bool DebugWaitForDebuggerAttach { get; set; } + [Required] public bool DocumentationFailuresAsErrors { get; set; } + [Required] public string[]? ExtensionAssemblies { get; set; } + [Required] public string[]? ExternalDocumentation { get; set; } + [Required] public ITaskItem[]? GlobalNamespaceOverrides { get; set; } + [Required] public string[]? Macros { get; set; } + [Required] public string? IntermediateOutputDirectory { get; set; } + public string? PlatformName { get; set; } + [Required] public string[]? Platforms { get; set; } + + [Output] + public string ProfilePath + { + get => profilePath ?? throw new InvalidOperationException("Profile not set"); + set + { + if (profilePath is not null) + throw new InvalidOperationException("Profile cannot be set twice"); + profilePath = Utilities.EnsureTrailingSlash(value ?? throw new InvalidOperationException("Profile cannot be null")); + } + } + + public string? RuntimeIdentifier { get; set; } + [Required] public string[]? SilenceMissingDocumentationErrorIdentifierPatterns { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global, MemberCanBePrivate.Global + + private string GeneratedCodeFile => GetProfileChild("SharpGen.Bindings.g.cs"); + private string InputsCache => GetProfileChild("InputsCache.txt"); + private string PropertyCache => GetProfileChild("PropertyCache.txt"); + private string DocumentationCache => GetProfileChild("DocumentationCache.json"); + private string DirtyMarkerFile => GetProfileChild("dirty"); + private string PackagePropsFile => GetProfileChild("Package.props"); + + private string ConsumerBindMappingConfig => GetProfileChild( + (ConsumerBindMappingConfigId ?? throw new InvalidOperationException(nameof(ConsumerBindMappingConfigId))) + + ".BindMapping.xml" + ); + + private string GetProfileChild(string childName) => Path.Combine(ProfilePath, childName); + + private Logger SharpGenLogger { get; set; } + + [Conditional("DEBUG")] + private void WaitForDebuggerAttach() + { + if (!Debugger.IsAttached) + { + SharpGenLogger.Warning(null, $"{GetType().Name} is waiting for attach: {Process.GetCurrentProcess().Id}"); + Thread.Yield(); + } + + while (!Debugger.IsAttached && !isCancellationRequested) + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + + public void Cancel() => isCancellationRequested = true; + + private static string[] PreprocessPathListProperty(string[] items) => + items.Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(static x => x, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + public override bool Execute() + { + BindingRedirectResolution.Enable(); + + SharpGenLogger = new Logger(new MSBuildSharpGenLogger(Log)); + +#if DEBUG + if (DebugWaitForDebuggerAttach) + WaitForDebuggerAttach(); +#endif + + var success = false; + + try + { + ConfigFiles = PreprocessPathListProperty(ConfigFiles); + ExtensionAssemblies = PreprocessPathListProperty(ExtensionAssemblies); + ExternalDocumentation = PreprocessPathListProperty(ExternalDocumentation); + + if (GeneratePropertyCache()) + { + return success = ExecuteImpl(); + } + else + { + if (AbortExecution) + return success = false; + + if (IsInputsCacheValid()) + return success = true; + + return success = ExecuteImpl(); + } + } + catch (CodeGenFailedException ex) + { + SharpGenLogger.Fatal("Internal SharpGen exception", ex); + return success = false; + } + finally + { + Debug.Assert(workerLock != null, nameof(workerLock) + " != null"); + + try + { + GeneratePackagesProps(); + } + catch (Exception e) + { + SharpGenLogger.LogRawMessage( + LogLevel.Warning, null, + "Internal SharpGen exception while generating NuGet package properties file", e + ); + } + + if (regenerationStarted) + { + try + { + GenerateInputsCache(); + } + catch (Exception e) + { + SharpGenLogger.LogRawMessage( + LogLevel.Warning, null, + "Internal SharpGen exception while generating input file cache", e + ); + } + } + + if (success && !AbortExecution && File.Exists(DirtyMarkerFile)) + File.Delete(DirtyMarkerFile); + + if (workerLockAcquired) + workerLock.ReleaseMutex(); + + workerLock.Dispose(); + workerLock = null; + } + } + + private bool AbortExecution => SharpGenLogger.HasErrors || isCancellationRequested; + + private bool ExecuteImpl() + { + File.WriteAllBytes(DirtyMarkerFile, Array.Empty()); + regenerationStarted = true; + + if (AbortExecution) + return false; + + serviceContainer.AddService(SharpGenLogger); + serviceContainer.AddService(); + serviceContainer.AddService(); + serviceContainer.AddService(new TypeRegistry(ioc)); + ioc.ConfigureServices(serviceContainer); + + ExtensibilityDriver.Instance.LoadExtensions(SharpGenLogger, ExtensionAssemblies); + AddInputsCacheFiles(ExtensionAssemblies); + + ConfigFile config = new() + { + Files = ConfigFiles.ToList(), + Id = "SharpGen-MSBuild" + }; + + LoadConfig(config); + + config.GetFilesWithIncludesAndExtensionHeaders( + out var configsWithHeaders, + out var configsWithExtensionHeaders + ); + + AddInputsCacheFiles(config.ConfigFilesLoaded.Select(x => x.AbsoluteFilePath)); + + CppHeaderGenerator cppHeaderGenerator = new(ProfilePath, ioc); + + var cppHeaderGenerationResult = cppHeaderGenerator.GenerateCppHeaders(config, configsWithHeaders, configsWithExtensionHeaders); + + if (AbortExecution) + return false; + + IncludeDirectoryResolver resolver = new(ioc); + resolver.Configure(config); + + AddInputsCacheFile(CastXmlExecutable); + CastXmlRunner castXml = new(resolver, CastXmlExecutable, CastXmlArguments, ioc) + { + OutputPath = ProfilePath + }; + + var module = config.CreateSkeletonModule(); + + MacroManager macroManager = new(castXml); + macroManager.Parse(Path.Combine(ProfilePath, config.HeaderFileName), module); + AddInputsCacheFiles(macroManager.IncludedFiles); + + new CppExtensionHeaderGenerator().GenerateExtensionHeaders( + config, ProfilePath, module, configsWithExtensionHeaders, cppHeaderGenerationResult.UpdatedConfigs + ); + + AddInputsCacheFiles(configsWithExtensionHeaders.Select(x => Path.Combine(ProfilePath, x.ExtensionFileName))); + + if (AbortExecution) + return false; + + // Run the parser + var parser = new CppParser(config, ioc) + { + OutputPath = ProfilePath + }; + + if (AbortExecution) + return false; + + CppModule group; + + using (var xmlReader = castXml.Process(parser.RootConfigHeaderFileName)) + { + // Run the C++ parser + group = parser.Run(module, xmlReader); + } + + if (AbortExecution) + return false; + + config.ExpandDynamicVariables(SharpGenLogger, group); + + var docLinker = ioc.DocumentationLinker; + var globalNamespace = ioc.GlobalNamespace; + NamingRulesManager namingRules = new(); + + foreach (var nameOverride in GlobalNamespaceOverrides) + { + var wellKnownName = nameOverride.ItemSpec; + var overridenName = nameOverride.GetMetadata("Override"); + + if (string.IsNullOrEmpty(overridenName)) + continue; + + if (Enum.TryParse(wellKnownName, out WellKnownName name)) + { + globalNamespace.OverrideName(name, overridenName); + } + else + { + SharpGenLogger.Warning( + LoggingCodes.InvalidGlobalNamespaceOverride, + "Invalid override of \"{0}\": unknown class name, ignoring the override.", + wellKnownName + ); + } + } + + serviceContainer.AddService( + new GeneratorConfig + { + Platforms = ConfigPlatforms + } + ); + + // Run the main mapping process + TransformManager transformer = new( + namingRules, + new ConstantManager(namingRules, ioc), + ioc + ); + + var (solution, defines) = transformer.Transform(group, config); + + var consumerConfig = new ConfigFile + { + Id = ConsumerBindMappingConfigId, + IncludeProlog = {cppHeaderGenerationResult.Prologue}, + Extension = new List(defines) + }; + + var (bindings, generatedDefines) = transformer.GenerateTypeBindingsForConsumers(); + + consumerConfig.Bindings.AddRange(bindings); + consumerConfig.Extension.AddRange(generatedDefines); + + consumerConfig.Mappings.AddRange( + docLinker.GetAllDocLinks().Select( + link => new MappingRule + { + DocItem = link.cppName, + MappingNameFinal = link.cSharpName + } + ) + ); + + GenerateConfigForConsumers(consumerConfig); + + if (AbortExecution) + return false; + + var documentationCacheItemSpec = DocumentationCache; + + Utilities.RequireAbsolutePath(documentationCacheItemSpec, nameof(DocumentationCache)); + + var cache = File.Exists(documentationCacheItemSpec) + ? DocItemCache.Read(documentationCacheItemSpec) + : new DocItemCache(); + + DocumentationLogger docLogger = new(SharpGenLogger) {MaxLevel = LogLevel.Warning}; + var docContext = new Lazy(() => new DocumentationContext(docLogger)); + ExtensibilityDriver.Instance.DocumentModule(SharpGenLogger, cache, solution, docContext).Wait(); + + if (docContext.IsValueCreated) + { + Regex[] silencePatterns = null; + var docLogLevelDefault = DocumentationFailuresAsErrors ? LogLevel.Error : LogLevel.Warning; + + foreach (var queryFailure in docContext.Value.Failures) + { + if (silencePatterns == null) + { + silencePatterns = new Regex[SilenceMissingDocumentationErrorIdentifierPatterns.Length]; + for (var i = 0; i < silencePatterns.Length; i++) + silencePatterns[i] = new Regex( + SilenceMissingDocumentationErrorIdentifierPatterns[i], + RegexOptions.CultureInvariant + ); + } + + if (silencePatterns.Length != 0) + { + bool SilencePredicate(Regex x) => x.Match(queryFailure.Query).Success; + + if (silencePatterns.Any(SilencePredicate)) + continue; + } + + var providerName = queryFailure.FailedProviderName ?? ""; + + var docLogLevel = queryFailure.TreatProviderFailuresAsErrors + ? docLogLevelDefault + : docLogLevelDefault > LogLevel.Warning + ? LogLevel.Warning + : docLogLevelDefault; + + switch (queryFailure.Exceptions) + { + case { Count: > 1 } exceptions: + { + var exceptionsCount = exceptions.Count; + for (var index = 0; index < exceptionsCount; index++) + { + var exception = exceptions[index]; + + SharpGenLogger.LogRawMessage( + docLogLevel, + LoggingCodes.DocumentationProviderInternalError, + "Documentation provider [{0}] query for \"{1}\" failed ({2}/{3}).", + exception, + providerName, + queryFailure.Query, + index + 1, + exceptionsCount + ); + } + + break; + } + case { Count: 1 } exceptions: + SharpGenLogger.LogRawMessage( + docLogLevel, + LoggingCodes.DocumentationProviderInternalError, + "Documentation provider [{0}] query for \"{1}\" failed.", + exceptions[0], + providerName, + queryFailure.Query + ); + break; + default: + SharpGenLogger.LogRawMessage( + docLogLevel, + LoggingCodes.DocumentationProviderInternalError, + "Documentation provider [{0}] query for \"{1}\" failed.", + null, + providerName, + queryFailure.Query + ); + break; + } + } + } + + cache.WriteIfDirty(documentationCacheItemSpec); + + if (AbortExecution) + return false; + + var documentationFiles = new Dictionary(); + + AddInputsCacheFiles(ExternalDocumentation); + foreach (var file in ExternalDocumentation) + { + using var stream = File.OpenRead(file); + + var xml = new XmlDocument(); + xml.Load(stream); + documentationFiles.Add(file, xml); + } + + if (AbortExecution) + return false; + + serviceContainer.AddService(new ExternalDocCommentsReader(documentationFiles)); + serviceContainer.AddService(new DefaultGenerators(ioc)); + + RoslynGenerator generator = new(); + + using var codeStream = File.Open(GeneratedCodeFile, FileMode.Create, FileAccess.Write); + using var codeWriter = new StreamWriter(codeStream, DefaultEncoding); + generator.Run(solution, ioc).GetCompilationUnitRoot().WriteTo(codeWriter); + + return !SharpGenLogger.HasErrors; + } + + private PlatformDetectionType ConfigPlatforms + { + get + { + PlatformDetectionType platformMask = 0; + + foreach (var platform in Platforms) + { + if (!Enum.TryParse(platform, out var parsedPlatform)) + { + SharpGenLogger.Warning( + LoggingCodes.InvalidPlatformDetectionType, + "The platform type {0} is an unknown platform to SharpGenTools. Falling back to Any platform detection.", + platform + ); + platformMask = PlatformDetectionType.Any; + } + else + { + platformMask |= parsedPlatform; + } + } + + return platformMask == 0 ? PlatformDetectionType.Any : platformMask; + } + } + + private void GenerateConfigForConsumers(ConfigFile consumerConfig) + { + using var consumerBindMapping = File.Create(ConsumerBindMappingConfig); + + consumerConfig.Write(consumerBindMapping); + } + + private void LoadConfig(ConfigFile config) + { + config.Load(null, Macros, SharpGenLogger); + + AddInputsCacheEnvironmentVariable("SHARPGEN_VS_OVERRIDE"); + AddInputsCacheEnvironmentVariable("SHARPGEN_SDK_OVERRIDE"); + + SdkResolver sdkResolver = new(SharpGenLogger); + SharpGenLogger.Message("Resolving SDKs..."); + foreach (var cfg in config.ConfigFilesLoaded) + { + SharpGenLogger.Message("Resolving SDK for Config {0}", cfg); + foreach (var sdk in cfg.Sdks) + { + SharpGenLogger.Message("Resolving {0}: Version {1}", sdk.Name, sdk.Version); + foreach (var directory in sdkResolver.ResolveIncludeDirsForSdk(sdk)) + { + SharpGenLogger.Message("Resolved include directory {0}", directory); + cfg.IncludeDirs.Add(directory); + } + } + } + } + + private void GeneratePackagesProps() + { + using CacheFile cacheFile = new(new FileInfo(PackagePropsFile)); + + { + using var writer = cacheFile.StreamWriter; + + writer.WriteLine(@""); + writer.WriteLine(@" "); + writer.WriteLine( + $@" " + ); + writer.WriteLine(@" "); + writer.WriteLine(@""); + } + + SharpGenLogger.Message( + cacheFile.State switch + { + CacheFile.CacheState.Hit => "NuGet package properties file is already up-to-date.", + CacheFile.CacheState.Miss => "NuGet package properties file is out-of-date.", + CacheFile.CacheState.Absent => "NuGet package properties file doesn't exist.", + _ => throw new ArgumentOutOfRangeException() + } + ); + + if (cacheFile.IsWriteNeeded) + cacheFile.Write(); + } +} \ No newline at end of file diff --git a/SharpGenTools.Sdk/SharpGenTools.Sdk.csproj b/SharpGenTools.Sdk/SharpGenTools.Sdk.csproj index 6da3093a..244f6721 100644 --- a/SharpGenTools.Sdk/SharpGenTools.Sdk.csproj +++ b/SharpGenTools.Sdk/SharpGenTools.Sdk.csproj @@ -1,85 +1,97 @@  - - - - - net472;net5.0 - SharpGenTools.Sdk - true - false - - MSBuildSdk - true - true - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'Sdk.Version.props')) - - - true - true - win;unix - MSBuild tooling for SharpGen. Tooling for generating efficient C# code from C++ and COM headers. - true - false - IncludeDefaultProjectBuildOutputInPack - - - - - - - - - - - - - - - - - - - - Content - PreserveNewest - - Build;DebugSymbolsProjectOutputGroup - - - - - - - - - - - - - - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'Sdk.Version.props')) - - - - - <_VersionPropsLine Include="<Project>" /> - <_VersionPropsLine Include="<PropertyGroup>" /> - <_VersionPropsLine Include="<SharpGenSdkVersion Condition="'%24(SharpGenSdkVersion)'==''">$(PackageVersion)</SharpGenSdkVersion>" /> - <_VersionPropsLine Include="</PropertyGroup>" /> - <_VersionPropsLine Include="</Project>" /> - - - - - - - - - - - + + + + netstandard2.0;net8.0 + SharpGenTools.Sdk + true + false + + MSBuildSdk + true + true + + $([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'Sdk.Version.props')) + + + true + true + win;unix + MSBuild tooling for SharpGen. Tooling for generating efficient C# code from C++ and COM headers. + true + false + IncludeDefaultProjectBuildOutputInPack + true + + + + + + + + + + + + Code + + + Code + + + Code + + + + + + + + + + + + + + + Content + PreserveNewest + + Build;DebugSymbolsProjectOutputGroup + + + + + + + + + + + + + + + $([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'Sdk.Version.props')) + + + + + <_VersionPropsLine Include="<Project>" /> + <_VersionPropsLine Include="<PropertyGroup>" /> + <_VersionPropsLine Include="<SharpGenSdkVersion Condition="'%24(SharpGenSdkVersion)'==''">$(PackageVersion)</SharpGenSdkVersion>" /> + <_VersionPropsLine Include="</PropertyGroup>" /> + <_VersionPropsLine Include="</Project>" /> + + + + + + + + + + \ No newline at end of file diff --git a/SharpGenTools.Sdk/Tasks/SharpGenTask.cs b/SharpGenTools.Sdk/Tasks/SharpGenTask.cs deleted file mode 100644 index 9dbe76ce..00000000 --- a/SharpGenTools.Sdk/Tasks/SharpGenTask.cs +++ /dev/null @@ -1,373 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using SharpGen; -using SharpGen.Config; -using SharpGen.CppModel; -using SharpGen.Generator; -using SharpGen.Logging; -using SharpGen.Model; -using SharpGen.Parser; -using SharpGen.Platform; -using SharpGen.Platform.Documentation; -using SharpGen.Transform; -using SharpGenTools.Sdk.Documentation; -using SharpGenTools.Sdk.Extensibility; -using SharpGenTools.Sdk.Internal; - -namespace SharpGenTools.Sdk.Tasks; - -public sealed class SharpGenTask : SharpGenTaskBase -{ - // Default encoding used by MSBuild ReadLinesFromFile task - private static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); - private readonly IocServiceContainer serviceContainer = new(); - private readonly Ioc ioc = new(); - - public override bool Execute() - { - PrepareExecute(); - - serviceContainer.AddService(SharpGenLogger); - serviceContainer.AddService(); - serviceContainer.AddService(); - serviceContainer.AddService(new TypeRegistry(ioc)); - ioc.ConfigureServices(serviceContainer); - - ExtensibilityDriver.Instance.LoadExtensions(SharpGenLogger, - ExtensionAssemblies.Select(x => x.ItemSpec).ToArray()); - - var config = new ConfigFile - { - Files = ConfigFiles.Select(file => file.ItemSpec).ToList(), - Id = "SharpGen-MSBuild" - }; - - try - { - config = LoadConfig(config); - - return !SharpGenLogger.HasErrors && Execute(config); - } - catch (CodeGenFailedException ex) - { - SharpGenLogger.Fatal("Internal SharpGen exception", ex); - return false; - } - } - - private bool AbortExecution => SharpGenLogger.HasErrors || IsCancellationRequested; - - private bool Execute(ConfigFile config) - { - config.GetFilesWithIncludesAndExtensionHeaders( - out var configsWithHeaders, - out var configsWithExtensionHeaders - ); - - CppHeaderGenerator cppHeaderGenerator = new(OutputPath, ioc); - - var cppHeaderGenerationResult = cppHeaderGenerator.GenerateCppHeaders(config, configsWithHeaders, configsWithExtensionHeaders); - - if (AbortExecution) - return false; - - IncludeDirectoryResolver resolver = new(ioc); - resolver.Configure(config); - - CastXmlRunner castXml = new(resolver, CastXmlExecutable.ItemSpec, CastXmlArguments, ioc) - { - OutputPath = OutputPath - }; - - var macroManager = new MacroManager(castXml); - - var cppExtensionGenerator = new CppExtensionHeaderGenerator(); - - var module = config.CreateSkeletonModule(); - - macroManager.Parse(Path.Combine(OutputPath, config.HeaderFileName), module); - - cppExtensionGenerator.GenerateExtensionHeaders( - config, OutputPath, module, configsWithExtensionHeaders, cppHeaderGenerationResult.UpdatedConfigs - ); - - GenerateInputsCache( - macroManager.IncludedFiles - .Concat(config.ConfigFilesLoaded.Select(x => x.AbsoluteFilePath)) - .Concat(configsWithExtensionHeaders.Select(x => Path.Combine(OutputPath, x.ExtensionFileName))) - .Select(s => Utilities.FixFilePath(s, Utilities.EmptyFilePathBehavior.Ignore)) - .Where(x => x != null) - .Distinct() - ); - - if (AbortExecution) - return false; - - // Run the parser - var parser = new CppParser(config, ioc) - { - OutputPath = OutputPath - }; - - if (AbortExecution) - return false; - - CppModule group; - - using (var xmlReader = castXml.Process(parser.RootConfigHeaderFileName)) - { - // Run the C++ parser - group = parser.Run(module, xmlReader); - } - - if (AbortExecution) - return false; - - config.ExpandDynamicVariables(SharpGenLogger, group); - - var docLinker = ioc.DocumentationLinker; - var globalNamespace = ioc.GlobalNamespace; - NamingRulesManager namingRules = new(); - - foreach (var nameOverride in GlobalNamespaceOverrides) - { - var wellKnownName = nameOverride.ItemSpec; - var overridenName = nameOverride.GetMetadata("Override"); - - if (string.IsNullOrEmpty(overridenName)) - continue; - - if (Enum.TryParse(wellKnownName, out WellKnownName name)) - { - globalNamespace.OverrideName(name, overridenName); - } - else - { - SharpGenLogger.Warning( - LoggingCodes.InvalidGlobalNamespaceOverride, - "Invalid override of \"{0}\": unknown class name, ignoring the override.", - wellKnownName - ); - } - } - - serviceContainer.AddService( - new GeneratorConfig - { - Platforms = ConfigPlatforms - } - ); - - // Run the main mapping process - TransformManager transformer = new( - namingRules, - new ConstantManager(namingRules, ioc), - ioc - ); - - var (solution, defines) = transformer.Transform(group, config); - - var consumerConfig = new ConfigFile - { - Id = ConsumerBindMappingConfigId, - IncludeProlog = {cppHeaderGenerationResult.Prologue}, - Extension = new List(defines) - }; - - var (bindings, generatedDefines) = transformer.GenerateTypeBindingsForConsumers(); - - consumerConfig.Bindings.AddRange(bindings); - consumerConfig.Extension.AddRange(generatedDefines); - - consumerConfig.Mappings.AddRange( - docLinker.GetAllDocLinks().Select( - link => new MappingRule - { - DocItem = link.cppName, - MappingNameFinal = link.cSharpName - } - ) - ); - - GenerateConfigForConsumers(consumerConfig); - - if (AbortExecution) - return false; - - var documentationCacheItemSpec = DocumentationCache.ItemSpec; - - Utilities.RequireAbsolutePath(documentationCacheItemSpec, nameof(DocumentationCache)); - - var cache = File.Exists(documentationCacheItemSpec) - ? DocItemCache.Read(documentationCacheItemSpec) - : new DocItemCache(); - - DocumentationLogger docLogger = new(SharpGenLogger) {MaxLevel = LogLevel.Warning}; - var docContext = new Lazy(() => new DocumentationContext(docLogger)); - ExtensibilityDriver.Instance.DocumentModule(SharpGenLogger, cache, solution, docContext).Wait(); - - if (docContext.IsValueCreated) - { - Regex[] silencePatterns = null; - var docLogLevelDefault = DocumentationFailuresAsErrors ? LogLevel.Error : LogLevel.Warning; - - foreach (var queryFailure in docContext.Value.Failures) - { - if (silencePatterns == null) - { - silencePatterns = new Regex[SilenceMissingDocumentationErrorIdentifierPatterns.Length]; - for (var i = 0; i < silencePatterns.Length; i++) - silencePatterns[i] = new Regex( - SilenceMissingDocumentationErrorIdentifierPatterns[i].ItemSpec, - RegexOptions.CultureInvariant - ); - } - - if (silencePatterns.Length != 0) - { - bool SilencePredicate(Regex x) => x.Match(queryFailure.Query).Success; - - if (silencePatterns.Any(SilencePredicate)) - continue; - } - - var providerName = queryFailure.FailedProviderName ?? ""; - - var docLogLevel = queryFailure.TreatProviderFailuresAsErrors - ? docLogLevelDefault - : docLogLevelDefault > LogLevel.Warning - ? LogLevel.Warning - : docLogLevelDefault; - - if (queryFailure.Exceptions == null || queryFailure.Exceptions.Count <= 1) - { - SharpGenLogger.LogRawMessage( - docLogLevel, - LoggingCodes.DocumentationProviderInternalError, - "Documentation provider [{0}] query for \"{1}\" failed.", - queryFailure.Exceptions?.FirstOrDefault(), - providerName, - queryFailure.Query - ); - } - else - { - var exceptionsCount = queryFailure.Exceptions.Count; - for (var index = 0; index < exceptionsCount; index++) - { - var exception = queryFailure.Exceptions[index]; - - SharpGenLogger.LogRawMessage( - docLogLevel, - LoggingCodes.DocumentationProviderInternalError, - "Documentation provider [{0}] query for \"{1}\" failed ({2}/{3}).", - exception, - providerName, - queryFailure.Query, - index + 1, - exceptionsCount - ); - } - } - } - } - - cache.WriteIfDirty(documentationCacheItemSpec); - - if (AbortExecution) - return false; - - var documentationFiles = new Dictionary(); - - foreach (var file in ExternalDocumentation) - { - using var stream = File.OpenRead(file.ItemSpec); - - var xml = new XmlDocument(); - xml.Load(stream); - documentationFiles.Add(file.ItemSpec, xml); - } - - if (AbortExecution) - return false; - - serviceContainer.AddService(new ExternalDocCommentsReader(documentationFiles)); - serviceContainer.AddService(new DefaultGenerators(ioc)); - - RoslynGenerator generator = new(); - generator.Run(solution, GeneratedCodeFolder, ioc); - - return !SharpGenLogger.HasErrors; - } - - private PlatformDetectionType ConfigPlatforms - { - get - { - PlatformDetectionType platformMask = 0; - - foreach (var platform in Platforms) - { - if (!Enum.TryParse(platform.ItemSpec, out var parsedPlatform)) - { - SharpGenLogger.Warning( - LoggingCodes.InvalidPlatformDetectionType, - "The platform type {0} is an unknown platform to SharpGenTools. Falling back to Any platform detection.", - platform - ); - platformMask = PlatformDetectionType.Any; - } - else - { - platformMask |= parsedPlatform; - } - } - - return platformMask == 0 ? PlatformDetectionType.Any : platformMask; - } - } - - private void GenerateConfigForConsumers(ConfigFile consumerConfig) - { - if (ConsumerBindMappingConfig == null) return; - - using var consumerBindMapping = File.Create(ConsumerBindMappingConfig.ItemSpec); - - consumerConfig.Write(consumerBindMapping); - } - - private void GenerateInputsCache(IEnumerable paths) - { - using var inputCacheStream = new StreamWriter(InputsCache.ItemSpec, false, DefaultEncoding); - - foreach (var path in paths) inputCacheStream.WriteLine(path); - } - - private ConfigFile LoadConfig(ConfigFile config) - { - config.Load(null, Macros, SharpGenLogger); - - var sdkResolver = new SdkResolver(SharpGenLogger); - SharpGenLogger.Message("Resolving SDKs..."); - foreach (var cfg in config.ConfigFilesLoaded) - { - SharpGenLogger.Message("Resolving SDK for Config {0}", cfg); - foreach (var sdk in cfg.Sdks) - { - SharpGenLogger.Message("Resolving {0}: Version {1}", sdk.Name, sdk.Version); - foreach (var directory in sdkResolver.ResolveIncludeDirsForSdk(sdk)) - { - SharpGenLogger.Message("Resolved include directory {0}", directory); - cfg.IncludeDirs.Add(directory); - } - } - } - - return config; - } -} \ No newline at end of file diff --git a/SharpGenTools.Sdk/Tasks/SharpGenTaskBase.cs b/SharpGenTools.Sdk/Tasks/SharpGenTaskBase.cs deleted file mode 100644 index b2944eb7..00000000 --- a/SharpGenTools.Sdk/Tasks/SharpGenTaskBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Build.Framework; - -namespace SharpGenTools.Sdk.Tasks; - -public abstract class SharpGenTaskBase : SharpTaskBase -{ - // ReSharper disable MemberCanBeProtected.Global, UnusedAutoPropertyAccessor.Global - [Required] public string[] CastXmlArguments { get; set; } - [Required] public ITaskItem CastXmlExecutable { get; set; } - [Required] public ITaskItem[] ConfigFiles { get; set; } - [Required] public string ConsumerBindMappingConfigId { get; set; } - [Required] public ITaskItem DocumentationCache { get; set; } - [Required] public bool DocumentationFailuresAsErrors { get; set; } - [Required] public ITaskItem[] ExtensionAssemblies { get; set; } - [Required] public ITaskItem[] ExternalDocumentation { get; set; } - [Required] public string GeneratedCodeFolder { get; set; } - [Required] public ITaskItem[] GlobalNamespaceOverrides { get; set; } - [Required] public ITaskItem InputsCache { get; set; } - [Required] public string[] Macros { get; set; } - [Required] public string OutputPath { get; set; } - [Required] public ITaskItem[] Platforms { get; set; } - [Required] public ITaskItem[] SilenceMissingDocumentationErrorIdentifierPatterns { get; set; } - public ITaskItem ConsumerBindMappingConfig { get; set; } - // ReSharper restore UnusedAutoPropertyAccessor.Global, MemberCanBeProtected.Global -} \ No newline at end of file diff --git a/SharpGenTools.Sdk/Tasks/SharpPropertyCacheTask.cs b/SharpGenTools.Sdk/Tasks/SharpPropertyCacheTask.cs deleted file mode 100644 index 5f29229a..00000000 --- a/SharpGenTools.Sdk/Tasks/SharpPropertyCacheTask.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using Microsoft.Build.Framework; -using SharpGenTools.Sdk.Internal; - -namespace SharpGenTools.Sdk.Tasks; - -public sealed class SharpPropertyCacheTask : SharpGenTaskBase -{ - [Required] public ITaskItem PropertyCache { get; set; } - - private const int CacheFormatSignature = ('S' << 0) | ('G' << 8) | ('P' << 16) | ('C' << 24); - private const int CacheFormatVersion = 1; - private static readonly Encoding TextEncoding = Encoding.UTF8; - - private static HashAlgorithm CreateSettingsHash() => SHA512.Create(); - - public override bool Execute() - { - PrepareExecute(); - - var cacheItemSpec = PropertyCache.ItemSpec; - Utilities.RequireAbsolutePath(cacheItemSpec, nameof(PropertyCache)); - - var currentHash = HashSettings(); - - bool writeNeeded; - - if (File.Exists(cacheItemSpec)) - { - ReadOnlySpan currentHashSpan = currentHash; - ReadOnlySpan cachedHash = File.ReadAllBytes(cacheItemSpec); - - if (currentHashSpan.SequenceEqual(cachedHash)) - { - SharpGenLogger.Message("Properties hash matches cached value."); - writeNeeded = false; - } - else - { - SharpGenLogger.Message("Properties hash mismatch, writing a new property cache file."); - writeNeeded = true; - } - } - else - { - SharpGenLogger.Message("Properties cache doesn't exist."); - writeNeeded = true; - } - - if (writeNeeded) - { - File.WriteAllBytes(cacheItemSpec, currentHash); - } - - return true; - } - - private byte[] HashSettings() - { - using var stream = new MemoryStream(); - WriteProperties(stream); - - stream.Position = 0; - - using var hash = CreateSettingsHash(); - return hash.ComputeHash(stream); - } - - private void WriteProperties(Stream stream) - { - using var writer = new BinaryWriter(stream, TextEncoding, true); - - writer.Write(CacheFormatSignature); - writer.Write(CacheFormatVersion); - WriteStringArray(CastXmlArguments); - WriteTaskItem(CastXmlExecutable); - WriteTaskItems(ConfigFiles); - WriteString(ConsumerBindMappingConfigId); - WriteTaskItem(DocumentationCache); - WriteBool(DocumentationFailuresAsErrors); - WriteTaskItems(ExtensionAssemblies); - WriteTaskItems(ExternalDocumentation); - WriteString(GeneratedCodeFolder); - WriteTaskItems(GlobalNamespaceOverrides); - WriteTaskItem(InputsCache); - WriteStringArray(Macros); - WriteString(OutputPath); - WriteTaskItems(Platforms); - WriteTaskItems(SilenceMissingDocumentationErrorIdentifierPatterns); - WriteTaskItem(ConsumerBindMappingConfig); - - void WriteString(string s) - { - if (s == null) - { - writer.Write(0); - return; - } - - writer.Write(s); - } - - void WriteStringArray(IReadOnlyList strings) - { - var length = strings.Count; - writer.Write(length); - for (var i = 0; i < length; i++) - { - WriteString(strings[i]); - } - } - - void WriteTaskItem(ITaskItem item) - { - if (item == null) - { - writer.Write(0); - return; - } - - writer.Write(item.ItemSpec); - var metadata = item.CloneCustomMetadata(); - var length = metadata.Count; - writer.Write(length); - } - - void WriteTaskItems(IReadOnlyList items) - { - var length = items.Count; - writer.Write(length); - for (var i = 0; i < length; i++) - { - WriteTaskItem(items[i]); - } - } - - void WriteBool(bool v) => writer.Write(v); - } -} \ No newline at end of file diff --git a/SharpGenTools.Sdk/Tasks/SharpTaskBase.cs b/SharpGenTools.Sdk/Tasks/SharpTaskBase.cs deleted file mode 100644 index f5bc2b3f..00000000 --- a/SharpGenTools.Sdk/Tasks/SharpTaskBase.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Logger = SharpGen.Logging.Logger; - -namespace SharpGenTools.Sdk.Tasks; - -public abstract class SharpTaskBase : Task, ICancelableTask -{ - private volatile bool isCancellationRequested; - - // ReSharper disable MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global - [Required] public bool DebugWaitForDebuggerAttach { get; set; } - // ReSharper restore UnusedAutoPropertyAccessor.Global, MemberCanBePrivate.Global - - protected Logger SharpGenLogger { get; private set; } - - protected bool IsCancellationRequested => isCancellationRequested; - - protected void PrepareExecute() - { - BindingRedirectResolution.Enable(); - - SharpGenLogger = new Logger(new MSBuildSharpGenLogger(Log)); - -#if DEBUG - if (DebugWaitForDebuggerAttach) - WaitForDebuggerAttach(); -#endif - } - - [Conditional("DEBUG")] - private void WaitForDebuggerAttach() - { - if (!Debugger.IsAttached) - { - SharpGenLogger.Warning(null, $"{GetType().Name} is waiting for attach: {Process.GetCurrentProcess().Id}"); - Thread.Yield(); - } - - while (!Debugger.IsAttached && !IsCancellationRequested) - Thread.Sleep(TimeSpan.FromSeconds(1)); - } - - public void Cancel() - { - isCancellationRequested = true; - } -} \ No newline at end of file diff --git a/SharpGenTools.sln b/SharpGenTools.sln index 3ef816a0..55d0e1b7 100644 --- a/SharpGenTools.sln +++ b/SharpGenTools.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen", "SharpGen\SharpGen.csproj", "{31F85A16-CB01-4456-BE3C-76E9FF3A1343}" EndProject @@ -11,9 +10,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.UnitTests", "Sharp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Runtime", "SharpGen.Runtime\SharpGen.Runtime.csproj", "{6302D087-BC5E-4AA0-BA4D-2115590D60E2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGen.Platform", "SharpGen.Platform\SharpGen.Platform.csproj", "{A7FF5742-4C58-487C-ADD1-2FF2382D7D99}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Platform", "SharpGen.Platform\SharpGen.Platform.csproj", "{A7FF5742-4C58-487C-ADD1-2FF2382D7D99}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpGen.Generator", "SharpGen.Generator\SharpGen.Generator.csproj", "{B4D7CA39-8C95-4472-91E6-7D9E51AC2E1F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Generator", "SharpGen.Generator\SharpGen.Generator.csproj", "{B4D7CA39-8C95-4472-91E6-7D9E51AC2E1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Runtime.Trim.Dummy", "SharpGen.Runtime.Trim.Dummy\SharpGen.Runtime.Trim.Dummy.csproj", "{D72FF74B-6FBB-4394-ADA5-E184339F8A80}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trimming", "Trimming", "{14DCB75C-3646-4E74-9B98-5ABF783F0F04}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGen.Runtime.Trim.Dummy.CallbackTest", "SharpGen.Runtime.Trim.Dummy.CallbackTest\SharpGen.Runtime.Trim.Dummy.CallbackTest.csproj", "{BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{A3BC86F0-22E9-426A-BE0E-A2176A405501}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + global.json = global.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F671E5B9-5D3D-44EF-8B6F-F3702FB70DF1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,11 +60,22 @@ Global {B4D7CA39-8C95-4472-91E6-7D9E51AC2E1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4D7CA39-8C95-4472-91E6-7D9E51AC2E1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4D7CA39-8C95-4472-91E6-7D9E51AC2E1F}.Release|Any CPU.Build.0 = Release|Any CPU + {D72FF74B-6FBB-4394-ADA5-E184339F8A80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D72FF74B-6FBB-4394-ADA5-E184339F8A80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D72FF74B-6FBB-4394-ADA5-E184339F8A80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D72FF74B-6FBB-4394-ADA5-E184339F8A80}.Release|Any CPU.Build.0 = Release|Any CPU + {BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {13B246C9-F127-4AC5-8847-E9214C0ABD70} = {F671E5B9-5D3D-44EF-8B6F-F3702FB70DF1} + {D72FF74B-6FBB-4394-ADA5-E184339F8A80} = {14DCB75C-3646-4E74-9B98-5ABF783F0F04} + {BB8F0A30-3FE6-4F26-9E5F-EF9A70198D18} = {14DCB75C-3646-4E74-9B98-5ABF783F0F04} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E022185F-62DA-4472-98A7-DA67215F61B1} diff --git a/artifacts/README.md b/artifacts/README.md new file mode 100644 index 00000000..d935006f --- /dev/null +++ b/artifacts/README.md @@ -0,0 +1,3 @@ +# Artifacts + +Package artifacts goes here \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index f8dae52c..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,120 +0,0 @@ -trigger: - branches: - include: - - master - tags: - include: - - v*.* -pr: -- master - -variables: - buildNumber: $[counter(variables['build.reason'], 1000)] - -stages: -- stage: Build - jobs: - - job: SharpGenTools_Windows - pool: - vmImage: 'windows-2022' - strategy: - matrix: - Debug: - Config: 'Debug' - Release: - Config: 'Release' - variables: - - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE - value: "true" - - name: DOTNET_ROLL_FORWARD - value: "Major" - - name: ReleaseVersion - value: '' - - name: ContinuousIntegrationBuild - value: true - - group: SharpGenTools - steps: - - pwsh: | - if ($env:SourceBranch -match 'refs/tags/v(?.+)') - { - Write-Host "##vso[task.setvariable variable=ReleaseVersion;]$($Matches.Version)" - } - displayName: 'Get Release Name (if this is a tag-triggered build)' - env: - SourceBranch : '$(Build.SourceBranch)' - - pwsh: | - $env:MSBuildEnableWorkloadResolver=$false - dotnet tool restore - displayName: 'Restore .NET tools' - - pwsh: ./build.ps1 -Configuration $(Config) - displayName: 'Build and test' - env: - ReleaseTag: '$(ReleaseVersion)' - BuildNumber: '$(buildNumber)' - - task: PublishTestResults@2 - displayName: 'Publish unit test results' - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - buildConfiguration: '$(Config)' - searchFolder: '$(Build.SourcesDirectory)/artifacts/test-results' - - pwsh: dotnet reportgenerator -reports:"artifacts\coverage\*.xml" -targetdir:"artifacts\reports" -reporttypes:Cobertura -title:SharpGen - condition: eq(variables['Config'], 'Debug') - displayName: 'Merge coverage reports' - - task: PublishCodeCoverageResults@1 - condition: eq(variables['Config'], 'Debug') - displayName: 'Publish code coverage results' - inputs: - summaryFileLocation: '$(Build.SourcesDirectory)/artifacts/reports/*.xml' - codeCoverageTool: Cobertura - - task: CopyFiles@2 - condition: eq(variables['Config'], 'Release') - displayName: 'Copy NuGet packages to Artifact staging directory' - inputs: - contents: 'Sharp*/bin/Release/Sharp*@(.nupkg|.snupkg)' - targetFolder: '$(Build.ArtifactStagingDirectory)' - flattenFolders: true - - task: PublishBuildArtifacts@1 - condition: eq(variables['Config'], 'Release') - displayName: Publish NuGet Packages - inputs: - pathToPublish: '$(Build.ArtifactStagingDirectory)' - artifactName: NuGet Packages -- stage: Deploy - condition: or(eq(variables['Build.SourceBranchName'], 'master'), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) - dependsOn: Build - jobs: - - job: MyGet_Deploy - condition: not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) - pool: - vmImage: 'windows-2022' - steps: - - checkout: none - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - artifactName: 'NuGet Packages' - downloadPath: '$(Build.ArtifactStagingDirectory)' - - task: NuGetCommand@2 - inputs: - command: 'push' - nugetFeedType: 'external' - publishFeedCredentials: 'MyGet' - - job: NuGet_Deploy - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - pool: - vmImage: 'windows-2022' - steps: - - checkout: none - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - artifactName: 'NuGet Packages' - downloadPath: '$(Build.ArtifactStagingDirectory)' - - task: NuGetCommand@2 - inputs: - command: 'push' - nugetFeedType: 'external' - publishFeedCredentials: 'NuGet' diff --git a/build.ps1 b/build.ps1 index f0c7b323..950ed067 100644 --- a/build.ps1 +++ b/build.ps1 @@ -13,7 +13,7 @@ if ($LastExitCode -ne 0) { } Write-Debug "Deploying built packages for COM Runtime build" -if (!(./build/deploy-test-packages -PackedConfiguration $Configuration -Project "SharpGen.Runtime.COM")) { +if (!(./build/deploy-test-packages -PackedConfiguration $Configuration -Project "SharpGen.Runtime.COM/SharpGen.Runtime.COM")) { Write-Error "Failed to deploy packages" exit 1 } @@ -99,7 +99,7 @@ if (!$SkipOuterloopTests -and !($env:ReleaseTag -and ($Configuration -eq "Releas $managedTests = "Interface", "Struct", "Functions" - $tfms = "net5.0" # "net472", "netcoreapp2.1", "net5.0" + $tfms = "net472", "netcoreapp3.1", "net6.0" $platforms = "x86", "x64" $matrix = CartesianProduct-Lists @($tfms, $platforms) diff --git a/build/build-com-runtime.ps1 b/build/build-com-runtime.ps1 index f3a73308..d23aa83f 100644 --- a/build/build-com-runtime.ps1 +++ b/build/build-com-runtime.ps1 @@ -4,15 +4,15 @@ Param( [string[]] $AdditionalParameters ) -Remove-Item -Recurse -Force "$RepoRoot/SharpGen.Runtime.COM/bin/$Configuration" -ErrorAction Ignore -Remove-Item -Recurse -Force "$RepoRoot/SharpGen.Runtime.COM/obj/$Configuration" -ErrorAction Ignore +Remove-Item -Recurse -Force "$RepoRoot/SharpGen.Runtime.COM/SharpGen.Runtime.COM/bin/$Configuration" -ErrorAction Ignore +Remove-Item -Recurse -Force "$RepoRoot/SharpGen.Runtime.COM/SharpGen.Runtime.COM/obj/$Configuration" -ErrorAction Ignore -$ComRuntimeSolution = "$RepoRoot/SharpGen.Runtime.COM/SharpGen.Runtime.COM.sln" +$ComRuntimeProject = "$RepoRoot/SharpGen.Runtime.COM/SharpGen.Runtime.COM/SharpGen.Runtime.COM.csproj" $Parameters = @( "pack", "-bl:$RepoRoot/artifacts/binlog/com-runtime-$Configuration.binlog", "-nr:false", "-c:$Configuration", "-v:m" -) + $AdditionalParameters + @($ComRuntimeSolution) +) + $AdditionalParameters + @($ComRuntimeProject) dotnet $Parameters | Write-Host diff --git a/build/run-outerloop-tests.ps1 b/build/run-outerloop-tests.ps1 index 8b989af2..72bb9fc9 100644 --- a/build/run-outerloop-tests.ps1 +++ b/build/run-outerloop-tests.ps1 @@ -51,7 +51,7 @@ $SdkAssemblyFolder = "$RepoRoot/SdkTests/RestoredPackages/sharpgentools.sdk/$Ver $dotnetExe = $(Get-Command dotnet).Path $dotnetArgs = $($BuildParameters -join ' ') -dotnet coverlet "$SdkAssemblyFolder/net5.0/win/SharpGenTools.Sdk.dll" -t $dotnetExe -a $dotnetArgs -f opencover ` +dotnet coverlet "$SdkAssemblyFolder/net6.0/win/SharpGenTools.Sdk.dll" -t $dotnetExe -a $dotnetArgs -f opencover ` -o "$RepoRoot/artifacts/coverage/outerloop-host-$Hint.xml" --include-test-assembly --include-directory $SdkAssemblyFolder ` | Write-Host diff --git a/docs/documentation.rst b/docs/documentation.rst index 3368fa2f..86b97a5d 100644 --- a/docs/documentation.rst +++ b/docs/documentation.rst @@ -12,12 +12,31 @@ You can supply external documentation though XML files structured as below: .. code-block:: xml - + Summary here + + An example summary for a C++ struct named DML_BUFFER_BINDING. + + + An example summary for a C++ field named Buffer within a struct named DML_BUFFER_BINDING. + -SharpGenTools will automatically include an ```` documentation tag that points to a matching element in an external documentation file. You can specify an external comments file to SharpGen by adding it as a ``SharpGenExternalDocs`` item in your project file. +SharpGenTools will automatically add an ```` documentation tag to matching elements in the generated code. This tag will point to the matching element in an external documentation file. You can specify an external comments file to SharpGen by adding it as a ``SharpGenExternalDocs`` item in your project file. + +An example project file that supports documentation: + +.. code-block:: xml + + + ... + + + + ... + + Doc Providers ================== diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 0dba94f6..7764b615 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -37,7 +37,7 @@ Create a file named ``Mapping.xml`` with the following content: Let's go through each of these elements. * The ``config`` tag: This is the root tag for your configuration file. The ``id`` attribute uniquely identifes your config file during your build. - * The ``assembly`` tag: Identifies what assembl for which you are generating code. + * The ``assembly`` tag: Identifies what assembly for which you are generating code. * The ``namespace`` tag: Identifies the root namespace for code generated from this config file. * The ``depends`` tag: Declares dependencies on other config files. Optional, but can help ensure that all dependencies are correctly loaded. diff --git a/global.json b/global.json index a1727a81..4c63b68d 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,10 @@ { - "msbuild-sdks": { - "MSBuild.Sdk.Extras": "3.0.44", - "Microsoft.Build.CentralPackageVersions" : "2.1.3", - "Microsoft.DotNet.PackageValidation" : "1.0.0-preview.7.21379.12" - } + "sdk": { + "version": "9.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + }, + "msbuild-sdks": { + "MSBuild.Sdk.Extras": "3.0.44" + } } \ No newline at end of file