Skip to content

Commit

Permalink
Merge pull request #1381 from microsoft/fix1307
Browse files Browse the repository at this point in the history
Add msbuild item to opt 3rd party libraries into app-local deployments
  • Loading branch information
AArnott authored Mar 7, 2025
2 parents ffad479 + 8139270 commit f278cef
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 25 deletions.
55 changes: 55 additions & 0 deletions docfx/docs/3rdPartyMetadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 3rd party metadata

CsWin32 comes with dependencies on Windows metadata for the SDK and WDK, allowing C# programs to generate interop code for Windows applications.
But the general transformation from metadata to C# code may be applied to other metadata inputs, allowing you to generate similar metadata for 3rd party native libraries and use CsWin32 to generate C# interop APIs for it.

## Constructing metadata for other libraries

Constructing metadata is outside the scope of this document.
However you may find [the win32metadata architecture](https://github.com/microsoft/win32metadata/blob/main/docs/architecture.md) document instructive.

## Hooking metadata into CsWin32

Metadata is fed into CsWin32 through MSBuild items.

Item Type | Purpose
--|--
`ProjectionMetadataWinmd` | Path to the .winmd file.
`ProjectionDocs` | Path to an optional msgpack data file that contains API-level documentation.
`AppLocalAllowedLibraries` | The filename (including extension) of a native library that is allowed to ship in the app directory (as opposed to only %windir%\system32).

## Packaging up metadata

Build a NuGet package with the following layout:

```
buildTransitive\
YourPackageId.props
yournativelib.winmd
runtimes\
win-x86\
yournativelib.dll
win-x64\
yournativelib.dll
win-arm64\
yournativelib.dll
...
```

Your package metadata may want to express a dependency on the Microsoft.Windows.CsWin32 package.

The `YourPackageId.props` file should include the msbuild items above, as appropriate.
For example:

```xml
<Project>
<ItemGroup>
<ProjectionMetadataWinmd Include="$(MSBuildThisFileDirectory)yournativelib.winmd" />
<AppLocalAllowedLibraries Include="yournativelib.dll" />
</ItemGroup>
</Project>
```

## Consuming your package

A project can reference your NuGet package to get both the native dll deployed with their app and the C# interop APIs generated as they require through NativeMethods.txt using CsWin32, just like they can for Win32 APIs.
2 changes: 2 additions & 0 deletions docfx/docs/toc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
items:
- href: features.md
- href: getting-started.md
- href: 3rdPartyMetadata.md

15 changes: 1 addition & 14 deletions src/Microsoft.Windows.CsWin32/Generator.Extern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,6 @@ internal void RequestExternMethod(MethodDefinitionHandle methodDefinitionHandle)
this.volatileCode.GenerateMethod(methodDefinitionHandle, () => this.DeclareExternMethod(methodDefinitionHandle));
}

private static bool IsLibraryAllowedAppLocal(string libraryName)
{
for (int i = 0; i < AppLocalLibraries.Length; i++)
{
if (string.Equals(libraryName, AppLocalLibraries[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

private string GetMethodNamespace(MethodDefinition methodDef) => this.Reader.GetString(this.Reader.GetTypeDefinition(methodDef.GetDeclaringType()).Namespace);

private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle)
Expand Down Expand Up @@ -233,7 +220,7 @@ AttributeListSyntax CreateDllImportAttributeList()
if (this.generateDefaultDllImportSearchPathsAttribute)
{
result = result.AddAttributes(
IsLibraryAllowedAppLocal(moduleName)
this.AppLocalLibraries.Contains(moduleName)
? DefaultDllImportSearchPathsAllowAppDirAttribute
: DefaultDllImportSearchPathsAttribute);
}
Expand Down
7 changes: 5 additions & 2 deletions src/Microsoft.Windows.CsWin32/Generator.Invariants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,13 @@ public partial class Generator
");

/// <summary>
/// The set of libraries that are expected to be allowed next to an application instead of being required to load from System32.
/// The initial set of libraries that are expected to be allowed next to an application instead of being required to load from System32.
/// </summary>
/// <remarks>
/// This list is combined with an MSBuild item list so that 3rd party metadata can document app-local DLLs.
/// </remarks>
/// <see href="https://docs.microsoft.com/en-us/windows/win32/debug/dbghelp-versions" />
private static readonly string[] AppLocalLibraries = new[] { "DbgHelp.dll", "SymSrv.dll", "SrcSrv.dll" };
private static readonly string[] BuiltInAppLocalLibraries = ["DbgHelp.dll", "SymSrv.dll", "SrcSrv.dll"];

// [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static readonly AttributeSyntax DefaultDllImportSearchPathsAttribute =
Expand Down
8 changes: 7 additions & 1 deletion src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ static Generator()
/// </summary>
/// <param name="metadataLibraryPath">The path to the winmd metadata to generate APIs from.</param>
/// <param name="docs">The API docs to include in the generated code.</param>
/// <param name="additionalAppLocalLibraries">The library file names (e.g. some.dll) that should be allowed as app-local.</param>
/// <param name="options">Options that influence the result of generation.</param>
/// <param name="compilation">The compilation that the generated code will be added to.</param>
/// <param name="parseOptions">The parse options that will be used for the generated code.</param>
public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null)
public Generator(string metadataLibraryPath, Docs? docs, IEnumerable<string> additionalAppLocalLibraries, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null)
{
if (options is null)
{
Expand All @@ -90,6 +91,9 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option

this.ApiDocs = docs;

this.AppLocalLibraries = new(BuiltInAppLocalLibraries, StringComparer.OrdinalIgnoreCase);
this.AppLocalLibraries.UnionWith(additionalAppLocalLibraries);

this.options = options;
this.options.Validate();
this.compilation = compilation;
Expand Down Expand Up @@ -291,6 +295,8 @@ internal Generator MainGenerator
/// </summary>
internal Context DefaultContext => new() { AllowMarshaling = this.options.AllowMarshaling };

private HashSet<string> AppLocalLibraries { get; }

private bool WideCharOnly => this.options.WideCharOnly;

private string Namespace => this.MetadataIndex.CommonNamespace;
Expand Down
28 changes: 21 additions & 7 deletions src/Microsoft.Windows.CsWin32/SourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ public class SourceGenerator : ISourceGenerator
'\u200B', // ZERO WIDTH SPACE (U+200B)
};

private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
Expand All @@ -173,12 +180,7 @@ public void Execute(GeneratorExecutionContext context)
string optionsJson = nativeMethodsJsonFile.GetText(context.CancellationToken)!.ToString();
try
{
options = JsonSerializer.Deserialize<GeneratorOptions>(optionsJson, new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
options = JsonSerializer.Deserialize<GeneratorOptions>(optionsJson, JsonOptions);
}
catch (JsonException ex)
{
Expand Down Expand Up @@ -210,8 +212,9 @@ public void Execute(GeneratorExecutionContext context)
context.ReportDiagnostic(Diagnostic.Create(MissingRecommendedReference, location: null, "System.Memory"));
}

IEnumerable<string> appLocalLibraries = CollectAppLocalAllowedLibraries(context);
Docs? docs = ParseDocs(context);
Generator[] generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)).ToArray();
Generator[] generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, appLocalLibraries, options, compilation, parseOptions)).ToArray();
if (TryFindNonUniqueValue(generators, g => g.InputAssemblyName, StringComparer.OrdinalIgnoreCase, out (Generator Item, string Value) nonUniqueGenerator))
{
context.ReportDiagnostic(Diagnostic.Create(NonUniqueMetadataInputs, null, nonUniqueGenerator.Value));
Expand Down Expand Up @@ -389,6 +392,17 @@ private static IReadOnlyList<string> CollectMetadataPaths(GeneratorExecutionCont
return metadataBasePaths;
}

private static IEnumerable<string> CollectAppLocalAllowedLibraries(GeneratorExecutionContext context)
{
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32AppLocalAllowedLibraries", out string? delimitedAppLocalLibraryPaths) ||
string.IsNullOrWhiteSpace(delimitedAppLocalLibraryPaths))
{
return Array.Empty<string>();
}

return delimitedAppLocalLibraryPaths.Split('|').Select(Path.GetFileName);
}

private static Docs? ParseDocs(GeneratorExecutionContext context)
{
Docs? docs = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
<!-- Provide the path to the winmds used as input into the analyzer. -->
<CompilerVisibleProperty Include="CsWin32InputMetadataPaths" />
<CompilerVisibleProperty Include="CsWin32InputDocPaths" />
<CompilerVisibleProperty Include="CsWin32AppLocalAllowedLibraries" />
</ItemGroup>

<Target Name="AssembleCsWin32InputPaths" BeforeTargets="GenerateMSBuildEditorConfigFileCore">
<!-- Roslyn only allows source generators to see msbuild properties, to lift msbuild items into semicolon-delimited properties. -->
<PropertyGroup>
<CsWin32InputMetadataPaths>@(ProjectionMetadataWinmd->'%(FullPath)','|')</CsWin32InputMetadataPaths>
<CsWin32InputDocPaths>@(ProjectionDocs->'%(FullPath)','|')</CsWin32InputDocPaths>
<CsWin32AppLocalAllowedLibraries>@(AppLocalAllowedLibraries->'%(FullPath)','|')</CsWin32AppLocalAllowedLibraries>
</PropertyGroup>
</Target>
</Project>
2 changes: 1 addition & 1 deletion test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ protected SuperGenerator CreateGenerator(GeneratorOptions? options = null, CShar
=> this.CreateSuperGenerator(DefaultMetadataPaths, options, compilation, includeDocs);

protected SuperGenerator CreateSuperGenerator(string[] metadataPaths, GeneratorOptions? options = null, CSharpCompilation? compilation = null, bool includeDocs = false) =>
SuperGenerator.Combine(metadataPaths.Select(path => new Generator(path, includeDocs ? Docs.Get(ApiDocsPath) : null, options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions)));
SuperGenerator.Combine(metadataPaths.Select(path => new Generator(path, includeDocs ? Docs.Get(ApiDocsPath) : null, [], options ?? DefaultTestGeneratorOptions, compilation ?? this.compilation, this.parseOptions)));

private static ImmutableArray<Diagnostic> FilterDiagnostics(ImmutableArray<Diagnostic> diagnostics) => diagnostics.Where(d => d.Severity > DiagnosticSeverity.Hidden && d.Descriptor.Id != "CS1701").ToImmutableArray();

Expand Down

0 comments on commit f278cef

Please sign in to comment.