Skip to content

Winsock in net.c, so the native path can build on Windows - #137

Open
sivert-io wants to merge 3 commits into
mainfrom
claude/voice-winsock-port
Open

sivert-io wants to merge 3 commits into
mainfrom
claude/voice-winsock-port

Conversation

@sivert-io

Copy link
Copy Markdown
Member

net.c was POSIX sockets only, and line 17 said exactly that: "Windows needs Winsock here before the desktop app can use the native path." build-helper.mjs already listed ws2_32, ole32 and winmm, so the link flags were staged for this and only the code was missing.

Shape of it

One typedef per platform difference rather than two copies of the body: gv_socket, gv_thread, gv_socklen, GV_BAD_SOCKET, gv_close_socket. The bodies of send_packet, the receive loop and resolve read the same on both.

What to look at

  • GV_BAD_SOCKET instead of fd < 0. Winsock's SOCKET is an unsigned handle, so the old test can never catch a failed socket. This is the one change that is a latent bug fix rather than a port.
  • WSAStartup is reference counted. It has to be stopped as many times as it was started, and a host process that called it before us must not be torn out from under. If you would rather @gryt/voice never touch global Winsock state and leave it to the embedder, say so — that is a reasonable different answer and it changes this function.
  • set_recv_timeout has its own function because SO_RCVTIMEO is a timeval on POSIX and a DWORD of milliseconds on Winsock. Pass the wrong one and you get a nonsense timeout rather than an error, which is the sort of thing that shows up months later as "voice is laggy on Windows".
  • The join still happens before the close, on both arms. Closing first leaves the receive thread blocked in recvfrom on a handle that is gone. The 200ms timeout is what bounds that wait, which is why it exists.

Verified

Windows, clang 23.1.5: the helper builds (1.0 MB), and native/tools/test_net.c exercises engine init, net open and net close with no audio device:

engine_init ... ok
net_open ... ok, chosen=-1
net_close ... ok
ALL OK

Linux is unaffected and still builds, checked on Debian 12 with gcc 12.2.

Two things found and deliberately not fixed here

gv_engine does not fit Windows' 1 MB default stack. Linux gives 8 MB. Anything holding one by value dies with 0xC00000FD before main prints. gv_voice already callocs it, so nothing shipping is affected, but it is a trap for the next person who writes a test, which is how I found it. The harness carries a comment saying so.

gryt-voice still hangs on Windows after this. Not in the socket layer: gv_voice_start goes engine, then net, then gv_device_start, and the harness above proves the first two complete. So it is device.c and miniaudio's WASAPI backend, which is its own problem and wants its own change.

Build flags

-lm is gone on Windows, where the MSVC runtime has no separate libm and it is a hard link error rather than a no-op. _CRT_SECURE_NO_WARNINGS replaces forty lines of the CRT's opinion of strtok. _DEFAULT_SOURCE is now POSIX-only.

🤖 Generated with Claude Code

sivert-io and others added 2 commits November 8, 2026 19:42
net.c was POSIX sockets only, and line 17 said so: Windows needs Winsock
before the desktop app can use the native path. build-helper.mjs already
listed ws2_32, ole32 and winmm, so the link flags were staged for this.

One typedef per difference rather than two copies of the body: gv_socket,
gv_thread, gv_socklen, GV_BAD_SOCKET, gv_close_socket. Winsock's socket is an
unsigned handle, so the old "fd < 0" test can never catch a failed one and
INVALID_SOCKET is the only correct check.

WSAStartup is reference counted, because it has to be stopped as many times as
it was started and a host that called it first must not be torn out from under.
The receive thread is CreateThread on Windows and still joins before the socket
closes, or it blocks in recvfrom on a handle that is gone. SO_RCVTIMEO is a
timeval on POSIX and a DWORD of milliseconds on Winsock; passing the wrong one
sets a nonsense timeout rather than failing, so it has its own function.

build-helper.mjs stops passing -lm on Windows, where the MSVC runtime has no
separate libm and it is a hard link error, and defines _CRT_SECURE_NO_WARNINGS
rather than printing the CRT's opinion of strtok forty times. _DEFAULT_SOURCE
is now POSIX-only.

