[Pitch] A `Win32` namespace and `Win32.Error` for System, with a roadmap for Windows-native file system APIs

Hi System community!

I'd like to pitch a series of proposals for Windows-native file system API in System, built on the Win32 base API and HANDLE rather than the C runtime's POSIX compatibility layer that backs FileDescriptor. This series aims to bring System's Windows support up to par with its POSIX counterpart, and it begins by introducing a Win32 namespace to hold these APIs and a Win32.Error type for them to throw.

The full series of proposals is available and tracked in this draft PR:

I'd like to first spend some time gathering feedback and discussing the Win32 namespace and error type, as the outcome here will shape the final spellings of the other APIs. Feedback on the overall direction is welcome here too, and each later proposal will get its own thread for its details. Please let me know your thoughts!

A Win32 Namespace and Win32.Error for Swift System

  • Proposal: SYS-0010
  • Author: Jonathan Flat
  • Review Manager: TBD
  • Status: Draft
  • Implementation: TBD
  • Review: TBD

Revision history

  • v1 Initial version.

Introduction

System's current file system APIs are POSIX-shaped, and on Windows they are built mostly on the Universal C Runtime's POSIX compatibility layer. That layer is a portability shim, not a model of the operating system. This proposal adds Win32, a namespace for Windows-native file system APIs, and Win32.Error, the error type those APIs report.

It's the foundation for a series of proposals that nest their types in the namespace and throw the error. Each is mostly reviewable on its own, but may depend on others in the series for the final shape:

API Proposal
Win32 namespace, Win32.Error This proposal
Win32.FileHandle, ownership and closing SYS-0011
Opening files and directories, duplication, and reopening SYS-0012
FileDescriptor bridging for Win32.FileHandle SYS-0013
Reading, writing, seeking, flushing, and resizing SYS-0014
Win32.FileType SYS-0015
Byte-range locking SYS-0016
Volume information for a handle SYS-0017
Final path name for a handle SYS-0018

See Future directions for additional APIs that could follow.

Motivation

Windows API layers

Windows exposes file system functionality at several layers, and it matters which one System wraps:

Layer Examples Expose this from System?
Kernel-mode file system interfaces IRPs, FltMgr minifilters, FsRtl* No. Driver-only.
NT Native API (ntdll.dll) NtCreateFile, NtQueryInformationFile No. Only partly documented, and not stability-committed.
Win32 base API CreateFileW, GetFileInformationByHandleEx Yes
C runtime (UCRT) _open_osfhandle, _get_osfhandle, _read, _close Yes. This is how FileDescriptor works today.
Shell, COM, WinRT, .NET IFileOperation, Windows.Storage No. Different app models.

Win32 is the documented, stable, ABI-committed system interface for desktop Windows, and System already uses it internally. System's Windows adapters implement open by calling CreateFileW directly rather than _wopen, then convert the handle to a descriptor with _open_osfhandle. What System lacks is a place to name the Win32 layer in its public APIs.

System has no home for Windows-native APIs

The library already treats POSIX and Windows file metadata as disjoint surfaces. UserID, GroupID, DeviceID, and Inode are declared entirely inside #if !os(Windows). SYS-0006 proposes Stat for Unix-like platforms only, where we argue in its Alternatives considered:

Rather than forcing Windows file metadata semantics into a cross-platform Stat type, we should instead create Windows-specific types that give developers full access to platform-native file metadata.

Those Windows-specific types need somewhere to live, and it shouldn't be the top level, where the natural names would conflict. System already declares FileType and FileFlags for other platforms, and SYS-0012 and SYS-0015 need Windows types with those names and different meanings. A top-level Error or FileHandle would collide with the standard library or Foundation. System has a shipped precedent for the alternative. Mach is a caseless enum that serves purely as a namespace for a family of platform-specific types. A Win32 namespace follows that precedent.

Errno can't express Windows failures

Every Windows failure in System is currently reported as an Errno, mapped either by the Microsoft C runtime's _dosmaperr table or by System's internal _mapWindowsErrorToErrno, which approximates that table with a few additions. The mapping's purpose is to make the POSIX shim behave: when FileDescriptor.read fails, something has to go in errno, and _dosmaperr is what the CRT would have put there. Errno is fit for that purpose, but unfit for reporting errors from a Win32-native API.

