std.File.MemoryMap updates

- change offset to u64
- make len non-optional
- make write take a file_size parameter
- std.Io.Threaded: introduce disable_memory_mapping flag to force it to
  take the fallback path.

Additionally:

- introduce BlockSize to File.Stat. On Windows, based on cached call to
  NtQuerySystemInformation. On unsupported OS's, set to 1.
- support File.NLink on Windows. this was available the whole time, we
  just didn't see the field at first.
- remove EBADF / INVALID_HANDLE from reading/writing file error sets
This commit is contained in:
Andrew Kelley
2026-01-14 18:23:40 -08:00
parent a70e006157
commit bed7bc37c4
7 changed files with 131 additions and 67 deletions
+1 -1
View File
@@ -658,7 +658,7 @@ pub const VTable = struct {
fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void,
fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void,
fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void,
fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void,
fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap, file_size: u64) File.WritePositionalError!void,
processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
+9
View File
@@ -22,6 +22,7 @@ pub const INode = std.posix.ino_t;
pub const NLink = std.posix.nlink_t;
pub const Uid = std.posix.uid_t;
pub const Gid = std.posix.gid_t;
pub const BlockSize = u32;
pub const Kind = enum {
block_device,
@@ -65,6 +66,14 @@ pub const Stat = struct {
mtime: Io.Timestamp,
/// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01.
ctime: Io.Timestamp,
/// Smallest chunk length in bytes appropriate for optimal I/O. This will
/// be set to `1` for operating systems or file systems that do not
/// recognize this concept. Not always a power of two. When creating a
/// `MemoryMap`, the mapping length must be a multiple of this value.
///
/// On Windows, this is whichever is larger: PageSize or
/// AllocationGranularity.
block_size: BlockSize,
};
pub fn stdout() File {
+31 -16
View File
@@ -10,10 +10,11 @@ const File = Io.File;
const Allocator = std.mem.Allocator;
file: File,
/// Byte index inside `file` where `memory` starts.
offset: usize,
/// Byte index inside `file` where `memory` starts. Page-aligned.
offset: u64,
/// Memory that may or may not remain consistent with file contents. Use `read`
/// and `write` to ensure synchronization points.
/// and `write` to ensure synchronization points. No minimum alignment on the
/// pointer is guaranteed, but the length is page-aligned.
memory: []u8,
/// Tells whether it is memory-mapped or file operations. On Windows this also
/// has a section handle.
@@ -22,10 +23,10 @@ section: ?Section,
pub const Section = if (is_windows) std.os.windows.HANDLE else void;
pub const CreateError = error{
/// A file descriptor refers to a non-regular file. Or a file mapping was requested,
/// but the file descriptor is not open for reading. Or `MAP.SHARED` was requested
/// and `PROT_WRITE` is set, but the file descriptor is not open in `RDWR` mode.
/// Or `PROT_WRITE` is set, but the file is append-only.
/// One of the following:
/// * The `File.Kind` is not `file`.
/// * The file is not open for reading and read access protections enabled.
/// * The file is not open for writing and write access protections enabled.
AccessDenied,
/// The `prot` argument asks for `PROT_EXEC` but the mapped area belongs to a file on
/// a filesystem that was mounted no-exec.
@@ -36,6 +37,12 @@ pub const CreateError = error{
} || Allocator.Error || File.ReadPositionalError;
pub const CreateOptions = struct {
/// Size of the mapping, in bytes. If this is longer than the file size, it
/// will be filled with zeroes.
///
/// Asserted to be a multiple of page size which can be obtained via
/// `std.heap.pageSize`.
len: usize,
/// When this has read set to false, bytes that are not modified before a
/// sync may have the original file contents, or may be set to zero.
protection: std.process.MemoryProtection = .{ .read = true, .write = true },
@@ -45,12 +52,9 @@ pub const CreateOptions = struct {
undefined_contents: bool = false,
/// Prefault the pages.
populate: bool = true,
/// Byte index of file to start from.
/// Asserted to be a multiple of page size which can be obtained via
/// `std.heap.pageSize`.
offset: u64 = 0,
/// `null` indicates to map the entire file. If mapping the entire file is
/// desired and the file size is known, it is more efficient to populate
/// the value here.
len: ?usize = null,
};
/// To release the resources associated with the returned `MemoryMap`, call
@@ -73,8 +77,15 @@ pub const SetLengthError = error{
/// of the file after calling this is unspecified until `write` is called.
///
/// May change the pointer address of `memory`.
pub fn setLength(mm: *MemoryMap, io: Io, n: usize) File.SetLengthError!void {
return io.vtable.fileMemoryMapSetLength(io.userdata, mm, n);
pub fn setLength(
mm: *MemoryMap,
io: Io,
/// New size of the mapping, in bytes. If this is longer than the file
/// size, it will be filled with zeroes. Asserted to be a multiple of page
/// size which can be obtained with `std.heap.pageSize`.
new_length: usize,
) File.SetLengthError!void {
return io.vtable.fileMemoryMapSetLength(io.userdata, mm, new_length);
}
/// Synchronizes the contents of `memory` from `file`.
@@ -83,6 +94,10 @@ pub fn read(mm: *MemoryMap, io: Io) File.ReadPositionalError!void {
}
/// Synchronizes the contents of `memory` to `file`.
pub fn write(mm: *MemoryMap, io: Io) File.WritePositionalError!void {
return io.vtable.fileMemoryMapWrite(io.userdata, mm);
///
/// Size of the mapping may be longer than the file size, so the `file_size`
/// argument is used to avoid writing too many bytes. If `file_size` is not
/// handy, use `File.length` to get it.
pub fn write(mm: *MemoryMap, io: Io, file_size: u64) File.WritePositionalError!void {
return io.vtable.fileMemoryMapWrite(io.userdata, mm, file_size);
}
+79 -32
View File
@@ -57,6 +57,7 @@ use_sendfile: UseSendfile = .default,
use_copy_file_range: UseCopyFileRange = .default,
use_fcopyfile: UseFcopyfile = .default,
use_fchmodat2: UseFchmodat2 = .default,
disable_memory_mapping: bool,
stderr_writer: File.Writer = .{
.io = undefined,
@@ -75,6 +76,13 @@ random_file: RandomFile = .{},
csprng: Csprng = .{},
system_basic_information: SystemBasicInformation = .{},
const SystemBasicInformation = if (!is_windows) struct {} else struct {
buffer: windows.SYSTEM_BASIC_INFORMATION = undefined,
initialized: std.atomic.Value(bool) = .{ .raw = false },
};
pub const Csprng = struct {
rng: std.Random.DefaultCsprng = .{
.state = undefined,
@@ -1220,6 +1228,8 @@ pub const InitOptions = struct {
/// * `processExecutablePath` on OpenBSD and Haiku (observes "PATH").
/// * `processSpawn`, `processSpawnPath`, `processReplace`, `processReplacePath`
environ: process.Environ,
/// If set to `true`, `File.MemoryMap` APIs will always take the fallback path.
disable_memory_mapping: bool = false,
};
/// Related:
@@ -1247,6 +1257,7 @@ pub fn init(
.argv0 = options.argv0,
.environ = .{ .process_environ = options.environ },
.worker_threads = init_single_threaded.worker_threads,
.disable_memory_mapping = options.disable_memory_mapping,
};
const cpu_count = std.Thread.getCpuCount();
@@ -1263,6 +1274,7 @@ pub fn init(
.argv0 = options.argv0,
.environ = .{ .process_environ = options.environ },
.worker_threads = .init(null),
.disable_memory_mapping = options.disable_memory_mapping,
};
if (posix.Sigaction != void) {
@@ -1299,6 +1311,7 @@ pub const init_single_threaded: Threaded = .{
.argv0 = .empty,
.environ = .{},
.worker_threads = .init(null),
.disable_memory_mapping = false,
};
var global_single_threaded_instance: Threaded = .init_single_threaded;
@@ -2935,7 +2948,11 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
const block_size: u32 = if (t.systemBasicInformation()) |sbi|
@intCast(@max(sbi.PageSize, sbi.AllocationGranularity))
else
std.heap.page_size_max;
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE.ALL_INFORMATION = undefined;
@@ -2997,10 +3014,31 @@ fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
.nlink = 0,
.nlink = info.StandardInformation.NumberOfLinks,
.block_size = block_size,
};
}
fn systemBasicInformation(t: *Threaded) ?*const windows.SYSTEM_BASIC_INFORMATION {
if (!t.system_basic_information.initialized.load(.acquire)) {
t.mutex.lock();
defer t.mutex.unlock();
switch (windows.ntdll.NtQuerySystemInformation(
.SystemBasicInformation,
&t.system_basic_information.buffer,
@sizeOf(windows.SYSTEM_BASIC_INFORMATION),
null,
)) {
.SUCCESS => {},
else => return null,
}
t.system_basic_information.initialized.store(true, .release);
}
return &t.system_basic_information.buffer;
}
fn fileStatWasi(userdata: ?*anyopaque, file: File) File.StatError!File.Stat {
if (builtin.link_libc) return fileStatPosix(userdata, file);
@@ -8008,10 +8046,10 @@ fn fileReadStreamingWindows(userdata: ?*anyopaque, file: File, data: []const []u
syscall.finish();
return 0;
},
.NETNAME_DELETED => return syscall.fail(error.ConnectionResetByPeer),
.NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected,
.LOCK_VIOLATION => return syscall.fail(error.LockViolation),
.ACCESS_DENIED => return syscall.fail(error.AccessDenied),
.INVALID_HANDLE => return syscall.fail(error.NotOpenForReading),
.INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected,
// TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing
// a handle to a directory.
.INVALID_FUNCTION => return syscall.fail(error.IsDir),
@@ -8158,10 +8196,10 @@ fn fileReadPositionalWindows(userdata: ?*anyopaque, file: File, data: []const []
syscall.finish();
return 0;
},
.NETNAME_DELETED => return syscall.fail(error.ConnectionResetByPeer),
.NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected,
.LOCK_VIOLATION => return syscall.fail(error.LockViolation),
.ACCESS_DENIED => return syscall.fail(error.AccessDenied),
.INVALID_HANDLE => return syscall.fail(error.NotOpenForReading),
.INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected,
// TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing
// a handle to a directory.
.INVALID_FUNCTION => return syscall.fail(error.IsDir),
@@ -8842,7 +8880,7 @@ fn writeFilePositionalWindows(
.NOT_ENOUGH_MEMORY => return syscall.fail(error.SystemResources),
.NOT_ENOUGH_QUOTA => return syscall.fail(error.SystemResources),
.NO_DATA => return syscall.fail(error.BrokenPipe),
.INVALID_HANDLE => return syscall.fail(error.NotOpenForWriting),
.INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected, // use after free
.LOCK_VIOLATION => return syscall.fail(error.LockViolation),
.ACCESS_DENIED => return syscall.fail(error.AccessDenied),
.WORKING_SET_QUOTA => return syscall.fail(error.SystemResources),
@@ -12470,6 +12508,7 @@ const linux_statx_request: std.os.linux.STATX = .{
.INO = true,
.SIZE = true,
.NLINK = true,
.BLOCKS = true,
};
const linux_statx_check: std.os.linux.STATX = .{
@@ -12481,6 +12520,7 @@ const linux_statx_check: std.os.linux.STATX = .{
.INO = true,
.SIZE = true,
.NLINK = true,
.BLOCKS = false,
};
fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat {
@@ -12499,6 +12539,7 @@ fn statFromLinux(stx: *const std.os.linux.Statx) Io.UnexpectedError!File.Stat {
},
.mtime = .{ .nanoseconds = @intCast(@as(i128, stx.mtime.sec) * std.time.ns_per_s + stx.mtime.nsec) },
.ctime = .{ .nanoseconds = @intCast(@as(i128, stx.ctime.sec) * std.time.ns_per_s + stx.ctime.nsec) },
.block_size = if (stx.mask.BLOCKS) stx.blksize else 1,
};
}
@@ -12547,6 +12588,7 @@ fn statFromPosix(st: *const posix.Stat) File.Stat {
.atime = timestampFromPosix(&atime),
.mtime = timestampFromPosix(&mtime),
.ctime = timestampFromPosix(&ctime),
.block_size = st.blksize,
};
}
@@ -12568,6 +12610,7 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) File.Stat {
.atime = .fromNanoseconds(st.atim),
.mtime = .fromNanoseconds(st.mtim),
.ctime = .fromNanoseconds(st.ctim),
.block_size = 1,
};
}
@@ -16127,28 +16170,29 @@ fn fileMemoryMapCreate(
) File.MemoryMap.CreateError!File.MemoryMap {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const offset = options.offset;
const len = options.len;
const page_size = std.heap.pageSize();
const aligned_len: usize = options.len.?; // TODO query if necessary
assert(std.mem.isAligned(len, std.heap.page_size_min));
if (createFileMap(file, options.protection, offset, options.populate, aligned_len)) |result| {
return result;
} else |err| switch (err) {
error.Unseekable, error.Canceled => |e| return e,
else => {
if (builtin.mode == .Debug)
std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err});
},
if (!t.disable_memory_mapping) {
if (createFileMap(file, options.protection, offset, options.populate, len)) |result| {
return result;
} else |err| switch (err) {
error.Unseekable, error.Canceled, error.AccessDenied => |e| return e,
else => {
if (builtin.mode == .Debug)
std.log.warn("memory mapping failed with {t}, falling back to file operations", .{err});
},
}
}
const gpa = t.allocator;
const alignment: Alignment = .fromByteUnits(page_size);
const memory = m: {
const ptr = gpa.rawAlloc(aligned_len, alignment, @returnAddress()) orelse
const ptr = gpa.rawAlloc(len, .@"1", @returnAddress()) orelse
return error.OutOfMemory;
break :m ptr[0..aligned_len];
break :m ptr[0..len];
};
errdefer gpa.rawFree(memory, alignment, @returnAddress());
errdefer gpa.rawFree(memory, .@"1", @returnAddress());
if (!options.undefined_contents) try mmSyncRead(file, memory, offset);
@@ -16180,12 +16224,13 @@ const CreateFileMapError = error{
OutOfMemory,
MappingAlreadyExists,
Unseekable,
FileLockConflict,
} || Io.Cancelable || Io.UnexpectedError;
fn createFileMap(
file: File,
protection: std.process.MemoryProtection,
offset: usize,
offset: u64,
populate: bool,
aligned_len: usize,
) CreateFileMapError!File.MemoryMap {
@@ -16212,7 +16257,7 @@ fn createFileMap(
file.handle,
)) {
.SUCCESS => {},
.FILE_LOCK_CONFLICT => return error.FileLocked,
.FILE_LOCK_CONFLICT => return error.FileLockConflict,
.INVALID_FILE_FOR_SECTION => return error.OperationUnsupported,
else => |status| return windows.unexpectedStatus(status),
}
@@ -16318,9 +16363,7 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void {
}
} else {
const gpa = t.allocator;
const page_size = std.heap.pageSize();
const alignment: Alignment = .fromByteUnits(page_size);
gpa.rawFree(memory, alignment, @returnAddress());
gpa.rawFree(memory, .@"1", @returnAddress());
}
mm.* = undefined;
}
@@ -16331,6 +16374,7 @@ fn fileMemoryMapSetLength(
new_len: usize,
) File.MemoryMap.SetLengthError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
assert(std.mem.isAligned(new_len, std.heap.page_size_min));
if (mm.section) |section| switch (native_os) {
.windows => {
_ = section;
@@ -16366,12 +16410,10 @@ fn fileMemoryMapSetLength(
},
} else {
const gpa = t.allocator;
const page_size = std.heap.pageSize();
const alignment: Alignment = .fromByteUnits(page_size);
if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| {
if (gpa.rawRemap(mm.memory, .@"1", new_len, @returnAddress())) |new_ptr| {
mm.memory = new_ptr[0..new_len];
} else {
const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse
const new_ptr = gpa.rawAlloc(new_len, .@"1", @returnAddress()) orelse
return error.OutOfMemory;
const copy_len = @min(new_len, mm.memory.len);
@memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]);
@@ -16387,11 +16429,16 @@ fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositi
return mmSyncRead(mm.file, mm.memory, mm.offset);
}
fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void {
fn fileMemoryMapWrite(
userdata: ?*anyopaque,
mm: *File.MemoryMap,
file_size: u64,
) File.WritePositionalError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
if (mm.section != null) return;
return mmSyncWrite(mm.file, mm.memory, mm.offset);
const offset = mm.offset;
return mmSyncWrite(mm.file, mm.memory[0..@intCast(file_size - offset)], offset);
}
fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void {
+9 -17
View File
@@ -605,31 +605,23 @@ test "memory mapping" {
});
{
var file = try tmp.dir.openFile(io, "blah.txt", .{});
var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write });
defer file.close(io);
var mm = try file.createMemoryMap(io, .{});
const stat = try file.stat(io);
const aligned_len = std.mem.alignForward(usize, @intCast(stat.size), std.heap.pageSize());
var mm = try file.createMemoryMap(io, .{ .len = aligned_len });
defer mm.destroy(io);
try expectEqualStrings("this is my data123", mm.memory);
mm.memory[5] = '9';
mm.memory[8] = '9';
try expectEqualStrings("this is my data123", std.mem.sliceTo(mm.memory, 0));
mm.memory[4] = '9';
mm.memory[7] = '9';
try mm.write(io);
try mm.write(io, stat.size);
}
var buffer: [100]u8 = undefined;
const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer);
try expectEqualStrings("this9is9my data123", updated_contents);
var file = try tmp.dir.openFile(io, "blah.txt", .{});
defer file.close(io);
var mm = try file.createMemoryMap(io, .{
.protection = .{ .read = true },
.offset = 2,
});
defer mm.destroy(io);
try expectEqualStrings("is9is9my data123", mm.memory);
}
+1 -1
View File
@@ -212,7 +212,7 @@ pub const nlink_t = switch (native_os) {
.wasi => c_ulonglong,
// https://github.com/SerenityOS/serenity/blob/b98f537f117b341788023ab82e0c11ca9ae29a57/Kernel/API/POSIX/sys/types.h#L45
.freebsd, .serenity => u64,
.openbsd, .netbsd, .dragonfly, .illumos => u32,
.openbsd, .netbsd, .dragonfly, .illumos, .windows => u32,
.haiku => i32,
.driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => u16,
else => u0,
+1
View File
@@ -55,6 +55,7 @@ else switch (native_os) {
pub const gid_t = void;
pub const mode_t = u0;
pub const nlink_t = u0;
pub const blksize_t = void;
pub const ino_t = void;
pub const IFNAMESIZE = {};
pub const SIG = void;