Skip to content

Commit f278cef

Browse files
authored
Merge pull request #1381 from microsoft/fix1307
Add msbuild item to opt 3rd party libraries into app-local deployments
2 parents ffad479 + 8139270 commit f278cef

File tree

8 files changed

+94
-25
lines changed

8 files changed

+94
-25
lines changed

docfx/docs/3rdPartyMetadata.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# 3rd party metadata
2+
3+
CsWin32 comes with dependencies on Windows metadata for the SDK and WDK, allowing C# programs to generate interop code for Windows applications.
4+
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.
5+
6+
## Constructing metadata for other libraries
7+
8+
Constructing metadata is outside the scope of this document.
9+
However you may find [the win32metadata architecture](https://github.com/microsoft/win32metadata/blob/main/docs/architecture.md) document instructive.
10+
11+
## Hooking metadata into CsWin32
12+
13+
Metadata is fed into CsWin32 through MSBuild items.
14+
15+
Item Type | Purpose
16+
--|--
17+
`ProjectionMetadataWinmd` | Path to the .winmd file.
18+
`ProjectionDocs` | Path to an optional msgpack data file that contains API-level documentation.
19+
`AppLocalAllowedLibraries` | The filename (including extension) of a native library that is allowed to ship in the app directory (as opposed to only %windir%\system32).
20+
21+
## Packaging up metadata
22+
23+
Build a NuGet package with the following layout:
24+
25+
```
26+
buildTransitive\
27+
YourPackageId.props
28+
yournativelib.winmd
29+
runtimes\
30+
win-x86\
31+
yournativelib.dll
32+
win-x64\
33+
yournativelib.dll
34+
win-arm64\
35+
yournativelib.dll
36+
...
37+
```
38+
39+
Your package metadata may want to express a dependency on the Microsoft.Windows.CsWin32 package.
40+
41+
The `YourPackageId.props` file should include the msbuild items above, as appropriate.
42+
For example:
43+
44+
```xml
45+
<Project>
46+
<ItemGroup>
47+
<ProjectionMetadataWinmd Include="$(MSBuildThisFileDirectory)yournativelib.winmd" />
48+
<AppLocalAllowedLibraries Include="yournativelib.dll" />
49+
</ItemGroup>
50+
</Project>
51+
```
52+
53+
## Consuming your package
54+
55+
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.

docfx/docs/toc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
items:
22
- href: features.md
33
- href: getting-started.md
4+
- href: 3rdPartyMetadata.md
5+

src/Microsoft.Windows.CsWin32/Generator.Extern.cs

+1-14
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,6 @@ internal void RequestExternMethod(MethodDefinitionHandle methodDefinitionHandle)
147147
this.volatileCode.GenerateMethod(methodDefinitionHandle, () => this.DeclareExternMethod(methodDefinitionHandle));
148148
}
149149

150-
private static bool IsLibraryAllowedAppLocal(string libraryName)
151-
{
152-
for (int i = 0; i < AppLocalLibraries.Length; i++)
153-
{
154-
if (string.Equals(libraryName, AppLocalLibraries[i], StringComparison.OrdinalIgnoreCase))
155-
{
156-
return true;
157-
}
158-
}
159-
160-
return false;
161-
}
162-
163150
private string GetMethodNamespace(MethodDefinition methodDef) => this.Reader.GetString(this.Reader.GetTypeDefinition(methodDef.GetDeclaringType()).Namespace);
164151

165152
private void DeclareExternMethod(MethodDefinitionHandle methodDefinitionHandle)
@@ -233,7 +220,7 @@ AttributeListSyntax CreateDllImportAttributeList()
233220
if (this.generateDefaultDllImportSearchPathsAttribute)
234221
{
235222
result = result.AddAttributes(
236-
IsLibraryAllowedAppLocal(moduleName)
223+
this.AppLocalLibraries.Contains(moduleName)
237224
? DefaultDllImportSearchPathsAllowAppDirAttribute
238225
: DefaultDllImportSearchPathsAttribute);
239226
}

src/Microsoft.Windows.CsWin32/Generator.Invariants.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,13 @@ public partial class Generator
144144
");
145145

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

152155
// [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
153156
private static readonly AttributeSyntax DefaultDllImportSearchPathsAttribute =