Many Windows failures have no POSIX analog, such as a sharing violation, a mapped-file conflict, or pending overlapped I/O. Errno has no value for them, so any mapping loses them, but System's mapping loses more than is appropriate for true Windows support.

It folds several thousand codes onto sixteen. The table names about fifty error codes and covers fewer than a hundred in total, while winerror.h defines several thousand. Every code the table does not recognize, such as ERROR_NOT_SUPPORTED (50), becomes EINVAL, indistinguishable from a genuinely invalid argument. Of the 32 failure codes this proposal names, 14 aren't in the table at all, and 24 end up as either EINVAL or EACCES.

It merges failures that need different responses. Everything from code 19 through 36 becomes EACCES:

Code Meaning Maps to
ERROR_NOT_READY (21) The device is not ready EACCES
ERROR_CRC (23) Cyclic redundancy check failed EACCES
ERROR_WRITE_FAULT (29) Write fault on the device EACCES
ERROR_SHARING_VIOLATION (32) Another process holds an incompatible open EACCES
ERROR_LOCK_VIOLATION (33) A byte-range lock blocked the access EACCES

A media failure, a device that's not ready, and a sharing violation are three different problems with three different responses, and all of them are indistinguishable from a genuine ERROR_ACCESS_DENIED. ERROR_SHARING_VIOLATION matters most to Windows callers, because it's retryable.

It turns control-flow signals into ordinary failures. ERROR_MORE_DATA (234) is a retry instruction, and a caller that can't distinguish it from EINVAL can't write the grow-and-retry loop that Windows' variable-length query APIs require. ERROR_NO_MORE_FILES (18) ends an enumeration, but it maps to ENOENT.

It misses even the POSIX analogs that exist. ERROR_NO_DATA (232) is what's reported when writing to a pipe whose reader has closed, so FileDescriptor.write throws EINVAL where Linux and Darwin report EPIPE. ERROR_FILE_TOO_LARGE (223), which reports a file system limit, likewise becomes EINVAL rather than EFBIG.

None of this is a defect in _mapWindowsErrorToErrno. The shim's job is to match what the C runtime does, and it does. The problem is that the destination type can't carry the code Windows reported.

Proposed solution

Add Win32, a namespace available on Windows and scoped to the Win32 layer, and Win32.Error, the error currency for everything in it. Also provide a public Errno(approximating: Win32.Error) conversion for callers who want a POSIX approximation (such as a cross-platform library that throws Errno on every platform), and document that it's lossy.

#if os(Windows)
do {
  // `Win32.FileHandle` and `open` are proposed separately, in SYS-0011 and SYS-0012.
  let handle = try Win32.FileHandle.open(path, access: .genericWrite)
  try handle.close()
} catch Win32.Error.sharingViolation {
  // Retryable: another process has the file open with an incompatible share mode.
} catch let error {
  log("open failed: \(error)")                   // localized system message
  log("open failed: \(error.debugDescription)")  // ERROR_ACCESS_DENIED (5, 0x5)
  throw Errno(approximating: error)              // an explicitly lossy conversion
}
#endif

Detailed design

Wrapper constants and functions introduced here are @_alwaysEmitIntoClient. All APIs carry the availability of the System release that introduces them.

The Win32 namespace

#if os(Windows)
/// A namespace for Win32 APIs on Windows.
///
/// The types and functions nested here wrap the Win32 base API, the
/// documented, ABI-stable system interface for desktop Windows.
@frozen
public enum Win32 {}
#else
@available(*, unavailable, message: "Win32 APIs are only available on Windows.")
public enum Win32 {
  public struct Error {}  // Each nested type gets an empty stub.
}
#endif

Other platforms get an unavailable stub for Win32 and for each type nested in it, so naming one as a type reports that it's Windows-only. Their members aren't declared on other platforms.

Win32.Error

extension Win32 {
  /// A Windows system error code.
  ///
  /// This represents a value reported by `GetLastError`, or a code that
  /// System synthesizes. See ``isSynthesized``.
  @frozen
  public struct Error: RawRepresentable, Swift.Error, Sendable, Hashable, Codable {
    /// The raw C error code.
    public let rawValue: DWORD

    /// Creates a strongly-typed error from a raw C error code.
    public init(rawValue: DWORD)

    /// Creates a strongly-typed error from a raw C error code.
    public init(_ rawValue: DWORD)

    /// The operation completed successfully.
    ///
    /// The corresponding C constant is `ERROR_SUCCESS`.
    public static var success: Error { get }

