zig icon indicating copy to clipboard operation
zig copied to clipboard

Polyfill for Windows 7 OS

Open top-master opened this issue 2 months ago • 0 comments

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:

  • RtlGetSystemTimePrecise
  • RtlWaitOnAddress
  • RtlWakeAddressSingle
  • RtlWakeAddressAll

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(...) to windows.polyfill.ZigRtlWaitOnAddress(...).

top-master avatar Nov 04 '25 05:11 top-master