Skip to content

Commit

Permalink
std: Add std.fs.MemoryMap.
Browse files Browse the repository at this point in the history
This new type provides a cross platform memory mapping API which
encompasses the common subset of both POSIX and Windows APIs.
  • Loading branch information
gcoakes committed Aug 18, 2024
1 parent 7d24647 commit 76cff7d
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/std/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const is_darwin = native_os.isDarwin();
pub const AtomicFile = @import("fs/AtomicFile.zig");
pub const Dir = @import("fs/Dir.zig");
pub const File = @import("fs/File.zig");
pub const MemoryMap = @import("fs/MemoryMap.zig");
pub const path = @import("fs/path.zig");

pub const has_executable_bit = switch (native_os) {
Expand Down Expand Up @@ -710,6 +711,7 @@ test {
_ = &AtomicFile;
_ = &Dir;
_ = &File;
_ = &MemoryMap;
_ = &path;
_ = @import("fs/test.zig");
_ = @import("fs/get_app_data_dir.zig");
Expand Down
206 changes: 206 additions & 0 deletions lib/std/fs/MemoryMap.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//! A cross-platform abstraction for memory-mapping files.
//!
//! The API here implements the common subset of functionality present in the supported operating
//! systems. Presently, Windows and all POSIX environments are supported.
//!
//! Operating system specific behavior is intended to be minimized; however, the following leak
//! through the abstraction:
//!
//! - Child processes sharing:
//! - POSIX: Shared with child processes upon `fork` and unshared upon `exec*`.
//! - Windows: Not shared with child processes.

const std = @import("../std.zig");
const builtin = @import("builtin");

const MemoryMap = @This();

/// An OS-specific reference to a kernel object for this mapping.
handle: switch (builtin.os.tag) {
.windows => std.os.windows.HANDLE,
else => void,
},
/// The region of virtual memory in which the file is mapped.
///
/// Accesses to this are subject to the protection semantics specified upon
/// initialization of the mapping. Failure to abide by those semantics has undefined
/// behavior (though should be well-defined by the OS).
mapped: []align(std.mem.page_size) volatile u8,

test MemoryMap {
if (builtin.os.tag == .wasi) return error.SkipZigTest;

var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();

var file = try tmp.dir.createFile("mmap.bin", .{
.exclusive = true,
.truncate = true,
.read = true,
});
defer file.close();

const magic = "\xde\xca\xfb\xad";
try file.writeAll(magic);

const len = try file.getEndPos();

var view = try MemoryMap.init(file, .{ .length = @intCast(len) });
defer view.deinit();

try std.testing.expectEqualSlices(u8, magic, @volatileCast(view.mapped));
}

pub const InitOptions = struct {
protection: ProtectionFlags = .{},
/// Whether changes to the memory-mapped region should be propogated into the backing file.
///
/// This only refers to the exclusivity of the memory-mapped region with respect to *other*
/// instances of `MemoryMap` of that same file. A single `MemoryMap` instance can be shared
/// within a process regardless of this option. Whether a single `MemoryMap` instance is shared
/// with child processes is operating system specific and independent of this option.
exclusivity: Exclusivity = .private,
/// The desired length of the mapping.
///
/// The backing file must be of at least `offset + length` size.
length: usize,
/// The desired offset of the mapping.
///
/// The backing file must be of at least `offset` size.
offset: usize = 0,
hint: ?[*]align(std.mem.page_size) u8 = null,
};

/// A description of OS protections to be applied to a memory-mapped region.
pub const ProtectionFlags = struct {
write: bool = false,
execute: bool = false,
};

pub const Exclusivity = enum {
/// The file's content may be read or written by external processes.
shared,
/// The file's content is exclusive to this process.
private,
};

/// Create a memory-mapped view into `file`.
///
/// Asserts `opts.length` is non-zero.
pub fn init(file: std.fs.File, opts: InitOptions) !MemoryMap {
std.debug.assert(opts.length > 0);
switch (builtin.os.tag) {
.wasi => @compileError("MemoryMap not supported on WASI OS; see also " ++
"https://github.com/WebAssembly/WASI/issues/304"),
.windows => {
// Create the kernel resource for the memory mapping.
var access: std.os.windows.ACCESS_MASK =
std.os.windows.STANDARD_RIGHTS_REQUIRED |
std.os.windows.SECTION_QUERY |
std.os.windows.SECTION_MAP_READ;
var page_attributes: std.os.windows.ULONG = 0;
if (opts.protection.execute) {
access |= std.os.windows.SECTION_MAP_EXECUTE;
if (opts.protection.write) {
access |= std.os.windows.SECTION_MAP_WRITE;
page_attributes = switch (opts.exclusivity) {
.shared => std.os.windows.PAGE_EXECUTE_READWRITE,
.private => std.os.windows.PAGE_EXECUTE_WRITECOPY,
};
} else {
page_attributes = std.os.windows.PAGE_EXECUTE_READ;
}
} else {
if (opts.protection.write) {
access |= std.os.windows.SECTION_MAP_WRITE;
page_attributes = switch (opts.exclusivity) {
.shared => std.os.windows.PAGE_READWRITE,
.private => std.os.windows.PAGE_WRITECOPY,
};
} else {
page_attributes = std.os.windows.PAGE_READONLY;
}
}
const handle = try std.os.windows.CreateSection(.{
.file = file.handle,
.access = access,
.size = opts.length,
.page_attributes = page_attributes,
});
errdefer std.os.windows.CloseHandle(handle);

// Create the mapping.
const mapped = try std.os.windows.MapViewOfSection(handle, .{
.inheritance = .ViewUnmap,
.protection = page_attributes,
.offset = opts.offset,
.length = opts.length,
.hint = opts.hint,
});

return .{
.handle = handle,
.mapped = mapped,
};
},
else => {
// The man page indicates the flags must be either `NONE` or an OR of the
// flags. That doesn't explicitly state that the absence of those flags is
// the same as `NONE`, so this static assertion is made. That'll break the
// build rather than behaving unexpectedly if some weird system comes up.
comptime std.debug.assert(std.posix.PROT.NONE == 0);

// Convert the public options into POSIX specific options.
var prot: u32 = std.posix.PROT.READ;
if (opts.protection.write)
prot |= std.posix.PROT.WRITE;
if (opts.protection.execute)
prot |= std.posix.PROT.EXEC;
const flags: std.posix.MAP = .{
.TYPE = switch (opts.exclusivity) {
.shared => .SHARED,
.private => .PRIVATE,
},
};

// Create the mapping.
const mapped = try std.posix.mmap(
opts.hint,
opts.length,
prot,
@bitCast(flags),
file.handle,
opts.offset,
);

return .{
.handle = {},
.mapped = mapped,
};
},
}
}

/// Unmap the file from virtual memory and deallocate kernel resources.
///
/// Invalidates references to `self.mapped`.
pub fn deinit(self: MemoryMap) void {
switch (builtin.os.tag) {
.windows => {
std.os.windows.UnmapViewOfSection(@volatileCast(self.mapped.ptr));
std.os.windows.CloseHandle(self.handle);
},
else => {
std.posix.munmap(@volatileCast(self.mapped));
},
}
}

/// Reinterpret `self.mapped` as `T`.
///
/// The returned pointer is aligned to the beginning of the mapping. The mapping may be
/// larger than `T`. The caller is responsible for determining whether volatility can be
/// stripped away through external synchronization.
pub inline fn cast(self: MemoryMap, comptime T: type) *align(std.mem.page_size) volatile T {
return std.mem.bytesAsValue(T, self.mapped[0..@sizeOf(T)]);
}

0 comments on commit 76cff7d

Please sign in to comment.