From bed7bc37c43afce335610867aedbcd506ade65f4 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 14 Jan 2026 18:23:40 -0800 Subject: [PATCH] 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 --- lib/std/Io.zig | 2 +- lib/std/Io/File.zig | 9 +++ lib/std/Io/File/MemoryMap.zig | 47 +++++++++----- lib/std/Io/Threaded.zig | 111 ++++++++++++++++++++++++---------- lib/std/Io/test.zig | 26 +++----- lib/std/c.zig | 2 +- lib/std/posix.zig | 1 + 7 files changed, 131 insertions(+), 67 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 1da10a027f..b1af9f150c 100644 --- 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 index 3127298676..dda3f9fe8e 100644 --- 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 index 1f210f4c18..3751a0c76d 100644 --- 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 index e48ce886a2..e4fd954707 100644 --- 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 index aff2aec048..43237fcb6f 100644 --- 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 index 5180dfbfa7..f10833cf0d 100644 --- 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 index b607cc8134..6b02914162 100644 --- 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;