Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ linker = "x86_64-sun-solaris-gcc"
# === Windows MSVC (not commonly used in cross-compilation) ===
[target.x86_64-pc-windows-msvc]
# linker = "link.exe" # MSVC requires Visual Studio environment

[target.aarch64-pc-windows-msvc]
# The native LLVM SDK embeds rpmalloc and is built with the static MSVC CRT.
# A dynamic CRT would supply _strdup while LLVMDisposeMessage calls rpmalloc's
# free. Keep CRT allocation helpers in the same linked allocator domain.
rustflags = ["-C", "target-feature=+crt-static"]
11 changes: 11 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,12 @@ jobs:
cargo build --locked --release --target aarch64-pc-windows-msvc
--no-default-features --features llvm-target-aarch64 --jobs 2

- name: Verify native LLVM message allocator ownership
run: >-
cargo test --locked --release -p llvm --test message_ownership
--target aarch64-pc-windows-msvc --no-default-features
--features llvm-target-aarch64 --jobs 2

- name: Diagnose native ARM64 code generation
id: native_codegen
timeout-minutes: 10
Expand Down Expand Up @@ -958,6 +964,11 @@ jobs:
--no-default-features --features llvm-target-aarch64 --jobs 2
msvc_link_companions_keep_final_names_and_survive_failed_replacement

- name: Run native ARM64 MSVC executor restart regression
run: >-
cargo test --locked --test runtime_regressions --target aarch64-pc-windows-msvc
--no-default-features --features llvm-target-aarch64 --jobs 2

- name: Install Wave stdlib
shell: pwsh
run: |
Expand Down
49 changes: 49 additions & 0 deletions llvm/tests/message_ownership.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Exercise C API allocations and their matching LLVM disposal functions.
//! Native Windows SDKs with an integrated allocator must use a compatible CRT.
use inkwell::context::Context;
use inkwell::targets::TargetData;
use llvm_sys::core::{LLVMCreateMessage, LLVMDisposeMessage};
use std::ffi::{CStr, CString};

#[test]
fn llvm_messages_and_target_layouts_share_allocator_ownership() {
#[cfg(all(target_arch = "aarch64", target_os = "windows", target_env = "msvc"))]
assert!(
cfg!(target_feature = "crt-static"),
"the pinned Windows ARM64 LLVM SDK requires the static CRT, including when RUSTFLAGS is set"
);
// Multiple sizes exercise short strings as well as heap-backed LLVM text.
for length in [0, 1, 15, 16, 127, 4096] {
let text = CString::new("x".repeat(length)).unwrap();
for _ in 0..32 {
// SAFETY: LLVM copies a live NUL-terminated string. The returned
// allocation is read while live and disposed exactly once by LLVM.
unsafe {
let message = LLVMCreateMessage(text.as_ptr());
assert!(!message.is_null());
assert_eq!(CStr::from_ptr(message), text.as_c_str());
LLVMDisposeMessage(message);
}
}
}

let context = Context::create();
let expected = "e-m:w-p:64:64-i64:64-i128:128-n32:64-S128";
for _ in 0..32 {
let module = context.create_module("allocator-contract");
let target_data = TargetData::create(expected);
let layout = target_data.get_data_layout();
module.set_data_layout(&layout);
// This explicit destruction is where #493 crashed on native ARM64.
drop(layout);
drop(target_data);
assert_eq!(
module.get_data_layout().as_str().to_str().unwrap(),
expected
);
let ir = module.print_to_string();
assert!(ir.to_str().unwrap().contains(expected));
drop(ir);
module.verify().unwrap();
}
}
99 changes: 79 additions & 20 deletions std/process/spawn.wave
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,88 @@ pub struct ProcSpawnStdoutResult {
read_fd: i64;
}

