Skip to content

Commit

Permalink
Windows Backend (IOCP) (#42)
Browse files Browse the repository at this point in the history
# Description
This PR brings support for Windows using the [I/O Completion
Port](https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports).

Closes #10 

# Notes for reviewers
Unfortunately, this is a pretty beefy PR. I tried to clean the git
history so that it's kind of reviewable commit by commit though.

My approach was similar to what @mitchellh suggested me: first the
backend core, then the watchers building on top of it.

I also added support for Windows tests in GH Actions as he told me he
didn't have access to a Windows machine.

Please note that this work is far from perfect, it was my first time
playing with IOCP so expect some weirdness/mistakes.

## Known pain-points/limitations
**Timers**
Timings rely on `GetQueuedCompletionStatusEx` timeout parameter. I read
multiple times that timings on Windows were, well, slightly inaccurate
(plot twist: that's a euphemism). I'm not confident enough to deep dive
into this rabbit hole, so if somebody has more knowledge on that, feel
free to improve my implementation.

**Async**
I have the feeling that there is a better way to implement the `Async`
watchers compared to what I did. I couldn't come up with a better way
than inspiring myself from the `WASI` backend.

The part I struggled the most with was the fact that `Async` can be
notified before being linked to a `Loop`. This requires to store the
fact that it's notified inside the `Async` struct, but then it opens up
a lot of other questions and failed to find a satisfying solution taking
care of all of them.

I'll try to come back to it with a fresh mind but I'm open to
suggestions in the meantime 😁

**Completion port `HANDLE` registering**
In order to be able to wait for asynchronous I/O, `HANDLE` needs to be
linked to a Completion Port. However, you can only register it once
otherwise, the linking call fails. I first tried to let the user handle
that by giving the possibility to link a handle to a `Loop`, but that
ended up being unreliable and cumbersome when implementing Watchers.

My final decision was to ignore the failure and suppose that it always
works. Win32 documentation is not what we could call exhaustive, so I
don't if it's possible for an association to fail on the first call to
`CreateIoCompletionPort` with a newly created `HANDLE`. Worst scenario
is that it fails silently and asynchronous wait on this `HANDLE` never
completes. If anyone has an idea as to how we could properly solve that,
don't hesitate 😁

**UDP Benchmark tweaks**
This is described in #38

**Process support**
I didn't implement support for `Process` watchers yet. I feel like it's
not required to merge this (already big) PR. I'll see if adding support
is possible in the future.
  • Loading branch information
mitchellh authored Sep 21, 2023
2 parents 9068c5d + b6827f9 commit 69c3ae0
Show file tree
Hide file tree
Showing 17 changed files with 2,826 additions and 48 deletions.
35 changes: 28 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ jobs:
x86_64-linux-musl,
aarch64-macos,
x86_64-macos,
wasm32-wasi
wasm32-wasi,
x86_64-windows-gnu

# Broken but not in any obvious way:
# x86-linux-gnu,
# x86-linux-musl,

# Not yet supported:
# i386-windows,
# x86_64-windows-gnu,
# x86-windows,
]
runs-on: ${{ matrix.os }}
needs: test
needs: [test-x86_64-linux, test-x86_64-windows]
steps:
- name: Checkout code
uses: actions/checkout@v3
Expand All @@ -45,7 +43,7 @@ jobs:
- name: test
run: nix develop -c zig build --summary all -Dtarget=${{ matrix.target }}

test:
test-x86_64-linux:
strategy:
matrix:
os: [ubuntu-latest]
Expand Down Expand Up @@ -77,3 +75,26 @@ jobs:

# Run a full build to ensure that works
- run: nix build

test-x86_64-windows:
strategy:
matrix:
os: [windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0

- name: Install zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.12.0-dev.256+8b74eae9c

- name: test
run: zig build test --summary all

- name: build all benchmarks and examples
run: zig build -Dexample -Dbench --summary all
16 changes: 11 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,16 @@ pub fn build(b: *std.Build) !void {
.target = target,
.optimize = optimize,
});
b.installArtifact(static_lib);

static_lib.linkLibC();

// Link required libraries if targeting Windows
if (target.getOsTag() == .windows) {
static_lib.linkSystemLibrary("ws2_32");
static_lib.linkSystemLibrary("mswsock");
}

b.installArtifact(static_lib);
b.default_step.dependOn(&static_lib.step);

const static_binding_test = b.addExecutable(.{
Expand All @@ -107,10 +115,7 @@ pub fn build(b: *std.Build) !void {
// Dynamic C lib. We only build this if this is the native target so we
// can link to libxml2 on our native system.
if (target.isNative()) {
const dynamic_lib_name = if (target.isWindows())
"xev.dll"
else
"xev";
const dynamic_lib_name = "xev";

const dynamic_lib = b.addSharedLibrary(.{
.name = dynamic_lib_name,
Expand Down Expand Up @@ -169,6 +174,7 @@ pub fn build(b: *std.Build) !void {

b.installFile(file, "share/pkgconfig/libxev.pc");
}

// Benchmarks
_ = try benchTargets(b, target, optimize, bench_install, bench_name);

Expand Down
25 changes: 25 additions & 0 deletions examples/million-timers.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,36 @@ xev_cb_action timer_cb(xev_loop* loop, xev_completion* c, int result, void *user
return XEV_DISARM;
}

#ifdef _WIN32
#include <windows.h>
uint64_t hrtime(void) {
static int initialized = 0;
static LARGE_INTEGER start_timestamp;
static uint64_t qpc_tick_duration;

if (!initialized) {
initialized = 1;

LARGE_INTEGER qpc_freq;
QueryPerformanceFrequency(&qpc_freq);
qpc_tick_duration = 1e9 / qpc_freq.QuadPart;

QueryPerformanceCounter(&start_timestamp);
}

LARGE_INTEGER t;
QueryPerformanceCounter(&t);
t.QuadPart -= start_timestamp.QuadPart;

return (uint64_t)t.QuadPart * qpc_tick_duration;
}
#else
uint64_t hrtime(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_nsec + (ts.tv_sec * 1e9);
}
#endif

int main(void) {
xev_watcher* timers;
Expand Down
Loading

0 comments on commit 69c3ae0

Please sign in to comment.