Expose setting/getting of system error code #46843
Description
Background and Motivation
.NET developers can use SetLastError on a P/Invoke to specify whether or not the last error from the invoked function should be stored. The error is then retrievable through Marshal.GetLastWin32Error.
The runtime's built-in interop marshallers are currently for clearing the system error before invoking the function, getting the system error after invoking the function, and storing the error as the last P/Invoke error code.
With the proposed P/Invoke source generation, a tool (Roslyn source generator) outside of the runtime is responsible for handling the system error such that it can be used/checked by the caller of the P/Invoke.
The proposed APIs would support a source generator's ability to handle this through new APIs for getting/setting the system error and setting the last P/Invoke error.
Proposed API
New APIs to allow getting and setting the system error:
namespace System.Runtime.InteropServices
{
public static class Marshal
{
+ /// <summary>
+ /// Get the last system error on the current thread
+ /// </summary>
+ /// <remarks>
+ /// The error is that for the current operating system (e.g. errno on Unix, GetLastError on Windows)
+ /// </remarks>
+ public static int GetLastSystemError();
+ /// <summary>
+ /// Set the last system error on the current thread
+ /// </summary>
+ /// <remarks>
+ /// The error is that for the current operating system (e.g. errno on Unix, SetLastError on Windows)
+ /// </remarks>
+ public static void SetLastSystemError(int error);
+ /// <summary>
+ /// Set the last platform invoke error on the current thread
+ /// </summary>
+ public static void SetLastWin32Error(int error);
}
}Usage Examples
Example possible usage from a source generator:
public static partial (int, bool) InvokeWithError(int i)
{
unsafe
{
bool __retVal = default;
int __retVal_gen_native = default;
int __lastError = default;
//
// Invoke
//
{
System.Runtime.InteropServices.Marshal.SetLastSystemError(0);
__retVal_gen_native = InvokeWithError__PInvoke__(i);
__lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError();
}
//
// Unmarshal
//
__retVal = __retVal_gen_native != 0;
System.Runtime.InteropServices.Marshal.SetLastWin32Error(__lastError);
return (__lastError, __retVal);
}
}
[System.Runtime.InteropServices.DllImportAttribute("Microsoft.Interop.Tests.NativeExportsNE", EntryPoint = "invoke_with_error")]
extern private static unsafe int InvokeWithError__PInvoke__(int i);Alternative Designs
Get/SetLastSystemError could be written outside of the runtime (e.g. by invoking into a native function for each platform to match how the runtime gets/sets the system error). Exposing a public API allows developers to get/set the error in a way that is consistent with the runtime on all platforms.
Source generators for P/Invokes technically only need to clear the last system error (set it to 0), not set it to any arbitrary value. Providing only a Get/Clear (instead of Get/Set) seems inconsistent with existing APIs. The SetLastSystemError would also give managed functions called from native code the option to explicitly set an error without handling different platforms themselves.
The name of SetLastWin32Error does not properly reflect the error it would actually set (the last P/Invoke error, not Win32 error), but it is named for consistency with the existing GetLastWin32Error. An alternative is to create a new pair that is more accurately named:
namespace System.Runtime.InteropServices
{
public static class Marshal
{
+ /// <summary>
+ /// Get the last platform invoke error on the current thread
+ /// </summary>
+ public static int GetLastPInvokeError();
+ /// <summary>
+ /// Set the last platform invoke error on the current thread
+ /// </summary>
+ public static void SetLastPInvokeError(int error);
}
}The proposed APIs will be needed for any handling of last system error by a third party. They will allow possible future additions such a specific types for propagating the system error to the caller - for example:
namespace System.Runtime.InteropServices
{
/// <summary>
/// Represents the return value and system error for a function
/// </summary>
/// <typeparam name="T">The type of the value</typeparam>
public readonly struct SystemErrorAndValue<T>
{
public int Error { get; }
public T Value { get; }
public SystemErrorAndValue(int error, T value);
[EditorBrowsable(EditorBrowsableState.Never)]
public void Deconstruct(out int error, out T value);
}
/// <summary>
/// Represents the system error for a function
/// </summary>
public readonly struct SystemError
{
public int Error { get; }
public SystemError(int error);
}
}Additions like these can be handled by the third party / source generator, so are not part of this proposal.
Risks
These APIs expose system error code setting that was hidden within the runtime, allowing an easy way for a developer could set the system error. This is already achievable without an API by explicitly P/Invoking into a native function. It would also be possible for a developer to set the last P/Invoke error (call Marshal.SetLastWin32Error) outside of a P/Invoke.
Activity
elinor-fung commentedon Jan 12, 2021
/cc @AaronRobinsonMSFT @jkoritzinsky
AaronRobinsonMSFT commentedon Jan 12, 2021
@elinor-fung I don't think
GetLastSystemErrororGetLastSystemErrorneed to be markedunsafe.elinor-fung commentedon Jan 12, 2021
Oops, fixed.
danmoseley commentedon Jan 12, 2021
The difference between XXXLastSystemError and XXXLastWin32Error is not indicated by their names, especially if not in Windows.
It seems to me that this is our opportunity to remove the Win32 part and create a new Get/Set pair with more explanatory names (Get/SetLastPinvokeError?) with GetLastWin32Error remaining for compatibility.
danmoseley commentedon Jan 12, 2021
#26227 (comment)
jkotas commentedon Jan 12, 2021
The last error pattern is bug-prone. I have seen countless of bugs over the years where the last error is accidentally overwritten. To improve this with source generated interop, we may want to get away with the cached last error concept completely and instead teach the marshalers to include the last system error in the method result. It would address the Win32 name problem as side-effect.
elinor-fung commentedon Jan 12, 2021
Definitely like the idea of getting rid of the existing last error mechanism.
So it seems like it would be:
SetLastErrorfield forGeneratedDllImportAttributeMarshal.GetLastWin32ErrorValueAndSystemError<T>andSystemError)danmoseley commentedon Jan 12, 2021
Is
ExceptionFromSystemErrora proposed new type? Or did you mean a utility method likeThrowExceptionFromSystemErrorand if so would that be often simplythrow new Win32Exception()?danmoseley commentedon Jan 12, 2021
Would eliminating
SetLastError(making it always true, same as VB default) cause a measurable perf impact (when calling random API that don't participate in setting the last error)? Although I certainly agree it would be nice to get rid of this error prone requirement.jkotas commentedon Jan 12, 2021
Yes, it would have a measurable perf impact for cheap APIs.
16 remaining items
terrajobst commentedon Jan 26, 2021
Video
SetLastWin32Error()is unfortunate, because it really sets the last P/Invoke error, regardless of OS but it matches the existing method name. Let's just obsolete the current concept and add a new pair. This also makes people aware of the conceptual difference betweenGetLastSystemError()andGetLastPInvokeError().jkotas commentedon Jan 26, 2021
This will be very noisy warning. We have ~500 calls to of
Marshal.GetLastWin32Errorin dotnet/runtime libraries alone and we won't be able to really fix many of them since the libraries cross-compile for down-level targets. None of the obsoletions so far were as noisy as this.Also, should the message just say
Use GetLastPInvokeError()? One cannot safely replace calls toGetLastWin32ErrorwithGetLastSystemErrorwithout auditing and changing other code.GetLastPInvokeError()is the right easy fix.terrajobst commentedon Jan 26, 2021
Looking at telemetry, you're right:
The assumption was that this is an advanced API. If we expect this API to be that widely used, obsoleting might be a bit harsh.
I think the reasoning to recommend either was that people didn't necessarily understand that
GetLastWin32Error()only returns the last trapped error, rather than calling straight to the Win32GetLastError()function.jkotas commentedon Jan 26, 2021
It would be fine to recommend
GetLastSystemErroras a potential replacement, but it would have to come with a longer explanation about other refactoring that one has to do do make it reliable. A simple TLDR suggestion is to replace it withGetLastPInvokeErrorthat is safe without much thinking 100% of time. This discussion may be irrelevant if we drop obsoleting the existing method.AraHaan commentedon Jan 26, 2021
I got a better idea, for places that use the GetLastWin32Error have ifdefs where if .NET 6+ then use the new api, else use old api for the downlevel stuff that way everyone is happy and then also obsolete the old as well too without too much noise.
plus the runtime uses ifdefs anyway on the versions of the TFMS they target anyway so.
jkotas commentedon Jan 26, 2021
We do use ifdefs in C# as last resort. We strongly prefer code without ifdefs.
terrajobst commentedon Jan 26, 2021
Why would we have to
#ifin dotnet/runtime? Why couldn't we just use the new API?jkotas commentedon Jan 27, 2021
There are 200+ projects under src/libraries that target netstandard that cannot use the new API. It is not unusual for these projects to multi-target or share files with projects that target netcoreapp current. So some of the ~500 calls Marshal.GetLastWin32Error would have to stay intact and some of them would have to be under ifdef or disable the obsolete warning.
elinor-fung commentedon Apr 27, 2021
Implemented in #51505.
danmoseley commentedon Oct 28, 2021
@elinor-fung would it make sense for you to add a little more to the doc pages for the new methods?
Gets the last system error on the current thread
Returns the error code returned by the last unmanaged function that was called using platform invoke that has the SetLastError flag set.
Get the last platform invoke error on the current thread
It isn't clear which to use when. We also do not have any note in GetLastWin32Error explaining to use other methods in preference when targeting >=.NET 6.0
Worst of all, two of these descriptions don't end with a period
elinor-fung commentedon Oct 29, 2021
dotnet/dotnet-api-docs#7334
This statement amused me more than is probably normal.