Repository navigation
Add addressed datagrams to the native socket runtime #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7228,7 +7228,7 @@ let native_ffi_names = | |
| length; the option's bytes cross as hex (an int option is 01000000) *) | ||
| "sock_getopt"; "sock_setopt"; | ||
| (* v0.1.682: send(2) with its flags (MSG_OOB), the bytes from the arena *) | ||
| "sock_send"; | ||
| "sock_send"; "sock_sendto"; "sock_recvfrom"; | ||
| (* v0.1.550: resource limits, scheduling priority and advisory file locks. | ||
| Each is out of an `extern`'s reach for its own reason: getrlimit and | ||
| setrlimit move the limits through a `struct rlimit *`; getpriority and | ||
|
|
@@ -8149,6 +8149,59 @@ let native_ffi_runtime ~tls ~midi ~window ~audio ~filestat ~fdio ~proclimit = | |
| " if (getpeername((int)fd, (struct sockaddr*)&ss, &sl) != 0) return __sock_none(errno);"; | ||
| " return __sock_text((struct sockaddr*)&ss, sl);"; | ||
| "}"; | ||
| "#include <limits.h>"; | ||
| "/* Addressed datagrams keep sockaddr layout in the host runtime. Addresses"; | ||
| " are numeric; name/service resolution belongs to the caller. The descriptor"; | ||
| " is never replaced or connected, so its binding and peer remain intact. */"; | ||
| "static long long sock_sendto(long long fd, long long p, long long len, long long flags, const char* host, long long port) {"; | ||
| " if (fd < 0 || fd > INT_MAX) { __fd_errno = EBADF; return -1; }"; | ||
| " if (p < 0 || p > __MEM_CAP || len < 0 || len > __MEM_CAP - p ||"; | ||
| " flags < INT_MIN || flags > INT_MAX || port < 0 || port > 65535) {"; | ||
| " __fd_errno = EINVAL; return -1;"; | ||
| " }"; | ||
| " struct sockaddr_storage ss; socklen_t sl = sizeof ss;"; | ||
| " memset(&ss, 0, sizeof ss);"; | ||
| " if (getsockname((int)fd, (struct sockaddr*)&ss, &sl) != 0) { __fd_errno = errno; return -1; }"; | ||
| " struct addrinfo hints, *res; char service[16];"; | ||
| " memset(&hints, 0, sizeof hints);"; | ||
| " hints.ai_family = ss.ss_family; hints.ai_socktype = SOCK_DGRAM;"; | ||
| " hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;"; | ||
| " snprintf(service, sizeof service, \"%lld\", port);"; | ||
| " if (!host || !host[0] || getaddrinfo(host, service, &hints, &res) != 0) {"; | ||
| " __fd_errno = 0; return -2;"; | ||
| " }"; | ||
| " ssize_t n = sendto((int)fd, __mem + p, (size_t)len, (int)flags, res->ai_addr, res->ai_addrlen);"; | ||
| " int err = errno;"; | ||
| " freeaddrinfo(res);"; | ||
| " __fd_errno = n < 0 ? err : 0;"; | ||
| " return n < 0 ? -1 : (long long)n;"; | ||
| "}"; | ||
| "/* Payload goes to the caller's arena. The result is \"count family port host\";"; | ||
| " an empty string means failure (fd_last_errno). Zero-byte packets still"; | ||
| " return an address. MSG_TRUNC may report more than cap: only min(count, cap)"; | ||
| " bytes in the arena are valid. No process-global last-sender slot is used. */"; | ||
| "static char* sock_recvfrom(long long fd, long long p, long long cap, long long flags) {"; | ||
| " if (fd < 0 || fd > INT_MAX) return __sock_none(EBADF);"; | ||
| " if (p < 0 || p > __MEM_CAP || cap < 0 || cap > __MEM_CAP - p ||"; | ||
| " flags < INT_MIN || flags > INT_MAX) return __sock_none(EINVAL);"; | ||
| " struct sockaddr_storage ss; socklen_t sl = sizeof ss;"; | ||
| " memset(&ss, 0, sizeof ss);"; | ||
| " ssize_t n = recvfrom((int)fd, __mem + p, (size_t)cap, (int)flags, (struct sockaddr*)&ss, &sl);"; | ||
| " if (n < 0) return __sock_none(errno);"; | ||
| " /* Darwin may return zero bytes and no sender for a zero-capacity"; | ||
| " receive with no queued datagram. Do not fabricate a successful"; | ||
| " packet; match the would-block/timeout result of a nonzero receive. */"; | ||
| " if (cap == 0 && sl == 0) return __sock_none(EAGAIN);"; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified: this resolves the previous zero-capacity finding on both native backends. Checking for missing sender metadata preserves successful zero-capacity receives of queued datagrams, while the original no-packet reproducer now returns an empty result with EAGAIN. The new regression also fails when this guard is removed. |
||
| " char* address = __sock_text((struct sockaddr*)&ss, sl);"; | ||
| " if (!address[0]) return address;"; | ||
|
Comment on lines
+8189
to
+8196
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Minor] Handle zero-capacity receives before reporting a datagram
Reproduced on both C and LLVM with the existing extern declarations: No sender is needed. Changing the capacity to 1 correctly returns Please either reject zero receive capacity with
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7b101ac. Added IPv4/IPv6 regressions for no queued packet on timeout-configured and nonblocking sockets, plus queued empty and nonempty packets received at zero capacity. The latter checks verify sender identity, unchanged payload storage, and removal from the receive queue. Existing positive-capacity empty-datagram coverage remains in place. The no-packet regression fails on the old implementation. The expanded gate passes on C and LLVM on macOS and C on Linux; the existing sockaddr gate passes all 116 checks. |
||
| " char out[1280];"; | ||
| " int len = snprintf(out, sizeof out, \"%lld %s\", (long long)n, address);"; | ||
| " if (len < 0 || (size_t)len >= sizeof out) return __sock_none(EOVERFLOW);"; | ||
| " char* result = __lang_str_alloc(__lang_current_region, (size_t)len);"; | ||
| " memcpy(result, out, (size_t)len + 1);"; | ||
| " __fd_errno = 0;"; | ||
| " return result;"; | ||
| "}"; | ||
| "/* tcp_accept: the connected client fd, or a coded failure -- the same"; | ||
| " codes tcp_read has used since v0.1.226 (-1 would-block, -2 gone,"; | ||
| " -3 other), so a nonblocking accept can tell \"no one is waiting\""; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #!/bin/sh | ||
| # Addressed UDP, flags, source metadata, bounds and errno on both native backends. | ||
| set -eu | ||
| ROOT="$(cd "$(dirname "$0")/.." && pwd)" | ||
| MERE="$ROOT/_build/default/bin/mere.exe" | ||
| CC="${CC:-clang}" | ||
| T=$(mktemp -d) | ||
| trap 'rm -rf "$T"' EXIT | ||
| P="$ROOT/test/sockaddr/datagram_probe.mere" | ||
| "$MERE" -c "$P" > "$T/probe.c" | ||
| "$CC" -O2 -w -pthread "$T/probe.c" -o "$T/c" -lm | ||
| "$MERE" -ll "$P" > "$T/probe.ll" | ||
| "$MERE" --ffi-c "$P" > "$T/ffi.c" | ||
| "$CC" -O2 -w -pthread "$T/probe.ll" "$T/ffi.c" -o "$T/llvm" -lm | ||
| printf '%s\n' 'datagrams: IPv4 and IPv6 passed' > "$T/expected" | ||
| for backend in c llvm; do | ||
| sh "$ROOT/scripts/bounded.sh" 15 "$T/$backend" > "$T/$backend.out" 2>&1 || { | ||
| cat "$T/$backend.out" | ||
| exit 1 | ||
| } | ||
| diff -u "$T/expected" "$T/$backend.out" | ||
| echo "datagrams: $backend passed" | ||
| done |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // Real loopback packets, including two destinations on one bound socket. | ||
| // All ports come from the kernel. No public network or DNS service is used. | ||
| extern fn socket: int -> int -> int -> int; | ||
| extern fn sock_bind: int -> str -> int -> int; | ||
| extern fn sock_local_addr: int -> str; | ||
| extern fn sock_peer_addr: int -> str; | ||
| extern fn sock_sendto: int -> int -> int -> int -> str -> int -> int; | ||
| extern fn sock_recvfrom: int -> int -> int -> int -> str; | ||
| extern fn udp_open: str -> int -> int; | ||
| extern fn udp_send: int -> int -> int -> int; | ||
| extern fn tcp_close: int -> int; | ||
| extern fn tcp_set_timeout: int -> int -> int; | ||
| extern fn io_set_nonblocking: int -> int; | ||
| extern fn fd_last_errno: unit -> int; | ||
| extern fn io_poll_new: int -> int; | ||
| extern fn io_poll_add: int -> int -> int -> int; | ||
| extern fn io_poll_wait: int -> int -> int; | ||
| extern fn io_poll_del: int -> int -> int; | ||
| extern fn mem_alloc: int -> int; | ||
| extern fn mem_set_u8: int -> int -> int -> int; | ||
| extern fn mem_to_str: int -> int -> str; | ||
|
|
||
| let check = fn (label: str) -> fn (ok: bool) -> | ||
| if ok then () else (let _ = print ("FAIL " ++ label) in exit 1); | ||
| let rec nth = fn (xs: str list) -> fn (i: int) -> | ||
| match xs with Nil -> "" | Cons (v, rest) -> if i == 0 then v else nth rest (i - 1); | ||
| let port = fn (s: str) -> int_of_str (nth (str_split s " ") 1); | ||
| let packet = fn (n: int) -> fn (addr: str) -> str_of_int n ++ " " ++ addr; | ||
|
|
||
| let run = fn (family: int) -> fn (host: str) -> | ||
| let a = socket family 2 0 in | ||
| let b = socket family 2 0 in | ||
| let c = socket family 2 0 in | ||
| let _ = check "open" (a >= 0 && b >= 0 && c >= 0) in | ||
| let _ = check "bind" (sock_bind a host 0 == 0 && sock_bind b host 0 == 0 && sock_bind c host 0 == 0) in | ||
| let _ = tcp_set_timeout a 1000 in | ||
| let _ = tcp_set_timeout b 1000 in | ||
| let _ = tcp_set_timeout c 1000 in | ||
| let aa = sock_local_addr a in | ||
| let ba = sock_local_addr b in | ||
| let ca = sock_local_addr c in | ||
| let p = mem_alloc 16 in | ||
| let q = mem_alloc 16 in | ||
| // Darwin can report a successful zero-capacity recvfrom without a packet. | ||
| // A timeout/error must stay distinguishable from a genuine empty datagram. | ||
| let _ = tcp_set_timeout b 100 in | ||
| let _ = check "no packet at zero capacity" (str_eq (sock_recvfrom b q 0 0) "" && fd_last_errno () > 0) in | ||
| let _ = check "no packet at positive capacity" (str_eq (sock_recvfrom b q 1 0) "" && fd_last_errno () > 0) in | ||
| let _ = tcp_set_timeout b 1000 in | ||
| let poll = io_poll_new 0 in | ||
| let _ = io_poll_add poll b 1 in | ||
| let _ = mem_set_u8 p 0 65 in | ||
| let _ = mem_set_u8 p 1 0 in | ||
| let _ = mem_set_u8 p 2 255 in | ||
| let data = mem_to_str p 3 in | ||
| let _ = check "send binary" (sock_sendto a p 3 0 host (port ba) == 3 && fd_last_errno () == 0) in | ||
| let _ = check "peek sender" (str_eq (sock_recvfrom b q 16 2) (packet 3 aa)) in | ||
| let _ = check "peek binary" (str_eq (mem_to_str q 3) data) in | ||
| let _ = check "receive after peek" (str_eq (sock_recvfrom b q 16 0) (packet 3 aa)) in | ||
| let _ = check "receive binary" (str_eq (mem_to_str q 3) data && fd_last_errno () == 0) in | ||
| let _ = check "second destination" (sock_sendto a p 0 0 host (port ca) == 0) in | ||
| let _ = check "empty packet has sender" (str_eq (sock_recvfrom c q 16 0) (packet 0 aa)) in | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good regression coverage: sending to two destinations through the same bound socket and checking its binding and connected peer helps catch implementations that reconnect or replace the descriptor while adding addressed sends. |
||
| // A queued packet may still be consumed with zero capacity, with a valid | ||
| // sender and without touching payload storage. Poll removes arrival races. | ||
| let _ = mem_set_u8 q 0 90 in | ||
| let _ = check "send for zero capacity" (sock_sendto a p 3 0 host (port ba) == 3) in | ||
| let _ = check "zero-capacity packet ready" (io_poll_wait poll 1000 > 0) in | ||
| let _ = check "zero-capacity sender" (str_eq (sock_recvfrom b q 0 0) (packet 0 aa) && fd_last_errno () == 0) in | ||
| let _ = check "zero capacity preserves storage" (str_eq (mem_to_str q 1) "Z") in | ||
| let _ = check "zero capacity consumes packet" (io_poll_wait poll 0 == 0) in | ||
| let _ = check "send empty for zero capacity" (sock_sendto a p 0 0 host (port ba) == 0) in | ||
| let _ = check "empty zero-capacity packet ready" (io_poll_wait poll 1000 > 0) in | ||
| let _ = check "empty zero-capacity sender" (str_eq (sock_recvfrom b q 0 0) (packet 0 aa) && fd_last_errno () == 0) in | ||
| let _ = check "empty zero-capacity packet consumed" (io_poll_wait poll 0 == 0) in | ||
| let _ = io_poll_del poll b in | ||
| let _ = check "binding unchanged" (str_eq (sock_local_addr a) aa) in | ||
| let _ = check "no connected peer" (str_eq (sock_peer_addr a) "" && fd_last_errno () > 0) in | ||
| let _ = check "send before truncation" (sock_sendto a p 3 0 host (port ba) == 3) in | ||
| let _ = check "truncate packet" (str_eq (sock_recvfrom b q 1 0) (packet 1 aa)) in | ||
| let _ = check "truncated byte" (str_eq (mem_to_str q 1) "A") in | ||
| let _ = check "nonblocking" (io_set_nonblocking b == 0) in | ||
| let _ = check "packet remainder discarded" (str_eq (sock_recvfrom b q 16 0) "" && fd_last_errno () > 0) in | ||
| let would_block = fd_last_errno () in | ||
| let _ = check "nonblocking zero capacity without packet" (str_eq (sock_recvfrom b q 0 0) "" && fd_last_errno () == would_block) in | ||
| let _ = check "hostname refused without lookup" (sock_sendto a p 1 0 "localhost" (port ba) == 0 - 2 && fd_last_errno () == 0) in | ||
| let _ = check "bad port" (sock_sendto a p 1 0 host 65536 == 0 - 1 && fd_last_errno () > 0) in | ||
| let _ = check "negative buffer" (sock_sendto a (0 - 1) 1 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in | ||
| let _ = check "oversized buffer" (sock_sendto a p 16777216 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in | ||
| let _ = check "negative receive capacity" (str_eq (sock_recvfrom b q (0 - 1) 0) "" && fd_last_errno () > 0) in | ||
| let _ = check "receive beyond arena" (str_eq (sock_recvfrom b 16777216 1 0) "" && fd_last_errno () > 0) in | ||
| let d = udp_open host (port ca) in | ||
| let _ = check "connect" (d >= 0) in | ||
| let da = sock_local_addr d in | ||
| let peer = sock_peer_addr d in | ||
| let sent = sock_sendto d p 3 0 host (port ba) in | ||
| // Darwin refuses sendto on a connected datagram socket; Linux permits it. | ||
| let _ = (if str_eq (sys_os ()) "darwin" then | ||
| check "connected send refused" (sent == 0 - 1 && fd_last_errno () > 0) | ||
| else (let _ = check "connected send to alternate" (sent == 3) in | ||
| check "alternate source" (str_eq (sock_recvfrom b q 16 0) (packet 3 da)))) in | ||
| let _ = check "connected peer unchanged" (str_eq (sock_peer_addr d) peer && str_eq (sock_local_addr d) da) in | ||
| let _ = check "connected default destination" (udp_send d p 3 == 3) in | ||
| let _ = check "default source" (str_eq (sock_recvfrom c q 16 0) (packet 3 da)) in | ||
| let _ = tcp_close d in | ||
| let _ = tcp_close a in | ||
| let _ = check "closed send" (sock_sendto a p 1 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in | ||
| let _ = check "closed receive" (str_eq (sock_recvfrom a q 16 0) "" && fd_last_errno () > 0) in | ||
| let _ = tcp_close b in | ||
| let _ = tcp_close c in | ||
| (); | ||
|
|
||
| let _ = run 2 "127.0.0.1"; | ||
| let _ = run (if str_eq (sys_os ()) "darwin" then 30 else 10) "::1"; | ||
| print "datagrams: IPv4 and IPv6 passed" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed in the completed Ubuntu 24.04 runtime-job log: this step executed successfully and reports both C and LLVM passing. Together with the earlier macOS checks, this validates the shared runtime and the zero-capacity regression on both supported host platforms.