Skip to content

Commit 6dff77d

Browse files
committed
std: Add std.fs.MemoryMap.
This new type provides a cross platform memory mapping API which encompasses the common subset of both POSIX and Windows APIs.
1 parent f781a59 commit 6dff77d

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

lib/std/fs.zig

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const is_darwin = native_os.isDarwin();
1717
pub const AtomicFile = @import("fs/AtomicFile.zig");
1818
pub const Dir = @import("fs/Dir.zig");
1919
pub const File = @import("fs/File.zig");
20+
pub const MemoryMap = @import("fs/MemoryMap.zig");
2021
pub const path = @import("fs/path.zig");
2122

2223
pub const has_executable_bit = switch (native_os) {
@@ -710,6 +711,7 @@ test {
710711
_ = &AtomicFile;
711712
_ = &Dir;
712713
_ = &File;
714+
_ = &MemoryMap;
713715
_ = &path;
714716
_ = @import("fs/test.zig");
715717
_ = @import("fs/get_app_data_dir.zig");

lib/std/fs/MemoryMap.zig

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
//! A cross-platform abstraction for memory-mapping files.
2+
//!
3+
//! The API here implements the common subset of functionality present in the supported operating
4+
//! systems. Presently, Windows and all POSIX environments are supported.
5+
//!
6+
//! Operating system specific behavior is intended to be minimized; however, the following leak
7+
//! through the abstraction:
8+
//!
9+
//! - Child processes sharing:
10+
//! - POSIX: Shared with child processes upon `fork` and unshared upon `exec*`.
11+
//! - Windows: Not shared with child processes.
12+
13+
const std = @import("../std.zig");
14+
const builtin = @import("builtin");
15+
16+
const MemoryMap = @This();
17+
18+
/// An OS-specific reference to a kernel object for this mapping.
19+
handle: switch (builtin.os.tag) {
20+
.windows => std.os.windows.HANDLE,
21+
else => void,
22+
},
23+
/// The region of virtual memory in which the file is mapped.
24+
///
25+
/// Accesses to this are subject to the protection semantics specified upon
26+
/// initialization of the mapping. Failure to abide by those semantics has undefined
27+
/// behavior (though should be well-defined by the OS).
28+
mapped: []align(std.mem.page_size) volatile u8,
29+
30+
test MemoryMap {
31+
if (builtin.os.tag == .wasi) return error.SkipZigTest;
32+
33+
var tmp = std.testing.tmpDir(.{});
34+
defer tmp.cleanup();
35+
36+
var file = try tmp.dir.createFile("mmap.bin", .{
37+
.exclusive = true,
38+
.truncate = true,
39+
.read = true,
40+
});
41+
defer file.close();
42+
43+
const magic = "\xde\xca\xfb\xad";
44+
try file.writeAll(magic);
45+
46+
const len = try file.getEndPos();
47+
48+
var view = try MemoryMap.init(file, .{ .length = @intCast(len) });
49+
defer view.deinit();
50+
51+
try std.testing.expectEqualSlices(u8, magic, @volatileCast(view.mapped));
52+
}
53+
54+
pub const InitOptions = struct {
55+
protection: ProtectionFlags = .{},
56+
/// Whether changes to the memory-mapped region should be propogated into the backing file.
57+
///
58+
/// This only refers to the exclusivity of the memory-mapped region with respect to *other*
59+
/// instances of `MemoryMap` of that same file. A single `MemoryMap` instance can be shared
60+
/// within a process regardless of this option. Whether a single `MemoryMap` instance is shared
61+
/// with child processes is operating system specific and independent of this option.
62+
exclusivity: Exclusivity = .private,
63+
/// The desired length of the mapping.
64+
///
65+
/// The backing file must be of at least `offset + length` size.
66+
length: usize,
67+
/// The desired offset of the mapping.
68+
///
69+
/// The backing file must be of at least `offset` size.
70+
offset: usize = 0,
71+
hint: ?[*]align(std.mem.page_size) u8 = null,
72+
};
73+
74+
/// A description of OS protections to be applied to a memory-mapped region.
75+
pub const ProtectionFlags = struct {
76+
write: bool = false,
77+
execute: bool = false,
78+
};
79+
80+
pub const Exclusivity = enum {
81+
/// The file's content may be read or written by external processes.
82+
shared,
83+
/// The file's content is exclusive to this process.
84+
private,
85+
};
86+
87+
/// Create a memory-mapped view into `file`.
88+
///
89+
/// Asserts `opts.length` is non-zero.
90+
pub fn init(file: std.fs.File, opts: InitOptions) !MemoryMap {
91+
std.debug.assert(opts.length > 0);
92+
switch (builtin.os.tag) {
93+
.wasi => @compileError("MemoryMap not supported on WASI OS; see also " ++
94+
"https://github.com/WebAssembly/WASI/issues/304"),
95+
.windows => {
96+
// Create the kernel resource for the memory mapping.
97+
var access: std.os.windows.ACCESS_MASK =
98+
std.os.windows.STANDARD_RIGHTS_REQUIRED |
99+
std.os.windows.SECTION_QUERY |
100+
std.os.windows.SECTION_MAP_READ;
101+
var page_attributes: std.os.windows.ULONG = 0;
102+
if (opts.protection.execute) {
103+
access |= std.os.windows.SECTION_MAP_EXECUTE;
104+
if (opts.protection.write) {
105+
access |= std.os.windows.SECTION_MAP_WRITE;
106+
page_attributes = switch (opts.exclusivity) {
107+
.shared => std.os.windows.PAGE_EXECUTE_READWRITE,
108+
.private => std.os.windows.PAGE_EXECUTE_WRITECOPY,
109+
};
110+
} else {
111+
page_attributes = std.os.windows.PAGE_EXECUTE_READ;
112+
}
113+
} else {
114+
if (opts.protection.write) {
115+
access |= std.os.windows.SECTION_MAP_WRITE;
116+
page_attributes = switch (opts.exclusivity) {
117+
.shared => std.os.windows.PAGE_READWRITE,
118+
.private => std.os.windows.PAGE_WRITECOPY,
119+
};
120+
} else {
121+
page_attributes = std.os.windows.PAGE_READONLY;
122+
}
123+
}
124+
const handle = try std.os.windows.CreateSection(.{
125+
.file = file.handle,
126+
.access = access,
127+
.size = opts.length,
128+
.page_attributes = page_attributes,
129+
});
130+
errdefer std.os.windows.CloseHandle(handle);
131+
132+
// Create the mapping.
133+
const mapped = try std.os.windows.MapViewOfSection(handle, .{
134+
.inheritance = .ViewUnmap,
135+
.protection = page_attributes,
136+
.offset = opts.offset,
137+
.length = opts.length,
138+
.hint = opts.hint,
139+
});
140+
141+
return .{
142+
.handle = handle,
143+
.mapped = mapped,
144+
};
145+
},
146+
else => {
147+
// The man page indicates the flags must be either `NONE` or an OR of the
148+
// flags. That doesn't explicitly state that the absence of those flags is
149+
// the same as `NONE`, so this static assertion is made. That'll break the
150+
// build rather than behaving unexpectedly if some weird system comes up.
151+
comptime std.debug.assert(std.posix.PROT.NONE == 0);
152+
153+
// Convert the public options into POSIX specific options.
154+
var prot: u32 = std.posix.PROT.READ;
155+
if (opts.protection.write)
156+
prot |= std.posix.PROT.WRITE;
157+
if (opts.protection.execute)
158+
prot |= std.posix.PROT.EXEC;
159+
const flags: std.posix.MAP = .{
160+
.TYPE = switch (opts.exclusivity) {
161+
.shared => .SHARED,
162+
.private => .PRIVATE,
163+
},
164+
};
165+
166+
// Create the mapping.
167+
const mapped = try std.posix.mmap(
168+
opts.hint,
169+
opts.length,
170+
prot,
171+
@bitCast(flags),
172+
file.handle,
173+
opts.offset,
174+
);
175+
176+
return .{
177+
.handle = {},
178+
.mapped = mapped,
179+
};
180+
},
181+
}
182+
}
183+
184+
/// Unmap the file from virtual memory and deallocate kernel resources.
185+
///
186+
/// Invalidates references to `self.mapped`.
187+
pub fn deinit(self: MemoryMap) void {
188+
switch (builtin.os.tag) {
189+
.windows => {
190+
std.os.windows.UnmapViewOfSection(@volatileCast(self.mapped.ptr));
191+
std.os.windows.CloseHandle(self.handle);
192+
},
193+
else => {
194+
std.posix.munmap(@volatileCast(self.mapped));
195+
},
196+
}
197+
}
198+
199+
/// Reinterpret `self.mapped` as `T`.
200+
///
201+
/// The returned pointer is aligned to the beginning of the mapping. The mapping may be
202+
/// larger than `T`. The caller is responsible for determining whether volatility can be
203+
/// stripped away through external synchronization.
204+
pub inline fn cast(self: MemoryMap, comptime T: type) *align(std.mem.page_size) volatile T {
205+
return std.mem.bytesAsValue(T, self.mapped[0..@sizeOf(T)]);
206+
}

0 commit comments

Comments
 (0)