Skip to content

Polyfill for Windows 7 OS #25806

@top-master

Description

@top-master

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(...).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions