From 0fea697b521a9eb42c2bdd7231bf67ea0b2bca87 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 21:29:59 +0800 Subject: [PATCH 1/5] Provide net, datagram, timeout and exec; decline space in terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implementation declined all five interfaces openkal 0.8 added. Four of them this system has, and the fifth it does not --- which is a legitimate outcome rather than a gap, and is now stated as one. src/net.cpp Winsock. ⚠️ `WSASocketW' with a flags word of zero rather than `socket': the latter makes an OVERLAPPED handle, upon which `ReadFile' returns before the bytes have arrived. A non-overlapped socket is what lets a connection BE a stream here --- openkal.stream is `ReadFile' and `WriteFile' on this system --- with no second transfer path. A change to that one zero would neither fail to compile nor fail to link. src/datagram.cpp the same calls with SOCK_DGRAM. ⚠️ This system reports a truncated message as a FAILURE where the other two truncate silently; the bytes that fit are delivered either way, and the interface states that the excess is lost. src/timeout.cpp `WSAPoll', which answers for sockets and for nothing else. A bounded transfer upon a stream that is not a socket reports kal_err_not_supported, which the interface's own header anticipates in terms. `kal_timeout_wait_process' is the one operation of this interface the system provides DIRECTLY. src/exec.cpp VirtualAlloc, VirtualProtect, FlushInstructionCache. The third is not optional. openkal.space stays declined. `CreateProcessW' starts a NAMED PROGRAM, which is openkal.process and a different operation; constructing a copy of the calling address space out of it would be the simulation clause 3.1 forbids. A program that calls `kal_space_start' fails at the link naming the operation, which is clause 6.1's report. ⚠️ THREE CONSTANTS DIFFER FROM THE OTHER SYSTEMS' WITHOUT ANNOUNCING IT: AF_INET6 is 23 here, 30 on macOS and 10 on Linux; SOL_SOCKET is 0xffff here and on macOS and 1 on Linux; and this system's `poll' has no bit named POLLIN --- what it has is POLLRDNORM, and the Linux value asks about out-of-band data instead. ⚠️⚠️ AND THE CONFORMANCE RUN WAS SELECTING LESS THAN ITS STEP NAME SAID. `full' expands to the HOSTED set, so every optional interface --- random and terminal included, which this implementation has provided since before 0.8 --- was compiled with its section body removed and reported as not examined. The set is now enumerated: the six this system provides, and not `optional', because `optional' names `space'. Measured here rather than remembered: 16 objects, exported surface complete and conforming at 88 names, and no undefined symbol outside the permitted set. --- .github/workflows/ci.yml | 19 +++- README.md | 43 +++++++-- build.mcpp | 3 +- mcpp.toml | 5 +- port/ws2_32.def | 37 ++++++++ src/datagram.cpp | 162 +++++++++++++++++++++++++++++++ src/endpoint.h | 200 +++++++++++++++++++++++++++++++++++++++ src/exec.cpp | 71 ++++++++++++++ src/net.cpp | 190 +++++++++++++++++++++++++++++++++++++ src/timeout.cpp | 144 ++++++++++++++++++++++++++++ src/win.cpp | 1 + src/win32.h | 111 ++++++++++++++++++++++ 12 files changed, 974 insertions(+), 12 deletions(-) create mode 100644 port/ws2_32.def create mode 100644 src/datagram.cpp create mode 100644 src/endpoint.h create mode 100644 src/exec.cpp create mode 100644 src/net.cpp create mode 100644 src/timeout.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef57f13..affdb02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -226,7 +226,21 @@ jobs: run: | extra='' [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' - bash .spec/tools/run-conformance.sh openkal-windows . full $extra + # ⚠️ THE SET IS ENUMERATED AND `optional` IS NOT NAMED, AND THE + # DIFFERENCE IS ONE INTERFACE. + # + # `optional` includes `space`, which this system does not provide: + # there is no primitive here that copies an address space and starts a + # context in the copy. A set demanding it would not report an + # observation that did not hold — it would fail to LINK, naming + # `kal_space_start`, which is clause 6.1's report arriving where a + # report was not wanted. + # + # So the six this implementation does provide are named, and the + # seventh is absent from the list for the same reason it is absent + # from the objects. + bash .spec/tools/run-conformance.sh openkal-windows . \ + full,exec,random,terminal,net,datagram,timeout $extra # --------------------------------------------------------------------------- # From a system that is not this one. @@ -343,7 +357,8 @@ jobs: env: WINEDEBUG: '-all' run: | - bash .spec/tools/run-conformance.sh openkal-windows . full \ + bash .spec/tools/run-conformance.sh openkal-windows . \ + full,exec,random,terminal,net,datagram,timeout \ --target x86_64-windows-gnu - name: The exported surface is complete and contains nothing else diff --git a/README.md b/README.md index e6107da..8811ec6 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.5.1" +openkal = "0.8.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.1.1" +openkal-windows = "0.3.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -33,6 +33,20 @@ path with `GetFinalPathNameByHandleW` and concatenate, which is a name resolver inside an implementation and is what clause 7.1 excludes. The interface asked for the operation this system already had. +**A declined interface is a legitimate outcome, and this system is where that +stops being theory.** `openkal.space` starts a context in a copy of the calling +address space. There is no primitive here that does it: `CreateProcessW` starts +a *named program*, which is `openkal.process` and is a different operation. +Clause 3 says an implementation provides an interface in whole or not at all, +and this implementation provides fourteen of the fifteen — so a program that +calls `kal_space_start` fails at the **link**, naming the operation, which is +clause 6.1's report and is the loudest one available. + +Constructing a copy out of `CreateProcessW` plus a mechanism for carrying the +caller's memory across would be the simulation clause 3.1 forbids: it would be +present, it would look like the operation, and what it produced would not be a +copy of the caller. + **Duplication of the calling image is not a Unix preference.** `openkal.process` starts a program and does not duplicate one, and the specification's reason is that duplication cannot be performed faithfully everywhere. This system has no @@ -95,17 +109,32 @@ function here, because this environment's loader has already established the argument vector, the named values and thread-local storage before it transfers control. +## Interfaces provided + +Fourteen of the fifteen. `openkal.space` is declined, for the reason above. + +The four that openkal 0.8 added and this implementation now provides: + +| | on this system | +| --- | --- | +| `openkal.net` | Winsock, started once at the first socket and never stopped. ⚠️ `WSASocketW` with a flags word of zero rather than `socket`: the latter makes an **overlapped** handle, and `ReadFile` upon one of those returns before the bytes arrive. A non-overlapped socket is what lets a connection be a stream here with no second transfer path | +| `openkal.datagram` | the same calls with `SOCK_DGRAM`. ⚠️ This system reports a truncated message as a **failure** where the other two truncate silently; the bytes that fit are delivered either way, and the interface says the excess is lost | +| `openkal.timeout` | `WSAPoll`, which answers for sockets and for nothing else. A bounded read of a stream that is not a socket reports `kal_err_not_supported` — which the interface's own header anticipates in terms. `kal_timeout_wait_process` is the one operation of the interface this system provides **directly**, because a bounded wait upon an object is the primitive here | +| `openkal.exec` | `VirtualAlloc` writable, `VirtualProtect` executable, `FlushInstructionCache`. The third call is not optional and the other two systems' implementations do not need to make it explicit | + ## Verification -The conformance suite in the specification package, built for this target: +The conformance suite in the specification package, built for this target. The +feature set is enumerated rather than `optional`, and the difference is the one +interface this implementation declines: a set demanding `space` would fail to +link naming `kal_space_start` rather than report an observation. ```bash -mcpp build --target x86_64-windows-gnu --features full -./target/*/*/bin/openkal-conformance.exe +git clone https://github.com/mcpplibs/openkal .spec +bash .spec/tools/run-conformance.sh openkal-windows . \ + full,exec,random,terminal,net,datagram,timeout --target x86_64-windows-gnu ``` -Ninety-one observations held, none failed, none went unexamined. - ## Toolchains `x86_64-windows-gnu` (GCC producing PE), and the MSVC ABI through `llvm`. The diff --git a/build.mcpp b/build.mcpp index 3f89376..deeb33a 100644 --- a/build.mcpp +++ b/build.mcpp @@ -117,6 +117,7 @@ int main() { mcpp::rerun_if_changed("port/shell32.def"); mcpp::rerun_if_changed("port/synchronization.def"); mcpp::rerun_if_changed("port/bcrypt.def"); + mcpp::rerun_if_changed("port/ws2_32.def"); if (host_is_windows()) return 0; @@ -139,7 +140,7 @@ int main() { const std::string tool = dlltool(); for (auto name : { "kernel32", "ntdll", "shell32", "synchronization", - "bcrypt" }) { + "bcrypt", "ws2_32" }) { const auto def = std::format("{}/port/{}.def", root, name); const auto lib = std::format("{}/lib{}.a", out, name); // ⚠️ `-m i386:x86-64` is stated. See port/README.md: the 32-bit ABI diff --git a/mcpp.toml b/mcpp.toml index ffc8aa0..1fa32c4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.2.0" +version = "0.3.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -49,7 +49,8 @@ openkal = "0.8.0" # predicate the second one linked with none of these libraries and failed on # `GetStdHandle`. [target.'cfg(all(windows, not(env = "msvc")))'.build] -ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt"] +ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt", + "-lws2_32"] # Exceptions and run-time type information, on the one ABI where their absence # is asserted. diff --git a/port/ws2_32.def b/port/ws2_32.def new file mode 100644 index 0000000..b476030 --- /dev/null +++ b/port/ws2_32.def @@ -0,0 +1,37 @@ +; This system's network interface, and nothing else from this library. +; +; ⭐ THE LIST IS WHAT src/net.cpp, src/datagram.cpp AND src/timeout.cpp CALL, +; obtained the way the other four lists were obtained: the objects of a complete +; build were given to `nm' and the undefined names are these. A list produced by +; reading the header would contain names the configured build never uses. +; +; ⚠️ `WSASocketW' AND NOT `socket', AND THE DIFFERENCE IS LOAD-BEARING. `socket' +; makes an OVERLAPPED handle, upon which `ReadFile' returns before the bytes +; have arrived; the flags word `WSASocketW' takes makes one that is not, and +; that is what lets openkal.stream's operations here --- which are `ReadFile' +; and `WriteFile' --- transfer a connection's bytes with no second code path. +; src/win32.h records the same thing beside the declaration. +; +; ⚠️ `WSACleanup' IS DELIBERATELY ABSENT. This implementation starts the network +; interface once and never stops it: the interface stays available for the life +; of the image, which is what a program that opened a socket wants, and a +; reference count that reached zero would close every socket the program still +; held. src/endpoint.h records it beside the one call that starts it. +; +; Generated into an import library by build.mcpp; see port/README.md. +LIBRARY ws2_32.dll +EXPORTS +WSAStartup +WSAGetLastError +WSASocketW +WSAPoll +closesocket +bind +listen +accept +connect +shutdown +getsockname +getpeername +sendto +recvfrom diff --git a/src/datagram.cpp b/src/datagram.cpp new file mode 100644 index 0000000..e1858e0 --- /dev/null +++ b/src/datagram.cpp @@ -0,0 +1,162 @@ +#include "win.h" +#include "endpoint.h" +#include + +// openkal.datagram upon this system's network interface. +// +// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for +// that reason: kal_stream_read reports a count and not a boundary, so reading a +// datagram through it would lose the property that distinguishes this interface. +// The packing is the same, the type is not, and the type is what prevents the +// mistake. + +namespace { + +SOCKET socket_of(kal_datagram d) { return okw::unpack_socket(d.h); } + +bool bad(SOCKET s) { return s == INVALID_SOCKET; } + +// The largest transfer one call accepts. This system states a count as an +// `int', and a datagram larger than that cannot exist, so the clamp is a +// statement about the type rather than a limit this implementation imposes. +constexpr kal_uintptr kMaxOne = 0x7fffffffu; + +} // namespace + +extern "C" { + +int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { + if (out == nullptr) return kal_err_invalid; + + // A null local endpoint asks for one that may send and whose receiving + // address is unspecified. IPv4 is chosen for it, because a family must be + // named at the point the socket is made and this is the one every + // environment that has a network at all provides. + int family = AF_INET_; + if (local != nullptr) { + family = okw::family_of(*local); + if (family < 0) return kal_err_invalid; + } + + okw::ensure_network(); + const SOCKET s = WSASocketW(family, SOCK_DGRAM_, IPPROTO_UDP_, nullptr, 0, 0); + if (bad(s)) return okw::last_socket_error(); + + if (local != nullptr) { + ksockaddr_storage ss{}; + int len = 0; + if (const int rc = okw::to_system(*local, ss, len); rc != kal_ok) { + closesocket(s); + return rc; + } + if (bind(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + closesocket(s); + return e; + } + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + return kal_ok; +} + +int kal_datagram_local(kal_datagram d, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + const SOCKET s = socket_of(d); + if (bad(s)) return kal_err_invalid; + + ksockaddr_storage ss{}; + int len = static_cast(sizeof ss); + if (getsockname(s, &ss, &len) != 0) return okw::last_socket_error(); + return okw::from_system(ss, *out); +} + +kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { + const SOCKET s = socket_of(d); + if (bad(s) || to == nullptr) return { 0, kal_err_invalid }; + if (len > kMaxOne) return { 0, kal_err_invalid }; + + ksockaddr_storage ss{}; + int addrlen = 0; + if (const int rc = okw::to_system(*to, ss, addrlen); rc != kal_ok) + return { 0, rc }; + + const int r = sendto(s, static_cast(buf), static_cast(len), + 0, &ss, addrlen); + if (r < 0) return { 0, okw::last_socket_error() }; + + // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface + // states. The system reports a count anyway; a count short of the length + // would mean the medium had split the message, which for a datagram socket + // it does not do. Reporting the short count as success would give a caller a + // partial send this interface says cannot occur, so it is reported as a + // failure of the medium instead. + const kal_uintptr n = static_cast(r); + return { n, n == len ? kal_ok : kal_err_io }; +} + +kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { + const SOCKET s = socket_of(d); + if (bad(s)) return { 0, kal_err_invalid }; + if (len > kMaxOne) len = kMaxOne; + + ksockaddr_storage ss{}; + int addrlen = static_cast(sizeof ss); + + const int r = recvfrom(s, static_cast(buf), static_cast(len), + 0, &ss, &addrlen); + if (r < 0) { + // ⚠️ THE ONE FAILURE THIS SYSTEM REPORTS THAT THE OTHER TWO DO NOT. + // + // A message longer than the buffer is truncated here AND reported as a + // failure --- `WSAEMSGSIZE' --- where the other two systems truncate + // silently. This interface states that "a message longer than the + // buffer is truncated and the excess is lost, which is what the medium + // does", so the truncation is the specified behaviour and the report is + // this system's addition. The bytes that fit are in the caller's buffer + // either way; refusing them would lose a message the interface says was + // delivered. + // + // The count is not recoverable from this call, so what is reported is + // the whole of the buffer, which is what was filled. + if (WSAGetLastError() == okw::WSAEMSGSIZE) { + if (from != nullptr && okw::from_system(ss, *from) != kal_ok) { + for (auto& b : from->addr) b = 0; + from->addr_len = 0; + from->port = 0; + } + return { len, kal_ok }; + } + return { 0, okw::last_socket_error() }; + } + + if (from != nullptr) { + // A sender whose family this implementation does not know leaves the + // endpoint zeroed rather than partly filled. The transfer still happened + // and is reported; what is unknown is who sent it. + if (okw::from_system(ss, *from) != kal_ok) { + for (auto& b : from->addr) b = 0; + from->addr_len = 0; + from->port = 0; + } + } + return { static_cast(r), kal_ok }; +} + +void kal_datagram_close(kal_datagram d) { + const SOCKET s = socket_of(d); + if (bad(s)) return; + closesocket(s); + okw::retire(d.h); +} + +// Broadcast is not claimed. The system provides it only after SO_BROADCAST has +// been set, and this interface has no operation that would set it; a word +// claiming a facility no operation reaches is the disagreement clause 6.2 exists +// to prevent. +const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; + +} // extern "C" diff --git a/src/endpoint.h b/src/endpoint.h new file mode 100644 index 0000000..00e90b1 --- /dev/null +++ b/src/endpoint.h @@ -0,0 +1,200 @@ +// Conversion between kal_endpoint and this system's socket addresses, the +// second error mapping, and the one initialisation this system's network +// interface requires. +// +// SHARED BY openkal.net AND openkal.datagram BECAUSE ALL THREE ARE. Either +// interface may be provided without the other, so none of this belongs to +// either; writing it twice would be one decision stated in two places, and the +// two would eventually disagree about which lengths are accepted. +#pragma once +#include "handle.h" +#include + +namespace okw { + +// ── the network's own error values ────────────────────────────────────────── +// +// ⚠️ A SECOND MAPPING, AND NOT AN ALTERNATIVE SPELLING OF THE FIRST. Every +// other call in this implementation reports through `GetLastError'; these +// report through `WSAGetLastError', and the numbering does not overlap --- +// every value below is ten thousand and something. Passing one of them to +// `translate_win32' produces `kal_err_io' for all of them, which is a mapping +// that compiles, runs, and tells a caller nothing. +enum : int { + WSAEINTR = 10004, WSAEACCES = 10013, WSAEFAULT = 10014, WSAEINVAL = 10022, + WSAEMFILE = 10024, WSAEWOULDBLOCK = 10035, WSAEINPROGRESS = 10036, + WSAEALREADY = 10037, WSAENOTSOCK = 10038, WSAEDESTADDRREQ = 10039, + WSAEMSGSIZE = 10040, WSAEAFNOSUPPORT = 10047, WSAEADDRINUSE = 10048, + WSAEADDRNOTAVAIL = 10049, WSAENETDOWN = 10050, WSAENETUNREACH = 10051, + WSAENETRESET = 10052, WSAECONNABORTED = 10053, WSAECONNRESET = 10054, + WSAENOBUFS = 10055, WSAEISCONN = 10056, WSAENOTCONN = 10057, + WSAESHUTDOWN = 10058, WSAETIMEDOUT = 10060, WSAECONNREFUSED = 10061, + WSAEHOSTUNREACH = 10065, WSANOTINITIALISED = 10093, +}; + +inline int translate_wsa(int e) { + switch (e) { + case 0: return kal_ok; + case WSAEINVAL: case WSAEFAULT: case WSAENOTSOCK: + case WSAEADDRNOTAVAIL: case WSAEAFNOSUPPORT: + case WSAEISCONN: case WSAENOTCONN: return kal_err_invalid; + case WSAEWOULDBLOCK: case WSAEINPROGRESS: + case WSAEALREADY: case WSAEINTR: return kal_err_again; + case WSAENOBUFS: return kal_err_no_memory; + case WSAEACCES: return kal_err_permission; + case WSAECONNRESET: case WSAECONNABORTED: + case WSAESHUTDOWN: case WSAENETRESET: return kal_err_closed; + case WSAECONNREFUSED: case WSAEHOSTUNREACH: + case WSAENETUNREACH: case WSAENETDOWN: return kal_err_not_found; + case WSAEADDRINUSE: return kal_err_exists; + case WSAEMFILE: return kal_err_no_space; + // ⚠️ NOT `kal_err_not_supported'. `WSANOTINITIALISED' means this + // implementation failed to start the network interface, which is a + // fault of this implementation and not an absence of the facility --- + // reporting it as unsupported would tell a caller to stop asking. + case WSANOTINITIALISED: return kal_err_io; + default: return kal_err_io; + } +} + +inline int last_socket_error() { return translate_wsa(WSAGetLastError()); } + +// ── starting this system's network interface ──────────────────────────────── +// +// ⚠️ IT HAS TO BE STARTED, AND THERE IS NO OTHER PLACE TO DO IT. Every socket +// call fails with `WSANOTINITIALISED' until `WSAStartup' has been called in +// this image, and openkal has no operation a program calls first. +// +// ⭐ NOT A FUNCTION-LOCAL STATIC WITH A RUNTIME INITIALISER, AND THE MANIFEST +// SAYS WHY: every static in this package is initialised by a constant, so no +// guard variable is emitted. One with a runtime initialiser would emit a call +// to `__cxa_guard_acquire' --- a C runtime symbol, in the one package whose +// continuous integration asserts it references none. +// +// ⚠️ THE FLAG IS READ AND WRITTEN WITHOUT SYNCHRONISATION, AND THAT IS SAFE +// HERE RATHER THAN OVERLOOKED. Two contexts racing produce a second +// `WSAStartup', which this system reference-counts and which is documented as +// callable more than once. This implementation never calls `WSACleanup' --- the +// interface stays started for the life of the image, which is what a program +// that opened a socket wants --- so the count never reaches zero and the +// duplicate costs nothing. +inline void ensure_network() { + static int started = 0; + if (started) return; + unsigned char record[1024]; // larger than the documented layout; see win32.h + if (WSAStartup(0x0202 /* version 2.2 */, record) == 0) started = 1; +} + +// ── addresses ─────────────────────────────────────────────────────────────── + +// The port is carried in host order by kal_endpoint and in network order by the +// system. The conversion is written out rather than taken from `htons', which +// is in the same library and would be one more name to import for two lines. +inline unsigned short to_net_port(kal_u32 port) { + const unsigned short p = static_cast(port & 0xffffu); + return static_cast((p << 8) | (p >> 8)); +} +inline kal_u32 from_net_port(unsigned short net) { + return static_cast((net << 8) | (net >> 8)) & 0xffffu; +} + +// Fills a system address from an endpoint, and reports its length. +// +// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE +// IT DOES. The specification defines the set of lengths and allows it to grow; +// an implementation that ignored the field would misread every address a later +// revision defines, and would do so silently. +inline int to_system(const kal_endpoint& ep, ksockaddr_storage& out, int& len) { + for (auto& b : out.pad) b = 0; + + if (ep.addr_len == 4) { + auto* v4 = reinterpret_cast(&out); + v4->family = static_cast(AF_INET_); + v4->port = to_net_port(ep.port); + DWORD a = 0; + for (int i = 0; i < 4; ++i) + a |= static_cast(ep.addr[i]) << (i * 8); // already network order + v4->addr = a; + len = static_cast(sizeof(ksockaddr_in)); + return kal_ok; + } + + // Sixteen bytes is an address; twenty is an address followed by a scope + // identifier, which is carried in the four bytes after it. + if (ep.addr_len == 16 || ep.addr_len == 20) { + auto* v6 = reinterpret_cast(&out); + v6->family = static_cast(AF_INET6_); + v6->port = to_net_port(ep.port); + v6->flowinfo = 0; + for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i]; + DWORD scope = 0; + if (ep.addr_len == 20) + for (int i = 0; i < 4; ++i) + scope |= static_cast(ep.addr[16 + i]) << (i * 8); + v6->scope_id = scope; + len = static_cast(sizeof(ksockaddr_in6)); + return kal_ok; + } + + return kal_err_invalid; +} + +// Fills an endpoint from a system address. A family this implementation does +// not know leaves the endpoint zeroed and reports it, for the same reason. +inline int from_system(const ksockaddr_storage& in, kal_endpoint& out) { + for (auto& b : out.addr) b = 0; + out.addr_len = 0; + out.port = 0; + + const auto* head = reinterpret_cast(&in); + + if (head->family == AF_INET_) { + const DWORD a = head->addr; + for (int i = 0; i < 4; ++i) + out.addr[i] = static_cast((a >> (i * 8)) & 0xffu); + out.addr_len = 4; + out.port = from_net_port(head->port); + return kal_ok; + } + + if (head->family == AF_INET6_) { + const auto* v6 = reinterpret_cast(&in); + for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i]; + // A zero scope identifier is reported as the shorter form. The two + // lengths denote the same address when the scope is zero, and reporting + // the shorter one keeps an address that came in as sixteen bytes going + // back out as sixteen. + if (v6->scope_id == 0) { + out.addr_len = 16; + } else { + for (int i = 0; i < 4; ++i) + out.addr[16 + i] = static_cast((v6->scope_id >> (i * 8)) & 0xffu); + out.addr_len = 20; + } + out.port = from_net_port(v6->port); + return kal_ok; + } + + return kal_err_invalid; +} + +// Which socket family an endpoint asks for, or -1 for a length that is not one +// of the defined ones. +inline int family_of(const kal_endpoint& ep) { + if (ep.addr_len == 4) return AF_INET_; + if (ep.addr_len == 16 || ep.addr_len == 20) return AF_INET6_; + return -1; +} + +// A socket, as an owned handle. The packing is handle.h's, which is what every +// owned handle in this implementation uses; a socket value is a handle value on +// this system, so the same arithmetic recovers it. +inline kal_uintptr pack_socket(SOCKET s) { + return s == INVALID_SOCKET ? 0u : pack(reinterpret_cast(s)); +} +inline SOCKET unpack_socket(kal_uintptr w) { + void* h = unpack(w); + return h == nullptr ? INVALID_SOCKET : reinterpret_cast(h); +} + +} // namespace okw diff --git a/src/exec.cpp b/src/exec.cpp new file mode 100644 index 0000000..2ce0472 --- /dev/null +++ b/src/exec.cpp @@ -0,0 +1,71 @@ +#include "win.h" +#include + +// openkal.exec on this system. +// +// A reservation obtained writable and made executable afterwards. This system +// permits a region that is both at once and this implementation does not +// produce one: two of the three environments the specification targets refuse +// it, so an implementation that returned one here would be offering a program a +// shape it could not use elsewhere --- and the program would discover that only +// on the other system. The interface states the narrower contract and this +// implementation keeps to it. +// +// ⭐ THE THIRD CALL IS NOT OPTIONAL AND IS NOT PRESENT ON THE OTHER TWO SYSTEMS' +// IMPLEMENTATIONS. A processor whose instruction path does not observe the data +// path's writes must be told; this system publishes an operation for exactly +// that and documents it as required after writing code into memory. On the +// architecture this package is built for it is inexpensive, and on the other +// architecture this system runs on it is the difference between executing what +// was written and executing what was there before. + +namespace { + +// The reservation granularity. `VirtualAlloc' rounds a size up to a page and an +// address down to the allocation granularity, so a caller's size is rounded +// here only so that the reservation and the release agree about the number --- +// which they must, because the release takes the size the reservation was given. +constexpr unsigned long long kPage = 4096; + +unsigned long long round_up(unsigned long long n, unsigned long long to) { + return (n + to - 1) & ~(to - 1); +} + +} // namespace + +extern "C" { + +void* kal_exec_alloc(kal_uintptr size) { + if (size == 0) return nullptr; + const unsigned long long bytes = round_up(size, kPage); + return VirtualAlloc(nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +} + +int kal_exec_publish(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return kal_err_invalid; + const unsigned long long bytes = round_up(size, kPage); + DWORD previous = 0; + if (!VirtualProtect(p, bytes, PAGE_EXECUTE_READ, &previous)) + return okw::translate_win32(GetLastError()); + if (!FlushInstructionCache(GetCurrentProcess(), p, bytes)) + return okw::translate_win32(GetLastError()); + return kal_ok; +} + +void kal_exec_free(void* p, kal_uintptr size) { + if (p == nullptr || size == 0) return; + // ⚠️ THE SIZE IS ZERO AND THAT IS NOT AN OVERSIGHT. This system's release + // takes a size of zero with `MEM_RELEASE' and refuses any other value: the + // region released is the whole of the one that was reserved, which is what + // this operation means. Passing the caller's size would fail with an + // invalid-parameter report and leak the reservation. + VirtualFree(p, 0, MEM_RELEASE); +} + +// A published region may be reserved for writing again: this system's +// protection call is not one-way, and a second `VirtualProtect' to +// PAGE_READWRITE succeeds. The position is set accordingly, and a caller that +// must change published bytes need not abandon the region. +const kal_uintptr kal_exec_props = KAL_EXEC_PROP_REPUBLISH; + +} // extern "C" diff --git a/src/net.cpp b/src/net.cpp new file mode 100644 index 0000000..323fcb0 --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,190 @@ +#include "win.h" +#include "endpoint.h" +#include + +// openkal.net upon this system's network interface. +// +// A CONNECTION IS AN OWNED HANDLE AND THE STREAM IS BORROWED FROM IT, exactly as +// kal_file and kal_fs_stream are here. The owned handle carries a generation so +// that a released one stops being valid, which clause 7.2 requires; the stream +// it yields is the socket itself, because a socket IS a handle on this system +// and openkal.stream's operations here are `ReadFile' and `WriteFile'. +// +// ⭐⭐ THAT LAST SENTENCE IS THE WHOLE REASON THIS IMPLEMENTATION NEEDS NO +// SECOND TRANSFER PATH, AND IT IS TRUE ONLY BECAUSE OF THE FLAGS WORD PASSED TO +// `WSASocketW'. The ordinary `socket' makes an OVERLAPPED handle, whose reads +// and writes complete asynchronously; `ReadFile' upon one of those returns +// before the bytes have arrived. A flags word of zero makes a socket that is +// not overlapped, and the two calls then behave exactly as they do upon a pipe. +// This is stated here rather than left in the argument list because a change to +// that one zero would not fail to compile and would not fail to link. + +namespace { + +SOCKET socket_of(kal_net_conn c) { return okw::unpack_socket(c.h); } +SOCKET socket_of(kal_net_listener l) { return okw::unpack_socket(l.h); } + +bool bad(SOCKET s) { return s == INVALID_SOCKET; } + +SOCKET make(int family, int type, int protocol) { + okw::ensure_network(); + return WSASocketW(family, type, protocol, nullptr, 0, 0); +} + +int report_address(bool peer, SOCKET s, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + if (bad(s)) return kal_err_invalid; + ksockaddr_storage ss{}; + int len = static_cast(sizeof ss); + const int r = peer ? getpeername(s, &ss, &len) : getsockname(s, &ss, &len); + if (r != 0) return okw::last_socket_error(); + return okw::from_system(ss, *out); +} + +} // namespace + +extern "C" { + +int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { + if (to == nullptr || out == nullptr) return kal_err_invalid; + const int family = okw::family_of(*to); + if (family < 0) return kal_err_invalid; + + ksockaddr_storage ss{}; + int len = 0; + if (const int rc = okw::to_system(*to, ss, len); rc != kal_ok) return rc; + + const SOCKET s = make(family, SOCK_STREAM_, IPPROTO_TCP_); + if (bad(s)) return okw::last_socket_error(); + + if (connect(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + closesocket(s); + return e; + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { + if (local == nullptr || out == nullptr) return kal_err_invalid; + const int family = okw::family_of(*local); + if (family < 0) return kal_err_invalid; + + ksockaddr_storage ss{}; + int len = 0; + if (const int rc = okw::to_system(*local, ss, len); rc != kal_ok) return rc; + + const SOCKET s = make(family, SOCK_STREAM_, IPPROTO_TCP_); + if (bad(s)) return okw::last_socket_error(); + + // ⚠️ SO_REUSEADDR DOES NOT MEAN HERE WHAT IT MEANS ON THE OTHER TWO SYSTEMS, + // AND THAT IS WHY IT IS NOT SET. + // + // There it permits a listener whose predecessor is lingering. Here it + // permits TWO LISTENERS ON THE SAME ADDRESS AT ONCE --- a second program + // may bind the port a first one is already serving, and which of them + // receives a connection is unspecified. This system does not need the + // option for the case the other two need it for: a listening socket's + // address is released when the handle closes. + // + // Setting it for symmetry would therefore not make the three + // implementations behave alike; it would make this one behave differently + // from the other two while looking the same. + + if (bind(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + closesocket(s); + return e; + } + + // The backlog the system is asked for. A number rather than a name, because + // this interface does not expose one and a caller has no way to state it. + if (listen(s, 128) != 0) { + const int e = okw::last_socket_error(); + closesocket(s); + return e; + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_accept(kal_net_listener l, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const SOCKET s = socket_of(l); + if (bad(s)) return kal_err_invalid; + + const SOCKET c = accept(s, nullptr, nullptr); + if (bad(c)) return okw::last_socket_error(); + + // ⚠️ A CONNECTION INHERITS THE LISTENER'S PROPERTIES AND NOT ITS FLAGS WORD. + // The listener was made non-overlapped; an accepted connection is + // non-overlapped too, which is what keeps `ReadFile' synchronous upon it. + out->h = okw::pack_socket(c); + if (out->h == 0) { closesocket(c); return kal_err_no_memory; } + return kal_ok; +} + +kal_uintptr kal_net_stream(kal_net_conn c) { + // The socket itself, for the reason kal_fs_stream gives: openkal.stream's + // operations take whatever this system's transfer calls take, and a packed + // word is not that. + const SOCKET s = socket_of(c); + return bad(s) ? 0u : static_cast(s); +} + +int kal_net_peer(kal_net_conn c, kal_endpoint* out) { + return report_address(true, socket_of(c), out); +} + +int kal_net_local(kal_net_conn c, kal_endpoint* out) { + return report_address(false, socket_of(c), out); +} + +int kal_net_listener_local(kal_net_listener l, kal_endpoint* out) { + return report_address(false, socket_of(l), out); +} + +int kal_net_shutdown(kal_net_conn c, int direction) { + const SOCKET s = socket_of(c); + if (bad(s)) return kal_err_invalid; + + // This system numbers the directions from zero and this interface from one, + // so the mapping is written out rather than arithmetic upon the argument. A + // direction this interface does not define is refused rather than passed + // through, because the system would read an unknown number as "receive". + int how; + switch (direction) { + case KAL_SHUT_READ: how = SD_RECEIVE_; break; + case KAL_SHUT_WRITE: how = SD_SEND_; break; + case KAL_SHUT_BOTH: how = SD_BOTH_; break; + default: return kal_err_invalid; + } + + if (shutdown(s, how) != 0) return okw::last_socket_error(); + return kal_ok; +} + +void kal_net_close(kal_net_conn c) { + const SOCKET s = socket_of(c); + if (bad(s)) return; + closesocket(s); + okw::retire(c.h); +} + +void kal_net_close_listener(kal_net_listener l) { + const SOCKET s = socket_of(l); + if (bad(s)) return; + closesocket(s); + okw::retire(l.h); +} + +// Both positions hold on this system: it speaks IPv6, and its `shutdown' ends +// transfer in one direction while the other continues. +const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; + +} // extern "C" diff --git a/src/timeout.cpp b/src/timeout.cpp new file mode 100644 index 0000000..e0d7565 --- /dev/null +++ b/src/timeout.cpp @@ -0,0 +1,144 @@ +#include "win.h" +#include "endpoint.h" +#include + +// openkal.timeout on this system. +// +// THE BOUND IS APPLIED BEFORE THE OPERATION AND NOT DURING IT, which is what +// clause 6.3 leaves an implementation able to do: `WSAPoll' reports whether a +// socket would transfer without blocking, so a bounded read is a bounded wait +// for readiness followed by the ordinary read. +// +// ⚠️⚠️ AND `WSAPoll' ANSWERS FOR SOCKETS AND FOR NOTHING ELSE, WHICH IS THE ONE +// PLACE THIS SYSTEM DIFFERS FROM THE OTHER TWO IN KIND RATHER THAN IN SPELLING. +// +// There, one call answers for every descriptor. Here a socket and a file are +// different kinds of object and the readiness call takes only the first; a pipe +// is asked with `PeekNamedPipe', a file is always ready, and a console has its +// own enquiry. openkal.timeout's header anticipates exactly this: "AN +// IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, and +// reports kal_err_not_supported for the rest. That is not the defect clause 6.4 +// describes." +// +// ⇒ `kal_timeout_read' and `kal_timeout_write' upon a stream that is not a +// socket report `kal_err_not_supported'. That is a stated answer a caller can +// act upon --- and it is the honest one, because the alternative is to wait a +// while and then attempt the transfer anyway, which would report `kal_err_again' +// for a pipe that had data and block for one that did not. + +namespace { + +// A duration of zero denotes no bound, which is the convention kal_task_wait +// establishes. `WSAPoll' expresses that with a negative number. +// +// ⚠️ A BOUND SHORTER THAN A MILLISECOND ROUNDS UP TO ONE AND NOT DOWN TO NONE. +// Rounding down would turn a wait into a poll, and the header is explicit: a +// caller that asks for less is not refused and does not get less. +int bound_ms(kal_u64 ns) { + if (ns == 0) return -1; + kal_u64 ms = ns / 1000000ull; + if (ms == 0) ms = 1; + if (ms > 0x7fffffffull) ms = 0x7fffffffull; + return static_cast(ms); +} + +// Whether this word names a socket, which is the question the note above makes +// unavoidable. `getsockname' is the enquiry that answers it: upon a socket it +// succeeds or fails for a reason of its own, and upon anything else this system +// reports `WSAENOTSOCK'. +bool is_socket(SOCKET s) { + ksockaddr_storage ss{}; + int len = static_cast(sizeof ss); + if (getsockname(s, &ss, &len) == 0) return true; + return WSAGetLastError() != okw::WSAENOTSOCK; +} + +// Waits for one socket. Reports kal_ok when it is ready, kal_err_again when the +// bound expired, and a translated error otherwise. +int await(SOCKET s, short events, kal_u64 ns) { + WSAPOLLFD_ p{ s, events, 0 }; + const int r = WSAPoll(&p, 1, bound_ms(ns)); + if (r < 0) return okw::last_socket_error(); + if (r == 0) return kal_err_again; // the bound expired + // A socket reported as failed or hung up is ready in the sense that the + // operation which follows will not wait: it will report what happened. + return kal_ok; +} + +int await_stream(kal_stream s, short events, kal_u64 ns) { + okw::ensure_network(); + const SOCKET raw = static_cast(s.h); + if (raw == 0 || raw == INVALID_SOCKET) return kal_err_invalid; + if (!is_socket(raw)) return kal_err_not_supported; + return await(raw, events, ns); +} + +} // namespace + +extern "C" { + +kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { + // A transfer of zero bytes does not wait and is not bounded. Waiting first + // would turn a call that always succeeds into one that can expire. + if (len == 0) return { 0, kal_ok }; + + if (const int rc = await_stream(s, POLLRDNORM_, ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_read(s, buf, len); +} + +kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return { 0, kal_ok }; + + if (const int rc = await_stream(s, POLLWRNORM_, ns); rc != kal_ok) + return { 0, rc }; + return kal_stream_write(s, buf, len); +} + +int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const SOCKET s = okw::unpack_socket(l.h); + if (s == INVALID_SOCKET) return kal_err_invalid; + + if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return rc; + return kal_net_accept(l, out); +} + +kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { + const SOCKET s = okw::unpack_socket(d.h); + if (s == INVALID_SOCKET) return { 0, kal_err_invalid }; + + if (const int rc = await(s, POLLRDNORM_, ns); rc != kal_ok) return { 0, rc }; + return kal_datagram_recv_from(d, buf, len, from); +} + +int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* terminated) { + void* h = okw::unpack(p.h); + if (h == nullptr) return kal_err_invalid; + + // ⭐ THE ONE OPERATION OF THIS INTERFACE THIS SYSTEM PROVIDES DIRECTLY. The + // other two poll a child in a loop because neither has a bounded wait for + // one; here waiting upon an object with a bound IS the primitive, and the + // bound is stated in the same milliseconds `WSAPoll' takes. + const DWORD r = WaitForSingleObject(h, ns == 0 ? INFINITE + : static_cast(bound_ms(ns))); + if (r == WAIT_TIMEOUT_) return kal_err_again; + if (r != WAIT_OBJECT_0) return okw::translate_win32(GetLastError()); + + DWORD code = 0; + if (!GetExitCodeProcess(h, &code)) return okw::translate_win32(GetLastError()); + if (status) *status = static_cast(code); + // This system does not distinguish a program that ended by returning from + // one the environment ended: both are an exit code, and process.cpp reports + // the same thing. + if (terminated) *terminated = 0; + return kal_ok; +} + +// The bound this system distinguishes. Both `WSAPoll' and the wait upon an +// object state theirs in milliseconds, and there is no call here that takes +// less --- so a millisecond is what an implementation can honestly report. +const kal_uintptr kal_timeout_granularity_ns = 1000000u; + +} // extern "C" diff --git a/src/win.cpp b/src/win.cpp index c5576ca..ecf4eb8 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -20,6 +20,7 @@ #pragma comment(lib, "synchronization.lib") #pragma comment(lib, "shell32.lib") #pragma comment(lib, "kernel32.lib") +#pragma comment(lib, "ws2_32.lib") #endif diff --git a/src/win32.h b/src/win32.h index 7e0199f..6e9b83e 100644 --- a/src/win32.h +++ b/src/win32.h @@ -196,6 +196,7 @@ enum : DWORD { INFINITE = 0xFFFFFFFFu, WAIT_OBJECT_0 = 0x00000000u, + WAIT_TIMEOUT_ = 0x00000102u, CP_UTF8 = 65001, MB_ERR_INVALID_CHARS = 0x8, @@ -234,6 +235,13 @@ enum : DWORD { ERROR_DIRECTORY = 267, ERROR_DIR_NOT_EMPTY = 145, ERROR_IO_PENDING = 997, + + // For openkal.exec. + MEM_COMMIT = 0x00001000u, + MEM_RESERVE = 0x00002000u, + MEM_RELEASE = 0x00008000u, + PAGE_READWRITE = 0x04u, + PAGE_EXECUTE_READ = 0x20u, }; // ── the functions ─────────────────────────────────────────────────────────── @@ -313,6 +321,15 @@ OKW_IMPORT int OKW_API MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, OKW_IMPORT int OKW_API WideCharToMultiByte(UINT, DWORD, LPCWSTR, int, LPSTR, int, LPCSTR, BOOL*); +// Memory a program may execute, for openkal.exec. The reservation and the +// change of protection are two calls here as they are on every system this +// specification targets, and the third is the one that matters on a processor +// whose instruction path does not see the data path's writes. +OKW_IMPORT LPVOID OKW_API VirtualAlloc(LPVOID, unsigned long long, DWORD, DWORD); +OKW_IMPORT BOOL OKW_API VirtualProtect(LPVOID, unsigned long long, DWORD, DWORD*); +OKW_IMPORT BOOL OKW_API VirtualFree(LPVOID, unsigned long long, DWORD); +OKW_IMPORT BOOL OKW_API FlushInstructionCache(HANDLE, LPCVOID, unsigned long long); + // From shell32, and the only name this package takes from it. OKW_IMPORT LPWSTR* OKW_API CommandLineToArgvW(LPCWSTR, int*); @@ -324,3 +341,97 @@ OKW_IMPORT LPWSTR* OKW_API CommandLineToArgvW(LPCWSTR, int*); OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); } // extern "C" + +// ── ws2_32: this system's network interface ───────────────────────────────── +// +// ⚠️⚠️ THIS IS THE ONE PART OF THE SYSTEM WHOSE ERROR VALUES ARE NOT THE ONES +// EVERYTHING ELSE HERE REPORTS. `GetLastError' answers for the calls above and +// `WSAGetLastError' for these, and the two numbering schemes do not overlap --- +// a network failure is ten thousand and something. src/endpoint.h carries the +// second mapping for that reason; using `translate_win32' on a socket error +// would produce `kal_err_io' for every one of them. +// +// ⚠️ AND THREE CONSTANTS DIFFER FROM THE OTHER SYSTEMS' WITHOUT ANNOUNCING IT: +// `AF_INET6' is 23 here, 30 on macOS and 10 on Linux; `SOL_SOCKET' is 0xffff +// here and on macOS and 1 on Linux; and this system's `poll' has no bit named +// POLLIN --- what it has is POLLRDNORM, and a caller that passed the Linux +// value would be asking about out-of-band data. +// +// A socket address here has no length byte, unlike macOS: the family occupies +// two bytes, as on Linux. + +// UINT_PTR on this ABI. It is a handle value and is used as one below. +using SOCKET = unsigned long long; + +inline const SOCKET INVALID_SOCKET = static_cast(-1); + +enum : int { SOCKET_ERROR_ = -1 }; + +enum : int { + AF_INET_ = 2, AF_INET6_ = 23, + SOCK_STREAM_ = 1, SOCK_DGRAM_ = 2, + IPPROTO_TCP_ = 6, IPPROTO_UDP_ = 17, + SOL_SOCKET_ = 0xffff, SO_REUSEADDR_ = 0x0004, + SD_RECEIVE_ = 0, SD_SEND_ = 1, SD_BOTH_ = 2, +}; + +// What this system's `poll' names its bits. POLLRDNORM and POLLWRNORM are what +// "there is ordinary data to read" and "an ordinary write would proceed" are +// called here; POLLIN as a name exists and includes a band this implementation +// has no operation for. +enum : short { + POLLRDNORM_ = 0x0100, POLLWRNORM_ = 0x0010, + POLLERR_ = 0x0001, POLLHUP_ = 0x0002, POLLNVAL_ = 0x0004, +}; + +struct WSAPOLLFD_ { SOCKET fd; short events; short revents; }; + +// This system's socket addresses. The family occupies two bytes and the +// structure carries no length of its own. +struct ksockaddr_in { + unsigned short family; + unsigned short port; // network order + DWORD addr; // network order + unsigned char zero[8]; +}; + +struct ksockaddr_in6 { + unsigned short family; + unsigned short port; // network order + DWORD flowinfo; + unsigned char addr[16]; + DWORD scope_id; +}; + +struct ksockaddr_storage { unsigned char pad[128]; }; + +extern "C" { + +// ⚠️ THE STARTUP RECORD IS OPAQUE AND LARGER THAN THE DOCUMENTED LAYOUT, WHICH +// IS THE OPPOSITE OF THE RULE THIS FILE FOLLOWS FOR EVERY OTHER STRUCTURE. +// The rule is there because a structure this package FILLS IN and the system +// reads must have the documented shape. This one the system fills in and this +// package never reads: nothing here needs a member of it, so a buffer larger +// than the record cannot be the wrong shape. +OKW_IMPORT int OKW_API WSAStartup(WORD, void*); +OKW_IMPORT int OKW_API WSAGetLastError(void); + +// `WSASocketW' rather than `socket', and the last argument is why. `socket' +// makes an overlapped handle; a flags word of zero does not, and a +// non-overlapped socket is one `ReadFile' and `WriteFile' transfer through +// synchronously --- which is what openkal.stream's operations use here and is +// what makes a connection a stream on this system without a second code path. +OKW_IMPORT SOCKET OKW_API WSASocketW(int, int, int, void*, unsigned, DWORD); +OKW_IMPORT int OKW_API closesocket(SOCKET); +OKW_IMPORT int OKW_API bind(SOCKET, const void*, int); +OKW_IMPORT int OKW_API listen(SOCKET, int); +OKW_IMPORT SOCKET OKW_API accept(SOCKET, void*, int*); +OKW_IMPORT int OKW_API connect(SOCKET, const void*, int); +OKW_IMPORT int OKW_API shutdown(SOCKET, int); +OKW_IMPORT int OKW_API getsockname(SOCKET, void*, int*); +OKW_IMPORT int OKW_API getpeername(SOCKET, void*, int*); +OKW_IMPORT int OKW_API sendto(SOCKET, const char*, int, int, const void*, int); +OKW_IMPORT int OKW_API recvfrom(SOCKET, char*, int, int, void*, int*); +OKW_IMPORT int OKW_API WSAPoll(WSAPOLLFD_*, ULONG, int); + +} // extern "C" From 1f6fcaed96799d082e0e6ac892dee070b972f360 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 22:08:02 +0800 Subject: [PATCH 2/5] Reach the network at run time: its names are the C library's names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THIS SYSTEM'S NETWORK LIBRARY EXPORTS THE BSD NAMES, AND SO DOES THE C LIBRARY ABOVE THIS IMPLEMENTATION. openkal-musl compiles musl's own `src/network/*.c', which define `bind', `listen', `accept' and `connect' and route them through this port. Naming `-lws2_32' on the link line put both definitions in one program: ld.exe: libws2_32.a(libws2_32s00165.o): multiple definition of `connect'; musl/src/network/connect.o: first defined here Measured on the GNU/PE row of the C library's own continuous integration, on the first run of this change. It is not an ordering problem: an import library's member defines the thunk AND the `__imp_' pointer together, so reaching for either brings both. ⭐ THE NAMES ARE THEREFORE REACHED THROUGH THE SYSTEM'S OWN LOADER --- `LoadLibraryW' and `GetProcAddress', both of which this package already links --- into a table resolved once. Nothing of ws2_32 enters the program's symbol table, so the C library above keeps its `bind' and this implementation still reaches the system's. `port/ws2_32.def', the `-lws2_32' flag and the `#pragma comment' all come out. The table is resolved ENTIRELY OR NOT AT ALL: a table with one null entry would make the operations that resolved work and the one that did not call through zero, which is the failure clause 6.1 exists to turn into a link error and this arrangement cannot. Measured here after the change: `examples/cross-hello' links and produces `cross-hello.exe' over this implementation and that C library; 16 objects; exported surface complete and conforming at 88 names; no undefined symbol outside the permitted set; and NO BSD NAME DEFINED in this package's own objects, which is the property the collision was about. --- build.mcpp | 3 +- mcpp.toml | 3 +- port/kernel32.def | 2 + port/ws2_32.def | 37 --------------- src/datagram.cpp | 39 ++++++++++------ src/endpoint.h | 117 +++++++++++++++++++++++++++++++++++++++------- src/net.cpp | 44 ++++++++++------- src/timeout.cpp | 11 +++-- src/win.cpp | 1 - src/win32.h | 84 +++++++++++++++++---------------- 10 files changed, 208 insertions(+), 133 deletions(-) delete mode 100644 port/ws2_32.def diff --git a/build.mcpp b/build.mcpp index deeb33a..3f89376 100644 --- a/build.mcpp +++ b/build.mcpp @@ -117,7 +117,6 @@ int main() { mcpp::rerun_if_changed("port/shell32.def"); mcpp::rerun_if_changed("port/synchronization.def"); mcpp::rerun_if_changed("port/bcrypt.def"); - mcpp::rerun_if_changed("port/ws2_32.def"); if (host_is_windows()) return 0; @@ -140,7 +139,7 @@ int main() { const std::string tool = dlltool(); for (auto name : { "kernel32", "ntdll", "shell32", "synchronization", - "bcrypt", "ws2_32" }) { + "bcrypt" }) { const auto def = std::format("{}/port/{}.def", root, name); const auto lib = std::format("{}/lib{}.a", out, name); // ⚠️ `-m i386:x86-64` is stated. See port/README.md: the 32-bit ABI diff --git a/mcpp.toml b/mcpp.toml index 1fa32c4..aec11c6 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -49,8 +49,7 @@ openkal = "0.8.0" # predicate the second one linked with none of these libraries and failed on # `GetStdHandle`. [target.'cfg(all(windows, not(env = "msvc")))'.build] -ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt", - "-lws2_32"] +ldflags = ["-lntdll", "-lsynchronization", "-lshell32", "-lkernel32", "-lbcrypt"] # Exceptions and run-time type information, on the one ABI where their absence # is asserted. diff --git a/port/kernel32.def b/port/kernel32.def index 2a5e1ee..ebda436 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -23,11 +23,13 @@ GetFileType GetFinalPathNameByHandleW GetLastError GetLogicalDriveStringsW +GetProcAddress GetProcessHeap GetStdHandle GetSystemTimePreciseAsFileTime HeapAlloc HeapFree +LoadLibraryW LocalFree MultiByteToWideChar QueryPerformanceCounter diff --git a/port/ws2_32.def b/port/ws2_32.def deleted file mode 100644 index b476030..0000000 --- a/port/ws2_32.def +++ /dev/null @@ -1,37 +0,0 @@ -; This system's network interface, and nothing else from this library. -; -; ⭐ THE LIST IS WHAT src/net.cpp, src/datagram.cpp AND src/timeout.cpp CALL, -; obtained the way the other four lists were obtained: the objects of a complete -; build were given to `nm' and the undefined names are these. A list produced by -; reading the header would contain names the configured build never uses. -; -; ⚠️ `WSASocketW' AND NOT `socket', AND THE DIFFERENCE IS LOAD-BEARING. `socket' -; makes an OVERLAPPED handle, upon which `ReadFile' returns before the bytes -; have arrived; the flags word `WSASocketW' takes makes one that is not, and -; that is what lets openkal.stream's operations here --- which are `ReadFile' -; and `WriteFile' --- transfer a connection's bytes with no second code path. -; src/win32.h records the same thing beside the declaration. -; -; ⚠️ `WSACleanup' IS DELIBERATELY ABSENT. This implementation starts the network -; interface once and never stops it: the interface stays available for the life -; of the image, which is what a program that opened a socket wants, and a -; reference count that reached zero would close every socket the program still -; held. src/endpoint.h records it beside the one call that starts it. -; -; Generated into an import library by build.mcpp; see port/README.md. -LIBRARY ws2_32.dll -EXPORTS -WSAStartup -WSAGetLastError -WSASocketW -WSAPoll -closesocket -bind -listen -accept -connect -shutdown -getsockname -getpeername -sendto -recvfrom diff --git a/src/datagram.cpp b/src/datagram.cpp index e1858e0..aab9b1d 100644 --- a/src/datagram.cpp +++ b/src/datagram.cpp @@ -14,6 +14,8 @@ namespace { SOCKET socket_of(kal_datagram d) { return okw::unpack_socket(d.h); } +okw::network_calls* net() { return okw::net_or_null(); } + bool bad(SOCKET s) { return s == INVALID_SOCKET; } // The largest transfer one call accepts. This system states a count as an @@ -38,26 +40,27 @@ int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { if (family < 0) return kal_err_invalid; } - okw::ensure_network(); - const SOCKET s = WSASocketW(family, SOCK_DGRAM_, IPPROTO_UDP_, nullptr, 0, 0); + auto* n = net(); + if (n == nullptr) return kal_err_io; + const SOCKET s = n->socket(family, SOCK_DGRAM_, IPPROTO_UDP_, nullptr, 0, 0); if (bad(s)) return okw::last_socket_error(); if (local != nullptr) { ksockaddr_storage ss{}; int len = 0; if (const int rc = okw::to_system(*local, ss, len); rc != kal_ok) { - closesocket(s); + n->close(s); return rc; } - if (bind(s, &ss, len) != 0) { + if (n->bind(s, &ss, len) != 0) { const int e = okw::last_socket_error(); - closesocket(s); + n->close(s); return e; } } out->h = okw::pack_socket(s); - if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + if (out->h == 0) { n->close(s); return kal_err_no_memory; } return kal_ok; } @@ -66,9 +69,11 @@ int kal_datagram_local(kal_datagram d, kal_endpoint* out) { const SOCKET s = socket_of(d); if (bad(s)) return kal_err_invalid; + auto* n = net(); + if (n == nullptr) return kal_err_io; ksockaddr_storage ss{}; int len = static_cast(sizeof ss); - if (getsockname(s, &ss, &len) != 0) return okw::last_socket_error(); + if (n->sockname(s, &ss, &len) != 0) return okw::last_socket_error(); return okw::from_system(ss, *out); } @@ -78,13 +83,15 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr if (bad(s) || to == nullptr) return { 0, kal_err_invalid }; if (len > kMaxOne) return { 0, kal_err_invalid }; + auto* n = net(); + if (n == nullptr) return { 0, kal_err_io }; ksockaddr_storage ss{}; int addrlen = 0; if (const int rc = okw::to_system(*to, ss, addrlen); rc != kal_ok) return { 0, rc }; - const int r = sendto(s, static_cast(buf), static_cast(len), - 0, &ss, addrlen); + const int r = n->send_to(s, static_cast(buf), static_cast(len), + 0, &ss, addrlen); if (r < 0) return { 0, okw::last_socket_error() }; // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface @@ -93,8 +100,8 @@ kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr // it does not do. Reporting the short count as success would give a caller a // partial send this interface says cannot occur, so it is reported as a // failure of the medium instead. - const kal_uintptr n = static_cast(r); - return { n, n == len ? kal_ok : kal_err_io }; + const kal_uintptr sent = static_cast(r); + return { sent, sent == len ? kal_ok : kal_err_io }; } kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, @@ -103,11 +110,13 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, if (bad(s)) return { 0, kal_err_invalid }; if (len > kMaxOne) len = kMaxOne; + auto* n = net(); + if (n == nullptr) return { 0, kal_err_io }; ksockaddr_storage ss{}; int addrlen = static_cast(sizeof ss); - const int r = recvfrom(s, static_cast(buf), static_cast(len), - 0, &ss, &addrlen); + const int r = n->recv_from(s, static_cast(buf), static_cast(len), + 0, &ss, &addrlen); if (r < 0) { // ⚠️ THE ONE FAILURE THIS SYSTEM REPORTS THAT THE OTHER TWO DO NOT. // @@ -122,7 +131,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, // // The count is not recoverable from this call, so what is reported is // the whole of the buffer, which is what was filled. - if (WSAGetLastError() == okw::WSAEMSGSIZE) { + if (n->last_error() == okw::WSAEMSGSIZE) { if (from != nullptr && okw::from_system(ss, *from) != kal_ok) { for (auto& b : from->addr) b = 0; from->addr_len = 0; @@ -149,7 +158,7 @@ kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, void kal_datagram_close(kal_datagram d) { const SOCKET s = socket_of(d); if (bad(s)) return; - closesocket(s); + if (auto* n = net()) n->close(s); okw::retire(d.h); } diff --git a/src/endpoint.h b/src/endpoint.h index 00e90b1..7ad464c 100644 --- a/src/endpoint.h +++ b/src/endpoint.h @@ -57,13 +57,18 @@ inline int translate_wsa(int e) { } } -inline int last_socket_error() { return translate_wsa(WSAGetLastError()); } - -// ── starting this system's network interface ──────────────────────────────── +// ── reaching this system's network interface ──────────────────────────────── +// +// ⚠️⚠️ RESOLVED AT RUN TIME RATHER THAN LINKED, AND src/win32.h RECORDS THE +// MEASUREMENT: this library's names are the BSD names, the C library above this +// implementation defines the same names, and an import library puts both +// definitions in one program. Nothing of ws2_32 enters this program's symbol +// table now. // -// ⚠️ IT HAS TO BE STARTED, AND THERE IS NO OTHER PLACE TO DO IT. Every socket -// call fails with `WSANOTINITIALISED' until `WSAStartup' has been called in -// this image, and openkal has no operation a program calls first. +// ⚠️ IT ALSO HAS TO BE STARTED. Every socket call fails with +// `WSANOTINITIALISED' until `WSAStartup' has been called in this image, and +// openkal has no operation a program calls first --- so the first operation +// that needs the interface starts it. // // ⭐ NOT A FUNCTION-LOCAL STATIC WITH A RUNTIME INITIALISER, AND THE MANIFEST // SAYS WHY: every static in this package is initialised by a constant, so no @@ -71,18 +76,96 @@ inline int last_socket_error() { return translate_wsa(WSAGetLastError()); } // to `__cxa_guard_acquire' --- a C runtime symbol, in the one package whose // continuous integration asserts it references none. // -// ⚠️ THE FLAG IS READ AND WRITTEN WITHOUT SYNCHRONISATION, AND THAT IS SAFE -// HERE RATHER THAN OVERLOOKED. Two contexts racing produce a second -// `WSAStartup', which this system reference-counts and which is documented as -// callable more than once. This implementation never calls `WSACleanup' --- the -// interface stays started for the life of the image, which is what a program -// that opened a socket wants --- so the count never reaches zero and the -// duplicate costs nothing. -inline void ensure_network() { - static int started = 0; - if (started) return; +// ⚠️ THE TABLE IS READ AND WRITTEN WITHOUT SYNCHRONISATION, AND THAT IS SAFE +// HERE RATHER THAN OVERLOOKED. Two contexts racing resolve the same pointers +// from the same library to the same values and perform a second `WSAStartup', +// which this system reference-counts and documents as callable more than once. +// This implementation never calls `WSACleanup' --- the interface stays +// available for the life of the image, which is what a program that opened a +// socket wants --- so the count never reaches zero. +struct network_calls { + int ready; // 0 not tried, 1 available, -1 absent + pfn_WSAGetLastError last_error; + pfn_WSASocketW socket; + pfn_closesocket close; + pfn_bind bind; + pfn_listen listen; + pfn_accept accept; + pfn_connect connect; + pfn_shutdown shutdown; + pfn_getsockname sockname; + pfn_getpeername peername; + pfn_sendto send_to; + pfn_recvfrom recv_from; + pfn_WSAPoll poll; +}; + +inline network_calls& net_calls() { + static network_calls c = {}; // constant-initialised: no guard is emitted + return c; +} + +// ⚠️ THE LIBRARY'S NAME IS WRITTEN AS WIDE CHARACTERS BY HAND. This package has +// no C library to take a literal converter from, and `L"ws2_32.dll"' is the +// language's own; it is spelled out so that no header is needed for it. +inline bool ensure_network() { + network_calls& c = net_calls(); + if (c.ready != 0) return c.ready > 0; + + static const wchar_t name[] = { L'w', L's', L'2', L'_', L'3', L'2', L'.', + L'd', L'l', L'l', L'\0' }; + HANDLE lib = LoadLibraryW(name); + if (lib == nullptr) { c.ready = -1; return false; } + + auto at = [lib](const char* n) { return GetProcAddress(lib, n); }; + auto start = reinterpret_cast(at("WSAStartup")); + c.last_error = reinterpret_cast(at("WSAGetLastError")); + c.socket = reinterpret_cast(at("WSASocketW")); + c.close = reinterpret_cast(at("closesocket")); + c.bind = reinterpret_cast(at("bind")); + c.listen = reinterpret_cast(at("listen")); + c.accept = reinterpret_cast(at("accept")); + c.connect = reinterpret_cast(at("connect")); + c.shutdown = reinterpret_cast(at("shutdown")); + c.sockname = reinterpret_cast(at("getsockname")); + c.peername = reinterpret_cast(at("getpeername")); + c.send_to = reinterpret_cast(at("sendto")); + c.recv_from = reinterpret_cast(at("recvfrom")); + c.poll = reinterpret_cast(at("WSAPoll")); + + // ⚠️ EVERY ONE OF THEM, OR NONE. A table with one null entry is worse than + // no table: the operations that resolved would work and the one that did + // not would call through zero, which is the failure clause 6.1 exists to + // turn into a link error and this arrangement cannot. + if (!start || !c.last_error || !c.socket || !c.close || !c.bind || + !c.listen || !c.accept || !c.connect || !c.shutdown || !c.sockname || + !c.peername || !c.send_to || !c.recv_from || !c.poll) { + c.ready = -1; + return false; + } + unsigned char record[1024]; // larger than the documented layout; see win32.h - if (WSAStartup(0x0202 /* version 2.2 */, record) == 0) started = 1; + if (start(0x0202 /* version 2.2 */, record) != 0) { c.ready = -1; return false; } + c.ready = 1; + return true; +} + +// The error this system last reported for a socket operation. ⚠️ Reached through +// the table, so a caller that failed BEFORE the table was built --- which is the +// only way `ensure_network' returns false --- is told `kal_err_io' rather than +// calling through a null pointer. +inline int last_socket_error() { + network_calls& c = net_calls(); + if (c.ready <= 0 || c.last_error == nullptr) return kal_err_io; + return translate_wsa(c.last_error()); +} + +// The table, or a null pointer when this system's network interface could not +// be reached at all. ⚠️ Every operation of both interfaces begins here, so a +// system without `ws2_32.dll' --- which is not a system this package expects to +// meet --- reports `kal_err_io' rather than calling through zero. +inline network_calls* net_or_null() { + return ensure_network() ? &net_calls() : nullptr; } // ── addresses ─────────────────────────────────────────────────────────────── diff --git a/src/net.cpp b/src/net.cpp index 323fcb0..d4daba6 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -26,17 +26,21 @@ SOCKET socket_of(kal_net_listener l) { return okw::unpack_socket(l.h); } bool bad(SOCKET s) { return s == INVALID_SOCKET; } +okw::network_calls* net() { return okw::net_or_null(); } + SOCKET make(int family, int type, int protocol) { - okw::ensure_network(); - return WSASocketW(family, type, protocol, nullptr, 0, 0); + auto* n = net(); + return n ? n->socket(family, type, protocol, nullptr, 0, 0) : INVALID_SOCKET; } int report_address(bool peer, SOCKET s, kal_endpoint* out) { if (out == nullptr) return kal_err_invalid; if (bad(s)) return kal_err_invalid; + auto* n = net(); + if (n == nullptr) return kal_err_io; ksockaddr_storage ss{}; int len = static_cast(sizeof ss); - const int r = peer ? getpeername(s, &ss, &len) : getsockname(s, &ss, &len); + const int r = peer ? n->peername(s, &ss, &len) : n->sockname(s, &ss, &len); if (r != 0) return okw::last_socket_error(); return okw::from_system(ss, *out); } @@ -54,17 +58,19 @@ int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { int len = 0; if (const int rc = okw::to_system(*to, ss, len); rc != kal_ok) return rc; + auto* n = net(); + if (n == nullptr) return kal_err_io; const SOCKET s = make(family, SOCK_STREAM_, IPPROTO_TCP_); if (bad(s)) return okw::last_socket_error(); - if (connect(s, &ss, len) != 0) { + if (n->connect(s, &ss, len) != 0) { const int e = okw::last_socket_error(); - closesocket(s); + n->close(s); return e; } out->h = okw::pack_socket(s); - if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + if (out->h == 0) { n->close(s); return kal_err_no_memory; } return kal_ok; } @@ -77,6 +83,8 @@ int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { int len = 0; if (const int rc = okw::to_system(*local, ss, len); rc != kal_ok) return rc; + auto* n = net(); + if (n == nullptr) return kal_err_io; const SOCKET s = make(family, SOCK_STREAM_, IPPROTO_TCP_); if (bad(s)) return okw::last_socket_error(); @@ -94,22 +102,22 @@ int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { // implementations behave alike; it would make this one behave differently // from the other two while looking the same. - if (bind(s, &ss, len) != 0) { + if (n->bind(s, &ss, len) != 0) { const int e = okw::last_socket_error(); - closesocket(s); + n->close(s); return e; } // The backlog the system is asked for. A number rather than a name, because // this interface does not expose one and a caller has no way to state it. - if (listen(s, 128) != 0) { + if (n->listen(s, 128) != 0) { const int e = okw::last_socket_error(); - closesocket(s); + n->close(s); return e; } out->h = okw::pack_socket(s); - if (out->h == 0) { closesocket(s); return kal_err_no_memory; } + if (out->h == 0) { n->close(s); return kal_err_no_memory; } return kal_ok; } @@ -117,15 +125,17 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { if (out == nullptr) return kal_err_invalid; const SOCKET s = socket_of(l); if (bad(s)) return kal_err_invalid; + auto* n = net(); + if (n == nullptr) return kal_err_io; - const SOCKET c = accept(s, nullptr, nullptr); + const SOCKET c = n->accept(s, nullptr, nullptr); if (bad(c)) return okw::last_socket_error(); // ⚠️ A CONNECTION INHERITS THE LISTENER'S PROPERTIES AND NOT ITS FLAGS WORD. // The listener was made non-overlapped; an accepted connection is // non-overlapped too, which is what keeps `ReadFile' synchronous upon it. out->h = okw::pack_socket(c); - if (out->h == 0) { closesocket(c); return kal_err_no_memory; } + if (out->h == 0) { n->close(c); return kal_err_no_memory; } return kal_ok; } @@ -165,21 +175,23 @@ int kal_net_shutdown(kal_net_conn c, int direction) { default: return kal_err_invalid; } - if (shutdown(s, how) != 0) return okw::last_socket_error(); + auto* n = net(); + if (n == nullptr) return kal_err_io; + if (n->shutdown(s, how) != 0) return okw::last_socket_error(); return kal_ok; } void kal_net_close(kal_net_conn c) { const SOCKET s = socket_of(c); if (bad(s)) return; - closesocket(s); + if (auto* n = net()) n->close(s); okw::retire(c.h); } void kal_net_close_listener(kal_net_listener l) { const SOCKET s = socket_of(l); if (bad(s)) return; - closesocket(s); + if (auto* n = net()) n->close(s); okw::retire(l.h); } diff --git a/src/timeout.cpp b/src/timeout.cpp index e0d7565..1d9e9bb 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -47,17 +47,21 @@ int bound_ms(kal_u64 ns) { // succeeds or fails for a reason of its own, and upon anything else this system // reports `WSAENOTSOCK'. bool is_socket(SOCKET s) { + auto* n = okw::net_or_null(); + if (n == nullptr) return false; ksockaddr_storage ss{}; int len = static_cast(sizeof ss); - if (getsockname(s, &ss, &len) == 0) return true; - return WSAGetLastError() != okw::WSAENOTSOCK; + if (n->sockname(s, &ss, &len) == 0) return true; + return n->last_error() != okw::WSAENOTSOCK; } // Waits for one socket. Reports kal_ok when it is ready, kal_err_again when the // bound expired, and a translated error otherwise. int await(SOCKET s, short events, kal_u64 ns) { + auto* n = okw::net_or_null(); + if (n == nullptr) return kal_err_not_supported; WSAPOLLFD_ p{ s, events, 0 }; - const int r = WSAPoll(&p, 1, bound_ms(ns)); + const int r = n->poll(&p, 1, bound_ms(ns)); if (r < 0) return okw::last_socket_error(); if (r == 0) return kal_err_again; // the bound expired // A socket reported as failed or hung up is ready in the sense that the @@ -66,7 +70,6 @@ int await(SOCKET s, short events, kal_u64 ns) { } int await_stream(kal_stream s, short events, kal_u64 ns) { - okw::ensure_network(); const SOCKET raw = static_cast(s.h); if (raw == 0 || raw == INVALID_SOCKET) return kal_err_invalid; if (!is_socket(raw)) return kal_err_not_supported; diff --git a/src/win.cpp b/src/win.cpp index ecf4eb8..c5576ca 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -20,7 +20,6 @@ #pragma comment(lib, "synchronization.lib") #pragma comment(lib, "shell32.lib") #pragma comment(lib, "kernel32.lib") -#pragma comment(lib, "ws2_32.lib") #endif diff --git a/src/win32.h b/src/win32.h index 6e9b83e..303b0d5 100644 --- a/src/win32.h +++ b/src/win32.h @@ -333,6 +333,11 @@ OKW_IMPORT BOOL OKW_API FlushInstructionCache(HANDLE, LPCVOID, unsigned long l // From shell32, and the only name this package takes from it. OKW_IMPORT LPWSTR* OKW_API CommandLineToArgvW(LPCWSTR, int*); +// Reaching a library by name at run time, which is how this implementation +// obtains the network interface. src/endpoint.h says why it is not linked. +OKW_IMPORT HANDLE OKW_API LoadLibraryW(LPCWSTR); +OKW_IMPORT void* OKW_API GetProcAddress(HANDLE, LPCSTR); + // ── ntdll ─────────────────────────────────────────────────────────────────── // // The object-manager entry points. Their STRUCTURES are declared in win.h and @@ -344,12 +349,28 @@ OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); // ── ws2_32: this system's network interface ───────────────────────────────── // -// ⚠️⚠️ THIS IS THE ONE PART OF THE SYSTEM WHOSE ERROR VALUES ARE NOT THE ONES -// EVERYTHING ELSE HERE REPORTS. `GetLastError' answers for the calls above and -// `WSAGetLastError' for these, and the two numbering schemes do not overlap --- -// a network failure is ten thousand and something. src/endpoint.h carries the -// second mapping for that reason; using `translate_win32' on a socket error -// would produce `kal_err_io' for every one of them. +// ⚠️⚠️ NOT DECLARED AS IMPORTS AND NOT LINKED, AND THE REASON IS A COLLISION +// RATHER THAN A PREFERENCE. +// +// This library's names ARE the BSD names --- `bind', `listen', `accept', +// `connect'. So does the C library above this implementation: openkal-musl +// compiles musl's own `src/network/*.c', which define those names and route +// them through this port. Naming `-lws2_32' on the link line puts BOTH +// definitions in one program: +// +// ld.exe: libws2_32.a(libws2_32s00165.o): multiple definition of `connect'; +// musl/src/network/connect.o: first defined here +// +// Measured on the first run of this change, on the GNU/PE row of the C +// library's own continuous integration. It is not an ordering problem: an +// import library's member defines the thunk AND the `__imp_' pointer together, +// so reaching for either brings both. +// +// ⭐ THE NAMES ARE THEREFORE REACHED AT RUN TIME, THROUGH THE LIBRARY'S OWN +// LOADER. Nothing of ws2_32 enters this program's symbol table, so the C +// library above keeps its `bind' and this implementation still reaches the +// system's. `ws2_32.dll' is a core component of every installation of this +// system, and src/endpoint.h states what happens if it is somehow absent. // // ⚠️ AND THREE CONSTANTS DIFFER FROM THE OTHER SYSTEMS' WITHOUT ANNOUNCING IT: // `AF_INET6' is 23 here, 30 on macOS and 10 on Linux; `SOL_SOCKET' is 0xffff @@ -365,13 +386,10 @@ using SOCKET = unsigned long long; inline const SOCKET INVALID_SOCKET = static_cast(-1); -enum : int { SOCKET_ERROR_ = -1 }; - enum : int { AF_INET_ = 2, AF_INET6_ = 23, SOCK_STREAM_ = 1, SOCK_DGRAM_ = 2, IPPROTO_TCP_ = 6, IPPROTO_UDP_ = 17, - SOL_SOCKET_ = 0xffff, SO_REUSEADDR_ = 0x0004, SD_RECEIVE_ = 0, SD_SEND_ = 1, SD_BOTH_ = 2, }; @@ -405,33 +423,21 @@ struct ksockaddr_in6 { struct ksockaddr_storage { unsigned char pad[128]; }; -extern "C" { - -// ⚠️ THE STARTUP RECORD IS OPAQUE AND LARGER THAN THE DOCUMENTED LAYOUT, WHICH -// IS THE OPPOSITE OF THE RULE THIS FILE FOLLOWS FOR EVERY OTHER STRUCTURE. -// The rule is there because a structure this package FILLS IN and the system -// reads must have the documented shape. This one the system fills in and this -// package never reads: nothing here needs a member of it, so a buffer larger -// than the record cannot be the wrong shape. -OKW_IMPORT int OKW_API WSAStartup(WORD, void*); -OKW_IMPORT int OKW_API WSAGetLastError(void); - -// `WSASocketW' rather than `socket', and the last argument is why. `socket' -// makes an overlapped handle; a flags word of zero does not, and a -// non-overlapped socket is one `ReadFile' and `WriteFile' transfer through -// synchronously --- which is what openkal.stream's operations use here and is -// what makes a connection a stream on this system without a second code path. -OKW_IMPORT SOCKET OKW_API WSASocketW(int, int, int, void*, unsigned, DWORD); -OKW_IMPORT int OKW_API closesocket(SOCKET); -OKW_IMPORT int OKW_API bind(SOCKET, const void*, int); -OKW_IMPORT int OKW_API listen(SOCKET, int); -OKW_IMPORT SOCKET OKW_API accept(SOCKET, void*, int*); -OKW_IMPORT int OKW_API connect(SOCKET, const void*, int); -OKW_IMPORT int OKW_API shutdown(SOCKET, int); -OKW_IMPORT int OKW_API getsockname(SOCKET, void*, int*); -OKW_IMPORT int OKW_API getpeername(SOCKET, void*, int*); -OKW_IMPORT int OKW_API sendto(SOCKET, const char*, int, int, const void*, int); -OKW_IMPORT int OKW_API recvfrom(SOCKET, char*, int, int, void*, int*); -OKW_IMPORT int OKW_API WSAPoll(WSAPOLLFD_*, ULONG, int); - -} // extern "C" +// The shapes of the calls, so that a pointer obtained at run time is still +// type-checked. ⚠️ THE LAYOUT RULE OF THIS FILE APPLIES HERE TOO: a signature +// that is wrong does not fail to compile, because nothing checks it against the +// system --- it produces a call with the wrong arguments in the wrong places. +using pfn_WSAStartup = int (OKW_API*)(WORD, void*); +using pfn_WSAGetLastError = int (OKW_API*)(void); +using pfn_WSASocketW = SOCKET (OKW_API*)(int, int, int, void*, unsigned, DWORD); +using pfn_closesocket = int (OKW_API*)(SOCKET); +using pfn_bind = int (OKW_API*)(SOCKET, const void*, int); +using pfn_listen = int (OKW_API*)(SOCKET, int); +using pfn_accept = SOCKET (OKW_API*)(SOCKET, void*, int*); +using pfn_connect = int (OKW_API*)(SOCKET, const void*, int); +using pfn_shutdown = int (OKW_API*)(SOCKET, int); +using pfn_getsockname = int (OKW_API*)(SOCKET, void*, int*); +using pfn_getpeername = int (OKW_API*)(SOCKET, void*, int*); +using pfn_sendto = int (OKW_API*)(SOCKET, const char*, int, int, const void*, int); +using pfn_recvfrom = int (OKW_API*)(SOCKET, char*, int, int, void*, int*); +using pfn_WSAPoll = int (OKW_API*)(WSAPOLLFD_*, ULONG, int); From 3370889e9a079faaf70f4179b52287448ed92a61 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 22:14:45 +0800 Subject: [PATCH 3/5] Export the four names openkal.exec calls, and refuse for one reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two failures, both from the conformance run this change made selective. ⚠️ `VirtualAlloc', `VirtualProtect', `VirtualFree' and `FlushInstructionCache' were declared in src/win32.h and exported by no `.def'. This package's own check says so in as many words --- "declared in src/win32.h and exported by no .def" --- and it is the check that exists because an import library here is a list of names rather than code, so a name absent from the list is a link that finds nothing on a machine without a vendor SDK. ⚠️⚠️ AND `kal_timeout_read' UPON THE STANDARD INPUT HAD TWO REFUSALS WHERE THE INTERFACE HAS ONE. An earlier form answered a null or invalid handle with `kal_err_invalid' and a valid non-socket with `kal_err_not_supported'; a run whose standard input is not attached has a handle of zero, so both bounded reads in the suite were reported as not holding. ⭐ The early return was answering a DIFFERENT QUESTION. "Is this handle valid" is what the unbounded operation answers. What this interface answers is whether this implementation can bound an operation upon this resource, and timeout.h sanctions exactly one refusal for that: "AN IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, and reports kal_err_not_supported for the rest." A handle that is not a socket is one of the rest, and zero is not a socket. The name/export reading is also performed here before pushing, so that a round is not spent discovering a fifth name: 49 declared, 55 exported, none missing. --- port/kernel32.def | 4 ++++ src/timeout.cpp | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/port/kernel32.def b/port/kernel32.def index ebda436..c311355 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -10,6 +10,7 @@ CreatePipe CreateProcessW CreateThread FlushFileBuffers +FlushInstructionCache FreeEnvironmentStringsW GetCommandLineW GetConsoleMode @@ -41,6 +42,9 @@ SetHandleInformation Sleep SwitchToThread TerminateProcess +VirtualAlloc +VirtualFree +VirtualProtect WaitForSingleObject WideCharToMultiByte WriteFile diff --git a/src/timeout.cpp b/src/timeout.cpp index 1d9e9bb..5930344 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -70,8 +70,21 @@ int await(SOCKET s, short events, kal_u64 ns) { } int await_stream(kal_stream s, short events, kal_u64 ns) { + // ⚠️ ONE REASON TO REFUSE, AND NOT TWO. An earlier form answered a null or + // invalid handle with `kal_err_invalid' and a valid non-socket with + // `kal_err_not_supported', and the conformance suite reported both bounded + // reads of the standard input as not holding: a run whose standard input is + // not attached has a handle of zero, and the suite's list of admissible + // answers is the interface's --- success, an expiry, or a refusal. + // + // ⭐ The early return was answering a DIFFERENT QUESTION. "Is this handle + // valid" is what the unbounded operation answers; what this interface + // answers is whether this implementation can bound an operation upon this + // resource, and the header sanctions exactly one refusal for that: "AN + // IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, + // and reports kal_err_not_supported for the rest." A handle that is not a + // socket is one of the rest, and zero is not a socket. const SOCKET raw = static_cast(s.h); - if (raw == 0 || raw == INVALID_SOCKET) return kal_err_invalid; if (!is_socket(raw)) return kal_err_not_supported; return await(raw, events, ns); } From 6e47851fab513ae35fcd340b15d9c8bbfd280ed6 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 22:30:56 +0800 Subject: [PATCH 4/5] Bound a pipe as well as a socket: a channel here is a pipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ `WSAPoll' takes sockets and nothing else, so `kal_timeout_read' upon a pipe reported `kal_err_not_supported' --- and `openkal.process' makes a channel out of a pipe on this system. A C library above this implementation therefore reaches `poll' and `select' upon one, and openkal-musl's network probe reported it on the row that builds for this system: FAIL: select reports the read end ready (errno=38) A `select' that refuses a pipe makes every program waiting on a subprocess's output stop. ⭐ THREE ENQUIRIES, ONE PER KIND OF OBJECT, chosen by asking what the handle is: `WSAPoll' for a socket, `PeekNamedPipe' for a pipe, and always ready for a file, because a read from one does not wait. ⚠️ A socket also reports `FILE_TYPE_PIPE', so the socket enquiry is made FIRST and the file type only decides what a non-socket is. ⭐ `PeekNamedPipe' is the one NON-DESTRUCTIVE readiness enquiry in this whole ecosystem: it reports how many bytes are there and takes none, which is why this implementation needs no read-ahead where the port above it does. ⚠️ A closed writing end is READY and not an error --- the call then fails with `ERROR_BROKEN_PIPE' and the read that follows reports the end of input without waiting, which is what readiness asserts. Reporting the failure would make a program that reads until end-of-input wait for ever instead. What remains unbounded is a character device, and `kal_err_not_supported' is what timeout.h states for a resource an implementation does not cover. Measured here: 16 objects, exported surface complete at 88 names, no undefined symbol outside the permitted set, 50 declared names and none missing from a `.def'. --- port/kernel32.def | 1 + src/timeout.cpp | 105 +++++++++++++++++++++++++++++++++++----------- src/win32.h | 6 +++ 3 files changed, 88 insertions(+), 24 deletions(-) diff --git a/port/kernel32.def b/port/kernel32.def index c311355..93380a3 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -33,6 +33,7 @@ HeapFree LoadLibraryW LocalFree MultiByteToWideChar +PeekNamedPipe QueryPerformanceCounter QueryPerformanceFrequency ReadFile diff --git a/src/timeout.cpp b/src/timeout.cpp index 5930344..038689c 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -9,22 +9,30 @@ // socket would transfer without blocking, so a bounded read is a bounded wait // for readiness followed by the ordinary read. // -// ⚠️⚠️ AND `WSAPoll' ANSWERS FOR SOCKETS AND FOR NOTHING ELSE, WHICH IS THE ONE +// ⚠️⚠️ AND NO SINGLE CALL ANSWERS FOR EVERY RESOURCE HERE, WHICH IS THE ONE // PLACE THIS SYSTEM DIFFERS FROM THE OTHER TWO IN KIND RATHER THAN IN SPELLING. // -// There, one call answers for every descriptor. Here a socket and a file are -// different kinds of object and the readiness call takes only the first; a pipe -// is asked with `PeekNamedPipe', a file is always ready, and a console has its -// own enquiry. openkal.timeout's header anticipates exactly this: "AN -// IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, and -// reports kal_err_not_supported for the rest. That is not the defect clause 6.4 -// describes." +// There, one call answers for every descriptor. Here a socket, a pipe and a +// file are different kinds of object: `WSAPoll' takes only the first, a pipe is +// asked with `PeekNamedPipe', and a file is always ready because a read from +// one does not wait. Three enquiries, one per kind, chosen by asking what the +// handle is. // -// ⇒ `kal_timeout_read' and `kal_timeout_write' upon a stream that is not a -// socket report `kal_err_not_supported'. That is a stated answer a caller can -// act upon --- and it is the honest one, because the alternative is to wait a -// while and then attempt the transfer anyway, which would report `kal_err_again' -// for a pipe that had data and block for one that did not. +// ⭐ AND THE PIPE IS NOT OPTIONAL. `openkal.process' makes a channel out of a +// pipe here, so a C library above this implementation reaches `poll' and +// `select' upon one --- and a `select' that reported `kal_err_not_supported' +// for a pipe would make every program that waits on a subprocess's output stop. +// Measured: openkal-musl's own network probe, on the row that builds for this +// system, reported `select reports the read end ready (errno=38)'. +// +// ⚠️ A SOCKET ALSO REPORTS `FILE_TYPE_PIPE', so the socket enquiry is made +// FIRST and the file type only decides what a non-socket is. +// +// ⇒ What remains unbounded is a character device --- a console --- and +// `kal_err_not_supported' is what this interface states for a resource an +// implementation does not cover: "AN IMPLEMENTATION MAY PROVIDE THIS FOR SOME +// OF ITS RESOURCES AND NOT OTHERS, and reports kal_err_not_supported for the +// rest. That is not the defect clause 6.4 describes." namespace { @@ -42,10 +50,10 @@ int bound_ms(kal_u64 ns) { return static_cast(ms); } -// Whether this word names a socket, which is the question the note above makes -// unavoidable. `getsockname' is the enquiry that answers it: upon a socket it -// succeeds or fails for a reason of its own, and upon anything else this system -// reports `WSAENOTSOCK'. +// Whether this word names a socket, which is the first question because a +// socket also reports `FILE_TYPE_PIPE'. `getsockname' is the enquiry that +// answers it: upon a socket it succeeds or fails for a reason of its own, and +// upon anything else this system reports `WSAENOTSOCK'. bool is_socket(SOCKET s) { auto* n = okw::net_or_null(); if (n == nullptr) return false; @@ -55,6 +63,17 @@ bool is_socket(SOCKET s) { return n->last_error() != okw::WSAENOTSOCK; } +enum class shape { socket, pipe, ready, none }; + +shape shape_of(SOCKET raw) { + if (is_socket(raw)) return shape::socket; + switch (GetFileType(reinterpret_cast(raw))) { + case FILE_TYPE_PIPE: return shape::pipe; + case FILE_TYPE_DISK: return shape::ready; // a read from a file does not wait + default: return shape::none; // a console, or nothing at all + } +} + // Waits for one socket. Reports kal_ok when it is ready, kal_err_again when the // bound expired, and a translated error otherwise. int await(SOCKET s, short events, kal_u64 ns) { @@ -69,9 +88,39 @@ int await(SOCKET s, short events, kal_u64 ns) { return kal_ok; } +// Waits for a pipe to have bytes, without taking them. +// +// ⭐ `PeekNamedPipe' IS THE ONE NON-DESTRUCTIVE READINESS ENQUIRY IN THIS WHOLE +// ECOSYSTEM, and it is why this implementation needs no read-ahead where the +// port above it does. It reports how many bytes are there and takes none. +// +// ⚠️ A CLOSED WRITING END IS READY AND NOT AN ERROR. The call then fails with +// `ERROR_BROKEN_PIPE', and a read that follows reports the end of input without +// waiting --- which is what readiness asserts. Reporting the failure here would +// make a program that reads until end-of-input wait for ever instead. +int await_pipe(HANDLE h, kal_u64 ns) { + const int ms = bound_ms(ns); + kal_u64 waited = 0; + for (;;) { + DWORD available = 0; + if (!PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr)) { + const DWORD e = GetLastError(); + if (e == ERROR_BROKEN_PIPE || e == ERROR_PIPE_NOT_CONNECTED) return kal_ok; + return okw::translate_win32(e); + } + if (available > 0) return kal_ok; + if (ms == 0) return kal_err_again; + if (ms > 0 && waited >= static_cast(ms)) return kal_err_again; + // The interval this interface reports as its granularity, so the cost of + // the loop is the number already stated rather than a second one. + Sleep(1); + waited += 1; + } +} + int await_stream(kal_stream s, short events, kal_u64 ns) { // ⚠️ ONE REASON TO REFUSE, AND NOT TWO. An earlier form answered a null or - // invalid handle with `kal_err_invalid' and a valid non-socket with + // invalid handle with `kal_err_invalid' and everything else with // `kal_err_not_supported', and the conformance suite reported both bounded // reads of the standard input as not holding: a run whose standard input is // not attached has a handle of zero, and the suite's list of admissible @@ -80,13 +129,21 @@ int await_stream(kal_stream s, short events, kal_u64 ns) { // ⭐ The early return was answering a DIFFERENT QUESTION. "Is this handle // valid" is what the unbounded operation answers; what this interface // answers is whether this implementation can bound an operation upon this - // resource, and the header sanctions exactly one refusal for that: "AN - // IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, - // and reports kal_err_not_supported for the rest." A handle that is not a - // socket is one of the rest, and zero is not a socket. + // resource. const SOCKET raw = static_cast(s.h); - if (!is_socket(raw)) return kal_err_not_supported; - return await(raw, events, ns); + switch (shape_of(raw)) { + case shape::socket: return await(raw, events, ns); + case shape::pipe: + // Writability is not enquired of: this system has no call that + // reports whether a pipe would accept bytes without blocking, and a + // write to one completes or reports. openkal-musl's okm_poll.c + // records the same answer for the same reason. + if (events == POLLWRNORM_) return kal_ok; + return await_pipe(reinterpret_cast(raw), ns); + case shape::ready: return kal_ok; + case shape::none: return kal_err_not_supported; + } + return kal_err_not_supported; } } // namespace diff --git a/src/win32.h b/src/win32.h index 303b0d5..565a96c 100644 --- a/src/win32.h +++ b/src/win32.h @@ -190,6 +190,8 @@ enum : DWORD { FILE_NAME_NORMALIZED = 0x0, VOLUME_NAME_DOS = 0x0, FILE_TYPE_DISK = 0x0001, + FILE_TYPE_CHAR = 0x0002, + FILE_TYPE_PIPE = 0x0003, HANDLE_FLAG_INHERIT = 0x1, STARTF_USESTDHANDLES = 0x00000100u, @@ -255,6 +257,10 @@ OKW_IMPORT BOOL OKW_API SetHandleInformation(HANDLE, DWORD, DWORD); // For kal_process_channel. The security attributes decide whether the ends are // inheritable, which is what makes one of them able to cross a spawn. OKW_IMPORT BOOL OKW_API CreatePipe(HANDLE*, HANDLE*, SECURITY_ATTRIBUTES*, DWORD); +// How many bytes a pipe has without taking them, which is the one readiness +// enquiry on this system that is not `WSAPoll'. src/timeout.cpp says why both +// are needed. +OKW_IMPORT BOOL OKW_API PeekNamedPipe(HANDLE, LPVOID, DWORD, DWORD*, DWORD*, DWORD*); OKW_IMPORT BOOL OKW_API GetConsoleMode(HANDLE, DWORD*); OKW_IMPORT BOOL OKW_API SetConsoleMode(HANDLE, DWORD); From fcb1b26feb8dbeb7a16c2b6718b964bc0cf3dc79 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 22:37:51 +0800 Subject: [PATCH 5/5] A bounded wait answers with one of three values and no others MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ MEASURED TWICE, THE SECOND TIME ONLY UNDER WINE, WHICH IS WHAT MADE THE SHAPE VISIBLE. `kal_timeout_read' upon the standard input reported an error belonging to the RESOURCE where the interface defines a set for the WAIT: first `kal_err_invalid' for a handle of zero, then whatever `PeekNamedPipe' or `WSAPoll' had failed with. The conformance suite reported "a bounded read reports success, an expiry, or a refusal" as not holding both times, and the second time on one runner out of three --- which is to say, only where the system chose a different error for the same condition. ⇒ `await_stream' now answers with `kal_ok', `kal_err_again' or `kal_err_not_supported' and nothing else. An error belonging to the resource is the TRANSFER's to report, and the transfer follows the wait. ⭐ AND THE REAL ERROR IS KEPT WHERE THE RESOURCE IS KNOWN. `kal_timeout_accept' and `kal_timeout_recv_from' are reached with a socket this implementation made, so a failure there carries information a caller can act upon; the narrowing happens only on the path that takes a caller's stream, where the resource is not known. ⚠️ `is_socket' also stops depending on WHICH error a system reports for a handle that is not a socket. It read "it is a socket unless the failure was WSAENOTSOCK", and Wine does not choose the same value. Every socket this implementation hands out has been connected, bound or accepted, so `getsockname' succeeds upon all of them; the test is that it succeeds. --- src/timeout.cpp | 50 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/timeout.cpp b/src/timeout.cpp index 038689c..d678801 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -51,16 +51,22 @@ int bound_ms(kal_u64 ns) { } // Whether this word names a socket, which is the first question because a -// socket also reports `FILE_TYPE_PIPE'. `getsockname' is the enquiry that -// answers it: upon a socket it succeeds or fails for a reason of its own, and -// upon anything else this system reports `WSAENOTSOCK'. +// socket also reports `FILE_TYPE_PIPE'. +// +// ⚠️ THE TEST IS THAT `getsockname' SUCCEEDS, AND NOT THAT IT FAILED FOR SOME +// PARTICULAR REASON. The first form of this function read "it is a socket +// unless the failure was WSAENOTSOCK", which makes the answer depend on which +// error a system chooses for a handle that is not one --- and Wine does not +// choose the same one. Every socket this implementation hands out has been +// connected, bound or accepted, so `getsockname' succeeds upon all of them; +// anything it cannot answer for is treated as not a socket, and the file type +// then decides. bool is_socket(SOCKET s) { auto* n = okw::net_or_null(); if (n == nullptr) return false; ksockaddr_storage ss{}; int len = static_cast(sizeof ss); - if (n->sockname(s, &ss, &len) == 0) return true; - return n->last_error() != okw::WSAENOTSOCK; + return n->sockname(s, &ss, &len) == 0; } enum class shape { socket, pipe, ready, none }; @@ -81,6 +87,13 @@ int await(SOCKET s, short events, kal_u64 ns) { if (n == nullptr) return kal_err_not_supported; WSAPOLLFD_ p{ s, events, 0 }; const int r = n->poll(&p, 1, bound_ms(ns)); + // ⭐ THE REAL ERROR IS KEPT HERE AND NARROWED IN `await_stream'. This + // function is reached with a socket this implementation made --- from + // `kal_timeout_accept' and `kal_timeout_recv_from', where the resource is + // known --- so a failure carries information a caller can act upon. It is + // reached with a caller's stream through `await_stream', where the resource + // is not known, and that is where the answer is narrowed to the set the + // interface defines. if (r < 0) return okw::last_socket_error(); if (r == 0) return kal_err_again; // the bound expired // A socket reported as failed or hung up is ready in the sense that the @@ -106,7 +119,12 @@ int await_pipe(HANDLE h, kal_u64 ns) { if (!PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr)) { const DWORD e = GetLastError(); if (e == ERROR_BROKEN_PIPE || e == ERROR_PIPE_NOT_CONNECTED) return kal_ok; - return okw::translate_win32(e); + // ⚠️ A FAILURE OF THE ENQUIRY IS NOT AN ERROR OF THE TRANSFER, and + // reporting it as one would put this operation's answer outside the + // set the interface defines for it. What this call could not do is + // BOUND the operation; the transfer that follows reports whatever + // is wrong with the resource, in the words it already uses. + return kal_err_not_supported; } if (available > 0) return kal_ok; if (ms == 0) return kal_err_again; @@ -118,6 +136,20 @@ int await_pipe(HANDLE h, kal_u64 ns) { } } +// ⭐⭐ EVERY PATH OUT OF THIS FUNCTION IS ONE OF THREE: kal_ok, kal_err_again, +// kal_err_not_supported. +// +// That is the set `openkal.timeout' defines for the WAIT it adds, and keeping +// to it is what makes a bounded operation distinguishable from an ordinary one. +// An error belonging to the RESOURCE --- an invalid handle, a reset connection +// --- is the transfer's to report, and the transfer follows this call. +// +// ⚠️ MEASURED TWICE, BOTH TIMES AS THE SAME SHAPE. An earlier form returned +// `kal_err_invalid' for a handle of zero; a later one returned whatever +// `PeekNamedPipe' or `WSAPoll' had failed with. The conformance suite reported +// both as "a bounded read reports success, an expiry, or a refusal" not +// holding, and the second time only under Wine --- which is to say, only where +// the system chose a different error for the same condition. int await_stream(kal_stream s, short events, kal_u64 ns) { // ⚠️ ONE REASON TO REFUSE, AND NOT TWO. An earlier form answered a null or // invalid handle with `kal_err_invalid' and everything else with @@ -132,7 +164,11 @@ int await_stream(kal_stream s, short events, kal_u64 ns) { // resource. const SOCKET raw = static_cast(s.h); switch (shape_of(raw)) { - case shape::socket: return await(raw, events, ns); + case shape::socket: { + const int r = await(raw, events, ns); + // Narrowed here and not in `await': see the note there. + return (r == kal_ok || r == kal_err_again) ? r : kal_err_not_supported; + } case shape::pipe: // Writability is not enquired of: this system has no call that // reports whether a pipe would accept bytes without blocking, and a