    // ... the curated set of named codes:
    public static var invalidFunction: Error { get }        // ERROR_INVALID_FUNCTION
    public static var fileNotFound: Error { get }           // ERROR_FILE_NOT_FOUND
    public static var pathNotFound: Error { get }           // ERROR_PATH_NOT_FOUND
    public static var accessDenied: Error { get }           // ERROR_ACCESS_DENIED
    public static var invalidHandle: Error { get }          // ERROR_INVALID_HANDLE
    public static var deviceNotReady: Error { get }         // ERROR_NOT_READY
    public static var cyclicRedundancyCheck: Error { get }  // ERROR_CRC
    public static var writeFault: Error { get }             // ERROR_WRITE_FAULT
    public static var sharingViolation: Error { get }       // ERROR_SHARING_VIOLATION
    public static var lockViolation: Error { get }          // ERROR_LOCK_VIOLATION
    public static var notLocked: Error { get }              // ERROR_NOT_LOCKED
    public static var negativeSeek: Error { get }           // ERROR_NEGATIVE_SEEK
    public static var endOfFile: Error { get }              // ERROR_HANDLE_EOF
    public static var brokenPipe: Error { get }             // ERROR_BROKEN_PIPE
    public static var notSupported: Error { get }           // ERROR_NOT_SUPPORTED
    public static var fileExists: Error { get }             // ERROR_FILE_EXISTS
    public static var invalidParameter: Error { get }       // ERROR_INVALID_PARAMETER
    public static var insufficientBuffer: Error { get }     // ERROR_INSUFFICIENT_BUFFER
    public static var invalidName: Error { get }            // ERROR_INVALID_NAME
    public static var badPathName: Error { get }            // ERROR_BAD_PATHNAME
    public static var alreadyExists: Error { get }          // ERROR_ALREADY_EXISTS
    public static var fileTooLarge: Error { get }           // ERROR_FILE_TOO_LARGE
    public static var diskFull: Error { get }               // ERROR_DISK_FULL
    public static var pipeBusy: Error { get }               // ERROR_PIPE_BUSY
    public static var noData: Error { get }                 // ERROR_NO_DATA
    public static var pipeNotConnected: Error { get }       // ERROR_PIPE_NOT_CONNECTED
    public static var moreData: Error { get }               // ERROR_MORE_DATA
    public static var notADirectory: Error { get }          // ERROR_DIRECTORY
    public static var operationAborted: Error { get }       // ERROR_OPERATION_ABORTED
    public static var ioPending: Error { get }              // ERROR_IO_PENDING
    public static var userMappedFile: Error { get }         // ERROR_USER_MAPPED_FILE
    public static var cannotResolveFileName: Error { get }  // ERROR_CANT_RESOLVE_FILENAME

    // ... and a code System synthesizes, which Windows never reports:
    public static var incompleteTransfer: Error { get }     // 0xA0535901

    /// Whether System synthesized this error instead of Windows reporting it.
    public var isSynthesized: Bool { get }
  }
}

extension Win32.Error: CustomStringConvertible, CustomDebugStringConvertible {
  public var description: String { get }
  public var debugDescription: String { get }
}

extension Win32.Error {
  public static func ~= (_ lhs: Win32.Error, _ rhs: Swift.Error) -> Bool
}

The named constants are not exhaustive. They cover every code that System's Win32 APIs document plus codes that callers commonly branch on, and rawValue covers everything else. A struct over DWORD keeps unknown codes representable and round-trippable, which is required for a type modeling an error space of several thousand values that any application can extend with SetLastError.

The last code is System's own and uses a base of 0xA0535900. Windows reserves bit 29 (APPLICATION_ERROR_MASK) for application-defined codes, and setting bit 31 makes the code negative so HRESULT_FROM_WIN32 leaves it unchanged. isSynthesized checks for System's base.

System may reuse a Windows code if it accurately describes the condition, even for a check Windows doesn't make itself. For example, a wrapper throws .invalidParameter for an argument it rejects before calling Win32, such as an overlapped flag or a negative offset, and .notADirectory when a directory open doesn't resolve to a directory. System only synthesizes a code when no Windows code fits. One example is .incompleteTransfer, which is thrown in SYS-0014 when a WriteFile succeeds without writing anything.

~= lets a catch clause match a Win32.Error pattern against an untyped error, as Errno's does.