Verified on Windows with clang 23.1.5: the helper builds, and
native/tools/test_net.c exercises engine init, net open and net close with no
audio device and reports ALL OK.

Two things found on the way and not fixed here. gv_engine does not fit
Windows' 1MB default stack, where Linux gives 8MB, so anything holding one by
value crashes with 0xC00000FD; gv_voice already callocs it, and the test
harness says why it does too. And gryt-voice still hangs on Windows after this
port, in gv_device_start rather than in the socket layer, which is device.c and
its own problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_net.c already covered engine and net. test_device.c adds the audio device
and test_voice.c drives gv_voice_start, levels and stop. All three pass on
Windows with clang 23.1.5:

  engine_init ... ok
  device_start(period=960) ... ok, mic channels=1
  voice_start(period=960) ... ok, route=-1
  voice_levels ... ok, 0 senders, mic -inf
  ALL OK

So the library works on Windows after the Winsock port. What still crashes is
gryt-voice itself, with 0xC0000409, and the difference is voice_helper.c.

Not the reader thread's stack: giving CreateThread 8MB changes nothing, and
that attempt is not in this commit. Not the audio period either; 960 and 20
both work through the harnesses. ASan reports nothing before the fast-fail.
It wants a debugger rather than another guess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sivert-io

Copy link
Copy Markdown
Member Author

Chased the remaining Windows crash. The port is better than I claimed and the crash is narrower than I thought.

The library works on Windows

Three harnesses, each adding one layer, all passing with clang 23.1.5:

Harness Covers Result
test_net.c engine, net open/close ALL OK
test_device.c + gv_device_start ALL OK, mic channels 1
test_voice.c + gv_voice_start, levels, stop ALL OK, route -1
voice_start(period=960) ... ok, route=-1
voice_levels ... ok, 0 senders, mic -inf
voice_stop ... ok
ALL OK

So net.c, device.c and voice.c are all fine on Windows after the Winsock change. miniaudio's WASAPI backend works, which I had assumed was the problem and it was not.

What still crashes is voice_helper.c

gryt-voice.exe dies with 0xC0000409 (stack cookie / __fastfail) before printing anything. Same sources, same period, same key — the only difference from test_voice.exe is the helper wrapper.

Ruled out, so nobody repeats it:

  • Audio period. 960 and 20 both work through the harnesses. My first run passed 20, which is 20 frames, and that was my mistake rather than a bug.
  • Reader thread stack. CreateThread(NULL, 0, ...) gets 1MB on Windows against a pthread's 8MB on Linux, and gv_voice_start runs on that thread with opus built -DUSE_ALLOCA=1. Good theory, wrong: passing 8MB explicitly changes nothing. That attempt is deliberately not in the branch.
  • Main thread stack. Linking /stack:8388608 changes nothing either.
  • ASan. Reports nothing before the fast-fail.

0xC0000409 is the cookie check rather than exhaustion (0xC00000FD), so something is writing past a stack object. It wants WinDbg on the dump rather than another hypothesis from me, and that is where I have stopped.

Separate, and real

gv_engine does not fit a 1MB stack. My first harness held one by value and died with 0xC00000FD before main printed. Nothing shipping does that, but it is a trap worth knowing about, and the harnesses carry a comment saying so.

gryt-voice died on Windows with 0xC0000409 before printing anything. It was
not the socket layer, the audio device, the period, the thread stack or the
link stack. It was line 164:

  setvbuf(stdout, NULL, _IOLBF, 0);

glibc reads a NULL buffer with size 0 as "pick a size". The MSVC runtime
requires a size of at least 2 for _IOLBF, so this left stdout in a state where
the first write corrupted the stack and the cookie check fast-failed. Hence no
output at all rather than a partial line, and hence a corruption code rather
than exhaustion.

Unbuffered instead of line buffered, which is also simply correct here: every
write is a whole line followed by fflush, so the buffer bought nothing. The
three harnesses used _IONBF, where size is ignored, which is exactly why they
all passed while the real binary did not.

Windows now matches Linux byte for byte:

  started 1
  r -1 -120.0 0 l= b= s= t=

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant