authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-01 17:53:02-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:08-08:00
log60447ea97cd86e38e566c23d40c123e19d50eb1a
tree49766d483d7acbce49239ba4a1d8055af068ff1e
parent08447ca47ed2ece816de9b0c5735be3f17edc6ca

std: fix windows compilation errors


7 files changed, 336 insertions(+), 216 deletions(-)

lib/std/Io/Threaded.zig+256-146
......@@ -5654,7 +5654,7 @@ fn dirSymLinkWindows(
56545654 // Already an NT path, no need to do anything to it
56555655 break :target_path target_path_w.span();
56565656 } else {
5657 switch (w.getWin32PathType(u16, target_path_w.span())) {
5657 switch (Dir.path.getWin32PathType(u16, target_path_w.span())) {
56585658 // Rooted paths need to avoid getting put through wToPrefixedFileW
56595659 // (and they are treated as relative in this context)
56605660 // Note: It seems that rooted paths in symbolic links are relative to
......@@ -12617,6 +12617,45 @@ fn initializeWsa(t: *Threaded) error{ NetworkDown, Canceled }!void {
1261712617
1261812618fn doNothingSignalHandler(_: posix.SIG) callconv(.c) void {}
1261912619
12620const WindowsEnvironStrings = struct {
12621 PATH: ?[:0]const u16 = null,
12622 PATHEXT: ?[:0]const u16 = null,
12623
12624 fn scan() WindowsEnvironStrings {
12625 const ptr = windows.peb().ProcessParameters.Environment;
12626
12627 var result: WindowsEnvironStrings = .{};
12628 var i: usize = 0;
12629 while (ptr[i] != 0) {
12630 const key_start = i;
12631
12632 // There are some special environment variables that start with =,
12633 // so we need a special case to not treat = as a key/value separator
12634 // if it's the first character.
12635 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
12636 if (ptr[key_start] == '=') i += 1;
12637
12638 while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {}
12639 const key_w = ptr[key_start..i];
12640
12641 if (ptr[i] == '=') i += 1;
12642
12643 const value_start = i;
12644 while (ptr[i] != 0) : (i += 1) {}
12645 const value_w = ptr[value_start..i :0];
12646
12647 i += 1; // skip over null byte
12648
12649 inline for (@typeInfo(WindowsEnvironStrings).@"struct".fields) |field| {
12650 const field_name_w = comptime std.unicode.wtf8ToWtf16LeStringLiteral(field.name);
12651 if (std.mem.eql(u16, key_w, field_name_w)) @field(result, field.name) = value_w;
12652 }
12653 }
12654
12655 return result;
12656 }
12657};
12658
1262012659fn scanEnviron(t: *Threaded) void {
1262112660 t.mutex.lock();
1262212661 defer t.mutex.unlock();
......@@ -12625,6 +12664,9 @@ fn scanEnviron(t: *Threaded) void {
1262512664 t.environ.initialized = true;
1262612665
1262712666 if (is_windows) {
12667 // This value expires with any call that modifies the environment,
12668 // which is outside of this Io implementation's control, so references
12669 // must be short-lived.
1262812670 const ptr = windows.peb().ProcessParameters.Environment;
1262912671
1263012672 var i: usize = 0;
......@@ -12779,6 +12821,7 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce
1277912821 };
1278012822
1278112823 const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);
12824 // TODO: cache file handle of /dev/null!
1278212825 const dev_null_fd = if (any_ignore)
1278312826 posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) {
1278412827 error.PathAlreadyExists => unreachable,
......@@ -12962,55 +13005,88 @@ fn childWait(userdata: ?*anyopaque, child: *std.process.Child) process.Child.Wai
1296213005fn childKill(userdata: ?*anyopaque, child: *std.process.Child) void {
1296313006 const t: *Threaded = @ptrCast(@alignCast(userdata));
1296413007 if (is_windows) {
12965 childKillWindows(t, child, 1) catch {
12966 childCleanupStreams(child);
12967 };
13008 childKillWindows(t, child, 1) catch childCleanupWindows(child);
1296813009 } else {
12969 childKillPosix(t, child) catch {
12970 childCleanupStreams(child);
12971 };
13010 childKillPosix(t, child) catch childCleanupPosix(child);
1297213011 }
1297313012}
1297413013
1297513014fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT) !void {
12976 windows.TerminateProcess(child.id, exit_code) catch |err| switch (err) {
12977 error.AccessDenied => {
12978 // Usually when TerminateProcess triggers a ACCESS_DENIED error, it
12979 // indicates that the process has already exited, but there may be
12980 // some rare edge cases where our process handle no longer has the
12981 // PROCESS_TERMINATE access right, so let's do another check to make
12982 // sure the process is really no longer running:
12983 windows.WaitForSingleObjectEx(child.id, 0, false) catch return err;
12984 return error.AlreadyTerminated;
12985 },
12986 else => return err,
12987 };
12988 try childWaitWindows(t, child);
13015 _ = t; // TODO cancelation
13016 const handle = child.id.?;
13017 if (windows.kernel32.TerminateProcess(handle, exit_code) == 0) {
13018 switch (windows.GetLastError()) {
13019 .ACCESS_DENIED => {
13020 // Usually when TerminateProcess triggers a ACCESS_DENIED error, it
13021 // indicates that the process has already exited, but there may be
13022 // some rare edge cases where our process handle no longer has the
13023 // PROCESS_TERMINATE access right, so let's do another check to make
13024 // sure the process is really no longer running:
13025 windows.WaitForSingleObjectEx(handle, 0, false) catch return error.AccessDenied;
13026 return error.AlreadyTerminated;
13027 },
13028 else => |err| return windows.unexpectedError(err),
13029 }
13030 }
13031 _ = windows.kernel32.WaitForSingleObjectEx(handle, windows.INFINITE, windows.FALSE);
13032 childCleanupWindows(child);
1298913033}
1299013034
1299113035fn childWaitWindows(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term {
12992 _ = t; // TODO cancelation
12993 windows.WaitForSingleObjectEx(child.id, windows.INFINITE, false);
13036 const current_thread = Thread.getCurrent(t);
13037 const handle = child.id.?;
13038
13039 while (true) {
13040 try current_thread.checkCancel();
13041 switch (windows.kernel32.WaitForSingleObjectEx(handle, windows.INFINITE, windows.FALSE)) {
13042 windows.WAIT_OBJECT_0 => break,
13043 windows.WAIT_ABANDONED, windows.WAIT_TIMEOUT => continue,
13044 windows.WAIT_FAILED => switch (windows.GetLastError()) {
13045 else => |err| return windows.unexpectedError(err),
13046 },
13047 else => return error.Unexpected,
13048 }
13049 }
1299413050
1299513051 const term: process.Child.Term = x: {
1299613052 var exit_code: windows.DWORD = undefined;
12997 if (windows.kernel32.GetExitCodeProcess(child.id, &exit_code) == 0) {
13053 if (windows.kernel32.GetExitCodeProcess(handle, &exit_code) == 0) {
1299813054 break :x .{ .unknown = 0 };
1299913055 } else {
1300013056 break :x .{ .exited = @as(u8, @truncate(exit_code)) };
1300113057 }
1300213058 };
1300313059
13004 if (child.request_resource_usage_statistics) {
13005 child.resource_usage_statistics.rusage = try windows.GetProcessMemoryInfo(child.id);
13006 }
13007
13008 posix.close(child.id);
13009 posix.close(child.thread_handle);
13010 childCleanupStreams(child);
13060 childCleanupWindows(child);
1301113061 return term;
1301213062}
1301313063
13064fn childCleanupWindows(child: *process.Child) void {
13065 const handle = child.id orelse return;
13066
13067 if (child.request_resource_usage_statistics)
13068 child.resource_usage_statistics.rusage = windows.GetProcessMemoryInfo(handle) catch null;
13069
13070 windows.CloseHandle(handle);
13071 child.id = null;
13072
13073 windows.CloseHandle(child.thread_handle);
13074 child.thread_handle = undefined;
13075
13076 if (child.stdin) |*stdin| {
13077 windows.CloseHandle(stdin.handle);
13078 child.stdin = null;
13079 }
13080 if (child.stdout) |*stdout| {
13081 windows.CloseHandle(stdout.handle);
13082 child.stdout = null;
13083 }
13084 if (child.stderr) |*stderr| {
13085 windows.CloseHandle(stderr.handle);
13086 child.stderr = null;
13087 }
13088}
13089
1301413090fn childWaitPosix(t: *Threaded, child: *process.Child) process.Child.WaitError!process.Child.Term {
1301513091 _ = t; // TODO cancelation
1301613092 const pid = child.id.?;
......@@ -13023,7 +13099,7 @@ fn childWaitPosix(t: *Threaded, child: *process.Child) process.Child.WaitError!p
1302313099 }
1302413100 break :res posix.waitpid(pid, 0);
1302513101 };
13026 childCleanupStreams(child);
13102 childCleanupPosix(child);
1302713103 return statusToTerm(res.status);
1302813104}
1302913105
......@@ -13050,7 +13126,7 @@ fn childKillPosix(t: *Threaded, child: *process.Child) !void {
1305013126 _ = try childWaitPosix(t, child);
1305113127}
1305213128
13053fn childCleanupStreams(child: *process.Child) void {
13129fn childCleanupPosix(child: *process.Child) void {
1305413130 if (child.stdin) |*stdin| {
1305513131 posix.close(stdin.handle);
1305613132 child.stdin = null;
......@@ -13140,9 +13216,8 @@ fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32
1314013216 }
1314113217}
1314213218
13143fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.SpawnError!void {
13219fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
1314413220 const t: *Threaded = @ptrCast(@alignCast(userdata));
13145 _ = t;
1314613221
1314713222 var saAttr: windows.SECURITY_ATTRIBUTES = .{
1314813223 .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES),
......@@ -13151,10 +13226,11 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1315113226 };
1315213227
1315313228 const any_ignore =
13154 child.stdin_behavior == .ignore or
13155 child.stdout_behavior == .ignore or
13156 child.stderr_behavior == .ignore;
13229 options.stdin == .ignore or
13230 options.stdout == .ignore or
13231 options.stderr == .ignore;
1315713232
13233 // TODO: cache the handle to null file!
1315813234 const nul_handle = if (any_ignore)
1315913235 // "\Device\Null" or "\??\NUL"
1316013236 windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{
......@@ -13185,7 +13261,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1318513261
1318613262 var g_hChildStd_IN_Rd: ?windows.HANDLE = null;
1318713263 var g_hChildStd_IN_Wr: ?windows.HANDLE = null;
13188 switch (child.stdin_behavior) {
13264 switch (options.stdin) {
1318913265 .pipe => {
1319013266 try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr);
1319113267 },
......@@ -13198,14 +13274,15 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1319813274 .close => {
1319913275 g_hChildStd_IN_Rd = null;
1320013276 },
13277 .file => @panic("TODO implement passing file stdio in processSpawnWindows"),
1320113278 }
13202 errdefer if (child.stdin_behavior == .pipe) {
13279 errdefer if (options.stdin == .pipe) {
1320313280 windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr);
1320413281 };
1320513282
1320613283 var g_hChildStd_OUT_Rd: ?windows.HANDLE = null;
1320713284 var g_hChildStd_OUT_Wr: ?windows.HANDLE = null;
13208 switch (child.stdout_behavior) {
13285 switch (options.stdout) {
1320913286 .pipe => {
1321013287 try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr);
1321113288 },
......@@ -13218,14 +13295,15 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1321813295 .close => {
1321913296 g_hChildStd_OUT_Wr = null;
1322013297 },
13298 .file => @panic("TODO implement passing file stdio in processSpawnWindows"),
1322113299 }
13222 errdefer if (child.stdout_behavior == .pipe) {
13300 errdefer if (options.stdout == .pipe) {
1322313301 windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr);
1322413302 };
1322513303
1322613304 var g_hChildStd_ERR_Rd: ?windows.HANDLE = null;
1322713305 var g_hChildStd_ERR_Wr: ?windows.HANDLE = null;
13228 switch (child.stderr_behavior) {
13306 switch (options.stderr) {
1322913307 .pipe => {
1323013308 try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr);
1323113309 },
......@@ -13238,12 +13316,13 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1323813316 .close => {
1323913317 g_hChildStd_ERR_Wr = null;
1324013318 },
13319 .file => @panic("TODO implement passing file stdio in processSpawnWindows"),
1324113320 }
13242 errdefer if (child.stderr_behavior == .pipe) {
13321 errdefer if (options.stderr == .pipe) {
1324313322 windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr);
1324413323 };
1324513324
13246 var siStartInfo = windows.STARTUPINFOW{
13325 var siStartInfo: windows.STARTUPINFOW = .{
1324713326 .cb = @sizeOf(windows.STARTUPINFOW),
1324813327 .hStdError = g_hChildStd_ERR_Wr,
1324913328 .hStdOutput = g_hChildStd_OUT_Wr,
......@@ -13266,63 +13345,63 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1326613345 };
1326713346 var piProcInfo: windows.PROCESS_INFORMATION = undefined;
1326813347
13269 const cwd_w = if (child.cwd) |cwd| try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd) else null;
13270 defer if (cwd_w) |cwd| child.allocator.free(cwd);
13348 var arena_allocator = std.heap.ArenaAllocator.init(t.allocator);
13349 defer arena_allocator.deinit();
13350 const arena = arena_allocator.allocator();
13351
13352 const cwd_w = if (options.cwd) |cwd| try std.unicode.wtf8ToWtf16LeAllocZ(arena, cwd) else null;
1327113353 const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null;
1327213354
13273 const maybe_envp_buf = if (child.env_map) |env_map| try process.createWindowsEnvBlock(child.allocator, env_map) else null;
13274 defer if (maybe_envp_buf) |envp_buf| child.allocator.free(envp_buf);
13355 const maybe_envp_buf = if (options.env_map) |env_map| try env_map.createBlockWindows(arena) else null;
1327513356 const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null;
1327613357
13277 const app_name_wtf8 = child.argv[0];
13358 const app_name_wtf8 = options.argv[0];
1327813359 const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8);
1327913360
13280 // the cwd set in Child is in effect when choosing the executable path
13281 // to match posix semantics
13361 // The cwd provided by options is in effect when choosing the executable
13362 // path to match POSIX semantics.
1328213363 var cwd_path_w_needs_free = false;
1328313364 const cwd_path_w = x: {
1328413365 // If the app name is absolute, then we need to use its dirname as the cwd
1328513366 if (app_name_is_absolute) {
1328613367 cwd_path_w_needs_free = true;
1328713368 const dir = Dir.path.dirname(app_name_wtf8).?;
13288 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, dir);
13289 } else if (child.cwd) |cwd| {
13369 break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, dir);
13370 } else if (options.cwd) |cwd| {
1329013371 cwd_path_w_needs_free = true;
13291 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, cwd);
13372 break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, cwd);
1329213373 } else {
1329313374 break :x &[_:0]u16{}; // empty for cwd
1329413375 }
1329513376 };
13296 defer if (cwd_path_w_needs_free) child.allocator.free(cwd_path_w);
1329713377
13298 // If the app name has more than just a filename, then we need to separate that
13299 // into the basename and dirname and use the dirname as an addition to the cwd
13300 // path. This is because NtQueryDirectoryFile cannot accept FileName params with
13301 // path separators.
13378 // If the app name has more than just a filename, then we need to separate
13379 // that into the basename and dirname and use the dirname as an addition to
13380 // the cwd path. This is because NtQueryDirectoryFile cannot accept
13381 // FileName params with path separators.
1330213382 const app_basename_wtf8 = Dir.path.basename(app_name_wtf8);
1330313383 // If the app name is absolute, then the cwd will already have the app's dirname in it,
1330413384 // so only populate app_dirname if app name is a relative path with > 0 path separators.
1330513385 const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) Dir.path.dirname(app_name_wtf8) else null;
1330613386 const app_dirname_w: ?[:0]u16 = x: {
1330713387 if (maybe_app_dirname_wtf8) |app_dirname_wtf8| {
13308 break :x try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_dirname_wtf8);
13388 break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_dirname_wtf8);
1330913389 }
1331013390 break :x null;
1331113391 };
13312 defer if (app_dirname_w != null) child.allocator.free(app_dirname_w.?);
13313
13314 const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(child.allocator, app_basename_wtf8);
13315 defer child.allocator.free(app_name_w);
13392 const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_basename_wtf8);
1331613393
1331713394 const flags: windows.CreateProcessFlags = .{
13318 .create_suspended = child.start_suspended,
13395 .create_suspended = options.start_suspended,
1331913396 .create_unicode_environment = true,
13320 .create_no_window = child.create_no_window,
13397 .create_no_window = options.create_no_window,
1332113398 };
1332213399
1332313400 run: {
13324 const PATH: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{};
13325 const PATHEXT: [:0]const u16 = process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{};
13401 // We have to scan each time because the PEB environment pointer is not stable.
13402 const env_strings: WindowsEnvironStrings = .scan();
13403 const PATH = env_strings.PATH orelse &[_:0]u16{};
13404 const PATHEXT = env_strings.PATHEXT orelse &[_:0]u16{};
1332613405
1332713406 // In case the command ends up being a .bat/.cmd script, we need to escape things using the cmd.exe rules
1332813407 // and invoke cmd.exe ourselves in order to mitigate arbitrary command execution from maliciously
......@@ -13331,26 +13410,34 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1333113410 // We'll need to wait until we're actually trying to run the command to know for sure
1333213411 // if the resolved command has the `.bat` or `.cmd` extension, so we defer actually
1333313412 // serializing the command line until we determine how it should be serialized.
13334 var cmd_line_cache = WindowsCommandLineCache.init(child.allocator, child.argv);
13335 defer cmd_line_cache.deinit();
13413 var cmd_line_cache = WindowsCommandLineCache.init(arena, options.argv);
1333613414
1333713415 var app_buf: std.ArrayList(u16) = .empty;
13338 defer app_buf.deinit(child.allocator);
13339
13340 try app_buf.appendSlice(child.allocator, app_name_w);
13416 try app_buf.appendSlice(arena, app_name_w);
1334113417
1334213418 var dir_buf: std.ArrayList(u16) = .empty;
13343 defer dir_buf.deinit(child.allocator);
1334413419
1334513420 if (cwd_path_w.len > 0) {
13346 try dir_buf.appendSlice(child.allocator, cwd_path_w);
13421 try dir_buf.appendSlice(arena, cwd_path_w);
1334713422 }
1334813423 if (app_dirname_w) |app_dir| {
13349 if (dir_buf.items.len > 0) try dir_buf.append(child.allocator, Dir.path.sep);
13350 try dir_buf.appendSlice(child.allocator, app_dir);
13351 }
13352
13353 windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo) catch |no_path_err| {
13424 if (dir_buf.items.len > 0) try dir_buf.append(arena, Dir.path.sep);
13425 try dir_buf.appendSlice(arena, app_dir);
13426 }
13427
13428 windowsCreateProcessPathExt(
13429 t,
13430 arena,
13431 &dir_buf,
13432 &app_buf,
13433 PATHEXT,
13434 &cmd_line_cache,
13435 envp_ptr,
13436 cwd_w_ptr,
13437 flags,
13438 &siStartInfo,
13439 &piProcInfo,
13440 ) catch |no_path_err| {
1335413441 const original_err = switch (no_path_err) {
1335513442 // argv[0] contains unsupported characters that will never resolve to a valid exe.
1335613443 error.InvalidArg0 => return error.FileNotFound,
......@@ -13362,7 +13449,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1336213449 // If the app name had path separators, that disallows PATH searching,
1336313450 // and there's no need to search the PATH if the app name is absolute.
1336413451 // We still search the path if the cwd is absolute because of the
13365 // "cwd set in Child is in effect when choosing the executable path
13452 // "cwd provided by options is in effect when choosing the executable path
1336613453 // to match posix semantics" behavior--we don't want to skip searching
1336713454 // the PATH just because we were trying to set the cwd of the child process.
1336813455 if (app_dirname_w != null or app_name_is_absolute) {
......@@ -13372,9 +13459,21 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1337213459 var it = std.mem.tokenizeScalar(u16, PATH, ';');
1337313460 while (it.next()) |search_path| {
1337413461 dir_buf.clearRetainingCapacity();
13375 try dir_buf.appendSlice(child.allocator, search_path);
13376
13377 if (windowsCreateProcessPathExt(child.allocator, io, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, flags, &siStartInfo, &piProcInfo)) {
13462 try dir_buf.appendSlice(arena, search_path);
13463
13464 if (windowsCreateProcessPathExt(
13465 t,
13466 arena,
13467 &dir_buf,
13468 &app_buf,
13469 PATHEXT,
13470 &cmd_line_cache,
13471 envp_ptr,
13472 cwd_w_ptr,
13473 flags,
13474 &siStartInfo,
13475 &piProcInfo,
13476 )) {
1337813477 break :run;
1337913478 } else |err| switch (err) {
1338013479 // argv[0] contains unsupported characters that will never resolve to a valid exe.
......@@ -13389,35 +13488,18 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1338913488 };
1339013489 }
1339113490
13392 if (g_hChildStd_IN_Wr) |h| {
13393 child.stdin = File{ .handle = h };
13394 } else {
13395 child.stdin = null;
13396 }
13397 if (g_hChildStd_OUT_Rd) |h| {
13398 child.stdout = File{ .handle = h };
13399 } else {
13400 child.stdout = null;
13401 }
13402 if (g_hChildStd_ERR_Rd) |h| {
13403 child.stderr = File{ .handle = h };
13404 } else {
13405 child.stderr = null;
13406 }
13407
13408 child.id = piProcInfo.hProcess;
13409 child.thread_handle = piProcInfo.hThread;
13410 child.term = null;
13491 if (options.stdin == .pipe) windows.CloseHandle(g_hChildStd_IN_Rd.?);
13492 if (options.stderr == .pipe) windows.CloseHandle(g_hChildStd_ERR_Wr.?);
13493 if (options.stdout == .pipe) windows.CloseHandle(g_hChildStd_OUT_Wr.?);
1341113494
13412 if (child.stdin_behavior == .pipe) {
13413 posix.close(g_hChildStd_IN_Rd.?);
13414 }
13415 if (child.stderr_behavior == .pipe) {
13416 posix.close(g_hChildStd_ERR_Wr.?);
13417 }
13418 if (child.stdout_behavior == .pipe) {
13419 posix.close(g_hChildStd_OUT_Wr.?);
13420 }
13495 return .{
13496 .id = piProcInfo.hProcess,
13497 .thread_handle = piProcInfo.hThread,
13498 .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h } else null,
13499 .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h } else null,
13500 .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h } else null,
13501 .request_resource_usage_statistics = options.request_resource_usage_statistics,
13502 };
1342113503}
1342213504
1342313505/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path.
......@@ -13425,12 +13507,12 @@ fn processSpawnWindows(userdata: ?*anyopaque, child: *process.Child) process.Spa
1342513507/// Note: `app_buf` should not contain any leading path separators.
1342613508/// Note: If the dir is the cwd, dir_buf should be empty (len = 0).
1342713509fn windowsCreateProcessPathExt(
13428 allocator: Allocator,
13510 arena: Allocator,
1342913511 dir_buf: *std.ArrayList(u16),
1343013512 app_buf: *std.ArrayList(u16),
1343113513 pathext: [:0]const u16,
1343213514 cmd_line_cache: *WindowsCommandLineCache,
13433 envp_ptr: ?[*]u16,
13515 envp_ptr: ?[*:0]const u16,
1343413516 cwd_ptr: ?[*:0]u16,
1343513517 flags: windows.CreateProcessFlags,
1343613518 lpStartupInfo: *windows.STARTUPINFOW,
......@@ -13471,7 +13553,7 @@ fn windowsCreateProcessPathExt(
1347113553 // that scenario.
1347213554 var dir = dir: {
1347313555 // needs to be null-terminated
13474 try dir_buf.append(allocator, 0);
13556 try dir_buf.append(arena, 0);
1347513557 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
1347613558 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1347713559 const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);
......@@ -13482,8 +13564,8 @@ fn windowsCreateProcessPathExt(
1348213564 defer windows.CloseHandle(dir.handle);
1348313565
1348413566 // Add wildcard and null-terminator
13485 try app_buf.append(allocator, '*');
13486 try app_buf.append(allocator, 0);
13567 try app_buf.append(arena, '*');
13568 try app_buf.append(arena, 0);
1348713569 const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0];
1348813570
1348913571 // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries
......@@ -13563,10 +13645,10 @@ fn windowsCreateProcessPathExt(
1356313645 if (unappended_exists) {
1356413646 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1356513647 '/', '\\' => {},
13566 else => try dir_buf.append(allocator, Dir.path.sep),
13648 else => try dir_buf.append(arena, Dir.path.sep),
1356713649 };
13568 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
13569 try dir_buf.append(allocator, 0);
13650 try dir_buf.appendSlice(arena, app_buf.items[0..app_name_len]);
13651 try dir_buf.append(arena, 0);
1357013652 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1357113653
1357213654 const is_bat_or_cmd = bat_or_cmd: {
......@@ -13588,7 +13670,15 @@ fn windowsCreateProcessPathExt(
1358813670 else
1358913671 full_app_name;
1359013672
13591 if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| {
13673 if (windowsCreateProcess(
13674 app_name_w.ptr,
13675 cmd_line_w.ptr,
13676 envp_ptr,
13677 cwd_ptr,
13678 flags,
13679 lpStartupInfo,
13680 lpProcessInformation,
13681 )) |_| {
1359213682 return;
1359313683 } else |err| switch (err) {
1359413684 error.FileNotFound,
......@@ -13623,11 +13713,11 @@ fn windowsCreateProcessPathExt(
1362313713 dir_buf.shrinkRetainingCapacity(dir_path_len);
1362413714 if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) {
1362513715 '/', '\\' => {},
13626 else => try dir_buf.append(allocator, Dir.path.sep),
13716 else => try dir_buf.append(arena, Dir.path.sep),
1362713717 };
13628 try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]);
13629 try dir_buf.appendSlice(allocator, ext);
13630 try dir_buf.append(allocator, 0);
13718 try dir_buf.appendSlice(arena, app_buf.items[0..app_name_len]);
13719 try dir_buf.appendSlice(arena, ext);
13720 try dir_buf.append(arena, 0);
1363113721 const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
1363213722
1363313723 const is_bat_or_cmd = switch (ext_enum) {
......@@ -13667,41 +13757,61 @@ fn windowsCreateProcessPathExt(
1366713757fn windowsCreateProcess(
1366813758 app_name: [*:0]u16,
1366913759 cmd_line: [*:0]u16,
13670 envp_ptr: ?[*]u16,
13760 env_ptr: ?[*:0]const u16,
1367113761 cwd_ptr: ?[*:0]u16,
1367213762 flags: windows.CreateProcessFlags,
1367313763 lpStartupInfo: *windows.STARTUPINFOW,
1367413764 lpProcessInformation: *windows.PROCESS_INFORMATION,
1367513765) !void {
13676 // TODO the docs for environment pointer say:
13677 // > A pointer to the environment block for the new process. If this parameter
13678 // > is NULL, the new process uses the environment of the calling process.
13679 // > ...
13680 // > An environment block can contain either Unicode or ANSI characters. If
13681 // > the environment block pointed to by lpEnvironment contains Unicode
13682 // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
13683 // > If this parameter is NULL and the environment block of the parent process
13684 // > contains Unicode characters, you must also ensure that dwCreationFlags
13685 // > includes CREATE_UNICODE_ENVIRONMENT.
13686 // This seems to imply that we have to somehow know whether our process parent passed
13687 // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter.
13688 // Since we do not know this information that would imply that we must not pass NULL
13689 // for the parameter.
13690 // However this would imply that programs compiled with -DUNICODE could not pass
13691 // environment variables to programs that were not, which seems unlikely.
13692 // More investigation is needed.
13693 return windows.CreateProcessW(
13766 if (windows.kernel32.CreateProcessW(
1369413767 app_name,
1369513768 cmd_line,
1369613769 null,
1369713770 null,
1369813771 windows.TRUE,
1369913772 flags,
13700 @as(?*anyopaque, @ptrCast(envp_ptr)),
13773 env_ptr,
1370113774 cwd_ptr,
1370213775 lpStartupInfo,
1370313776 lpProcessInformation,
13704 );
13777 ) == 0) switch (windows.GetLastError()) {
13778 .FILE_NOT_FOUND => return error.FileNotFound,
13779 .PATH_NOT_FOUND => return error.FileNotFound,
13780 .DIRECTORY => return error.FileNotFound,
13781 .ACCESS_DENIED => return error.AccessDenied,
13782 .INVALID_PARAMETER => unreachable,
13783 .INVALID_NAME => return error.InvalidName,
13784 .FILENAME_EXCED_RANGE => return error.NameTooLong,
13785 .SHARING_VIOLATION => return error.FileBusy,
13786
13787 // These are all the system errors that are mapped to ENOEXEC by
13788 // the undocumented _dosmaperr (old CRT) or __acrt_errno_map_os_error
13789 // (newer CRT) functions. Their code can be found in crt/src/dosmap.c (old SDK)
13790 // or urt/misc/errno.cpp (newer SDK) in the Windows SDK.
13791 .BAD_FORMAT,
13792 .INVALID_STARTING_CODESEG, // MIN_EXEC_ERROR in errno.cpp
13793 .INVALID_STACKSEG,
13794 .INVALID_MODULETYPE,
13795 .INVALID_EXE_SIGNATURE,
13796 .EXE_MARKED_INVALID,
13797 .BAD_EXE_FORMAT,
13798 .ITERATED_DATA_EXCEEDS_64k,
13799 .INVALID_MINALLOCSIZE,
13800 .DYNLINK_FROM_INVALID_RING,
13801 .IOPL_NOT_ENABLED,
13802 .INVALID_SEGDPL,
13803 .AUTODATASEG_EXCEEDS_64k,
13804 .RING2SEG_MUST_BE_MOVABLE,
13805 .RELOC_CHAIN_XEEDS_SEGLIM,
13806 .INFLOOP_IN_RELOC_CHAIN, // MAX_EXEC_ERROR in errno.cpp
13807 // This one is not mapped to ENOEXEC but it is possible, for example
13808 // when calling CreateProcessW on a plain text file with a .exe extension
13809 .EXE_MACHINE_TYPE_MISMATCH,
13810 => return error.InvalidExe,
13811
13812 .COMMITMENT_LIMIT => return error.SystemResources,
13813 else => |err| return windows.unexpectedError(err),
13814 };
1370513815}
1370613816
1370713817/// Case-insensitive WTF-16 lookup
lib/std/fs/test.zig+1-1
......@@ -79,7 +79,7 @@ const PathType = enum {
7979 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
8080 var fd_path_buf: [Dir.max_path_bytes]u8 = undefined;
8181 const dir_path = fd_path_buf[0..try dir.realPath(io, &fd_path_buf)];
82 const windows_path_type = windows.getWin32PathType(u8, dir_path);
82 const windows_path_type = Dir.path.getWin32PathType(u8, dir_path);
8383 switch (windows_path_type) {
8484 .unc_absolute => return Dir.path.joinZ(allocator, &.{ dir_path, relative_path }),
8585 .drive_absolute => {
lib/std/os/windows.zig+2-13
......@@ -3756,17 +3756,6 @@ pub fn GetModuleFileNameW(hModule: ?HMODULE, buf_ptr: [*]u16, buf_len: DWORD) Ge
37563756 return buf_ptr[0..rc :0];
37573757}
37583758
3759pub const TerminateProcessError = error{ AccessDenied, Unexpected };
3760
3761pub fn TerminateProcess(hProcess: HANDLE, uExitCode: UINT) TerminateProcessError!void {
3762 if (kernel32.TerminateProcess(hProcess, uExitCode) == 0) {
3763 switch (GetLastError()) {
3764 Win32Error.ACCESS_DENIED => return error.AccessDenied,
3765 else => |err| return unexpectedError(err),
3766 }
3767 }
3768}
3769
37703759pub const NtAllocateVirtualMemoryError = error{
37713760 AccessDenied,
37723761 InvalidParameter,
......@@ -3919,7 +3908,7 @@ pub fn CreateProcessW(
39193908 lpThreadAttributes: ?*SECURITY_ATTRIBUTES,
39203909 bInheritHandles: BOOL,
39213910 dwCreationFlags: CreateProcessFlags,
3922 lpEnvironment: ?*anyopaque,
3911 lpEnvironment: ?[*:0]u16,
39233912 lpCurrentDirectory: ?LPCWSTR,
39243913 lpStartupInfo: *STARTUPINFOW,
39253914 lpProcessInformation: *PROCESS_INFORMATION,
......@@ -4539,7 +4528,7 @@ const LocalDevicePathType = enum {
45394528};
45404529
45414530/// Only relevant for Win32 -> NT path conversion.
4542/// Asserts `path` is of type `Win32PathType.local_device`.
4531/// Asserts `path` is of type `std.fs.path.Win32PathType.local_device`.
45434532fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
45444533 if (std.debug.runtime_safety) {
45454534 assert(std.fs.path.getWin32PathType(T, path) == .local_device);
lib/std/os/windows/kernel32.zig+1-1
......@@ -265,7 +265,7 @@ pub extern "kernel32" fn CreateProcessW(
265265 lpThreadAttributes: ?*SECURITY_ATTRIBUTES,
266266 bInheritHandles: BOOL,
267267 dwCreationFlags: windows.CreateProcessFlags,
268 lpEnvironment: ?LPVOID,
268 lpEnvironment: ?[*:0]const u16,
269269 lpCurrentDirectory: ?LPCWSTR,
270270 lpStartupInfo: *STARTUPINFOW,
271271 lpProcessInformation: *PROCESS_INFORMATION,
lib/std/os/windows/test.zig+3-3
......@@ -274,8 +274,8 @@ test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
274274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275275
276276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = windows.getWin32PathType(u16, path);
278 const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
277 const wtf16_type = std.fs.path.getWin32PathType(u16, path);
278 const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items);
279279
280280 checkPathType(windows_type, wtf16_type) catch |err| {
281281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
......@@ -295,7 +295,7 @@ test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
295295 }
296296}
297297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void {
299299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300300 .unc_absolute => .UncAbsolute,
301301 .drive_absolute => .DriveAbsolute,
lib/std/process/Args.zig+8-13
......@@ -29,13 +29,8 @@ pub const Iterator = struct {
2929 /// Initialize the args iterator. Consider using `initAllocator` instead
3030 /// for cross-platform compatibility.
3131 pub fn init(a: Args) Iterator {
32 if (native_os == .wasi) {
33 @compileError("In WASI, use initAllocator instead.");
34 }
35 if (native_os == .windows) {
36 @compileError("In Windows, use initAllocator instead.");
37 }
38
32 if (native_os == .wasi) @compileError("In WASI, use initAllocator instead.");
33 if (native_os == .windows) @compileError("In Windows, use initAllocator instead.");
3934 return .{ .inner = .init(a) };
4035 }
4136
......@@ -44,10 +39,10 @@ pub const Iterator = struct {
4439 /// You must deinitialize iterator's internal buffers by calling `deinit` when done.
4540 pub fn initAllocator(a: Args, gpa: Allocator) InitError!Iterator {
4641 if (native_os == .wasi and !builtin.link_libc) {
47 return .{ .inner = try .init(a, gpa) };
42 return .{ .inner = try .init(gpa) };
4843 }
4944 if (native_os == .windows) {
50 return .{ .inner = try .init(a, gpa) };
45 return .{ .inner = try .init(gpa, a.vector) };
5146 }
5247
5348 return .{ .inner = .init(a) };
......@@ -111,7 +106,7 @@ pub const Iterator = struct {
111106 ///
112107 /// The iterator stores and uses `cmd_line_w`, so its memory must be valid for
113108 /// at least as long as the returned Windows.
114 pub fn init(allocator: Allocator, cmd_line_w: []const u16) Windows.InitError!Windows {
109 pub fn init(gpa: Allocator, cmd_line_w: []const u16) Windows.InitError!Windows {
115110 const wtf8_len = std.unicode.calcWtf8Len(cmd_line_w);
116111
117112 // This buffer must be large enough to contain contiguous NUL-terminated slices
......@@ -121,11 +116,11 @@ pub const Iterator = struct {
121116 // - The first argument needs one extra byte of space allocated for its NUL
122117 // terminator, but for each subsequent argument the necessary whitespace
123118 // between arguments guarantees room for their NUL terminator(s).
124 const buffer = try allocator.alloc(u8, wtf8_len + 1);
125 errdefer allocator.free(buffer);
119 const buffer = try gpa.alloc(u8, wtf8_len + 1);
120 errdefer gpa.free(buffer);
126121
127122 return .{
128 .allocator = allocator,
123 .allocator = gpa,
129124 .cmd_line = cmd_line_w,
130125 .buffer = buffer,
131126 };
lib/std/process/Environ.zig+65-39
......@@ -15,7 +15,7 @@ const mem = std.mem;
1515block: Block,
1616
1717pub const empty: Environ = .{
18 .block = switch (@TypeOf(Block)) {
18 .block = switch (Block) {
1919 void => {},
2020 else => &.{},
2121 },
......@@ -65,7 +65,7 @@ pub const Map = struct {
6565 @as(u8, @intCast((cp_upper >> 0) & 0xff)),
6666 });
6767 }
68 return h.final();
68 return @truncate(h.final());
6969 }
7070 return std.array_hash_map.hashString(s);
7171 }
......@@ -293,8 +293,8 @@ pub const Map = struct {
293293 return envp_buf;
294294 }
295295
296 /// Caller must free result.
297 pub fn createBlockWindows(map: *const Map, gpa: Allocator) Allocator.Error![]u16 {
296 /// Caller owns result.
297 pub fn createBlockWindows(map: *const Map, gpa: Allocator) error{ OutOfMemory, InvalidWtf8 }![:0]u16 {
298298 // count bytes needed
299299 const max_chars_needed = x: {
300300 // Only need 2 trailing NUL code units for an empty environment
......@@ -330,54 +330,27 @@ pub const Map = struct {
330330 result[i] = 0;
331331 i += 1;
332332 }
333 return try gpa.realloc(result, i);
333 const reallocated = try gpa.realloc(result, i);
334 return reallocated[0 .. i - 1 :0];
334335 }
335336};
336337
337338pub const CreateMapError = error{
338339 OutOfMemory,
339340 /// WASI-only. `environ_sizes_get` or `environ_get` failed for an
340 /// unexpected reason.
341 /// unanticipated, undocumented reason.
341342 Unexpected,
342343};
343344
344345/// Allocates a `Map` and copies environment block into it.
345346pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {
347 if (native_os == .windows)
348 return createMapWide(std.os.windows.peb().ProcessParameters.Environment, allocator);
349
346350 var result = Map.init(allocator);
347351 errdefer result.deinit();
348352
349 if (native_os == .windows) {
350 const ptr = std.os.windows.peb().ProcessParameters.Environment;
351
352 var i: usize = 0;
353 while (ptr[i] != 0) {
354 const key_start = i;
355
356 // There are some special environment variables that start with =,
357 // so we need a special case to not treat = as a key/value separator
358 // if it's the first character.
359 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
360 if (ptr[key_start] == '=') i += 1;
361
362 while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {}
363 const key_w = ptr[key_start..i];
364 const key = try unicode.wtf16LeToWtf8Alloc(allocator, key_w);
365 errdefer allocator.free(key);
366
367 if (ptr[i] == '=') i += 1;
368
369 const value_start = i;
370 while (ptr[i] != 0) : (i += 1) {}
371 const value_w = ptr[value_start..i];
372 const value = try unicode.wtf16LeToWtf8Alloc(allocator, value_w);
373 errdefer allocator.free(value);
374
375 i += 1; // skip over null byte
376
377 try result.putMove(key, value);
378 }
379 return result;
380 } else if (native_os == .wasi and !builtin.link_libc) {
353 if (native_os == .wasi and !builtin.link_libc) {
381354 var environ_count: usize = undefined;
382355 var environ_buf_size: usize = undefined;
383356
......@@ -439,6 +412,40 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {
439412 }
440413}
441414
415pub fn createMapWide(ptr: [*:0]u16, gpa: Allocator) CreateMapError!Map {
416 var result = Map.init(gpa);
417 errdefer result.deinit();
418
419 var i: usize = 0;
420 while (ptr[i] != 0) {
421 const key_start = i;
422
423 // There are some special environment variables that start with =,
424 // so we need a special case to not treat = as a key/value separator
425 // if it's the first character.
426 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
427 if (ptr[key_start] == '=') i += 1;
428
429 while (ptr[i] != 0 and ptr[i] != '=') : (i += 1) {}
430 const key_w = ptr[key_start..i];
431 const key = try unicode.wtf16LeToWtf8Alloc(gpa, key_w);
432 errdefer gpa.free(key);
433
434 if (ptr[i] == '=') i += 1;
435
436 const value_start = i;
437 while (ptr[i] != 0) : (i += 1) {}
438 const value_w = ptr[value_start..i];
439 const value = try unicode.wtf16LeToWtf8Alloc(gpa, value_w);
440 errdefer gpa.free(value);
441
442 i += 1; // skip over null byte
443
444 try result.putMove(key, value);
445 }
446 return result;
447}
448
442449pub const ContainsError = error{
443450 OutOfMemory,
444451 /// On Windows, environment variable keys provided by the user must be
......@@ -777,7 +784,7 @@ test "convert from Environ to Map and back again" {
777784 const arena = arena_allocator.allocator();
778785
779786 const environ: Environ = switch (native_os) {
780 .windows => .{ .block = try map.createBlockWindows(arena) },
787 .windows => return error.SkipZigTest,
781788 .wasi => if (!builtin.libc) return error.SkipZigTest,
782789 else => .{ .block = try map.createBlockPosix(arena, .{}) },
783790 };
......@@ -804,3 +811,22 @@ test "convert from Environ to Map and back again" {
804811 try testing.expectEqualDeep(map.keys(), map2.keys());
805812 try testing.expectEqualDeep(map.values(), map2.values());
806813}
814
815test createMapWide {
816 const gpa = testing.allocator;
817
818 var map: Map = .init(gpa);
819 defer map.deinit();
820 try map.put("FOO", "BAR");
821 try map.put("A", "");
822 try map.put("", "B");
823
824 const environ: [:0]u16 = try map.createBlockWindows(gpa);
825 defer gpa.free(environ);
826
827 var map2 = try createMapWide(environ, gpa);
828 defer map2.deinit();
829
830 try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "=B" }, map2.keys());
831 try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "" }, map2.values());
832}