Skip to content

Commit 5626020

Browse files
Preserve Win32 resources in aggregate executable shims (#128971)
## Summary Adds support for carrying managed assembly Win32 resources into NativeAOT aggregate executable shim binaries on Windows. ## Changes - Adds `DumpNativeResources` to convert PE Win32 resources into `.res` files (restored from 4e48d2d that deleted it) - Links generated resource files into Windows aggregate executable shims. - Includes shim resource files in incremental build inputs. - Adds an aggregate executable test validating shim execution and resource preservation. - De-duplicates the Win32 resource test logic and `test.res` asset. ## Validation - Shared Win32 resource sources compile successfully in temp projects. - `Win32Resources.csproj` builds successfully. - Targeted NativeAOT aggregate build is blocked by missing local ILCompiler targets in this fresh checkout, matching the pre-existing baseline failure. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
1 parent ea282a9 commit 5626020

6 files changed

Lines changed: 303 additions & 16 deletions

File tree

src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Windows.targets

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ The .NET Foundation licenses this file to you under the MIT license.
149149
<Target Name="CreateShimExes"
150150
Condition="'$(_AggregateExecutable)' == 'true'"
151151
DependsOnTargets="LinkNative;WriteAggregateExecutableShimRspFiles"
152-
Inputs="$(NativeImportLibrary);@(_AggregateExecutableReferencePath->'%(ShimSource)');@(_AggregateExecutableReferencePath->'%(ShimCompileRspFile)');@(_AggregateExecutableReferencePath->'%(ShimLinkRspFile)')"
152+
Inputs="$(NativeImportLibrary);@(_AggregateExecutableReferencePath->'%(ShimSource)');@(_AggregateExecutableReferencePath->'%(ShimCompileRspFile)');@(_AggregateExecutableReferencePath->'%(ShimLinkRspFile)');@(_AggregateExecutableReferencePath->'%(ShimResourceFile)')"
153153
Outputs="@(_AggregateExecutableShim);@(_AggregateExecutableShimSymbol)">
154154

155155
<MakeDir Directories="$(NativeIntermediateOutputPath);$(NativeOutputPath)" />
@@ -159,7 +159,7 @@ The .NET Foundation licenses this file to you under the MIT license.
159159

160160
<Target Name="WriteAggregateExecutableShimRspFiles"
161161
Condition="'$(_AggregateExecutable)' == 'true'"
162-
DependsOnTargets="LinkNative">
162+
DependsOnTargets="LinkNative;GenerateAggregateExecutableShimResFiles">
163163

164164
<ItemGroup>
165165
<_AggregateExecutableReferencePath>
@@ -171,6 +171,7 @@ int wmain(int argc, unsigned short** argv)
171171
}
172172
]]></ShimSourceCode>
173173
<ShimPdbLinkerArg Condition="'$(NativeDebugSymbols)' == 'true'">/PDB:"%(_AggregateExecutableReferencePath.ShimSymbol)"</ShimPdbLinkerArg>
174+
<ShimResourceLinkerArg>"%(_AggregateExecutableReferencePath.ShimResourceFile)"</ShimResourceLinkerArg>
174175
</_AggregateExecutableReferencePath>
175176
</ItemGroup>
176177