src/Microsoft.Windows.CsWin32/Generator.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,11 @@ static Generator()
7474
/// </summary>
7575
/// <param name="metadataLibraryPath">The path to the winmd metadata to generate APIs from.</param>
7676
/// <param name="docs">The API docs to include in the generated code.</param>
77+
/// <param name="additionalAppLocalLibraries">The library file names (e.g. some.dll) that should be allowed as app-local.</param>
7778
/// <param name="options">Options that influence the result of generation.</param>
7879
/// <param name="compilation">The compilation that the generated code will be added to.</param>
7980
/// <param name="parseOptions">The parse options that will be used for the generated code.</param>
80-
public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null)
81+
public Generator(string metadataLibraryPath, Docs? docs, IEnumerable<string> additionalAppLocalLibraries, GeneratorOptions options, CSharpCompilation? compilation = null, CSharpParseOptions? parseOptions = null)
8182
{
8283
if (options is null)
8384
{
@@ -90,6 +91,9 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
9091

9192
this.ApiDocs = docs;
9293

94+
this.AppLocalLibraries = new(BuiltInAppLocalLibraries, StringComparer.OrdinalIgnoreCase);
95+
this.AppLocalLibraries.UnionWith(additionalAppLocalLibraries);
96+
9397
this.options = options;
9498
this.options.Validate();
9599
this.compilation = compilation;
@@ -291,6 +295,8 @@ internal Generator MainGenerator
291295
/// </summary>
292296
internal Context DefaultContext => new() { AllowMarshaling = this.options.AllowMarshaling };
293297

298+
private HashSet<string> AppLocalLibraries { get; }
299+
294300
private bool WideCharOnly => this.options.WideCharOnly;
295301

296302
private string Namespace => this.MetadataIndex.CommonNamespace;

src/Microsoft.Windows.CsWin32/SourceGenerator.cs

+21-7
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ public class SourceGenerator : ISourceGenerator
152152
'\u200B', // ZERO WIDTH SPACE (U+200B)
153153
};
154154

155+
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
156+
{
157+
AllowTrailingCommas = true,
158+
ReadCommentHandling = JsonCommentHandling.Skip,
159+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
160+
};
161+
155162
/// <inheritdoc/>
156163
public void Initialize(GeneratorInitializationContext context)
157164
{
@@ -173,12 +180,7 @@ public void Execute(GeneratorExecutionContext context)
173180
string optionsJson = nativeMethodsJsonFile.GetText(context.CancellationToken)!.ToString();
174181
try
175182
{
176-
options = JsonSerializer.Deserialize<GeneratorOptions>(optionsJson, new JsonSerializerOptions
177-
{
178-
AllowTrailingCommas = true,
179-
ReadCommentHandling = JsonCommentHandling.Skip,
180-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
181-
});
183+
options = JsonSerializer.Deserialize<GeneratorOptions>(optionsJson, JsonOptions);
182184
}
183185
catch (JsonException ex)
184186
{
@@ -210,8 +212,9 @@ public void Execute(GeneratorExecutionContext context)
210212
context.ReportDiagnostic(Diagnostic.Create(MissingRecommendedReference, location: null, "System.Memory"));
211213
}
212214

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

395+
private static IEnumerable<string> CollectAppLocalAllowedLibraries(GeneratorExecutionContext context)
396+
{
397+
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32AppLocalAllowedLibraries", out string? delimitedAppLocalLibraryPaths) ||
398+
string.IsNullOrWhiteSpace(delimitedAppLocalLibraryPaths))
399+
{
400+
return Array.Empty<string>();
401+
}
402+
403+
return delimitedAppLocalLibraryPaths.Split('|').Select(Path.GetFileName);
404+
}
405+
392406
private static Docs? ParseDocs(GeneratorExecutionContext context)
393407
{
394408
Docs? docs = null;

src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.props

+2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
<!-- Provide the path to the winmds used as input into the analyzer. -->
1515
<CompilerVisibleProperty Include="CsWin32InputMetadataPaths" />
1616
<CompilerVisibleProperty Include="CsWin32InputDocPaths" />
17+
<CompilerVisibleProperty Include="CsWin32AppLocalAllowedLibraries" />
1718
</ItemGroup>
1819

1920
<Target Name="AssembleCsWin32InputPaths" BeforeTargets="GenerateMSBuildEditorConfigFileCore">
2021
<!-- Roslyn only allows source generators to see msbuild properties, to lift msbuild items into semicolon-delimited properties. -->
2122
<PropertyGroup>
2223
<CsWin32InputMetadataPaths>@(ProjectionMetadataWinmd->'%(FullPath)','|')</CsWin32InputMetadataPaths>
2324
<CsWin32InputDocPaths>@(ProjectionDocs->'%(FullPath)','|')</CsWin32InputDocPaths>
25+
<CsWin32AppLocalAllowedLibraries>@(AppLocalAllowedLibraries->'%(FullPath)','|')</CsWin32AppLocalAllowedLibraries>
2426
</PropertyGroup>
2527
</Target>
2628
</Project>

test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ protected SuperGenerator CreateGenerator(GeneratorOptions? options = null, CShar
377377
=> this.CreateSuperGenerator(DefaultMetadataPaths, options, compilation, includeDocs);
378378

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

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

0 commit comments

Comments
 (0)