Repository navigation
Description
A prepared cpp.net.Poll does not keep its registered native socket wrappers alive. If the caller drops its last separate reference, GC finalizes the wrapper and closes the connection even though the poll still contains that socket's descriptor.
This becomes observable with the socket finalizer added for #221. It is separate from leaking unclosed sockets: the poll registration is still active, but it is invisible to the collector.
Reproduced at d734565 on Windows x64, MSVC 19.44.35213, Haxe 4.3.7, release HXCPP_M64.
The local regression opens a loopback listener and creates a client in a joined native worker with hx::NativeAttach. The worker prepares a poll with that client and returns. The main thread keeps the poll alive, accepts the connection, then runs four full collections. Joining the worker ensures its detached stack cannot conservatively retain the client.
A weak reference observes the native socket wrapper. A nonblocking read at the peer distinguishes an open idle connection (Blocked) from a closed connection (EOF). The control keeps the native wrapper in an additional static root; the second case relies on the poll registration alone.
Observed, consistently across four runs:
strong-root control retains native socket: true
strong-root control keeps connection open: true
poll registration retains native socket: false
poll registration keeps connection open: false
Expected: all four are true while that poll registration remains active. Requiring a separate caller-owned array or handle to survive is easy to miss when using the prepare/events API, since prepare copies the registrations into the poll.
In src/hx/libs/std/Socket.cpp, polldata stores fd_set/pollfd data plus ridx and widx. socket_poll_prepare extracts numeric handles with val_sock. polldata::__Mark and __Visit trace only the index arrays. There is no GC edge from a poll registration to its SocketWrapper. The finalizer therefore closes a descriptor that the prepared poll still uses. The POSIX representation also stores only numeric descriptors; I have not run this reproduction on POSIX.
Retain and trace the registered native wrappers for the lifetime of each prepared registration, with the appropriate write barriers and relocation support. Replacing registrations should drop the previous references. Explicit socket.close must continue to close immediately. Test read and write registrations, replacing registrations, collection after registration removal, and both Windows and POSIX polling.
Conventional regression files are added locally under test/regression/PollSocketLifetime: Main.hx, build.hxml and stdout.txt. The expected output records the live-registration behavior. The current runtime fails the two poll-only checks. The program closes its peer/listener and clears registrations. No runtime implementation changes have been made. This can be fixed within libhxcpp without Haxe compiler or stdlib changes.
Activity
dimensionscape commentedon Oct 24, 2026
Fixed in
af9f6277a23921987366da5c773bbed8081e890d. A prepared Poll now keeps copies of the socket arrays from its last prepare and marks them, so a socket it is watching is not finalized and closed while nothing else references it. A caller changing its own arrays afterwards cannot drop one either. Test: PollSocketLifetime.