Skip to content
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

Tour of .NET Behavior on Windows 11 Arm64 #7709

Open
richlander opened this issue Aug 15, 2022 · 22 comments
Open

Tour of .NET Behavior on Windows 11 Arm64 #7709

richlander opened this issue Aug 15, 2022 · 22 comments

Comments

@richlander
Copy link
Member

richlander commented Aug 15, 2022

Tour of .NET Behavior on Windows 11 Arm64

We're going to take a tour of the basic behavior of .NET 6 and .NET Framework 4.8.1 on Windows 11 Arm64. This tour of the experience is going to give us a sense of which .NET variants can be installed on the machine, how to use them to target various architectures, and what various key APIs report. This tour will provide a good sense of the experience that you an expect and that is on offer.

The primary questions it answers are:

  • When I build an app, which architecture does it target/support by default?
  • How do I build my app for a different/particular architecture?
  • Is there a way to get my app to run a different architecture than what it was built for as part of launching it?

A subset of this document will be moved to official docs. In fact, it was created as a set of working notes to do just that. The author of those notes is sharing them with the hope that they will be a benefit for someone trying to figure out how best to target Windows Arm64.

The tour is on Windows 11 Arm64, however, you could just as easily use these instructions to build for Windows Arm64 on Windows x64 and then run the built apps on Windows Arm64. Also, much of what you'll see equally applies to macOS x64 and Arm64.

The key -- potentially surprising -- experience is that .NET Framework AnyCPU apps run as emulated x64 on Windows Arm64. That choice was made for the benefit of compatibility. The document describes multiple ways to get your .NET Framework apps to run natively, either as a build-time or launch-time decision.

I'm using .NET 6.0.8 (.NET SDK 6.0.400) and .NET Framework 4.8.1. I'll be using Arm64, x64, and x86 variants of both. .NET 6 is not included in Windows 11 (or any other Windows version), while all three .NET Framework architecture variants are included. I'm using a Windows 11 Insider build (10.0.22621). The machine and OS are Arm64.

There is also one honorable mention of .NET 7, where its behavior differs from .NET 6.

Much of the tour relies on the Developer Command Prompt that comes with Visual Studio 2022. Visual Studio 2022 will soon be supported on Windows 11 Arm64.

Test samples

We'll be using the following samples to test our environment:

Those are samples we maintain for containers, but they are also great for this use case.

PATH

All the .NET variants mentioned are installed on the Windows 11 Arm64 test machine that will be used for the tour.

The PATH is a key aspect of any dev experience and is critical to understand to using platform components (particularly tools).

Let's take a look at the PATH:

 $env:PATH
C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Git\cmd;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Users\rich\AppData\Local\Microsoft\WindowsApps;C:\Users\rich\.dotnet\tools;C:\Users\rich\AppData\Local\Programs\Microsoft VS Code\bin

The following path segments relate to .NET:

  • C:\Program Files\dotnet\
  • C:\Users\rich\.dotnet\tools

There are two interesting aspects here:

  • Only the native architecture .NET (Core) location is registered. That's Arm64.
  • There is just one location for .NET tools. That feature is built in such a way that tools from multiple architectures can be co-located without issue.

There is no PATH entry for .NET Framework. That's nothing new. There has never been a PATH entry for .NET Framework. It doesn't explicitly need one. If you want to use .NET Framework tools, you need to rely on the Developer Command Prompt for Visual Studio.

Building and running by default

First, .NET 6:

PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

The app is running on Arm64, as expected.

Next, .NET Framework 4.8.1:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8

In contrast, the .NET Framework app is running on x64.

.NET Framework apps run as x64 by default on Windows Arm64. That was a key design decision made to aid compatibility. .NET Framework apps have been primarily running as x64 for over a decade on pervasively deployed x64 desktops, laptops, and servers. It's quite likely that many .NET Framework apps have x64 dependencies (like native libraries). Running them as emulated seemed like the safest bet, and to make Arm64 opt-in.

