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/mcpp.toml b/mcpp.toml index ffc8aa0..aec11c6 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" diff --git a/port/kernel32.def b/port/kernel32.def index 2a5e1ee..93380a3 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -10,6 +10,7 @@ CreatePipe CreateProcessW CreateThread FlushFileBuffers +FlushInstructionCache FreeEnvironmentStringsW GetCommandLineW GetConsoleMode @@ -23,13 +24,16 @@ GetFileType GetFinalPathNameByHandleW GetLastError GetLogicalDriveStringsW +GetProcAddress GetProcessHeap GetStdHandle GetSystemTimePreciseAsFileTime HeapAlloc HeapFree +LoadLibraryW LocalFree MultiByteToWideChar +PeekNamedPipe QueryPerformanceCounter QueryPerformanceFrequency ReadFile @@ -39,6 +43,9 @@ SetHandleInformation Sleep SwitchToThread TerminateProcess +VirtualAlloc +VirtualFree +VirtualProtect WaitForSingleObject WideCharToMultiByte WriteFile diff --git a/src/datagram.cpp b/src/datagram.cpp new file mode 100644 index 0000000..aab9b1d --- /dev/null +++ b/src/datagram.cpp @@ -0,0 +1,171 @@ +#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); } + +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 +// `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; + } + + 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) { + n->close(s); + return rc; + } + if (n->bind(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + n->close(s); + return e; + } + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { n->close(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; + + auto* n = net(); + if (n == nullptr) return kal_err_io; + ksockaddr_storage ss{}; + int len = static_cast(sizeof ss); + if (n->sockname(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 }; + + 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 = 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 + // 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 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, + kal_endpoint* from) { + const SOCKET s = socket_of(d); + 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 = 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. + // + // 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 (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; + 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; + if (auto* n = net()) n->close(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..7ad464c --- /dev/null +++ b/src/endpoint.h @@ -0,0 +1,283 @@ +// 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; + } +} + +// ── 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 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 +// 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 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 (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 ─────────────────────────────────────────────────────────────── + +// 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..d4daba6 --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,202 @@ +#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; } + +okw::network_calls* net() { return okw::net_or_null(); } + +SOCKET make(int family, int type, int protocol) { + 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 ? n->peername(s, &ss, &len) : n->sockname(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; + + 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 (n->connect(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + n->close(s); + return e; + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { n->close(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; + + 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(); + + // ⚠️ 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 (n->bind(s, &ss, len) != 0) { + const int e = okw::last_socket_error(); + 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 (n->listen(s, 128) != 0) { + const int e = okw::last_socket_error(); + n->close(s); + return e; + } + + out->h = okw::pack_socket(s); + if (out->h == 0) { n->close(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; + auto* n = net(); + if (n == nullptr) return kal_err_io; + + 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) { n->close(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; + } + + 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; + 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; + if (auto* n = net()) n->close(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..d678801 --- /dev/null +++ b/src/timeout.cpp @@ -0,0 +1,253 @@ +#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 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, 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. +// +// ⭐ 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 { + +// 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 first question because a +// 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); + return n->sockname(s, &ss, &len) == 0; +} + +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) { + auto* n = okw::net_or_null(); + 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 + // operation which follows will not wait: it will report what happened. + 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; + // ⚠️ 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; + 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; + } +} + +// ⭐⭐ 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 + // `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. + const SOCKET raw = static_cast(s.h); + switch (shape_of(raw)) { + 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 + // 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 + +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/win32.h b/src/win32.h index 7e0199f..565a96c 100644 --- a/src/win32.h +++ b/src/win32.h @@ -190,12 +190,15 @@ 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, INFINITE = 0xFFFFFFFFu, WAIT_OBJECT_0 = 0x00000000u, + WAIT_TIMEOUT_ = 0x00000102u, CP_UTF8 = 65001, MB_ERR_INVALID_CHARS = 0x8, @@ -234,6 +237,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 ─────────────────────────────────────────────────────────── @@ -247,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); @@ -313,9 +327,23 @@ 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*); +// 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 @@ -324,3 +352,98 @@ OKW_IMPORT LPWSTR* OKW_API CommandLineToArgvW(LPCWSTR, int*); OKW_IMPORT DWORD OKW_API RtlNtStatusToDosError(long); } // extern "C" + +// ── ws2_32: this system's network interface ───────────────────────────────── +// +// ⚠️⚠️ 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 +// 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 { + AF_INET_ = 2, AF_INET6_ = 23, + SOCK_STREAM_ = 1, SOCK_DGRAM_ = 2, + IPPROTO_TCP_ = 6, IPPROTO_UDP_ = 17, + 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]; }; + +// 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);