description and debugDescription

Like Errno, Win32.Error provides a human-readable description using FormatMessageW. As with strerror, the message is localized to the system or thread locale, so it's suitable for display or logging, but not for programmatic matching. FormatMessageW allocates, so description should be avoided in hot paths. description strips the trailing \r\n that system messages end with, and falls back to a numeric rendering when FormatMessageW fails.

debugDescription gives the symbolic constant with the decimal and hexadecimal value, such as ERROR_SHARING_VIOLATION (32, 0x20), and the numeric form alone for codes that have no name. It's locale-independent and suitable for tests or structured logs.

A synthesized code has no system message, so both properties render it from a local table instead.

Capturing the last error

GetLastError is thread-local and volatile. The next Win32 call on the thread clobbers it, and so can an ARC release that runs a deinit between the failing call and the error read. Every wrapper in the namespace must capture the code into a Win32.Error immediately at the failure site.

Some Win32 functions return a value that can mean either success or failure, such as GetFileType's FILE_TYPE_UNKNOWN (SYS-0015), and don't clear the last error when they succeed. In this case, a System wrapper must call SetLastError(ERROR_SUCCESS) before the function so users don't have to.

Certain Win32 functions may succeed and set the last error to a code that carries information. For example, CreateFileW with OPEN_ALWAYS or CREATE_ALWAYS can return a valid handle and set ERROR_ALREADY_EXISTS to report that the file already existed. A throws signature can't express this, so System APIs must surface it through parameter or return types instead. See SYS-0012. If the code instead means the operation didn't happen, the wrapper throws it. For instance, a console ReadFile interrupted by Ctrl+C succeeds with ERROR_OPERATION_ABORTED, so the reads in SYS-0014 throw .operationAborted instead of reporting the end of the file.

A Win32 function may also fail while GetLastError reports ERROR_SUCCESS. Wrappers report the error directly from the system, so a throws(Win32.Error) API can throw .success.

Interoperating with Errno

The conversion to Errno is lossy:

extension Errno {
  /// The closest POSIX equivalent of a Windows system error.
  ///
  /// This mapping is lossy. It approximates the C runtime's `_dosmaperr`
  /// behavior, which folds Windows' several thousand system error codes onto
  /// sixteen ``Errno`` values. Unrecognized codes become
  /// ``Errno/invalidArgument``. Prefer handling ``Win32/Error`` directly.
  public init(approximating error: Win32.Error)
}

This gives the existing internal Errno(windowsError:) a public spelling. Its behavior does not change, because the POSIX shim still needs the behavior, but the lossiness becomes part of the contract instead of an implementation detail. The approximating: label carries that warning at the call site.

Source compatibility

This proposal is additive and source-compatible with existing code.

ABI compatibility

This proposal is additive and ABI-compatible with existing code.

Implications on adoption

Win32 and everything in it is only available on Windows, so cross-platform callers must guard their uses. This is intentional so the platform dependency is visible at the use site rather than hidden behind a portable-looking type with unportable behavior.

Future directions

Later stages

These stages could follow the proposals in the Introduction:

Future direction Mentioned in
Querying file information Below
Setting file information Below
Security descriptors, ACLs, and SIDs Below, SYS-0012
Directory enumeration, change notification, and creation SYS-0012
Creating and querying named pipes SYS-0012, SYS-0015
Standard handles and console detection SYS-0011, SYS-0015
Asynchronous I/O, including scatter/gather SYS-0011, SYS-0014
Volume queries by path, including free space SYS-0017
DeviceIoControl and FSCTL codes SYS-0018
Portable metadata over Stat and the Win32 types SYS-0006, SYS-0012, SYS-0017

Querying file information. GetFileInformationByHandle and GetFileInformationByHandleEx provide file metadata on Windows: attributes, file identifiers, alignment, compression, remote protocol details, alternate data streams, and directory enumeration. A future proposal should nest those information classes and their associated types in Win32.

Setting file information. SetFileInformationByHandle is the natural companion and reaches semantics that have no FileDescriptor spelling at all, such as FILE_DISPOSITION_POSIX_SEMANTICS and FILE_RENAME_POSIX_SEMANTICS. It should follow once the query side has settled the shape of the information-class types.

Security descriptors. GetSecurityInfo, SetSecurityInfo, ACLs, and SIDs reach beyond the file system. They're the Windows analog of FilePermissions and UserID, and are large enough to be their own effort. That effort would build on the Win32.SecurityDescriptor that SYS-0012 introduces.

Win32.HResult

COM-shaped APIs in the Win32 surface (the PathCch* family in particular) report an HRESULT rather than a system error code, and System's Windows path canonicalization already flattens one with a private helper. The namespace will eventually need a public story for this, most likely a minimal Win32.HResult wrapper with a var win32Error: Win32.Error? that extracts the code only when the facility is FACILITY_WIN32. None of the proposals in this series need to surface it publicly, so it's left out for now.

Alternatives considered

Throw Errno from Win32 APIs too

Report failures from the new Win32 APIs as Errno, converting through the existing mapping table. This would be consistent with the rest of System and would avoid leaving Windows with two error types.

Rejected for the reasons in Errno can't express Windows failures.

Extend _mapWindowsErrorToErrno instead of adding a type

Give the mapping table more entries and finer distinctions, so that Errno remains System's only error type on every platform.

Rejected because:

  • A better table still couldn't express the failures that Errno has no values for, as Errno can't express Windows failures describes.
  • Changing the table would change the errno values that existing FileDescriptor operations report, which would be a breaking behavior change. The shim should keep matching the C runtime.

No namespace, prefixed top-level names

Spell the types Win32Error, Win32FileHandle, and so on, with no enclosing type.

Rejected because:

  • A namespace is the canonical way to organize and document which of Windows' several overlapping API layers is being wrapped.
  • Mach and Mach.Port already provide precedent in System.

A separate Win32System module

Vend the Windows APIs from their own module instead of a namespace, possibly alongside a POSIXSystem module for System's Unix-only types.

Rejected because Swift modules don't namespace their top-level names. A top-level Error would shadow Swift.Error in any file that imports the module, and a FileHandle would clash with Foundation's, so the module would still need the Win32 namespace, or prefixed names. Whether that namespace lives in its own module is a packaging choice that doesn't change these APIs.

Name the namespace Windows

Name the namespace for the platform rather than the API layer. This may be safer if System later wraps something that's not Win32.

Rejected because:

  • It's more vague and reads as a synonym for os(Windows).
  • The APIs it would future-proof against (the NT Native API, the Shell, WinRT) are explicitly excluded from this proposal.

Expose a CInterop.DWORD typealias

Spell raw Win32 values through CInterop, so that Win32.Error.rawValue would be a CInterop.DWORD. This would match how UserID stores a CInterop.UserID.

Rejected because:

  • CInterop aliases C types whose definitions vary by platform, such as mode_t and uid_t. DWORD has one definition, and every API that uses it is Windows-only.
  • Mach, the precedent for this namespace, spells Darwin's types directly, such as mach_port_name_t.
  • Code that reads or passes a raw value is calling Win32 functions, so it already imports WinSDK.

Name the error type Win32.ErrorCode

Win32.ErrorCode is more literal, since Windows calls these "system error codes", and it would avoid shadowing Swift.Error inside the namespace.

Rejected because it's verbose and breaks the convention Foundation and other Swift projects follow, where error types are named like POSIXError and URLError, and Code names their nested code types. The shadowing only affects code written inside extension Win32 { }, which would be mostly System's own implementations.

An enum with cases instead of a struct

Give Win32.Error a case per named code, making a switch over it exhaustive.

Rejected because an enum can't represent a code the library has not enumerated, and the Windows error space is open-ended.

A public Win32.Error.current

A GetLastError wrapper, with a setter for SetLastError, would let callers read and clear the last error themselves.

Rejected because:

  • Such an accessor can't be made correct by construction. Swift may insert an ARC release between the failing call and the read, and the caller's own intervening work could clobber the value, too. Errno.current is internal for the same reason.
  • Callers using raw Win32 functions already have GetLastError and SetLastError, and can wrap a result with Win32.Error.init(_:).

Synthesize a code instead of throwing .success

When a Win32 function fails but GetLastError reports ERROR_SUCCESS, throw a synthesized code, so that a logged error doesn't read "The operation completed successfully."

Rejected because it trades fidelity for a friendlier log line. When Windows reports a failure, the thrown error is the code Windows reported, even ERROR_SUCCESS. System synthesizes codes only for results that Windows reports as success, like the WriteFile behind .incompleteTransfer. The case is rare, and debugDescription still identifies it as ERROR_SUCCESS (0, 0x0).