Targeting Arm64

Targeting Arm64 is easy with .NET 6. Your app will target the same architecture as the SDK you are using. You'll produce an Arm64 app by default as long as you are using the Arm64 SDK. That was just demonstrated above.

However, if you want to force targeting Arm64 (which will be more important later on the tour), you can use -a arm64 as demonstrated in the following:

PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run -a arm64
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

There is a similar option with MSBuild, for .NET Framework, using the Platform property:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build /p:Platform=arm64
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\arm64\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8

You can also use the .NET SDK for the same purpose:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>dotnet run -a arm64
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8

I'm using dotnet run for this demonstration, but dotnet build works the same way.

Targeting x64

Targeting x64 is just a variation on what you've just seen (mostly).

For .NET 6, we can continue using the Arm64 SDK, but target x64 with the same -a switch you just saw. That's the easiest and highest performance approach (the SDK is a lot of code and will run slowly when emulation).

PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run -a x64
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

We can also use the x64 SDK, which will produce x64 apps by default. That's not the recommended approach (due to the emulation performance cost), but works fine.

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"c:\Program Files\dotnet\x64\dotnet" run
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

There isn't a C:\Program Files (x64) on Windows Arm64. As a result, the x64 variant of .NET is installed in C:\Program Files\dotnet in the child x64 directory. That's what you see demonstrated above. the x64 directory will only be there if you install the x64 .NET SDK. It doesn't come with the Arm64 SDK.

To complete the demonstration, we can use the x64 SDK to produce Arm64 apps. That's also not recommended, but works.

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"c:\Program Files\dotnet\x64\dotnet" run -a arm64
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

For .NET Framework, x64 is the default target as demonstrated above. There isn't anything more to show.

Targeting x86

For .NET 6, this experience is very similar to targeting x64. We'll use the same approach.

First, using Arm64 SDK, again using the -a switch:

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run -a x86
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X86
ProcessorCount: 8
TotalAvailableMemoryBytes: 2.00 GiB

Huh. Why did we drop from (near) 32GB to 2GB? Are 32-bit apps running in some small virtual machine used for emulation? How do I make it bigger? That's not it. 32-bit processes are limited to 4GB of memory and it's split 50/50 between kernel and user mode. That's why we're seeing 2GB.

We see slightly difference behavior with .NET 7 (specifically for OSArchitecture).

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run -a x86
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 7.0.0-preview.7.22375.6
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8
TotalAvailableMemoryBytes: 2.00 GiB

Let's try with .NET Framework.

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build /p:Platform=x86
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\x86\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8

We can try the same thing with the .NET SDK:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>dotnet run -a x86
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8

Hmmm. That's interesting. OSArchitecture is reported differently for .NET 6 on one side and .NET 7 and .NET Framework 4.8.1 on the other when running x86 apps. They must use different Windows APIs to report OSArchitecture. They don't all report the same thing.

Coercing already-built apps to Arm64

With .NET 6, you can (in some cases) run apps targeted for one architecture to another. We won't go into all the details on that, but will demonstrate the mechanism.

We'll first build and run the app natively, as has been demonstrated earlier.

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

Now, we'll use the x64 dotnet to run the app.

C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"C:\Program Files\dotnet\x64\dotnet" bin\Debug\net6.0\dotnetapp.dll
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 6.0.8
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB

Every .NET (Core) app has a native launcher. It's a component of every app and ties each app to an operating system and architecture. However, you don't need to launch an app with the native launcher that the SDK provides. You can launch it with the dotnet host instead. That's what is being demonstrated above.

.NET Framework works differently. It has special executables that are not tied to an architecture in quite as direct a way. Lets try some techniques to coerce an x64 app to Arm64.

We'll rebuild the app again to ensure we're starting from a clean slate.

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8

That looks good for our baseline.

Let's first try start:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>start /MACHINE arm64 /B /WAIT bin\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8

Our app now runs as Arm64.