@@ -195,6 +196,18 @@ int wmain(int argc, unsigned short** argv)
195196
<MakeDir Directories="$(NativeIntermediateOutputPath);$(NativeOutputPath)" />
196197
<WriteLinesToFile File="%(_AggregateExecutableReferencePath.ShimSource)" Lines="$([MSBuild]::Escape('%(_AggregateExecutableReferencePath.ShimSourceCode)'))" Overwrite="true" Encoding="utf-8" WriteOnlyWhenDifferent="true" />
197198
<WriteLinesToFile File="%(_AggregateExecutableReferencePath.ShimCompileRspFile)" Lines="@(_AggregateExecutableShimCompilerArg);&quot;%(_AggregateExecutableReferencePath.ShimSource)&quot;;/Fo&quot;%(_AggregateExecutableReferencePath.ShimObject)&quot;" Overwrite="true" Encoding="utf-8" WriteOnlyWhenDifferent="true" />
198-
<WriteLinesToFile File="%(_AggregateExecutableReferencePath.ShimLinkRspFile)" Lines="&quot;%(_AggregateExecutableReferencePath.ShimObject)&quot;;&quot;$(NativeImportLibrary)&quot;;/OUT:&quot;%(_AggregateExecutableReferencePath.ShimBinary)&quot;;/SUBSYSTEM:%(_AggregateExecutableReferencePath.ShimLinkerSubsystem);%(_AggregateExecutableReferencePath.ShimPdbLinkerArg);@(_AggregateExecutableShimLinkerArg)" Overwrite="true" Encoding="utf-8" WriteOnlyWhenDifferent="true" />
199+
<WriteLinesToFile File="%(_AggregateExecutableReferencePath.ShimLinkRspFile)" Lines="&quot;%(_AggregateExecutableReferencePath.ShimObject)&quot;;&quot;$(NativeImportLibrary)&quot;;%(_AggregateExecutableReferencePath.ShimResourceLinkerArg);/OUT:&quot;%(_AggregateExecutableReferencePath.ShimBinary)&quot;;/SUBSYSTEM:%(_AggregateExecutableReferencePath.ShimLinkerSubsystem);%(_AggregateExecutableReferencePath.ShimPdbLinkerArg);@(_AggregateExecutableShimLinkerArg)" Overwrite="true" Encoding="utf-8" WriteOnlyWhenDifferent="true" />
200+
</Target>
201+
202+
<UsingTask TaskName="DumpNativeResources" AssemblyFile="$(IlcBuildTasksPath)" />
203+
204+
<Target Name="GenerateAggregateExecutableShimResFiles"
205+
Condition="'$(_AggregateExecutable)' == 'true'"
206+
Inputs="@(_AggregateExecutableReferencePath)"
207+
Outputs="@(_AggregateExecutableReferencePath->'%(ShimResourceFile)')">
208+
209+
<DumpNativeResources
210+
MainAssembly="%(_AggregateExecutableReferencePath.Identity)"
211+
ResourceFile="%(_AggregateExecutableReferencePath.ShimResourceFile)" />
199212
</Target>
200213
</Project>

