-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Open
Description
In #7242, support for Windows 7 was/is requested.
There YY-Thunks getting used by Zig was suggested, which seems like an overkill, since only few APIs are missing:
RtlGetSystemTimePreciseRtlWaitOnAddressRtlWakeAddressSingleRtlWakeAddressAll
For example, in zig/lib/std/os/windows/polyfill.zig, I already implemented 3 out of said 4 functions, which you can merge if you want:
const std = @import("../../std.zig");
const builtin = @import("builtin");
const testing = std.testing;
const assert = std.debug.assert;
const math = std.math;
const posix = std.posix;
const windows = std.os.windows;
const kernel = windows.kernel32;
// MARK: type shorcuts.
const BOOL = windows.BOOL;
const BOOLEAN = windows.BOOLEAN;
const DWORD = windows.DWORD;
const HANDLE = windows.HANDLE;
const NTSTATUS = windows.NTSTATUS;
const SIZE_T = windows.SIZE_T;
const SRWLOCK = windows.SRWLOCK;
// MARK: constants.
const FALSE = windows.FALSE;
const INFINITE = windows.INFINITE;
const TRUE = windows.TRUE;
pub const HEAP_ZERO_MEMORY: u32 = 0x00000008;
// MARK: Windows 7 API missing from Zig (at time of writting).
pub extern "kernel32" fn RtlAcquireSRWLockExclusive(
SRWLock: *SRWLOCK,
) callconv(.winapi) void;
pub extern "kernel32" fn RtlReleaseSRWLockExclusive(
SRWLock: *SRWLOCK,
) callconv(.winapi) void;
pub const EVENT_TYPE = enum(u32) {
NotificationEvent = 0,
SynchronizationEvent = 1,
};
pub extern "kernel32" fn NtCreateEvent(
eventHandle: *HANDLE,
desiredAccess: DWORD,
objectAttributes: ?*windows.OBJECT_ATTRIBUTES,
eventType: DWORD,
initialState: BOOLEAN,
) callconv(.winapi) NTSTATUS;
pub extern "kernel32" fn NtSetEvent(
eventHandle: HANDLE,
PreviousState: ?*windows.LONG
) callconv(.winapi) NTSTATUS;
pub extern "kernel32" fn NtClose(
eventHandle: HANDLE,
) callconv(.winapi) NTSTATUS;
pub extern "kernel32" fn NtWaitForSingleObject(
hHandle: HANDLE,
bAlertable: BOOL,
dwMilliseconds: ?*const windows.LARGE_INTEGER,
) callconv(.winapi) NTSTATUS;
// MARK: C/C++ implementations.
//
// NOTE: things like mesuring-time are `extern` C++, and
// that should improve performance, otherwise,
// we probably could write these in Zig as well.
/// WARNING: The C++ code was/is yet in progress.
pub extern fn ZigRtlGetSystemTimePrecise() windows.LARGE_INTEGER;
// MARK: Zig implementations.
pub inline fn NT_SUCCESS(status: NTSTATUS) bool { return @intFromEnum(status) >= 0; }
pub const WaitEntry = extern struct {
/// The address that's waited for by this thread.
address: *const anyopaque,
/// Native event created to pause/resume waiting thread.
eventHandle: HANDLE = undefined,
/// Points to the next WaitEntry in the linked list.
next: *WaitEntry = undefined,
/// Points to the previous WaitEntry in the linked list.
previous: *WaitEntry = undefined,
};
/// Linked-list responsible only for a specific address-range, where
/// said range is decided by our address-hashing logic.
pub const WaitRange = extern struct {
/// Used to ensure thread(s) calling wait/wake functions on this instance's
/// address-range get blocked until this instance's pending changes finish.
lock: SRWLOCK = .{},
/// First entry in this linked-list.
///
/// Is set to invalid if there are no threads waiting on this address-range.
firstEntry: *WaitEntry = &invalidWaitEntry,
};
var invalidWaitEntry: WaitEntry = undefined;
const waitHashTableSize: comptime_int = 128;
var g_waitHashTable: [waitHashTableSize]WaitRange = [1]WaitRange{ .{} } ** waitHashTableSize;
// Some checks for correct type and array size.
comptime {
const entryType = @TypeOf(g_waitHashTable[0].firstEntry);
if (entryType != *WaitEntry)
@compileError("Type mismatch, expected: " ++ @typeName(*WaitEntry)
++ " but was: " ++ @typeName(entryType));
}
/// Finds the WaitRange for given address's address-range.
///
/// A.K.A. our address-hashing logic.
inline fn findWaitRange(address: *const volatile anyopaque) *WaitRange {
return &g_waitHashTable[
(@as(usize, @intFromPtr(address)) >> 4) % g_waitHashTable.len
];
}
inline fn atomicLoadPtr(comptime T: type, ptr: *const volatile anyopaque) T {
return @atomicLoad(T,
@as(*const volatile T, @alignCast(@ptrCast(ptr))),
.acquire);
}
inline fn loadPtr(comptime T: type, ptr: *const anyopaque) T {
return @as(*const T, @alignCast(@ptrCast(ptr))).*;
}
/// Compares given volatile memory against given normal memory.
///
/// WARNING: panics if you pass any unsupported size.
pub inline fn isVolatileDataEqual(
volatileData: *const volatile anyopaque,
normalData: *const anyopaque,
dataSize: usize,
) bool {
switch (dataSize) {
1 => {
const actual = atomicLoadPtr(u8, volatileData);
const expected = loadPtr(u8, normalData);
return actual == expected;
},
2 => {
const actual = atomicLoadPtr(u16, volatileData);
const expected = loadPtr(u16, normalData);
return actual == expected;
},
4 => {
const actual = atomicLoadPtr(u32, volatileData);
const expected = loadPtr(u32, normalData);
return actual == expected;
},
8 => {
const actual = atomicLoadPtr(u64, volatileData);
const expected = loadPtr(u64, normalData);
return actual == expected;
},
else => {
const msg = "ZigRtlWaitOnAddress: unsupported size: ";
var buf: [21]u8 = undefined;
_ = std.fmt.bufPrintZ(&buf, "{d}", .{dataSize})
catch @panic(msg);
@panic(msg ++ buf);
},
}
}
/// WARNING: Call this only if the `WaitRange` is already locked.
inline fn removeWaitEntryUnlocked(list: *WaitRange, entry: *WaitEntry) void {
// TRACE Zig/RtlWait/remove: Skips if already marked as removed.
if (entry.*.previous == &invalidWaitEntry) {
return;
}
if (entry.*.next == entry) {
// The entry being removed was the only entry.
list.*.firstEntry = &invalidWaitEntry;
} else {
const previous = entry.*.previous;
const next = entry.*.next;
previous.*.next = next;
next.*.previous = previous;
if (entry == list.*.firstEntry) {
// WaitRange's first entry was removed, hence
// we need to set new first entry.
list.*.firstEntry = next;
}
}
// TRACE Zig/RtlWait/remove: marks as removed.
entry.*.previous = &invalidWaitEntry;
}
comptime {
@export(&ZigRtlWaitOnAddress, .{ .name = "ZigRtlWaitOnAddress", .linkage = .strong });
}
/// Creates Read-Write listeners on given addresses, blocks current thread, and
/// returns only if the given addresses have no longer the same value.
pub fn ZigRtlWaitOnAddress(
addressArg: ?*const anyopaque,
compareAddressArg: ?*const anyopaque,
addressSize: windows.SIZE_T,
timeout: ?*const windows.LARGE_INTEGER,
) callconv(.C) NTSTATUS {
var status: NTSTATUS = .SUCCESS;
// Argument validation(s).
const address = addressArg orelse {
return .INVALID_PARAMETER;
};
const compareAddress = compareAddressArg orelse {
return .INVALID_PARAMETER;
};
if (addressSize != 1
and addressSize != 2
and addressSize != 4
and addressSize != 8
) {
return .INVALID_PARAMETER;
}
// Finds linked-list for address's address-range.
const list: *WaitRange = findWaitRange(address);
var entry: WaitEntry = .{
.address = address,
};
// Syncs.
RtlAcquireSRWLockExclusive(&list.*.lock); {
defer RtlReleaseSRWLockExclusive(&list.*.lock);
// Ensures values are NOT already different.
if ( ! isVolatileDataEqual(address, compareAddress, addressSize)) {
return .SUCCESS;
}
// Creates an event to wait on it.
status = NtCreateEvent(
&entry.eventHandle,
windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE,
null,
@intFromEnum(EVENT_TYPE.NotificationEvent),
FALSE,
);
if ( ! NT_SUCCESS(status)) {
return status;
}
errdefer _ = NtClose(entry.eventHandle);
// Inserts new entry into the linked-list, where
// if linked-list is empty, sets first entry.
if (list.*.firstEntry == &invalidWaitEntry) {
entry.previous = &entry;
entry.next = &entry;
list.*.firstEntry = &entry;
} else {
// Otherwise, adds to the end of the list.
const lastEntry = list.*.firstEntry.*.previous;
entry.previous = lastEntry;
entry.next = list.*.firstEntry;
lastEntry.*.next = &entry;
list.*.firstEntry.*.previous = &entry;
}
}
defer _ = NtClose(entry.eventHandle);
// At last, actual waiting.
status = NtWaitForSingleObject(
entry.eventHandle,
FALSE,
timeout);
assert (NT_SUCCESS(status));
// Removes entry on timeout, note that normally the
// other-thread which wakes this-thread is responsible for removing entry.
if (status == .TIMEOUT or ! NT_SUCCESS(status)) {
RtlAcquireSRWLockExclusive(&list.*.lock);
defer RtlReleaseSRWLockExclusive(&list.*.lock);
removeWaitEntryUnlocked(list, &entry);
}
return status;
}
fn wakeByAddressImpl(addressArg: ?*const anyopaque, wakeAll: bool) void {
const address = addressArg orelse return;
const list: *WaitRange = findWaitRange(address);
RtlAcquireSRWLockExclusive(&list.*.lock);
defer RtlReleaseSRWLockExclusive(&list.*.lock);
// Maybe there's nothing to wake.
var entry = list.*.firstEntry;
if (entry == &invalidWaitEntry) {
return;
}
// Starts waking threads from the beginning, like FIFO, and
// note that this is only because Windows-API docs mention such behavior.
while (true) {
const nextEntry: *WaitEntry = entry.*.next;
if (entry.*.address == address) {
removeWaitEntryUnlocked(list, entry);
// Wakes the thread(s).
const status: NTSTATUS = NtSetEvent(entry.*.eventHandle, null);
assert (NT_SUCCESS(status));
if ( ! wakeAll) {
break;
}
}
const firstEntry = list.*.firstEntry;
if (firstEntry == &invalidWaitEntry
or nextEntry == firstEntry
) {
break;
}
entry = nextEntry;
}
}
comptime {
@export(&ZigRtlWakeAddressSingle, .{ .name = "ZigRtlWakeAddressSingle", .linkage = .strong });
}
pub fn ZigRtlWakeAddressSingle(address: ?*const anyopaque) callconv(.C) void {
wakeByAddressImpl(address, false);
}
comptime {
@export(&ZigRtlWakeAddressAll, .{ .name = "ZigRtlWakeAddressAll", .linkage = .strong });
}
pub fn ZigRtlWakeAddressAll(address: ?*const anyopaque) callconv(.C) void {
wakeByAddressImpl(address, true);
}Usage:
Usage should be as simple as:
- Searching in Zig's source-code for mentiond functions,
- And then replacing each with the polyfill,
- Like from
windows.ntdll.RtlWaitOnAddress(...)towindows.polyfill.ZigRtlWaitOnAddress(...).
Metadata
Metadata
Assignees
Labels
No labels