runas is another option that you can use to get a .NET Framework app to run as Arm64.

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>runas /machine:arm64 /trustlevel:0x20000 bin\Debug\net48\dotnetapp.exe

This launches another console windows, which follows:

                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8

In this case, I had to add a Console.ReadLine() to the app. Otherwise, it would just open and close immediately. start offers /wait and /b to work around that. For UI apps, that won't be a problem.

You can also specify that you want your app to always run in the registry as Arm64.

Create the following registry key (rename for your app):

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\dotnetapp.exe

It would have this DWORD item and hex value:

PreferredMachine=aa64

or as decimal:

PreferredMachine=43620

You can see my registy entry.

image

Note: Is there a way to specify the absolute path for this key, not just the filename?

Let's try that:

C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
                                 ad88
                        ,d      d8"
                        88      88
8b,dPPYba,   ,adPPYba, MM88MMM MM88MMM 8b,     ,d8
88P'   `"8a a8P_____88   88      88     `Y8, ,8P'
88       88 8PP"""""""   88      88       )888(
88       88 "8b,   ,aa   88,     88     ,d8" "8b,
88       88  `"Ybbd8"'   "Y888   88    8P'     `Y8

.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621

OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8

That works as expected.

For folks that start processes from code, you can specify machine type by using the UpdateProcThreadAttribute function. See PROC_THREAD_ATTRIBUTE_MACHINE_TYPE.

Visual Studio

The targeting experience is the same in Visual Studio as it is for MSBuild.

image

@AntonLapounov
Copy link
Member

Note: Is there a way to specify the absolute path for this key, not just the filename?

Yes, with the undocumented FilterFullPath registry key.

Hmmm. That's interesting. OSArchitecture is reported differently for .NET 6 and .NET Framework 4.8.1 when running x86 apps. .NET Framework and .NET 6 use different Windows APIs to report OSArchitecture. They don't all report the same thing.

Isn't that a regression in .NET 6 comparing to .NET Framework 4.8.1?

The first three lines in the "netfx" logo need to be indented by one more space. Good article!

@richlander
Copy link
Member Author

Reported: dotnet/runtime#73974

@reflectronic
Copy link

