commit bed7bc37c43afce335610867aedbcd506ade65f4 (tree)
parent a70e0061579a4365da0092554470ffeadb4594a4
Author: Andrew Kelley <andrew@ziglang.org>
Date: Wed, 14 Jan 2026 18:23:40 -0800
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
Diffstat:
7 files changed, 131 insertions(+), 67 deletions(-)
diff --git a/lib/std/Io.zig b/lib/std/Io.zig
@@ -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,
diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig
@@ -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 {
diff --git a/lib/std/Io/File/MemoryMap.zig b/lib/std/Io/File/MemoryMap.zig
@@ -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);
}
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -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 {
diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig
@@ -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);
}
diff --git a/lib/std/c.zig b/lib/std/c.zig
@@ -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,
diff --git a/lib/std/posix.zig b/lib/std/posix.zig
@@ -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;