-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add NativeAOT entry point for the dotnet CLI #54002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,595
−5
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7c68f3f
Add NativeAOT entry point for the dotnet CLI
JeremyKuhne f3bacfc
Conditionalize launcher project on tool availability.
JeremyKuhne c0480f7
Address feedback
JeremyKuhne 0bde24d
Fix the pack error
JeremyKuhne f4d89ef
Remove the debugger detection code
JeremyKuhne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Generated by PublishAotForDebug MSBuild target with machine-specific paths | ||
| Properties/launchSettings.json | ||
| debug-dn.cmd | ||
| dn-native-debug.vcxproj.user | ||
| dn.csproj.Backup.tmp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Microsoft.DotNet.Cli; | ||
|
|
||
| partial class Program | ||
| { | ||
| [LibraryImport("dotnet-aot", EntryPoint = "dotnet_execute")] | ||
| private static partial int DotnetExecute( | ||
| nint hostPath, | ||
| nint dotnetRoot, | ||
| nint sdkDir, | ||
| nint hostfxrPath, | ||
| int argc, | ||
| nint argv); | ||
|
|
||
| static unsafe int Main(string[] args) | ||
| { | ||
| string hostPath = Environment.ProcessPath!; | ||
| string baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar); | ||
| string dotnetRoot = ResolveDotnetRoot(); | ||
| string sdkDir = baseDir; | ||
| string hostfxrPath = ResolveHostfxrPath(dotnetRoot); | ||
|
|
||
| // Marshal argv to native platform strings (UTF-16 on Windows, UTF-8 on Unix) | ||
| // to match hostfxr's char_t definition used by PlatformStringMarshaller | ||
| // in dotnet-aot.dll. | ||
| nint* nativeArgv = stackalloc nint[args.Length]; | ||
| try | ||
| { | ||
| for (int i = 0; i < args.Length; i++) | ||
| { | ||
| nativeArgv[i] = MarshalStringToNative(args[i]); | ||
| } | ||
|
|
||
| nint hpNative = MarshalStringToNative(hostPath); | ||
| nint drNative = MarshalStringToNative(dotnetRoot); | ||
| nint sdNative = MarshalStringToNative(sdkDir); | ||
| nint hfNative = MarshalStringToNative(hostfxrPath); | ||
|
|
||
| try | ||
| { | ||
| return DotnetExecute( | ||
| hpNative, | ||
| drNative, | ||
| sdNative, | ||
| hfNative, | ||
| args.Length, | ||
| (nint)nativeArgv); | ||
| } | ||
| finally | ||
| { | ||
| Marshal.FreeCoTaskMem(hpNative); | ||
| Marshal.FreeCoTaskMem(drNative); | ||
| Marshal.FreeCoTaskMem(sdNative); | ||
| Marshal.FreeCoTaskMem(hfNative); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| for (int i = 0; i < args.Length; i++) | ||
| { | ||
| if (nativeArgv[i] != 0) | ||
| { | ||
| Marshal.FreeCoTaskMem(nativeArgv[i]); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the .NET installation root directory, mimicking muxer behavior. | ||
| /// </summary> | ||
| private static string ResolveDotnetRoot() | ||
| { | ||
| // Check DOTNET_ROOT first (standard on all platforms) | ||
| string? dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); | ||
| if (!string.IsNullOrEmpty(dotnetRoot) && Directory.Exists(dotnetRoot)) | ||
| { | ||
| return dotnetRoot; | ||
| } | ||
|
|
||
| // On Windows, also check the architecture-specific variant | ||
| if (OperatingSystem.IsWindows()) | ||
| { | ||
| string archVar = RuntimeInformation.ProcessArchitecture switch | ||
| { | ||
| Architecture.X64 => "DOTNET_ROOT(x64)", | ||
| Architecture.X86 => "DOTNET_ROOT(x86)", | ||
| Architecture.Arm64 => "DOTNET_ROOT(ARM64)", | ||
| _ => "" | ||
| }; | ||
|
|
||
| if (!string.IsNullOrEmpty(archVar)) | ||
| { | ||
| dotnetRoot = Environment.GetEnvironmentVariable(archVar); | ||
| if (!string.IsNullOrEmpty(dotnetRoot) && Directory.Exists(dotnetRoot)) | ||
| { | ||
| return dotnetRoot; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Fall back to resolving from the process path | ||
| string? processPath = Environment.ProcessPath; | ||
| if (processPath is not null) | ||
| { | ||
| string? processDir = Path.GetDirectoryName(processPath); | ||
| if (processDir is not null) | ||
| { | ||
| // Walk up looking for a directory with dotnet(.exe) | ||
| string? candidate = processDir; | ||
| while (candidate is not null) | ||
| { | ||
| if (File.Exists(Path.Combine(candidate, "dotnet" + (OperatingSystem.IsWindows() ? ".exe" : "")))) | ||
| { | ||
| return candidate; | ||
| } | ||
| candidate = Path.GetDirectoryName(candidate); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Last resort: assume relative to AppContext.BaseDirectory | ||
| return Path.GetDirectoryName(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)) ?? AppContext.BaseDirectory; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Finds the hostfxr library path under the given .NET root. | ||
| /// </summary> | ||
| private static string ResolveHostfxrPath(string dotnetRoot) | ||
| { | ||
| string fxrDir = Path.Combine(dotnetRoot, "host", "fxr"); | ||
| if (!Directory.Exists(fxrDir)) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Pick the highest version directory by parsing version numbers | ||
| string? latestFxr = Directory.GetDirectories(fxrDir) | ||
| .Select(path => new | ||
| { | ||
| Path = path, | ||
| Version = Version.TryParse(Path.GetFileName(path), out Version? version) ? version : null | ||
| }) | ||
| .Where(candidate => candidate.Version is not null) | ||
| .OrderByDescending(candidate => candidate.Version) | ||
| .Select(candidate => candidate.Path) | ||
| .FirstOrDefault(); | ||
|
|
||
| if (latestFxr is null) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| string hostfxrName = OperatingSystem.IsWindows() | ||
| ? "hostfxr.dll" | ||
| : OperatingSystem.IsMacOS() | ||
| ? "libhostfxr.dylib" | ||
| : "libhostfxr.so"; | ||
|
|
||
| string hostfxrPath = Path.Combine(latestFxr, hostfxrName); | ||
| return File.Exists(hostfxrPath) ? hostfxrPath : string.Empty; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Marshals a string to a native platform string (UTF-16 on Windows, UTF-8 on Unix) | ||
| /// to match hostfxr's char_t definition. | ||
| /// </summary> | ||
| private static nint MarshalStringToNative(string value) | ||
| { | ||
| return OperatingSystem.IsWindows() | ||
| ? Marshal.StringToCoTaskMemUni(value) | ||
| : Marshal.StringToCoTaskMemUTF8(value); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <!-- | ||
| Makefile stub project for debugging NativeAOT dn.exe under Visual Studio's | ||
| native debugger. This project has NO C++ source files and performs NO C++ | ||
| compilation. It exists solely to provide an F5 launch target that uses the | ||
| native debugger engine (WindowsLocalDebugger), which correctly binds C# | ||
| breakpoints in AOT-compiled code via the native PDB. | ||
|
|
||
| WHY A SEPARATE PROJECT IS NEEDED: | ||
| The .NET project system (dn.csproj) uses launchSettings.json profiles, which | ||
| always select the managed debugger as the primary engine. There is no | ||
| "commandName" value in launchSettings.json that selects native-only debugging. | ||
| The "nativeDebugging" flag enables mixed-mode (managed primary + native addon), | ||
| but the managed engine cannot bind C# breakpoints in code that has no IL | ||
| (NativeAOT output). Only the native debugger can map C# source lines to native | ||
| addresses via the PDB generated by the ILC compiler. | ||
|
|
||
| The C++ project system honors DebuggerFlavor=WindowsLocalDebugger which uses | ||
| the native debugger as primary, allowing C# breakpoints in AOT code to bind. | ||
|
|
||
| ALTERNATIVES CONSIDERED: | ||
| - launchSettings.json profile: No commandName selects native-only debugging | ||
| - .csproj.user DebuggerFlavor: Ignored by .NET project system | ||
| - devenv /debugexe: Opens new VS instance, loses project context | ||
| - launch.vs.json: Only works in "Open Folder" mode, not Solution F5 | ||
|
|
||
| USAGE: | ||
| 1. Open cli.slnf (or sdk.slnx) in Visual Studio | ||
| 2. Set "dn-native-debug" as the startup project | ||
| 3. Set breakpoints in AOT source files (NativeEntryPoint.cs, ManagedHost.cs, etc.) | ||
| 4. Press F5 | ||
|
|
||
| For debugging the managed Layer 3 code (dotnet.dll after CLR loads), use the | ||
| dn.csproj project with its launchSettings.json profile instead. | ||
| --> | ||
| <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
|
|
||
| <!-- | ||
| This project requires the Visual Studio C++ workload (VCTargetsPath) and only | ||
| works on Windows. When built outside VS or on non-Windows platforms (e.g., CI), | ||
| it becomes a no-op. | ||
| --> | ||
| <PropertyGroup> | ||
| <_HasVCTargets Condition="'$(VCTargetsPath)' != '' and Exists('$(VCTargetsPath)\Microsoft.Cpp.Default.props')">true</_HasVCTargets> | ||
| </PropertyGroup> | ||
|
|
||
| <!-- | ||
| Early exit: define empty Build/Clean/Rebuild/Restore targets when C++ tooling | ||
| is unavailable. Also define NuGet restore stubs so that solution-level restore | ||
| (which calls _IsProjectRestoreSupported on every project) recognizes this project | ||
| as valid rather than emitting NU1503 (which TreatWarningsAsErrors promotes to an error). | ||
| When VCTargets IS available, Microsoft.Cpp.targets imports NuGet.targets which | ||
| redefines these targets with real implementations. | ||
| --> | ||
| <Target Name="Build" Condition="'$(_HasVCTargets)' != 'true'" /> | ||
| <Target Name="Clean" Condition="'$(_HasVCTargets)' != 'true'" /> | ||
| <Target Name="Rebuild" Condition="'$(_HasVCTargets)' != 'true'" /> | ||
| <Target Name="Restore" Condition="'$(_HasVCTargets)' != 'true'" /> | ||
| <Target Name="_IsProjectRestoreSupported" Returns="@(_ValidProjectsForRestore)"> | ||
| <ItemGroup> | ||
| <_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" /> | ||
| </ItemGroup> | ||
| </Target> | ||
| <Target Name="_GetRestoreProjectPathItems" /> | ||
| <Target Name="_GenerateRestoreGraphProjectEntry" Returns="@(_RestoreGraphEntry)" /> | ||
| <Target Name="_GenerateProjectRestoreGraph" Returns="@(_RestoreGraphEntry)" /> | ||
|
|
||
| <!-- Everything below is conditional on having the C++ toolset --> | ||
|
|
||
| <ItemGroup Label="ProjectConfigurations" Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <ProjectConfiguration Include="Debug|x64"> | ||
| <Configuration>Debug</Configuration> | ||
| <Platform>x64</Platform> | ||
| </ProjectConfiguration> | ||
| <ProjectConfiguration Include="Release|x64"> | ||
| <Configuration>Release</Configuration> | ||
| <Platform>x64</Platform> | ||
| </ProjectConfiguration> | ||
| </ItemGroup> | ||
|
|
||
| <PropertyGroup Label="Globals" Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <ProjectGuid>{E9A0B5D7-2F4A-4C8E-9D3B-1A6F5E8C7D2A}</ProjectGuid> | ||
| <RootNamespace>dn-native-debug</RootNamespace> | ||
| <ProjectName>dn-native-debug</ProjectName> | ||
| <!-- Minimum VS toolset - actual C++ compilation is not performed --> | ||
| <VCProjectVersion>17.0</VCProjectVersion> | ||
| </PropertyGroup> | ||
|
|
||
| <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" Condition="'$(_HasVCTargets)' == 'true'" /> | ||
|
|
||
| <PropertyGroup Label="Configuration" Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <!-- Makefile type: no C++ compiler is invoked, only NMake commands run --> | ||
| <ConfigurationType>Makefile</ConfigurationType> | ||
| <PlatformToolset>v143</PlatformToolset> | ||
| </PropertyGroup> | ||
|
|
||
| <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" Condition="'$(_HasVCTargets)' == 'true'" /> | ||
|
|
||
| <!-- | ||
| Compute paths to the repo's dotnet CLI and the published AOT binary. | ||
| These use MSBuild path utilities so they work on any developer machine. | ||
| --> | ||
| <PropertyGroup Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <_RepoRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '..', '..', '..'))</_RepoRoot> | ||
| <_DotNetTool>$([MSBuild]::NormalizePath('$(_RepoRoot)', '.dotnet', 'dotnet.exe'))</_DotNetTool> | ||
| <_DnPublishDir>$([MSBuild]::NormalizePath('$(_RepoRoot)', 'artifacts', 'bin', 'dn', '$(Configuration)', 'net11.0', 'win-x64', 'publish'))</_DnPublishDir> | ||
| <_DnExePath>$([MSBuild]::NormalizePath('$(_DnPublishDir)', 'dn.exe'))</_DnExePath> | ||
| <_DotnetRootPath>$([MSBuild]::NormalizePath('$(_RepoRoot)', '.dotnet'))</_DotnetRootPath> | ||
| </PropertyGroup> | ||
|
|
||
| <!-- | ||
| NMake build commands: publish all three layers (AOT exe, AOT shared lib, | ||
| managed DLL) and assemble them in the publish directory. | ||
| --> | ||
| <PropertyGroup Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <NMakeBuildCommandLine>"$(_DotNetTool)" publish "$(MSBuildThisFileDirectory)..\dotnet-aot\dotnet-aot.csproj" -r win-x64 -c $(Configuration) && "$(_DotNetTool)" publish "$(MSBuildThisFileDirectory)dn.csproj" -r win-x64 -c $(Configuration) && "$(_DotNetTool)" build "$(MSBuildThisFileDirectory)..\dotnet\dotnet.csproj" -c $(Configuration) && copy /Y "$(_RepoRoot)\artifacts\bin\dotnet-aot\$(Configuration)\net11.0\win-x64\publish\dotnet-aot.dll" "$(_DnPublishDir)\" && xcopy "$(_RepoRoot)\artifacts\bin\dotnet\$(Configuration)\net11.0\*" "$(_DnPublishDir)\" /S /Y /Q</NMakeBuildCommandLine> | ||
| <NMakeCleanCommandLine>if exist "$(_DnPublishDir)" rd /s /q "$(_DnPublishDir)"</NMakeCleanCommandLine> | ||
|
JeremyKuhne marked this conversation as resolved.
|
||
| <NMakeReBuildCommandLine>$(NMakeCleanCommandLine) && $(NMakeBuildCommandLine)</NMakeReBuildCommandLine> | ||
| <NMakeOutput>$(_DnExePath)</NMakeOutput> | ||
| </PropertyGroup> | ||
|
|
||
| <!-- | ||
| Native debugger launch settings. | ||
| DebuggerFlavor=WindowsLocalDebugger tells VS to use the native debugger as | ||
| primary. This is the key property that the .NET project system cannot provide. | ||
| C# breakpoints set in AOT-compiled source files bind via the native PDB. | ||
| --> | ||
| <PropertyGroup Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <LocalDebuggerCommand>$(_DnExePath)</LocalDebuggerCommand> | ||
| <LocalDebuggerCommandArguments>--info</LocalDebuggerCommandArguments> | ||
| <LocalDebuggerWorkingDirectory>$(_DnPublishDir)</LocalDebuggerWorkingDirectory> | ||
| <LocalDebuggerEnvironment>DOTNET_ROOT=$(_DotnetRootPath)</LocalDebuggerEnvironment> | ||
| <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor> | ||
| </PropertyGroup> | ||
|
|
||
| <!-- | ||
| Include AOT source files as "None" items so they appear in Solution Explorer | ||
| under this project. This makes it easy to open them and set breakpoints | ||
| without navigating to another project. The files are not compiled by this | ||
| project (Makefile projects don't compile anything). | ||
| --> | ||
| <ItemGroup Condition="'$(_HasVCTargets)' == 'true'"> | ||
| <None Include="..\dotnet-aot\NativeEntryPoint.cs" Link="AOT Sources\NativeEntryPoint.cs" /> | ||
| <None Include="..\dotnet-aot\ManagedHost.cs" Link="AOT Sources\ManagedHost.cs" /> | ||
| <None Include="Program.cs" Link="AOT Sources\dn.Program.cs" /> | ||
| <None Include="..\dotnet\Program.cs" Link="Managed Sources\dotnet.Program.cs" /> | ||
| <None Include="..\dotnet\CommandLineInfo.cs" Link="AOT Sources\CommandLineInfo.cs" /> | ||
| <None Include="debug-dn.cmd" /> | ||
| </ItemGroup> | ||
|
|
||
| <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" Condition="'$(_HasVCTargets)' == 'true'" /> | ||
|
|
||
| <!-- | ||
| No-op targets for Arcade build phases that don't apply to this debug-only | ||
| project. Arcade's Build.proj dispatches Pack, Test, IntegrationTest, and | ||
| PerformanceTest to every project in the solution; the C++ project system | ||
| doesn't define them, so we must provide empty stubs to avoid MSB4057. | ||
| --> | ||
| <Target Name="Pack" /> | ||
| <Target Name="Test" /> | ||
| <Target Name="IntegrationTest" /> | ||
| <Target Name="PerformanceTest" /> | ||
|
|
||
| </Project> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.