pub fun _proc_dup_child_fd(src_fd: i64, dst_fd: i64) -> i64 {
if (src_fd < 0) {
return 0;
// dup may return a closed standard descriptor. Hold those slots until a
// duplicate outside 0..2 is available, then restore the closed slots.
fun _proc_save_child_fd(source: i64) -> i64 {
var low: array<i64, 3>;
var count: i64 = 0;
var saved: i64 = io_dup(source);
while (saved >= 0 && saved <= IO_STDERR_FD) {
low[count] = saved;
count += 1;
saved = io_dup(source);
}

if (src_fd == dst_fd) {
return 0;
var i: i64 = 0;
while (i < count) {
io_close(low[i]);
i += 1;
}
return saved;
}

var r: i64 = io_dup2(src_fd, dst_fd);
if (r < 0) {
return r;
pub fun _proc_remap_child_fds(stdin_fd: i64, stdout_fd: i64, stderr_fd: i64) -> i64 {
var sources: array<i64, 3> = [stdin_fd, stdout_fd, stderr_fd];
var saved: array<i64, 3> = [-1, -1, -1];
var result: i64 = 0;
var i: i64 = 0;
// Validate before retaining duplicates: an allocated temporary must not
// make an originally closed source number appear valid later in the plan.
while (i < 3) {
if (sources[i] >= 0 && sources[i] != i) {
var probe: i64 = io_dup(sources[i]);
if (probe < 0) {
return probe;
}
io_close(probe);
}
i += 1;
}
i = 0;
// Snapshot every changing source before any destination can overwrite it.
while (i < 3 && result >= 0) {
if (sources[i] >= 0 && sources[i] != i) {
saved[i] = _proc_save_child_fd(sources[i]);
if (saved[i] < 0) {
result = saved[i];
}
}
i += 1;
}
i = 0;
while (i < 3 && result >= 0) {
if (saved[i] >= 0) {
result = io_dup2(saved[i], i);
}
i += 1;
}
i = 0;
while (i < 3) {
if (saved[i] >= 0) {
io_close(saved[i]);
}
i += 1;
}
if (result < 0) {
return result;
}
// Shared sources stay open through all mappings and are closed once.
i = 0;
while (i < 3) {
if (sources[i] > IO_STDERR_FD) {
var first: bool = true;
var previous: i64 = 0;
while (previous < i) {
if (sources[previous] == sources[i]) {
first = false;
}
previous += 1;
}
if (first) {
io_close(sources[i]);
}
}
i += 1;
}

io_close(src_fd);
return 0;
}

Expand All @@ -98,15 +165,7 @@ pub fun proc_spawn_exec_raw(
}

if (pid == 0) {
if (_proc_dup_child_fd(stdin_fd, IO_STDIN_FD) < 0) {
proc_exit(PROC_EXIT_DUP_FAIL);
}

if (_proc_dup_child_fd(stdout_fd, IO_STDOUT_FD) < 0) {
proc_exit(PROC_EXIT_DUP_FAIL);
}

if (_proc_dup_child_fd(stderr_fd, IO_STDERR_FD) < 0) {
if (_proc_remap_child_fds(stdin_fd, stdout_fd, stderr_fd) < 0) {
proc_exit(PROC_EXIT_DUP_FAIL);
}

Expand Down
114 changes: 114 additions & 0 deletions std/sys/windows/completion_port.wave
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// This file is part of the Wave language project.
// SPDX-License-Identifier: Apache-2.0

// A socket stays associated with its completion port until closesocket.
// The executor owns another reference until cancellation packets are drained.
// Access is confined to the cooperative executor thread, including socket close.
import("std::mem::alloc")::{mem_alloc_zeroed, mem_free};

extern(system, "CreateIoCompletionPort") fun win_create_port(
file: ptr<u8>, port: ptr<u8>, key: u64, threads: u32
) -> ptr<u8>;
extern(system, "CloseHandle") fun win_close_port(handle: ptr<u8>) -> i32;
extern(system, "GetLastError") fun win_port_error() -> u32;

struct Association {
socket: i64;
next: ptr<Association>;
}
// Both supported Windows ABIs are 64-bit: i64 followed by one 64-bit pointer.
const ASSOCIATION_SIZE: i64 = 16;
static _port: ptr<u8> = null;
static _executor_active: bool = false;
static _sockets: ptr<Association> = null;

pub fun completion_port_handle() -> ptr<u8> {
return _port;
}

pub fun completion_port_acquire() -> i64 {
if (_port == null) {
_port = win_create_port(-1 as ptr<u8>, null, 0, 1);
if (_port == null) {
return -(win_port_error() as i64);
}
}
_executor_active = true;
return 0;
}

fun _close_if_unused() {
if (!_executor_active && _sockets == null && _port != null) {
// Keep ownership on failure so another release can retry it.
if (win_close_port(_port) != 0) {
_port = null;
}
}
}

pub fun completion_port_associate(socket: i64) -> i64 {
var status: i64 = completion_port_acquire();
if (status < 0) {
return status;
}
var existing: ptr<Association> = _sockets;
while (existing != null) {
if (existing.socket == socket) {
return 0;
}
existing = existing.next;
}
// Allocate before associating: an allocation failure must not leave an
// untracked socket irreversibly bound to this port.
var node: ptr<Association> = mem_alloc_zeroed(ASSOCIATION_SIZE) as ptr<Association>;
if (node == null) {
return -12;
}
if (win_create_port(socket as ptr<u8>, _port, 0, 1) == null) {
var error: i64 = -(win_port_error() as i64);
mem_free(node as ptr<u8>, ASSOCIATION_SIZE);
return error;
}
node.socket = socket;
node.next = _sockets;
_sockets = node;
return 0;
}

// Called only after Winsock has successfully closed the socket.
pub fun completion_port_socket_closed(socket: i64) {
var previous: ptr<Association> = null;
var node: ptr<Association> = _sockets;
while (node != null) {
if (node.socket == socket) {
if (previous == null) {
_sockets = node.next;
} else {
previous.next = node.next;
}
mem_free(node as ptr<u8>, ASSOCIATION_SIZE);
_close_if_unused();
return;
}
previous = node;
node = node.next;
}
}

// Successful final WSACleanup closes the process's remaining Winsock sockets.
pub fun completion_port_sockets_closed() {
var node: ptr<Association> = _sockets;
while (node != null) {
var next: ptr<Association> = node.next;
mem_free(node as ptr<u8>, ASSOCIATION_SIZE);
node = next;
}
_sockets = null;
_close_if_unused();
}

// The caller must drain all operations and join wait callbacks before release.
pub fun completion_port_release_executor() {
_executor_active = false;
_close_if_unused();
}
Loading
Loading