SE-0529: Add FilePath to the Standard Library

Hello, Swift community.

The review of SE-0529: Add FilePath to the Standard Library begins now and runs through May 4th, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via either email or the forum messaging feature. When contacting me directly as the review manager, please put [SE-0529] at the start of the subject line.

Trying it out

Many aspects of the proposed type are already available in the swift-system package. The authors are currently working on a standalone package that will provide the proposed API, and I will update this thread (and this section) as soon as that's available.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here.

Thank you for helping to make Swift a better language through your review.

John McCall
Review Manager

13 Likes

Love the attention to detail in this pitch!

Agree wholeheartedly against "alternatives considered":

  • don't conform to Codable β€” too many problems when the desirable representation is usually a JSON String, but that doesn't work in all cases.
  • don't attempt to use Foundation.URL for this purpose. It's a big mess, barely suitable for use as a URL, let alone for filesystem usage.

Absolutely +1 on the proposal, desperately needed addition to the stdlib.

Nitpicks:

  public static var separator: Character { get }
    public var driveLetter: Character? { get }

Should these be exposed as FilePath.CodeUnit or even Span<FilePath.CodeUnit> instead (to cover a possible future need for > 1 code unit)? It doesn't seem very useful for the FilePath APIs to have a unicode character?

Relative path components are non-empty opaque bags of bytes

opaque sequences of FilePath.CodeUnit? They're not completely arbitrary bytes, depending on the platform

  public var nullTerminatedCodeUnits: Span<FilePath.CodeUnit> { get }

I couldn't find a precedent for this in the stdlib or Foundation, but most of the comments in the stdlib source I found make a distinction between "null" for a pointer, and "nul" for a code unit. Should this be spelled with only 1 l?

+1

This is a great addition! I’ve already had to implement basic paths and normalizing in several projects because this did not exist except for URL (which is way too generalized and requires Foundation).

Personally, I would love to see this conform to Codable just for the ease of use in JSON and YAML serialization, but I understand why this isn’t included.

I am curious why .. is not normalized when reconstructing the path. On MacOS and Linux, this would be pretty straightforward, although I have no clue if there are any weird cases on other non-POSIX platforms. For me, this would make equality a bit more meaningful without needing to resolve symlinks.

For the resolve() function, what error types would be thrown? Even though this may not be part of the public API, I’m just wondering what information you would be able to get out of it (error code and/or strerror-style description).

Years ago, I had raised the opposite concern about nul being weird-looking. The reply then was that the preference was for nul.

The reason you can't find the precedent so easily now, though, is that we renamed the APIs in SE-0134, in part because we convinced the core team that nul was weird-looking.

(As a sign that I am losing my marbles, I had no recollection that I was the first co-author of that proposal.)

6 Likes

.. cannot be normalized away on Unix-like systems because of symbolic links (and hard links, and probably some other corner cases).

3 Likes

I also thought there's this a dichotomy with null being used for pointers and nul for characters.

Edit: however we've already been through it (just found this),

so hiding to reduce noise

Wikipedia agrees:

In software documentation, the null character is often represented with the text NUL (or NULL although that may mean the "null pointer".

Precedents in Swift? Not many, mainly in comments, and there's some inconsistency, sometimes in the same file:

UnicodeScalar.swift

/// Creates an instance of the NUL scalar value.

StringObject.swift

// Whether this string is native, i.e. tail-allocated and nul-terminated,

StringObject.swift

// Whether the object provides fast UTF-8 contents that are nul-terminated

StringObject.swift

// Small strings nul-terminate when spilling for contiguous access

StringUTF8View.swift

/// A contiguously stored null-terminated UTF-8 representation of the string.

LexerDiagnosticMessages.swift
case nulCharacter = "nul character embedded in middle of file"

0464-utf8span-safe-utf8-processing.md

An overlong encoding of NUL, 0xC0 0x80, is used in Java's Modified UTF-8 but is invalid UTF-8.

0464-utf8span-safe-utf8-processing.md

Future work include tracking whether the contents are NULL-terminated (useful for C bridging)

I'm thrilled to see this proposal come to review. The journey from the initial discussion in Pitch: System in the toolchain to this really focused and well-written proposal was great to follow! I am very supportive of this proposal and think it fills an important gap in the ecosystem that deserves to be addressed with a standard library type. I have two minor questions:

Extracting documentation content from the proposal

The proposal goes to great length to explain the messy world of path handling across platforms. Can we add this information into an article in our new stdlib documentation catalog?

System.FilePath migration

I think migrating System.FilePath in the package and in any vendor's SDK through ABI redirection is great. I just want to clarify that all the extensions that would be added by SystemPackage and System are only available when developers import either SystemPackage or System, right?

If the outcome of SE-0134 was that neither null nor nul is desirable to refer to string termination in API names, should

 public var nullTerminatedCodeUnits: Span<FilePath.CodeUnit> { get }

be renamed to fileSystemRepresentation (several n precedents in Foundation)?

Thank you, this looks really good.

I have only one concern:

  public func resolve() throws -> FilePath

is (as I understand it) a function that consults the actual in-kernel file system. This means that this can and will block ~forever, especially on networked file systems or file systems in user space that require networked content.

At the very least, we must mark this function as noasync. But really, we should either

  • (preferred) remove this function from the pitch
  • (if need be) keep it, but change its name to be explicit. Something to the tune of blockingResolveWithFileSystem() (hopefully we can find a better name) that makes it abundantly clear that this could block forever and uses the actual file system.
5 Likes

There was some discussion in the pitch thread about whether this type should offer some kind of withCString-like API for interacting with other APIs. I won't move the whole conversation over, but let me just repeat what I said there:

I think it's a great goal for Swift code to always be able to pass around local file paths as FilePaths. However, most path manipulation is ultimately in service of creating a path that can be passed off to some OS API, and if there's no standard way for arbitrary code to actually do that without writing its own copy-into-a-buffer code, that seems to undermine that goal rather than support it. Presumably we will eventually have a batteries-included local I/O library that just works with FilePath, but we don't have that now, and even when we do, it won't wrap every OS API that takes a path, much less every third-party C API. I think not having a convenient way to get a const char * / LPCWSTR / whatever out of this API would be a mistake.

4 Likes

if I may hop on my soapbox… there will always be platform-specific or low-level code that Swift needs to interface with. Even if we wrote our own 100% comprehensive I/O package, that library would still eventually need to make syscalls etc. from Swift. Short of an OS whose entire stack is written in Swift (an unlikely future for, say, Windows!), there's no way to avoid the FFI and the need to get a C string from a file path.

I view C FFI as being something of a lingua franca between languages; while it's not necessary to have high ergonomics to make the interop work, it's not unreasonable to have some affordances to make it easier either.

Thanks for catching this. I didn't spot this when reading the proposal, but +1 from me on removing this. In my personal opinion, the FilePath type should act as a currency type that offers no interactions with the actual file system. File system operations are a gap currently, but I think we need to design new interfaces for that in separate proposals integrated with concurrency to leverage asynchronous non-blocking file operations on systems where possible.

4 Likes

Perhaps, if it’s felt that some way of performing this operation is essential, we might consider a spelling that makes it clear that the file system is involved, not as a member of FilePath. Maybe something like FileSystem.resolve(_: FilePath). This may require a holistic exploration of such a namespace though.

3 Likes

Hi all, here is a reference implementation as a package that you can try out.

Trying other platforms

Rather than use conditional compilation for the target platform, the reference package uses global variables so that reviewers can try out behavior on other platforms:

  public enum REVIEW_ONLY_Platform {
    case linux, darwin, windows
  }

  extension FilePath {
    public static var REVIEW_ONLY_platform: REVIEW_ONLY_Platform { get set }
  }

NOTE: This is not even remotely thread/concurrency safe, it's a dirty hack for review purposes.

Interactive path playground

We also have a command line tool where you can supply paths and it will show you the decomposition followed by API results for all platforms.

swift run filepath-play '/usr/local/bin' 'C:\Users\Admin\' '\\server\share\docs' '/.vol/1234/5678/file' '/file/..namedfork/rsrc'

Each path is decomposed across all three platforms. The summary line shows anchor | components | suffix:

  input: "/usr/local/bin"
    ═══ linux ═══   "/" | "usr", "local", "bin" | (none)
    ═══ darwin ═══  "/" | "usr", "local", "bin" | (none)
    ═══ windows ═══ "\" | "usr", "local", "bin" | (none)

  input: "C:\Users\Admin\"
    ═══ linux ═══   (none) | "C:\Users\Admin\" | (none)
    ═══ darwin ═══  (none) | "C:\Users\Admin\" | (none)
    ═══ windows ═══ "C:\"  | "Users", "Admin"  | trailing separator

  input: "\\server\share\docs"
    ═══ linux ═══   (none)            | "\\server\share\docs" | (none)
    ═══ darwin ═══  (none)            | "\\server\share\docs" | (none)
    ═══ windows ═══ "\\server\share"  | "docs"                | (none)

  input: "/.vol/1234/5678/file"
    ═══ linux ═══   "/"               | ".vol", "1234", "5678", "file" | (none)
    ═══ darwin ═══  "/.vol/1234/5678" | "file"                         | (none)
    ═══ windows ═══ "\"               | ".vol", "1234", "5678", "file" | (none)

  input: "/file/..namedfork/rsrc"
    ═══ linux ═══   "/" | "file", "..namedfork", "rsrc" | (none)
    ═══ darwin ═══  "/" | "file"                         | /..namedfork/rsrc
    ═══ windows ═══ "\" | "file", "..namedfork", "rsrc" | (none)

You can also run it interactively β€” just swift run filepath-play and type paths at the prompt.

4 Likes

100% agree: we can do much better than the standard C APIs at building non-blocking interfaces for real I/O, but this proposal isn't the place to do it.

1 Like

Wouldn't it be better if this was an async function in the API (even if for the pitch and for a short while) it will have blocking behaviour under the hood?

public func resolve() async throws -> FilePath {
    _someUnderlyingBlockingCall()
}

Then when time allows it could be reimplemented to be truly async without changing the API.

1 Like

My personal experience writing Swift tends to be at lower-level and dealing with nitty-gritty details, so I'm not necessarily the best arbiter of taste for these concerns. I proposed Character because it's the type people reach for when printing or appending directly to a String.

For separator and drive letter, we have a few options here:

  • Character: the high-level what-you-see when you print type. It's what you get when iterating a String by default, etc.
  • Unicode.Scalar: a more technically-precise fit, as it's a Unicode-correct wrapper around a single UInt32
  • Platform-specific numeric type, whether via FilePath.CodeUnit or UTF8/16.CodeUnit (since these are always valid Unicode)

The nice thing about either Character or Unicode.Scalar is that you can compare against a string literal. This wouldn't be the case for a numeric type. You can go from either Character or Unicode.Scalar to the equivalent of FilePath.CodeUnit via .utf8.first! and/or utf16.first!. You can go from Character to Unicode.Scalar via .unicodeScalars.first!. Unicode.Scalar is the closest to a single numeric value that's also ExpressibleByStringLiteral as you can go to UInt32 via .value without any optionals involved.

The nice thing about FilePath.CodeUnit would be that it's what you want when you're parsing the raw contents yourself. Path parsing primitives are called out as Future Work, but we might consider factoring some of it into the ABI to enable back-deployment of future API.

FilePath as proposed takes a platform (i.e. kernel)-level view of the path, and the kernel dictates path syntax and the valid set of bytes for components. Typically, not NUL and not a separator. The exact sequence . or .. is specially handled by the kernel before consulting the filesystem driver. For Windows verbatim-component paths (\\?\), it's only not NUL and not the platform separator, but the others (including forward slashes) are allowed and there is no special handling of . or ...

In the process of performing operations, the kernel will hand off these components to whatever file system happens to respond to that location in the VFS. Specific file systems have their own rules and interpretations, their own behaviors around things like case-sensitivity and normalization, their own sets of further valid and invalid bytes, etc. However, it is not known at the platform level what the eventual target file system (or file systems) are, nor how they are configured. Those particular details can, in theory, even change at run time.

The path's code units are more akin to a kernel-layer encoding of a set of instructions alongside a sequence of regions, where the contents of those regions are encoded in the filesystem's representation (which is not known until resolution-time).

The idea of the proposal is that the only API available in the Swift module (which is implicitly imported into every module) is what is being formally proposed here. If the user imports System/SystemPackage then they may get additional API (and conformances such as the existing Codable one from system).

A corollary to this proposal is an upcoming deprecation pass over swift-system to remove anything that's no longer needed or has been replaced by something better (e.g. FilePath.Root). That makes sense to do immediately following this review.

I'll have to think a little bit more about this. FilePath is very closely coordinating and co-evolving with the XNU kernel on Darwin and I'm not sure where the best place for documentation is at the moment.

One of the challenges in writing this proposal is determining the line between what is relevant to API review vs what is an implementation detail. To build an understanding of how paths work and why decomposition is presented as it is and why some APIs behave the way they do, I have to describe some fairly-arcane subjects that would normally be considered an implementation detail even in the context of the kernel itself, much less FilePath.


My reading of @glessard 's statement is that the way to do C interop would be to defer to Span, not to make C interop impossible. That is, at some point in the evolution of Swift and Span family of types, there will be no need for new API to use unsafe pointers if that API can instead vend a Span. I do not know if we are at present far enough along to start doing so.

// Older style:
myFP.withUnsafeCPointer { some_c_function($0) }

// Proposed:
myFP.nullTerminatedCodeUnits.withUnsafeBufferPointer { some_c_function($0.baseAddress!) }

Where there might be future improvements for Span to be able to be passed to C more directly. For example, completely hypothetical, but if a type can vend a span it should be able to bridge to C without closures:

// Hypothetical: If Span bridged directly to C
some_c_function(myFP.nullTerminatedCodeUnits)

// Alternative: if Span vended a non-escapable pointer that could bridge to C
some_c_function(myFP.nullTerminatedCodeUnits.startPointer)

That being said, I'm open to adding a convenience wrapper withCString on top of the Span-based API for discoverability, especially since folks are used to looking for that name to do C interop.


Thanks for this point. I want to think a bit harder/longer before responding fully. The intention was to provide a correct resolution method from day 1 so that developers don't feel compelled to implement their own incorrect one.

2 Likes