src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ The .NET Foundation licenses this file to you under the MIT license.
192192
<ShimObject>$(NativeIntermediateOutputPath)%(AssemblyName).aggregateexe$(NativeObjectExt)</ShimObject>
193193
<ShimCompileRspFile>$(NativeIntermediateOutputPath)%(AssemblyName).aggregateexe.cl.rsp</ShimCompileRspFile>
194194
<ShimLinkRspFile>$(NativeIntermediateOutputPath)%(AssemblyName).aggregateexe.link.rsp</ShimLinkRspFile>
195+
<ShimResourceFile>$(NativeIntermediateOutputPath)%(AssemblyName).aggregateexe.res</ShimResourceFile>
195196
<ShimCompileLinkRspFile>$(NativeIntermediateOutputPath)%(AssemblyName).aggregateexe.rsp</ShimCompileLinkRspFile>
196197
<ShimBinary>$(NativeOutputPath)%(AssemblyName)$(NativeExecutableExt)</ShimBinary>
197198
<ShimSymbol>$(NativeOutputPath)%(AssemblyName)$(NativeSymbolExt)</ShimSymbol>
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Reflection.Metadata;
7+
using System.Reflection.PortableExecutable;
8+
using System.Text;
9+
10+
using Microsoft.Build.Framework;
11+
using Microsoft.Build.Utilities;
12+
13+
namespace Build.Tasks
14+
{
15+
/// <summary>
16+
/// Dumps native Win32 resources in the given assembly into a specified *.res file.
17+
/// </summary>
18+
public class DumpNativeResources : Task
19+
{
20+
/// <summary>
21+
/// File name of the assembly with Win32 resources to be dumped.
22+
/// </summary>
23+
[Required]
24+
public string MainAssembly
25+
{
26+
get;
27+
set;
28+
}
29+
30+
/// <summary>
31+
/// File name into which to dump the Win32 resources.
32+
/// </summary>
33+
[Required]
34+
public string ResourceFile
35+
{
36+
get;
37+
set;
38+
}
39+
40+
public override bool Execute()
41+
{
42+
using (FileStream fs = File.OpenRead(MainAssembly))
43+
using (PEReader peFile = new PEReader(fs))
44+
{
45+
DirectoryEntry resourceDirectory = peFile.PEHeaders.PEHeader.ResourceTableDirectory;
46+
if (resourceDirectory.Size != 0
47+
&& peFile.PEHeaders.TryGetDirectoryOffset(resourceDirectory, out int rsrcOffset))
48+
{
49+
using (var bw = new BinaryWriter(File.Create(ResourceFile)))
50+
{
51+
ResWriter.WriteResources(peFile, rsrcOffset, resourceDirectory.Size, bw);
52+
}
53+
}
54+
else
55+
{
56+
using (var bw = new BinaryWriter(File.Create(ResourceFile)))
57+
{
58+
ResWriter.WriteEmptyResourceFile(bw);
59+
}
60+
}
61+
}
62+
63+
return true;
64+
}
65+
}
66+
67+
/// <summary>
68+
/// Helper class that converts from a Win32 PE resource directory format to
69+
/// a set of RESOURCEHEADER structures (https://docs.microsoft.com/en-us/windows/desktop/menurc/resourceheader)
70+
/// that form the basis of Win32 RES files.
71+
/// </summary>
72+
internal sealed class ResWriter
73+
{
74+
private readonly PEMemoryBlock _memoryBlock;
75+
private readonly PEReader _peReader;
76+
private readonly int _rsrcOffset;
77+
private readonly int _rsrcSize;
78+
private readonly BinaryWriter _bw;
79+
80+
private object _typeIdOrName;
81+
private object _resourceIdOrName;
82+
private int _languageId;
83+
84+
private ResWriter(PEMemoryBlock memoryBlock, PEReader peReader, int rsrcOffset, int rsrcSize, BinaryWriter bw)
85+
{
86+
_memoryBlock = memoryBlock;
87+
_peReader = peReader;
88+
_rsrcOffset = rsrcOffset;
89+
_rsrcSize = rsrcSize;
90+
_bw = bw;
91+
}
92+
93+
public static void WriteResources(PEReader reader, int rsrcOffset, int rsrcSize, BinaryWriter bw)
94+
{
95+
var rw = new ResWriter(reader.GetEntireImage(), reader, rsrcOffset, rsrcSize, bw);
96+
97+
WriteEmptyResourceFile(bw);
98+
99+
rw.DumpDirectory(reader.GetEntireImage().GetReader(rsrcOffset, rsrcSize), 0);
100+
}
101+
102+
public static void WriteEmptyResourceFile(BinaryWriter bw)
103+
{
104+
// First entry is a null resource entry
105+
bw.Write(new byte[] {
106+
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
107+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
108+
});
109+
}
110+
111+
private void DumpDirectory(BlobReader br, int level)
112+
{
113+
// Skip characteristics
114+
br.ReadInt32();
115+
116+
// Skip major/minor version
117+
br.ReadInt32();
118+
119+
// Skip time/date stamp
120+
br.ReadInt32();
121+
122+
ushort numNamed = br.ReadUInt16();
123+
ushort numId = br.ReadUInt16();
124+
125+
for (int i = 0; i < numNamed + numId; i++)
126+
{
127+
int nameOffsetOrId = br.ReadInt32();
128+
uint entryTableOrSubdirectoryOffset = br.ReadUInt32();
129+
130+
if (i < numNamed)
131+
{
132+
nameOffsetOrId &= 0x7FFFFFFF;
133+
BlobReader nameReader = _memoryBlock.GetReader(_rsrcOffset + nameOffsetOrId, _rsrcSize - nameOffsetOrId);
134+
ushort nameLength = nameReader.ReadUInt16();
135+
StringBuilder sb = new StringBuilder(nameLength);
136+
for (int charIndex = 0; charIndex < nameLength; charIndex++)
137+
sb.Append((char)nameReader.ReadUInt16());
138+
string name = sb.ToString();
139+
140+
if (level == 0)
141+
{
142+
_typeIdOrName = name;
143+
}
144+
else if (level == 1)
145+
{
146+
_resourceIdOrName = name;
147+
}
148+
else
149+
throw new BadImageFormatException();
150+
}
151+
else
152+
{
153+
if (level == 0)
154+
{
155+
_typeIdOrName = nameOffsetOrId;
156+
}
157+
else if (level == 1)
158+
{
159+
_resourceIdOrName = nameOffsetOrId;
160+
}
161+
else if (level == 2)
162+
{
163+
_languageId = nameOffsetOrId;
164+
}
165+
else
166+
throw new BadImageFormatException();
167+
}
168+
169+
if (level < 2)
170+
{
171+
if ((entryTableOrSubdirectoryOffset & 0x80000000) == 0)
172+
throw new BadImageFormatException();
173+
entryTableOrSubdirectoryOffset &= 0x7FFFFFFF;
174+
DumpDirectory(_memoryBlock.GetReader(_rsrcOffset + (int)entryTableOrSubdirectoryOffset, _rsrcSize - (int)entryTableOrSubdirectoryOffset), level + 1);
175+
}
176+
else
177+
{
178+
DumpEntry(_memoryBlock.GetReader(_rsrcOffset + (int)entryTableOrSubdirectoryOffset, _rsrcSize - (int)entryTableOrSubdirectoryOffset));
179+
}
180+
}
181+
}
182+
183+
private void DumpEntry(BlobReader br)
184+
{
185+
int dataRva = br.ReadInt32();
186+
int size = br.ReadInt32();
187+
188+
// Skip codepage
189+
br.ReadInt32();
190+
191+
// Skip reserved
192+
br.ReadInt32();
193+
194+
var ms = new MemoryStream();
195+
var hdr = new BinaryWriter(ms, System.Text.Encoding.Unicode);
196+
197+
hdr.Write(size); // DataSize
198+
hdr.Write(0); // HeaderSize
199+
hdr.WriteNameOrId(_typeIdOrName); // TYPE
200+
hdr.WriteNameOrId(_resourceIdOrName); // NAME
201+
202+
// round up to "DWORD" offset
203+
long curHeaderSize = hdr.BaseStream.Position;
204+
while (curHeaderSize % 4 != 0)
205+
{
206+
hdr.Write((byte)0);
207+
curHeaderSize++;
208+
}
209+
210+
hdr.Write(0); // DataVersion
211+
hdr.Write((short)0); // MemoryFlags
212+
hdr.Write((ushort)_languageId); // LanguageId
213+
hdr.Write(0); // Version
214+
hdr.Write(0); // Characteristics
215+
216+
// Patch up HeaderSize
217+
var headerSize = hdr.Seek(0, SeekOrigin.Current);
218+
hdr.Seek(4, SeekOrigin.Begin);
219+
hdr.Write((int)headerSize);
220+
221+
var hdrData = ms.ToArray();
222+
223+
_bw.Write(hdrData);
224+
_bw.Write(_peReader.GetSectionData(dataRva).GetReader().ReadBytes(size));
225+
226+
// Make sure we are DWORD aligned
227+
var totalSize = hdrData.Length + size;
228+
while (totalSize % 4 != 0)
229+
{
230+
_bw.Write((byte)0);
231+
totalSize++;
232+
}
233+
234+
}
235+
}
236+
237+
internal static class ResourceHelper
238+
{
239+
public static void WriteNameOrId(this BinaryWriter bw, object nameOrId)
240+
{
241+
if (nameOrId is string s)
242+
{
243+
// String
244+
bw.Write(s.ToCharArray());
245+
bw.Write((short)0);
246+
}
247+
else
248+
{
249+
// Integer
250+
bw.Write((short)-1);
251+
bw.Write((short)(int)nameOrId);
252+
}
253+
}
254+
}
255+
}

src/tests/nativeaot/AggregateExecutableLibrary/HelloExe/HelloExe.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@
33

44
using System;
55

6+
if (OperatingSystem.IsWindows())
7+
{
8+
Program.ValidateWin32Resources();
9+
}
10+
611
Console.WriteLine("Hello from HelloExe");

src/tests/nativeaot/AggregateExecutableLibrary/HelloExe/HelloExe.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
<PropertyGroup>
33
<OutputType>Exe</OutputType>
44
<CLRTestKind>SharedLibrary</CLRTestKind>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
6+
<DefineConstants>$(DefineConstants);EXCLUDE_WIN32RESOURCES_MAIN</DefineConstants>
7+
<Win32Resource Condition="'$(TargetsWindows)' == 'true'">..\..\SmokeTests\Win32Resources\test.res</Win32Resource>
58
</PropertyGroup>
69
<ItemGroup>
710
<Compile Include="HelloExe.cs" />
11+
<Compile Include="..\..\SmokeTests\Win32Resources\Win32Resources.cs" Link="Win32Resources.cs" />
812
</ItemGroup>
913
</Project>

src/tests/nativeaot/SmokeTests/Win32Resources/Win32Resources.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@
44
using System;
55
using System.Runtime.InteropServices;
66

7-
unsafe
7+
unsafe partial class Program
88
{
9-
nint lib = 0;
9+
#if !EXCLUDE_WIN32RESOURCES_MAIN
10+
private static int Main()
11+
{
12+
ValidateWin32Resources();
13+
return 100;
14+
}
15+
#endif
1016

11-
if (GetIntValueFromResource(lib, (ushort*)(nuint)(ushort)10, 0x041B) != 3)
12-
throw new Exception();
17+
public static void ValidateWin32Resources()
18+
{
19+
nint lib = 0;
1320

14-
ReadOnlySpan<char> resName = "funny";
15-
fixed (char* pResName = resName)
16-
if (GetIntValueFromResource(lib, (ushort*)pResName, 0x041B) != 1)
21+
if (GetIntValueFromResource(lib, (ushort*)(nuint)(ushort)10, 0x041B) != 3)
1722
throw new Exception();
1823

19-
return 100;
24+
ReadOnlySpan<char> resName = "funny";
25+
fixed (char* pResName = resName)
26+
if (GetIntValueFromResource(lib, (ushort*)pResName, 0x041B) != 1)
27+
throw new Exception();
28+
}
2029

21-
static int GetIntValueFromResource(nint hModule, ushort* lpName, ushort wLanguage)
30+
private static int GetIntValueFromResource(nint hModule, ushort* lpName, ushort wLanguage)
2231
{
2332
ushort* RT_RCDATA = (ushort*)(nuint)(ushort)10;
2433

@@ -36,14 +45,14 @@ static int GetIntValueFromResource(nint hModule, ushort* lpName, ushort wLanguag
3645
}
3746

3847
[DllImport("kernel32")]
39-
static extern nint FindResourceExW(nint hModule, ushort* lpType, ushort* lpName, ushort wLanguage);
48+
private static extern nint FindResourceExW(nint hModule, ushort* lpType, ushort* lpName, ushort wLanguage);
4049

4150
[DllImport("kernel32")]
42-
static extern nint LoadResource(nint hModule, nint hResInfo);
51+
private static extern nint LoadResource(nint hModule, nint hResInfo);
4352

4453
[DllImport("kernel32")]
45-
static extern void* LockResource(nint hResData);
54+
private static extern void* LockResource(nint hResData);
4655

4756
[DllImport("kernel32")]
48-
static extern uint SizeofResource(nint hModule, nint hResInfo);
57+
private static extern uint SizeofResource(nint hModule, nint hResInfo);
4958
}

0 commit comments

Comments
 (0)