Appendix

Swift API to C mappings

Swift C
Win32.Error DWORD system error code
Win32.Error.description FormatMessageW with FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_ALLOCATE_BUFFER, and FORMAT_MESSAGE_IGNORE_INSERTS
Errno.init(approximating:) _dosmaperr (as _mapWindowsErrorToErrno)

Testing was performed on an ARM64 Windows 11 VM (build 22631). The pipe error codes were reproduced on an x64 Windows 11 PC (build 26200).

6 Likes

Let me start off with, I think that this is amazing to see! I think that there are some points of merit to this pitch, but I feel that there is still space for refinement here. I'm also concerned about some of the design decisions it is forcing.

I'm confused - I thought that when I redid the work for swift-foundation, the majority of the file IO operations were moved back to the Win32 API surface. We bridge to ucrt (particularly for things like FileHandle with _get_osfhandle and such, but the core of file system APIs should be Win32 shaped. Am I misremembering or misunderstanding something?

I need to look through the other proposals but the immediate concern is would these be compatible? Or would this mean that more code would now be #if os(Windows) guarded?

This is primarly a point about the prose rather than anything else - the point that the section is making is absolutely correct, we shouldn't be trying to rehape the windows error codes as errno. errno is still unfortunately important for Windows. Any UCRT operation should consult errno, but any Win32 operation should consult GetLastError.

I think that I don't like the particular proposed shape: Win32.Error. I think that is a more natural spelling for the type. This does not interfere with the API layer being wrapped IMO. The different layers are clear:
errno: UCRT/MSVCRT
DWORD: Win32
HRESULT: User Space
NTSTATUS: Kernel Space

However, the shape of the API (a newtype wrapper over the underlying DWORD storage). With the comparator for constants, I think that this would be really nice to have.

For the system code, e.g. incompleteTransfer, bit-29 is the "C" bit, indicating that it is a customer error code. Similarly bit-31 is the severity bit (1 indicates a failure, 0 indicates a success). None of this changes the design, simply the text.

Now the pieces that I have stronger opinions on Win32.HResult. COM-shaped APIs IMO should raise COMError which is part of the COM module (see the previous post about COM interoperability). It sounds like you want to be able to decompose the contents of a HRESULT, which sounds very reasonable. However, IMO, that should be treated as extension HRESULT with getters for the components rather than a newtype.

I think that there is a type that I've been carrying for a while that I think is the proper long term shape for error handling on Windows.

public struct WindowsError: Error {
  public enum ErrorCode: Sendable {
    case nt(NTSTATUS)
    case win32(DWORD)
    case hresult(HRESULT)
  }

  internal let code: ErrorCode

  public init(_ status: NTSTATUS) {
    self.code = .nt(status)
  }

  public init(_ dwCode: DWORD = GetLastError()) {
    self.code = .win32(dwCode)
  }

  public init(hr: HRESULT) {
    self.code = .hresult(hr)
  }
}

Your proposal would change the enum variant from win32(DWORD) to win32(Win32Error), which is the right thing for Swift as it gives us a better type safe representation.

The throwing .success case is .... difficult to follow. I would suggest that a C code snippet would make it easier. Re-reading it a few times helped me understand what was being said, at which point I came to the same conclusion.

Overall, I think that there is a bunch of editorial noise, but we are pretty close to agreement here.

1 Like

You're remembering swift-foundation correctly, but it uses the raw Win32 APIs directly, it doesn't use System. The goal of these proposals is to introduce public swift-system APIs that are Win32-shaped. The Swift wrappers here aim to remove unsafety and the need for arcane knowledge, like knowing that FILE_FLAG_BACKUP_SEMANTICS is required to open a directory handle. A cross-platform library like swift-foundation could then use these System APIs in its Windows code instead of e.g. raw HANDLEs and GetLastError.

These APIs are additive so they're compatible with existing FileDescriptor/UCRT-based System APIs, and SYS-0013 converts between FileDescriptor and Win32.FileHandle. The Win32 namespace and everything in it would be #if os(Windows) guarded, so it's not portable, but it signifies that all APIs in the namespace carry the semantics of the underlying Win32 calls.

Agreed. FileDescriptor as a portable POSIX layer isn't going anywhere, and likewise cross-platform code might decide to use Errno as a portable error type. But for Windows-specific use-cases or implementation, these System APIs offer a nice abstraction over the GetLastError codes.

Good to know, will update the text!

An HRESULT wrapper isn't needed for any of the public APIs in the proposals, so I'm happy to punt this or reword the future directions. I do think a strongly-typed wrapper would be a better option than extending HRESULT since that's just a typealias for Int32, right?

To clarify, you agree that a typed wrapper for Win32 errors over a DWORD is a good idea, but don't like the namespaced shape Win32.Error and would prefer a top-level Win32Error instead? The namespace is mainly so later types in the series like Win32.FileHandle and Win32.FileType are hosted in one place and don't conflict with e.g. top-level FileHandle from Foundation or reuse the existing FileType name from System, which has different semantics on other platforms. All APIs in the Win32 namespace then throw Win32.Error, which is nice and consistent, but I'm interested to hear more of your thoughts on the name/shape.

The WindowsError type seems very useful. I don't think System should provide public wrappers over the NT APIs, but maybe it could hold the error type?

Yeah... can you tell it was a last-minute addition? :laughing: I'll make this clearer and add a snippet.

1 Like

cries oh, yes, I did commit those atrocious crimes of using the ucrt APIs as the System APIs were trying to be C shaped.

Only further pushes the "this is a really good idea".

While I don't generally like this model, I do think that in this case it happens to have some value. You have different pieces on Windows, and being able to use either is helpful. This allows the developer to choose the correct layer (Win32 vs UCRT).

Good point, a type wrapper might be needed :frowning:

Correct on both counts.

I should've been more explicit, I think that the error type should be Win32Error, but the remaining types (e.g. FileHandle, FileType, etc) should be namespaced. Yes, I realize this seems odd, but the problem is that Win32Error is pervasive and also one of many domains of errors. Even if Error didn't overlap with Swift.Error, I would say that Win32Error is a better name. There are domains, and we really want the shape of the type to be ABI faithful to the underlying type here. The error types often cross boundaries as they are somewhat of a currency type, and so it feels like Win32Error + the Win32 namespace gives us the best of both worlds.

I think that a newtype type wrapper, e.g. a complete implementation:

@frozen
public struct NTStatus: RawRepresentable {
  public let RawType = DWORD
}

wouldn't be unreasonable. I agree that we don't need to enumerate the helpers for it, but the typed representation isn't unreasonable. This is particularly because the case as use of Nt* APIs is somewhat common in systems work (this isn't the same as exposing Zw* APIs).

2 Likes

OK I'm starting to like the split, too. I can see how a Win32Error would be generally useful outside of the Win32 namespace, and it aligns nicely with other top-level errors (POSIXError, MachError, Errno). Future NTStatus and HResult error types would also fit in with their peers.

This then follows a clear rule: "error domains are top-level, API families can get namespaces." Win32Error would still be Windows-only like the rest of Win32 since Windows is the only platform that reports these codes.

Unless there are objections, I'll update the proposals to reflect this change to Win32Error.

I agree, especially given the reasoning above, but I'll keep it out-of-scope for these proposals and add it as a future direction (though I think wrapping NTSTATUS itself).

1 Like

It sounds like we are in agreement. This feels reasonably out of scope for this proposal. I might even go so far as to say, I don't see the immediate rush for this even. It can happen at the appropriate time.

PR is up for v2 of the proposal and implementation:

The PR incorporates the feedback discussed here and includes all doc comments for the error codes. Please let me know your thoughts, thanks!

, and setting bit 31 makes the code negative so HRESULT_FROM_WIN32 leaves it unchanged.

This bit is a bit confusing. AFAIK, the reason the macro is implemented as is (i.e. negative values are untouched) is because that is simply the encoding for bit-31. Win32 codes are positive (DWORD is unsigned) and technically 16-bit limited. Therefore, any value with bit-31 set is already likely a HRESULT rather than a win32 error code, so it simply does nothing.

It would be better to say that the extensions are not really win32 error codes but rather a HRESULT. In fact, if you attempt to extract the error code, i.e. HRESULT_CODE, you will find that the bits you are describing here are discarded as they are not considered as part of the code itself.

1 Like

That makes sense. I removed much of the previous wording so the entire paragraph is now:

The last code is System's own and uses a base of 0xA0535900. It sets the customer bit (29) and the severity bit (31), so it's a customer-defined failure HRESULT rather than a system error code. isSynthesized checks for System's base.

1 Like