[Pitch] StatFS and supporting types

Hi System enthusiasts!

I'd like to pitch new Swift API for the statfs family of syscalls and supporting types. The following shows a proposed solution with Swift API to C mappings, then some topics for discussion. Feedback is greatly appreciated!

Proposed solution

This proposal adds a struct StatFS that is available on Unix-like platforms. For Windows, a future pitch could provide Swift-native wrappers of functions like GetVolumeInformation and DeviceIoControl with their associated types.

StatFS is a Swift wrapper around the C statfs or statvfs struct, which provides information about file systems. On Darwin and BSD operating systems, this type uses statfs for the additional information it provides. On other Unix-like operating systems, this type uses the standard statvfs interface. All initializers use a typed throws(Errno) and require Swift 6.0 or later (note swift-system 1.7.x already requires Swift >= 6.1).

// Get file system information from a path
let statfs = try StatFS("/")

// From FileDescriptor
let statfs = try fd.statfs()

// From FilePath
let statfs = try filePath.statfs()

print("File system ID: \(statfs.fileSystemID)")
print("Total space: \(statfs.totalSpace) bytes")
print("Available space: \(statfs.availableSpace) bytes")

// Check mount flags
if statfs.mountFlags.contains(.readOnly) {
    print("File system is read-only")
}

// Platform-specific information when available
#if os(anyAppleOS) || os(FreeBSD) || os(OpenBSD)
print("File system is mounted at \(statfs.mountPoint)")
print("File system is mounted from \(statfs.mountSource)")
#endif

Alongside StatFS, we would also add types for MountFlags, FileSystemID, FileSystemType, and FileSystemSubtype members of the struct. The available values for MountFlags differ significantly between platforms as shown in the mappings below. FileSystemType is available on Darwin and FreeBSD where statfs reports f_type. FileSystemSubtype is Darwin-only.

Swift API to C Mapping

The following tables show the mapping between Swift APIs and their underlying C system calls across different operating systems:

StatFS Initializer Mappings

The retryOnInterrupt: Bool = true parameter is omitted for clarity.

Swift API Darwin / FreeBSD / OpenBSD Linux / Android / WASI
StatFS(_ path: UnsafePointer<CChar>) statfs() statvfs()
StatFS(_ path: FilePath) statfs() statvfs()
FilePath.statfs() statfs() statvfs()
StatFS(_ fd: FileDescriptor) fstatfs() fstatvfs()
FileDescriptor.statfs() fstatfs() fstatvfs()

StatFS Property Mappings

" denotes the same property name across all operating systems.

Swift Property Darwin (statfs) FreeBSD (statfs) OpenBSD (statfs) Linux (statvfs) Android (statvfs) WASI (statvfs)
blockSize f_bsize " " " " "
preferredIOBlockSize f_iosize f_iosize f_iosize N/A N/A N/A
fragmentSize N/A N/A N/A f_frsize f_frsize f_frsize
totalBlocks f_blocks " " " " "
freeBlocks f_bfree " " " " "
availableBlocks f_bavail " " " " "
totalInodes f_files " " " " "
freeInodes f_ffree " " " " "
availableInodes N/A N/A f_favail f_favail f_favail f_favail
maximumNameLength N/A f_namemax f_namemax f_namemax f_namemax f_namemax
fileSystemID f_fsid " " " " "
mountFlags f_flags f_flags f_flags f_flag f_flag f_flag
type f_type f_type N/A N/A N/A N/A
subtype f_fssubtype N/A N/A N/A N/A N/A
owner f_owner f_owner f_owner N/A N/A N/A
typeName f_fstypename f_fstypename f_fstypename N/A N/A N/A
mountPoint f_mntonname f_mntonname f_mntonname N/A N/A N/A
mountSource f_mntfromname f_mntfromname f_mntfromname N/A N/A N/A

MountFlags Mappings

Swift Flag Darwin FreeBSD OpenBSD Linux Android WASI
readOnly MNT_RDONLY MNT_RDONLY MNT_RDONLY ST_RDONLY ST_RDONLY ST_RDONLY
synchronous MNT_SYNCHRONOUS MNT_SYNCHRONOUS MNT_SYNCHRONOUS ST_SYNCHRONOUS ST_SYNCHRONOUS ST_SYNCHRONOUS
noExecution MNT_NOEXEC MNT_NOEXEC MNT_NOEXEC ST_NOEXEC ST_NOEXEC ST_NOEXEC
noSetUserID MNT_NOSUID MNT_NOSUID MNT_NOSUID ST_NOSUID ST_NOSUID ST_NOSUID
noAccessTime MNT_NOATIME MNT_NOATIME MNT_NOATIME ST_NOATIME ST_NOATIME ST_NOATIME
noDevices MNT_NODEV N/A MNT_NODEV ST_NODEV ST_NODEV ST_NODEV
mandatoryLockingPermitted N/A N/A N/A ST_MANDLOCK ST_MANDLOCK ST_MANDLOCK
noDirectoryAccessTime N/A N/A N/A ST_NODIRATIME ST_NODIRATIME ST_NODIRATIME
relativeAccessTime N/A N/A N/A ST_RELATIME ST_RELATIME ST_RELATIME
asynchronous MNT_ASYNC MNT_ASYNC MNT_ASYNC N/A N/A N/A
exported MNT_EXPORTED MNT_EXPORTED MNT_EXPORTED N/A N/A N/A
local MNT_LOCAL MNT_LOCAL MNT_LOCAL N/A N/A N/A
quota MNT_QUOTA MNT_QUOTA MNT_QUOTA N/A N/A N/A
rootFileSystem MNT_ROOTFS MNT_ROOTFS MNT_ROOTFS N/A N/A N/A
union MNT_UNION MNT_UNION N/A N/A N/A N/A
automounted MNT_AUTOMOUNTED MNT_AUTOMOUNTED N/A N/A N/A N/A
multiLabel MNT_MULTILABEL MNT_MULTILABEL N/A N/A N/A N/A
exportedReadOnly N/A MNT_EXRDONLY MNT_EXRDONLY N/A N/A N/A
exportedByDefault N/A MNT_DEFEXPORTED MNT_DEFEXPORTED N/A N/A N/A
exportedAnonymously N/A MNT_EXPORTANON MNT_EXPORTANON N/A N/A N/A
softUpdates N/A MNT_SOFTDEP MNT_SOFTDEP N/A N/A N/A
contentProtection MNT_CPROTECT N/A N/A N/A N/A N/A
removable MNT_REMOVABLE N/A N/A N/A N/A N/A
quarantine MNT_QUARANTINE N/A N/A N/A N/A N/A
volumeFileSystem MNT_DOVOLFS N/A N/A N/A N/A N/A
noBrowsing MNT_DONTBROWSE N/A N/A N/A N/A N/A
ignoreOwnership MNT_IGNORE_OWNERSHIP N/A N/A N/A N/A N/A
journaled MNT_JOURNALED N/A N/A N/A N/A N/A
noUserExtendedAttributes MNT_NOUSERXATTR N/A N/A N/A N/A N/A
deferWrites MNT_DEFWRITE N/A N/A N/A N/A N/A
noSymlinkFollowAtMountPoint MNT_NOFOLLOW N/A N/A N/A N/A N/A
snapshot MNT_SNAPSHOT N/A N/A N/A N/A N/A
strictAccessTime MNT_STRICTATIME N/A N/A N/A N/A N/A
exportedKerberos N/A MNT_EXKERB N/A N/A N/A N/A
exportedPublic N/A MNT_EXPUBLIC N/A N/A N/A N/A
posixACLs N/A MNT_ACLS N/A N/A N/A N/A
geomJournaled N/A MNT_GJOURNAL N/A N/A N/A N/A
excludedFromDiskFreeReports N/A MNT_IGNORE N/A N/A N/A N/A
nfs4ACLs N/A MNT_NFS4ACLS N/A N/A N/A N/A
noClusterRead N/A MNT_NOCLUSTERR N/A N/A N/A N/A
noClusterWrite N/A MNT_NOCLUSTERW N/A N/A N/A N/A
setUserIDDirectory N/A MNT_SUIDDIR N/A N/A N/A N/A
softUpdateJournaling N/A MNT_SUJ N/A N/A N/A N/A
untrusted N/A MNT_UNTRUSTED N/A N/A N/A N/A
mountedByUser N/A MNT_USER N/A N/A N/A N/A
verified N/A MNT_VERIFIED N/A N/A N/A N/A
noPermissionChecks N/A N/A MNT_NOPERM N/A N/A N/A
writeExecuteAllowed N/A N/A MNT_WXALLOWED N/A N/A N/A
write N/A N/A N/A ST_WRITE N/A ST_WRITE
appendOnly N/A N/A N/A ST_APPEND N/A ST_APPEND
immutable N/A N/A N/A ST_IMMUTABLE N/A ST_IMMUTABLE
noSymlinkFollow N/A MNT_NOSYMFOLLOW N/A ST_NOSYMFOLLOW ST_NOSYMFOLLOW N/A

Discussion

1. Handling platform-availability of mount flags

As shown in the matrix above, many flag values are unique across the vast array of platforms. The current approach would be to #if guard the flags available on each platform, with each flag's doc comment detailing the platforms where it's available. However, this may not easily answer the question: "What are all the flags that are available on X platform?" Having the table mapping as documentation could help alleviate this, but is there perhaps a better option?

2. StatFS as a single type for statfs and statvfs

Currently, the proposal maps StatFS to statfs for the additional information it provides on platforms where it's available, and statvfs otherwise. We could introduce a separate StatVFS, but having separate types for statfs and statvfs might increase cognitive overhead and confusion for developers deciding which to choose. It would also require additional documentation explaining differences that are otherwise handled by platform availability, and would require duplicate code. However, one upside is that all properties of StatVFS would be available where the struct itself is available. Does this convenience outweigh the cost of having two types?

3. WASI Support

WASI is included for parity, and statvfs/fstatvfs are present in wasi-libc, so this API would compile and link there. However, in the current wasi-libc, statvfs and fstatvfs are unconditional stubs (in libc-bottom-half/sources/posix.c) that set errno to ENOSYS and return -1:

int statvfs(const char *__restrict path, struct statvfs *__restrict buf) {
    // TODO: We plan to support this eventually in WASI, but not yet.
    errno = ENOSYS;
    return -1;
}

As a result, on WASI these StatFS initializers would always fail at runtime and throw Errno.noFunction. Callers targeting WASI must therefore be prepared for StatFS to be effectively unavailable at runtime. The current proposal keeps the API surface available on WASI so that code remains source-compatible across Unix-like platforms and can benefit automatically if a WASI gains real statvfs support in the future. I think this is reasonable because clients are expected to handle a potential Errno anyway, and it means once wasi-libc gains real statvfs support, System picks it up automatically without needing to track it. But I'm interested to hear your feedback!

Thanks again for your feedback and consideration!

2 Likes

One thing that I think that need to understand is how to actually ensure that these types do not get adopted within the toolchain itself (e.g. LSP). With the module being unavailable on Windows, this would be something that becomes problematic as that means that you could end up using APIs that are not available on Windows and might result in functionality being left out of the platform level support.

To make sure I'm understanding correctly, the concern is that:

  1. A toolchain project adds new functionality using Stat/StatFS, which builds locally on their macOS/Linux setup
  2. Windows CI breaks because Stat/StatFS are #if !os(Windows)
  3. The project decides to #if !os(Windows) guard their new feature, detracting from the goal of first-class Swift support on Windows

Is that correct?

I'm not sure System as a low-level library can technically prevent toolchain projects from adopting these types. I think the best answer might be to use these platform-specific types as a foundation for more portable types (e.g. in Foundation or another project), and to document the unavailability loudly. We could use an annotation like @available(*, unavailable, message: "StatFS is unavailable on Windows; use <portable API> for cross-platform code").

Yes, that is correct. There have many instances of Windows being excluded this way (macros, bootstrapping, etc all of which had to then be repaired urgently as a last second request, often without assistance from the feature's author).

Yes, a loud warning which cannot be silenced would be reasonable enough for this I think.
I think that should hopefully achieve the goal.