Fix Environment.GetCommandLineArgs()[0] to report the host invocation name
Project: dotnet/runtime | Language: C# | Concepts: api-design, backwards-compatibility, error-handling, testing-strategy | Difficulty: mid
Link: https://github.com/dotnet/runtime/pull/131671
The problem
Previously, Environment.GetCommandLineArgs()[0] in .NET apps reported the managed assembly path (or bundle path for single-file apps) as the first command-line argument, rather than the name used to invoke the native host process. This led to confusing or incorrect results, especially when launching an apphost through a symbolic link: the reported value did not match the actual invocation path (e.g., the symlink name).
Why it's non-trivial
A naive fix would just use the native process's argv[0] everywhere, but this would break compatibility with muxer hosts like dotnet and corerun, which expect the managed application path as the first element. The fix must distinguish between apphost and muxer invocations, handle edge cases (like missing arguments), and avoid breaking existing APIs or behaviors.
The approach
1. Documenting the new property
The design docs are updated to describe the new ARGV0 property:
+`ARGV0`
+
+The name used to invoke an application host, corresponding to the native process's `argv[0]`. The apphost provides this property for the first element returned by [`Environment.GetCommandLineArgs()`](https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs). Muxer-style hosts, including `dotnet` and `corerun`, do not provide this property, preserving the managed application path as the first element.
This documents the new behavior and clarifies when ARGV0 is present and what it means.
2. Add the ARGV0 property constant
A new constant is introduced for the runtime property key:
+#define HOST_PROPERTY_ARGV0 "ARGV0"
This allows hosts to communicate the invocation name to the runtime in a standardized way.
3. Capture and propagate the invocation name in hostpolicy
The native host captures argv[0] (the invocation name) and stores it in the arguments struct:
+ if (argc > 0)
+ {
+ assert(argv != nullptr);
+ args.invocation_name = argv[0];
+ args.app_argv = &argv[1];
+ args.app_argc = argc - 1;
+ }
This ensures the actual invocation name is available for later use, but only if arguments are present.
The context initialization copies this value:
+ invocation_name = args.invocation_name;
This makes the invocation name available to the managed runtime.
4. Expose ARGV0 as a runtime property
The runtime property getter is updated to handle ARGV0:
+ if (::strcmp(key, HOST_PROPERTY_ARGV0) == 0)
+ {
+ if (context->invocation_name.empty())
+ return -1;
+
+ return pal::pal_utf8string(context->invocation_name, value_buffer, value_buffer_size);
+ }
This allows managed code to retrieve the invocation name if it was set.
5. Use ARGV0 in Environment.GetCommandLineArgs()[0]
The managed runtime checks for the ARGV0 property and uses it if available:
- PCWSTR exePath = Bundle::AppIsBundle() ? static_cast<PCWSTR>(Bundle::AppBundle->Path()) : pwzAssemblyPath;
+ StackSString invocationName;
+ PCWSTR exePath = HostInformation::GetProperty(HOST_PROPERTY_ARGV0, invocationName)
+ ? invocationName.GetUnicode()
+ : (Bundle::AppIsBundle() ? static_cast<PCWSTR>(Bundle::AppBundle->Path()) : pwzAssemblyPath);
This ensures that, for apphost launches, the first argument reflects the actual invocation name; for muxer launches, the previous behavior is preserved.
6. Test updates to verify the new behavior
Tests are updated to assert the correct value for Environment.GetCommandLineArgs()[0]:
+ .And.HaveStdOutContaining($"Environment.GetCommandLineArgs()[0] = {appDll}")
+ .And.HaveStdOutContaining("Environment.GetCommandLineArgs()[1] = print_command_line_args");
This checks that the first argument matches the expected invocation name or managed path, depending on host type.
From the review thread
jkotas:
I do not think that this is the behavior we want.
IIRC, the desired behavior we have discussed is captured in this table https://github.com/dotnet/runtime/issues/101837#issuecomment-2094257858
jkotas: Host is very overloaded term. I have reservations about using "host" in names.
How about calling thisARGV0?xoofx: Agreed. Renamed the property to
ARGV0via dd32cfa6417.
Takeaways
- When fixing platform or API behaviors, preserve compatibility for existing usage patterns—distinguish between host types if necessary.
- Document new runtime properties and behaviors clearly, specifying when and how they apply.
- Always update and extend tests to cover new edge cases and verify intended outcomes for both old and new behaviors.
- Avoid ambiguous naming for new API properties; use terminology that is precise and familiar to the platform (e.g.,
ARGV0for the process invocation name).