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!