Regarding the behavior for .NET Framework 4.8.1: is there no mechanism to enable Arm64 support at build time, other than building an Arm64-specific executable? Many .NET Framework apps (I'm thinking of client apps in particular) are distributed/deployed as loose, portable executables with no installer. Given the options described above, there's currently no convenient way to enable Arm64 support for these apps without either:

  • complicating building & distribution by creating separate executables for Arm64
  • distributing some kind of launch script that will launch the real app using the preferred architecture
  • writing to the registry (which seems like a bad idea for a portable app)

I would have imagined that, for example, targeting .NET Framework 4.8.1 would automatically cause an AnyCPU app to start as Arm64 (i.e. changing the target framework would act as confirmation that the app was made to work with Arm64). Or, perhaps, an app.config option similar to <supportedRuntime> could indicate that the app should run using the Arm64 runtime instead of the x64 one. Or, one could even imagine a new "AnyCPU, Arm64 preferred" configuration that assemblies could be built with.

All of these alternatives would address the inability for .NET Framework apps to opt-in to Arm64 support without changing their deployment model. Many client apps, for better or for worse, are still built on .NET Framework, and this ability would allow them to trivially opt-in to Arm64 support. Given that "pure managed" apps with no unmanaged dependencies are very common, I would think that the return on a feature like this could be high, and the lack of it seems like a missed opportunity.

@richlander
Copy link
Member Author

Great question. We had the 32BitPrefered feature from the past. It is kinda similar. I think this would be NativeArchPrefered. We haven't built that. While simple, it is not straightforward. We are looking some other options to achieve similar results. At present, you've correctly determined the set of options available.

Note that there isn't a magic feature like this coming for .NET 6/7/8. This limits our desire to invest in an expensive magic feature for .NET Framework.

@driver1998
Copy link

driver1998 commented Aug 17, 2022

Now that .NET Framework 4.8.1 is available to Windows 10 on ARM as well, it would be helpful to document the behavior there too. I guess AnyCPU will run as x86 by default there.

Also IIRC start /machine is not available in in-market version of Windows 11 (21H2, Build 22000).

@richlander
Copy link
Member Author

Good point. Yes, that should added to the doc, too. I am away on a trip so it will be a bit before that happens.

@driver1998
Copy link

The problem with NativeArchPrefered is, it will create even more issues if .NET Framework add yet another new architecture (think RISC-V?) to support 5-10 years down the road.

Sure, hopefully .NET Framework would be dead by then, but you never know, it was presumed to be dead years ago when MS decided no .NET Framework on ARM64.

@jkotas
Copy link
Member

jkotas commented Aug 23, 2022

We are not going to build NativeArchPrefered for this reason. The options we are looking at are future proof and naturally extend to potential new architectures.

cc @davidwrighton

@sungaila
Copy link

What is the point of targeting .NET Framework 4.8.1 right now, if not for supporting ARM64? I mean, no legacy project will bother to target net481 for some WinForms/WPF accessibility enhancements.

AnyCPU being x64 for net48 (and older) makes perfectly sense. But it doesn't for net481.

@hamarb123
Copy link

Just my 2 cents,
My ideal solution would be a section in the config file that just lists the architectures in the order it would like them to be tried. eg. <idealRuntimes>arm64;x64;x86</idealRuntimes>, if this section is present, the .NET Framework will pick the first one it understands (being the installation, not the targeted framework) and that works on the OS. This config option would be ignored on older frameworks that don't know of this feature.

Ie. if I write an app targeting 4.6 or some other similar version and include this config option it will be ignored on all frameworks up to 4.8.1/2/whenever it's added and then followed on a framework that understands it. This solution would allow full developer control on what architectures they want to be able to run in.

Use case: I use the .NET Framework primarily as a bootstrapper for my .NET Core apps these days (and for some old apps and for older versions of some apps) - this allows a single exe to check .NET Core installations etc., and have no plans to stop using it since .NET Core will not be installed on Windows by default and isn't supported for like 10 years and isn't fully forwards compatible. I currently target .NET Framework 4.6.1 (for some reason?), but will eventually update to a newer version, but not past 4.8 since it doesn't support Windows 7 (until I eventually give up on supporting windows 7 but that's not directly related to what we're discussing here). I'd like the ability to be able to easily detect the os architecture in my bootstrapper which seems to be fixed in 4.8.1 (or earlier, see below), and be able to force older versions of my apps to be able to run in arm64 if I know it will work with ease.

Also, I have a question. If a user launches an app compiled with a version ≤4.8 on an arm64 machine, what will OSArchitecture return? It should probably return x64 in this scenario unless my above feature is implemented and includes that architecture in the list (since this is what it does with 4.8 and lower - or at least whatever version I tested this with I think).

Also a second question. If windows does support a new architecture in the future, is it safe to assume that they will all support x64 emulation? Or should I only assume x86 emulation is available? Is there a Windows API that tells me which processor architectures can be emulated from something like an image file machine constant

Thanks for your time reading this lol.

@driver1998
Copy link

Ie. if I write an app targeting 4.6 or some other similar version and include this config option it will be ignored on all frameworks up to 4.8.1/2/whenever it's added and then followed on a framework that understands it.

I don't think this is necessary, most AnyCPU .NET FX app can run as ARM64 unmodified, as long as it does not use things like P/Invoke to third party unmanaged dependencies. I even ran a .NET 4.5 app on ARM64 with start /machine arm64 and it work just fine.

If we does not limit it to .NET 4.8.x target where this is added, then end-user can try to make their app ARM64 native even without source code.

Is there a Windows API that tells me which processor architectures can be emulated from something like an image file machine constant

GetMachineTypeAttributes: https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-getmachinetypeattributes

@hamarb123
Copy link

I don't think this is necessary, most AnyCPU .NET FX app can run as ARM64 unmodified, as long as it does not use things like P/Invoke to third party unmanaged dependencies. I even ran a .NET 4.5 app on ARM64 with start /machine arm64 and it work just fine.

I do not expect my users to have to open the command line to open an app (which is why this doesn't solve it for a portable app), but I will use this when I have control over launching it.

If we does not limit it to .NET 4.8.x target where this is added, then end-user can try to make their app ARM64 native even without source code.

As you said above, a user can use start /machine arm64 already to do this, so I don't really see how this is an issue. I don't think these files show as being openable in notepad by default and if someone who doesn't do programming tries to open it then they would probably get scared by it looking like code and not change it anyway.

It might even be good that it's easy to change it permanently, e.g. if they have an old app that they want to force to run natively that doesn't have any native dependencies (they would probably find out very quickly whether it has native dependencies or not because it would crash, and could revert it).

GetMachineTypeAttributes: https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-getmachinetypeattributes

Thanks!

@hamarb123
Copy link

I don't think this is necessary, most AnyCPU .NET FX app can run as ARM64 unmodified

Also, can is different to do, as stated in the original post, these only run natively when compiled specifically for arm64. Otherwise they run in x64 emulation for compatibility (without things like running on the command line with start /machine arm64).

@driver1998
Copy link

driver1998 commented Aug 31, 2022

Ie. if I write an app targeting 4.6 or some other similar version and include this config option

OK, I misunderstood you, I thought you mean one have to target 4.8.x in order to use this config option. So we actually are on the same page.

@driver1998
Copy link

I just installed Windows 10 ARM for testing, and no, ARM64 .Net Framework is not available on Windows 10.

Not exactly sure why though.

@snickler
Copy link

snickler commented Oct 7, 2022

I just installed Windows 10 ARM for testing, and no, ARM64 .Net Framework is not available on Windows 10.

Not exactly sure why though.
https://devblogs.microsoft.com/dotnet/announcing-dotnet-framework-481/
image

@reflectronic
Copy link

Tangentially related question: Is there a plan to release a version of Microsoft.DotNet.Framework.NativeImageCompiler for Arm64? It works well for us on x86/x64 in places where we can't use Ngen.exe (the Microsoft Store), so it would be nice to provide the same experience on Arm64.

@snickler
Copy link

snickler commented Dec 7, 2022

Tangentially related question: Is there a plan to release a version of Microsoft.DotNet.Framework.NativeImageCompiler for Arm64? It works well for us on x86/x64 in places where we can't use Ngen.exe (the Microsoft Store), so it would be nice to provide the same experience on Arm64.

Looks like it's deprecated, per the individual runtime packages https://www.nuget.org/packages/runtime.win10-x64.Microsoft.DotNet.Framework.NativeImageCompiler

@reflectronic
Copy link

If it's really deprecated, it would be nice if the official documentation recommending its use was amended: https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-r2r

Q. Can I release binaries using this technology?

A. This version includes a Go Live license that you can use today.

So, is the NgenR2R tool deprecated, or were those packages marked accidentally in some automated proces? Will it receive Arm64 support? @jkotas?

@hrumhurum
Copy link

Please give us an option to opt-in into ARM64 with the usual XXX.exe.config files for .NET Framework apps.

@hmartinez82
Copy link

@hrumhurum Came here looking for the same, or at least a configuration manifest. I was also able to run a 4.5.2 target in ARM64 as long as it was being built with VS2022 and Platform set to ARM64, but I want my AnyCPU builds to run as ARM64 on ARM64 Windows :)

@driver1998
Copy link

driver1998 commented Mar 6, 2024

The new supportedArchitectures attribute in application manifest should be the way to go.

Kind of wonder why it is in application manifest though, that would be a little bit tricky to backport to downlevel OS...

https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#supportedarchitectures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests