Skip to content

Commit eed8f7e

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 eee8f7c commit eed8f7e

File tree

3 files changed

+188
-2
lines changed

3 files changed

+188
-2
lines changed

lib/std/fs.zig

Lines changed: 2 additions & 0 deletions
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

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

lib/std/os/windows.zig

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,11 @@ pub fn MapViewOfFile(
201201
size,
202202
hint,
203203
);
204-
if (ptr) |p|
205-
return @alignCast(p[0..size]);
204+
if (ptr) |p| {
205+
const cast: [*]align(std.mem.page_size) volatile u8 =
206+
@alignCast(@ptrCast(p));
207+
return @alignCast(cast[0..size]);
208+
}
206209
switch (GetLastError()) {
207210
else => |err| return unexpectedError(err),
208211
}

0 commit comments

Comments
 (0)