runtime: Expose setting/getting of system error code

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.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 34 (34 by maintainers)

Most upvoted comments

remove the Win32 part and create a new Get/Set pair with more explanatory names (Get/SetLastPinvokeError?) with GetLastWin32Error remaining for compatibility.

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.

[GeneratedDllImportAttribute("kernel32.dll")]
extern static ValueAndSystemError<bool> CreateFile(....);

...
    var result = CreateFile(...);
    if (!result.Value)
         throw ExceptionFromSystemError(result.SystemError);
...

Video

  • Looks good as proposed
  • The naming of 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 between GetLastSystemError() and GetLastPInvokeError().
namespace System.Runtime.InteropServices
{
    public static class Marshal
    {
        public static int GetLastSystemError();
        public static void SetLastSystemError(int error);

        public static int GetLastPInvokeError();
        public static void SetLastPInvokeError(int error);

        [Obsolete("Use GetLastSystemError() or GetLastPInvokeError()", DiagnosticId="<NextID>", UrlFormat="<the proper URL>")]
        public static int GetLastWin32Error();
    }
}

I would omit SystemErrorAndValue<T>/SystemError from the proposed/approved API, and instead just list them as potential future addition for reference only. For now, SystemErrorAndValue<T>/SystemError can be added as internal by the source generator prototype that will allow us validate how well it works in practice.

GetLastSystemError/SetLastSystemError are low-level and they will be needed by any kind of solutions one can imagine in this space, so we should be able to add those with very little risk of not getting it right.

Definitely like the idea of getting rid of the existing last error mechanism.

So it seems like it would be:

  • No SetLastError field for GeneratedDllImportAttribute
  • Source-generated P/Invokes don’t participate in Marshal.GetLastWin32Error
  • New type that developers can use as an indication for the marshallers that the system error is desired (e.g. ValueAndSystemError<T> and SystemError)