feat(udp): Udp.* - polled IPv4 datagrams on macOS and Windows

Udp.open/port/send/recv/from_ip/from_port/close/resolve/local_ip/ip/ip_text, native
BSD sockets (udp.ll) and Winsock (udp_win.ll, -lws2_32), linked only when a program
uses Udp.*; example, docs and tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-17 11:16:14 +03:00
parent 6a7f1dd393
commit ebb1a00352
24 changed files with 68630 additions and 66360 deletions

9
changes/udp-datagrams.md Normal file
View file

@ -0,0 +1,9 @@
bump: minor
type: feat
`Udp.*`: polled IPv4 datagrams - the transport under a game's own netcode. `Udp.open(port)`
binds a non-blocking socket (port 0 picks one, `Udp.port` says which), `Udp.send` sends one
datagram to an address and port, and `Udp.recv` drains what arrived without ever blocking;
`Udp.from_ip` / `Udp.from_port` name the sender. `Udp.ip` / `Udp.ip_text` convert addresses,
`Udp.resolve` looks a host name up and `Udp.local_ip` is the address a player on the same
network would dial. BSD sockets on macOS (`runtime/native/udp.ll`), Winsock on Windows
(`udp_win.ll`, linked with ws2_32), linked only into a program that uses `Udp.*`.

View file

@ -0,0 +1,9 @@
---
id: udp
title: Udp
order: 9
---
Plain IPv4 datagrams, polled — the transport under a game's own netcode: peer-to-peer play, a LAN lobby, a STUN query. Open a socket with <a href="udp-open.html"><code>Udp.open</code></a>, send with <a href="udp-send.html"><code>Udp.send</code></a>, and each frame drain what arrived with <a href="udp-recv.html"><code>Udp.recv</code></a>, which returns <code>0</code> when nothing is waiting and never blocks. The sender of the last datagram read is <a href="udp-from_ip.html"><code>Udp.from_ip</code></a> / <a href="udp-from_port.html"><code>from_port</code></a>.
An address is an <code>int</code>: <code>a.b.c.d</code> is <code>(a &lt;&lt; 24) | (b &lt;&lt; 16) | (c &lt;&lt; 8) | d</code>, converted by <a href="udp-ip.html"><code>Udp.ip</code></a> and <a href="udp-ip_text.html"><code>Udp.ip_text</code></a>. Datagrams can be lost, repeated and reordered; the protocol a game builds on top says what to do about that. Like <code>Http.*</code> it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation directly. BSD sockets on macOS, Winsock on Windows.

View file

@ -0,0 +1,23 @@
---
id: udp-close
name: Udp.close
category: udp
kind: namespace-method
tokens: Udp.close
sig: Udp.close(socket) -> void
tip: Close a socket.
order: 6
ns: Udp
member: close
---
Closes the socket and frees its handle.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
Udp.close(socket: s)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: udp-from_ip
name: Udp.from_ip
category: udp
kind: namespace-method
tokens: Udp.from_ip
sig: Udp.from_ip(socket) -> int
tip: The sender of the last datagram read.
order: 4
ns: Udp
member: from_ip
---
The address the last datagram read on this socket came from - the one to answer.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
print(Udp.from_ip(socket: s))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: udp-from_port
name: Udp.from_port
category: udp
kind: namespace-method
tokens: Udp.from_port
sig: Udp.from_port(socket) -> int
tip: The sender's port.
order: 5
ns: Udp
member: from_port
---
The port the last datagram read on this socket came from.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
print(Udp.from_port(socket: s))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: udp-ip
name: Udp.ip
category: udp
kind: namespace-method
tokens: Udp.ip
sig: Udp.ip(text) -> int
tip: Parse a dotted address.
order: 9
ns: Udp
member: ip
---
Turns <code>"a.b.c.d"</code> into an address, or <code>0</code> when the text is not one.
```ludic
program Demo {
entry {
print(Udp.ip(text: "10.0.0.2"))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: udp-ip_text
name: Udp.ip_text
category: udp
kind: namespace-method
tokens: Udp.ip_text
sig: Udp.ip_text(ip) -> string
tip: Format an address.
order: 10
ns: Udp
member: ip_text
---
Turns an address back into <code>"a.b.c.d"</code>.
```ludic
program Demo {
entry {
print(Udp.ip_text(ip: Udp.ip(text: "192.168.1.11")))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: udp-local_ip
name: Udp.local_ip
category: udp
kind: namespace-method
tokens: Udp.local_ip
sig: Udp.local_ip() -> int
tip: This machine's address.
order: 8
ns: Udp
member: local_ip
---
The address this machine reaches the internet from - the one a player on the same network would dial. No packet is sent; <code>0</code> when there is no route.
```ludic
program Demo {
entry {
print(Udp.ip_text(ip: Udp.local_ip()))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: udp-open
name: Udp.open
category: udp
kind: namespace-method
tokens: Udp.open
sig: Udp.open(port) -> int
tip: Open a socket on a port.
order: 0
ns: Udp
member: open
---
Opens a non-blocking IPv4 socket bound to <code>port</code> on every interface and returns its handle, or <code>0</code> when it could not. Port <code>0</code> lets the system pick a free one; <a href="udp-port.html"><code>Udp.port</code></a> says which.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
print(s > 0)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: udp-port
name: Udp.port
category: udp
kind: namespace-method
tokens: Udp.port
sig: Udp.port(socket) -> int
tip: The port a socket is bound to.
order: 1
ns: Udp
member: port
---
Returns the port the socket is bound to - the one the system chose when it was opened on port <code>0</code>.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
print(Udp.port(socket: s) > 0)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: udp-recv
name: Udp.recv
category: udp
kind: namespace-method
tokens: Udp.recv
sig: Udp.recv(socket, bytes, cap) -> int
tip: Read the next waiting datagram.
order: 3
ns: Udp
member: recv
---
Copies the next waiting datagram, up to <code>cap</code> bytes, into <code>bytes</code> and returns its length, or <code>0</code> when nothing is waiting. It never blocks: call it in a loop each frame until it returns <code>0</code>. The sender is <a href="udp-from_ip.html"><code>Udp.from_ip</code></a> / <a href="udp-from_port.html"><code>from_port</code></a>.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
let buf = Memory.bytes(64)
print(Udp.recv(socket: s, bytes: buf, cap: 64))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: udp-resolve
name: Udp.resolve
category: udp
kind: namespace-method
tokens: Udp.resolve
sig: Udp.resolve(name) -> int
tip: Look a host name up.
order: 7
ns: Udp
member: resolve
---
Returns the first IPv4 address of a host name, or <code>0</code>. It waits for the system's resolver, so call it outside the frame loop.
```ludic
program Demo {
entry {
print(Udp.ip_text(ip: Udp.resolve(name: "localhost")))
}
}
```

View file

@ -0,0 +1,25 @@
---
id: udp-send
name: Udp.send
category: udp
kind: namespace-method
tokens: Udp.send
sig: Udp.send(socket, ip, port, bytes, len) -> int
tip: Send one datagram.
order: 2
ns: Udp
member: send
---
Sends the first <code>len</code> bytes of <code>bytes</code> to <code>ip</code>:<code>port</code> as one datagram and returns the number of bytes sent, or <code>-1</code>.
```ludic
program Demo {
entry {
let s = Udp.open(port: 0)
let buf = Memory.bytes(4)
Memory.poke(buf, 0, 1)
print(Udp.send(socket: s, ip: Udp.ip(text: "127.0.0.1"), port: Udp.port(socket: s), bytes: buf, len: 1))
}
}
```

View file

@ -0,0 +1,27 @@
# udp.ludic — Udp.* on the loopback: two sockets, one datagram each way.
program UdpDemo {
entry {
let a = Udp.open(0)
let b = Udp.open(0)
let pa = Udp.port(a)
let pb = Udp.port(b)
let lo = Udp.ip("127.0.0.1")
let out = Memory.bytes(16)
Memory.poke(out, 0, 104)
Memory.poke(out, 1, 105)
let sent = Udp.send(a, lo, pb, out, 2)
let inb = Memory.bytes(64)
var got = 0
var tries = 0
while got == 0 and tries < 200000 { got = Udp.recv(b, inb, 64); tries += 1 }
let from_ok = Udp.from_ip(b) == lo and Udp.from_port(b) == pa
Udp.send(b, Udp.from_ip(b), Udp.from_port(b), inb, got)
var back = 0
tries = 0
while back == 0 and tries < 200000 { back = Udp.recv(a, out, 16); tries += 1 }
let bad = Udp.ip("1.2.3") == 0 and Udp.ip("300.1.1.1") == 0
print(`{sent} {got} {Memory.peek(inb, 0)} {Memory.peek(inb, 1)} {from_ok} {back} {Udp.ip_text(lo)} {bad} {Udp.recv(a, out, 16)} {pa > 0}`)
Udp.close(a)
Udp.close(b)
}
}

258
runtime/native/udp.ll Normal file
View file

@ -0,0 +1,258 @@
; ============================================================================
; udp.ll — UDP datagrams behind Udp.* (macOS / POSIX, BSD sockets).
;
; Linked into any program that uses Udp.* (selfhost/main.ludic, tools/ludic-cli/build.ludic).
; udp_win.ll is the same interface over Winsock.
;
; lu_udp_open(port) -> int a non-blocking IPv4 socket bound to 0.0.0.0:port
; (0 picks one); a handle 1..16, or 0 on failure
; lu_udp_port(h) -> int the port it is bound to
; lu_udp_send(h, ip, port, buf, len) one datagram; bytes sent, or -1
; lu_udp_recv(h, buf, cap, ipport) one waiting datagram into buf: its length, or 0 when
; none is waiting; the sender's ip and port go to
; ipport[0] and ipport[1] (two i32s)
; lu_udp_close(h)
; lu_udp_resolve(name) -> int the first IPv4 address of a host name, or 0
; lu_udp_local_ip() -> int the address this machine reaches the internet from
; (no packet is sent), or 0
;
; An address is a host-order i32: a.b.c.d is (a << 24) | (b << 16) | (c << 8) | d.
; The handle table keeps the file descriptors, so a handle is the same small number on both
; platforms (a Winsock SOCKET is 64 bits wide).
; ============================================================================
declare i32 @socket(i32, i32, i32)
declare i32 @bind(i32, ptr, i32)
declare i32 @connect(i32, ptr, i32)
declare i32 @getsockname(i32, ptr, ptr)
declare i32 @fcntl(i32, i32, ...)
declare i64 @sendto(i32, ptr, i64, i32, ptr, i32)
declare i64 @recvfrom(i32, ptr, i64, i32, ptr, ptr)
declare i32 @close(i32)
declare i32 @getaddrinfo(ptr, ptr, ptr, ptr)
declare void @freeaddrinfo(ptr)
declare i32 @llvm.bswap.i32(i32)
declare i16 @llvm.bswap.i16(i16)
@LU_fd = internal global [16 x i32] zeroinitializer ; fd + 1 per handle, 0 = free
; sockaddr_in (BSD): len, family, port (network order), addr (network order), 8 zero
define internal void @lu_addr(ptr %sa, i32 %ip, i32 %port) {
entry:
store i64 0, ptr %sa
%z8 = getelementptr i8, ptr %sa, i64 8
store i64 0, ptr %z8
store i8 16, ptr %sa
%fam = getelementptr i8, ptr %sa, i64 1
store i8 2, ptr %fam
%p16 = trunc i32 %port to i16
%pn = call i16 @llvm.bswap.i16(i16 %p16)
%pp = getelementptr i8, ptr %sa, i64 2
store i16 %pn, ptr %pp
%in = call i32 @llvm.bswap.i32(i32 %ip)
%ap = getelementptr i8, ptr %sa, i64 4
store i32 %in, ptr %ap
ret void
}
define internal i32 @lu_fd_of(i32 %h) {
entry:
%lo = icmp slt i32 %h, 1
%hi = icmp sgt i32 %h, 16
%bad = or i1 %lo, %hi
br i1 %bad, label %none, label %look
look:
%i = sub i32 %h, 1
%slot = getelementptr [16 x i32], ptr @LU_fd, i32 0, i32 %i
%v = load i32, ptr %slot
%fd = sub i32 %v, 1
ret i32 %fd
none:
ret i32 -1
}
define i32 @lu_udp_open(i32 %port) {
entry:
%sa = alloca [16 x i8]
%fd = call i32 @socket(i32 2, i32 2, i32 17)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %fail, label %bindit
bindit:
call void @lu_addr(ptr %sa, i32 0, i32 %port)
%b = call i32 @bind(i32 %fd, ptr %sa, i32 16)
%bb = icmp ne i32 %b, 0
br i1 %bb, label %closefail, label %nonblock
nonblock:
%fl = call i32 (i32, i32, ...) @fcntl(i32 %fd, i32 3, i32 0)
%nb = or i32 %fl, 4
%s = call i32 (i32, i32, ...) @fcntl(i32 %fd, i32 4, i32 %nb)
br label %find
find:
%i = phi i32 [ 0, %nonblock ], [ %inext, %next ]
%done = icmp sge i32 %i, 16
br i1 %done, label %closefail, label %check
check:
%slot = getelementptr [16 x i32], ptr @LU_fd, i32 0, i32 %i
%v = load i32, ptr %slot
%free = icmp eq i32 %v, 0
br i1 %free, label %take, label %next
next:
%inext = add i32 %i, 1
br label %find
take:
%fd1 = add i32 %fd, 1
store i32 %fd1, ptr %slot
%h = add i32 %i, 1
ret i32 %h
closefail:
%c = call i32 @close(i32 %fd)
br label %fail
fail:
ret i32 0
}
define i32 @lu_udp_port(i32 %h) {
entry:
%sa = alloca [16 x i8]
%len = alloca i32
store i32 16, ptr %len
%fd = call i32 @lu_fd_of(i32 %h)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %none, label %ask
ask:
%r = call i32 @getsockname(i32 %fd, ptr %sa, ptr %len)
%rb = icmp ne i32 %r, 0
br i1 %rb, label %none, label %read
read:
%pp = getelementptr i8, ptr %sa, i64 2
%pn = load i16, ptr %pp
%ph = call i16 @llvm.bswap.i16(i16 %pn)
%p = zext i16 %ph to i32
ret i32 %p
none:
ret i32 0
}
define i32 @lu_udp_send(i32 %h, i32 %ip, i32 %port, ptr %buf, i32 %n) {
entry:
%sa = alloca [16 x i8]
%fd = call i32 @lu_fd_of(i32 %h)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %none, label %go
go:
call void @lu_addr(ptr %sa, i32 %ip, i32 %port)
%n64 = sext i32 %n to i64
%r = call i64 @sendto(i32 %fd, ptr %buf, i64 %n64, i32 0, ptr %sa, i32 16)
%r32 = trunc i64 %r to i32
ret i32 %r32
none:
ret i32 -1
}
define i32 @lu_udp_recv(i32 %h, ptr %buf, i32 %cap, ptr %ipport) {
entry:
%sa = alloca [16 x i8]
%len = alloca i32
store i32 16, ptr %len
%fd = call i32 @lu_fd_of(i32 %h)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %none, label %go
go:
%c64 = sext i32 %cap to i64
%r = call i64 @recvfrom(i32 %fd, ptr %buf, i64 %c64, i32 0, ptr %sa, ptr %len)
%got = icmp sgt i64 %r, 0
br i1 %got, label %from, label %none
from:
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
store i32 %ah, ptr %ipport
%pp = getelementptr i8, ptr %sa, i64 2
%pn = load i16, ptr %pp
%ph = call i16 @llvm.bswap.i16(i16 %pn)
%p = zext i16 %ph to i32
%o = getelementptr i8, ptr %ipport, i64 4
store i32 %p, ptr %o
%r32 = trunc i64 %r to i32
ret i32 %r32
none:
ret i32 0
}
define void @lu_udp_close(i32 %h) {
entry:
%fd = call i32 @lu_fd_of(i32 %h)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %done, label %go
go:
%c = call i32 @close(i32 %fd)
%i = sub i32 %h, 1
%slot = getelementptr [16 x i32], ptr @LU_fd, i32 0, i32 %i
store i32 0, ptr %slot
br label %done
done:
ret void
}
; addrinfo (macOS): flags, family, socktype, protocol (i32 each), addrlen (u32), then
; canonname, addr, next (pointers at 24, 32, 40)
define i32 @lu_udp_resolve(ptr %name) {
entry:
%hints = alloca [48 x i8]
%res = alloca ptr
store ptr null, ptr %res
call void @llvm.memset.p0.i64(ptr %hints, i8 0, i64 48, i1 false)
%fam = getelementptr i8, ptr %hints, i64 4
store i32 2, ptr %fam
%st = getelementptr i8, ptr %hints, i64 8
store i32 2, ptr %st
%r = call i32 @getaddrinfo(ptr %name, ptr null, ptr %hints, ptr %res)
%rb = icmp ne i32 %r, 0
br i1 %rb, label %none, label %read
read:
%ai = load ptr, ptr %res
%isnull = icmp eq ptr %ai, null
br i1 %isnull, label %none, label %addr
addr:
%sap = getelementptr i8, ptr %ai, i64 32
%sa = load ptr, ptr %sap
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
call void @freeaddrinfo(ptr %ai)
ret i32 %ah
none:
ret i32 0
}
declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
; connect a scratch socket toward a public address (UDP: no packet leaves) and read back
; the local address the system picked for that route
define i32 @lu_udp_local_ip() {
entry:
%sa = alloca [16 x i8]
%len = alloca i32
store i32 16, ptr %len
%fd = call i32 @socket(i32 2, i32 2, i32 17)
%bad = icmp slt i32 %fd, 0
br i1 %bad, label %none, label %go
go:
; 8.8.8.8:53
call void @lu_addr(ptr %sa, i32 134744072, i32 53)
%c = call i32 @connect(i32 %fd, ptr %sa, i32 16)
%cb = icmp ne i32 %c, 0
br i1 %cb, label %closenone, label %ask
ask:
store i32 16, ptr %len
%r = call i32 @getsockname(i32 %fd, ptr %sa, ptr %len)
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
%x = call i32 @close(i32 %fd)
ret i32 %ah
closenone:
%y = call i32 @close(i32 %fd)
br label %none
none:
ret i32 0
}

124
runtime/native/udp.ludic Normal file
View file

@ -0,0 +1,124 @@
# ============================================================================
# udp.ludic — the Udp.* standard library.
#
# Plain IPv4 datagrams, polled. A socket is opened on a port (0 lets the system pick one),
# datagrams are sent to an address and a port, and each frame the game drains what arrived
# with Udp.recv, which never blocks: it returns 0 when nothing is waiting. The sender of the
# last datagram read on a socket is Udp.from_ip / Udp.from_port. This is the transport under
# a game's own netcode - peer-to-peer play, a LAN lobby, a STUN query - and it carries no
# guarantees of its own: datagrams can be lost, repeated and reordered, and a protocol built
# on it says what to do about that.
#
# Like Http.* and Time.now it is OUT-OF-BAND: it depends on the network and the wall clock
# and must never feed a deterministic lockstep/replay simulation directly.
#
# An address is an int: a.b.c.d is (a << 24) | (b << 16) | (c << 8) | d. Udp.ip("10.0.0.2")
# and Udp.ip_text(ip) convert, Udp.resolve("example.com") looks a host name up (it blocks
# for as long as the system's resolver takes), and Udp.local_ip() is the address this
# machine reaches the internet from - the one a player on the same network would dial.
#
# The transport is native: udp.ll (BSD sockets, macOS) and udp_win.ll (Winsock), linked
# only into a program that uses Udp.*.
# ============================================================================
extern function lu_udp_open(port: int) -> int = "lu_udp_open"
extern function lu_udp_port(h: int) -> int = "lu_udp_port"
extern function lu_udp_send(h: int, ip: int, port: int, buf: pointer, n: int) -> int = "lu_udp_send"
extern function lu_udp_recv(h: int, buf: pointer, cap: int, ipport: pointer) -> int = "lu_udp_recv"
extern function lu_udp_close(h: int) -> void = "lu_udp_close"
extern function lu_udp_resolve(name: pointer) -> int = "lu_udp_resolve"
extern function lu_udp_local_ip() -> int = "lu_udp_local_ip"
const UDP_HANDLES: int = 16
var u_ready: bool = false
var u_ipport: pointer = null # the native side writes the sender here (two i32s)
var u_from_ip: words = null # per handle: the sender of the last datagram read
var u_from_port: words = null
function udp_init() -> void {
if u_ready { return }
u_ipport = bytes(8)
u_from_ip = words(UDP_HANDLES + 1)
u_from_port = words(UDP_HANDLES + 1)
var i = 0
while i <= UDP_HANDLES { u_from_ip[i] = 0; u_from_port[i] = 0; i += 1 }
u_ready = true
}
# a socket bound to `port` on every interface (0 = any free port): a handle, or 0
function udp_open(port: int) -> int {
udp_init()
if (port < 0) or (port > 65535) { return 0 }
return lu_udp_open(port)
}
# the port a socket is bound to (the one the system picked, for port 0)
function udp_port(h: int) -> int { return lu_udp_port(h) }
# one datagram of `n` bytes from `buf` to ip:port: the bytes sent, or -1
function udp_send(h: int, ip: int, port: int, buf: pointer, n: int) -> int {
if (buf == null) or (n <= 0) { return -1 }
return lu_udp_send(h, ip, port, buf, n)
}
# the next waiting datagram, up to `cap` bytes, into `buf`: its length, or 0 when none waits
function udp_recv(h: int, buf: pointer, cap: int) -> int {
udp_init()
if (buf == null) or (cap <= 0) { return 0 }
let n = lu_udp_recv(h, buf, cap, u_ipport)
if (n > 0) and (h >= 1) and (h <= UDP_HANDLES) {
u_from_ip[h] = udp_le32(u_ipport, 0)
u_from_port[h] = udp_le32(u_ipport, 4)
}
return n
}
function udp_from_ip(h: int) -> int {
udp_init()
if (h < 1) or (h > UDP_HANDLES) { return 0 }
return u_from_ip[h]
}
function udp_from_port(h: int) -> int {
udp_init()
if (h < 1) or (h > UDP_HANDLES) { return 0 }
return u_from_port[h]
}
function udp_close(h: int) -> void { lu_udp_close(h) }
function udp_resolve(name: pointer) -> int {
if name == null { return 0 }
return lu_udp_resolve(name)
}
function udp_local_ip() -> int { return lu_udp_local_ip() }
# a little-endian i32 out of a byte buffer
function udp_le32(p: pointer, o: int) -> int {
return p[o] | (p[o + 1] << 8) | (p[o + 2] << 16) | (p[o + 3] << 24)
}
# "a.b.c.d" to an address; 0 when it is not one
function udp_ip(text: pointer) -> int {
if text == null { return 0 }
var ip = 0
var part = 0
var digits = 0
var dots = 0
var i = 0
while text[i] != 0 {
let c = text[i]
if (c >= '0') and (c <= '9') {
part = part * 10 + (c - '0')
digits += 1
if (part > 255) or (digits > 3) { return 0 }
} else if c == '.' {
if (digits == 0) or (dots == 3) { return 0 }
ip = (ip << 8) | part
part = 0; digits = 0; dots += 1
} else { return 0 }
i += 1
}
if (dots != 3) or (digits == 0) { return 0 }
return (ip << 8) | part
}
function udp_ip_text(ip: int) -> string {
let a = (ip >> 24) & 255
let b = (ip >> 16) & 255
let c = (ip >> 8) & 255
let d = ip & 255
return `{a}.{b}.{c}.{d}`
}

253
runtime/native/udp_win.ll Normal file
View file

@ -0,0 +1,253 @@
; ============================================================================
; udp_win.ll — UDP datagrams behind Udp.* (Windows, Winsock 2).
;
; The interface of udp.ll over ws2_32: WSAStartup on first use, a SOCKET per handle (64 bits,
; kept in the table), ioctlsocket(FIONBIO) for non-blocking reads, and the Windows
; sockaddr_in (a 16-bit family, no length byte). See udp.ll for the interface.
; ============================================================================
declare i32 @WSAStartup(i16, ptr)
declare i64 @socket(i32, i32, i32)
declare i32 @bind(i64, ptr, i32)
declare i32 @connect(i64, ptr, i32)
declare i32 @getsockname(i64, ptr, ptr)
declare i32 @ioctlsocket(i64, i32, ptr)
declare i32 @sendto(i64, ptr, i32, i32, ptr, i32)
declare i32 @recvfrom(i64, ptr, i32, i32, ptr, ptr)
declare i32 @closesocket(i64)
declare i32 @getaddrinfo(ptr, ptr, ptr, ptr)
declare void @freeaddrinfo(ptr)
declare i32 @llvm.bswap.i32(i32)
declare i16 @llvm.bswap.i16(i16)
declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
@LU_sock = internal global [16 x i64] zeroinitializer ; SOCKET + 1 per handle, 0 = free
@LU_wsa = internal global i32 0
@LU_wsadata = internal global [512 x i8] zeroinitializer
define internal void @lu_start() {
entry:
%s = load i32, ptr @LU_wsa
%ok = icmp ne i32 %s, 0
br i1 %ok, label %done, label %go
go:
%r = call i32 @WSAStartup(i16 514, ptr @LU_wsadata)
store i32 1, ptr @LU_wsa
br label %done
done:
ret void
}
; sockaddr_in (Winsock): family (u16), port (network order), addr (network order), 8 zero
define internal void @lu_addr(ptr %sa, i32 %ip, i32 %port) {
entry:
store i64 0, ptr %sa
%z8 = getelementptr i8, ptr %sa, i64 8
store i64 0, ptr %z8
store i16 2, ptr %sa
%p16 = trunc i32 %port to i16
%pn = call i16 @llvm.bswap.i16(i16 %p16)
%pp = getelementptr i8, ptr %sa, i64 2
store i16 %pn, ptr %pp
%in = call i32 @llvm.bswap.i32(i32 %ip)
%ap = getelementptr i8, ptr %sa, i64 4
store i32 %in, ptr %ap
ret void
}
define internal i64 @lu_sock_of(i32 %h) {
entry:
%lo = icmp slt i32 %h, 1
%hi = icmp sgt i32 %h, 16
%bad = or i1 %lo, %hi
br i1 %bad, label %none, label %look
look:
%i = sub i32 %h, 1
%slot = getelementptr [16 x i64], ptr @LU_sock, i32 0, i32 %i
%v = load i64, ptr %slot
%s = sub i64 %v, 1
ret i64 %s
none:
ret i64 -1
}
define i32 @lu_udp_open(i32 %port) {
entry:
call void @lu_start()
%sa = alloca [16 x i8]
%arg = alloca i32
%s = call i64 @socket(i32 2, i32 2, i32 17)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %fail, label %bindit
bindit:
call void @lu_addr(ptr %sa, i32 0, i32 %port)
%b = call i32 @bind(i64 %s, ptr %sa, i32 16)
%bb = icmp ne i32 %b, 0
br i1 %bb, label %closefail, label %nonblock
nonblock:
store i32 1, ptr %arg
; FIONBIO = 0x8004667E
%nb = call i32 @ioctlsocket(i64 %s, i32 -2147195266, ptr %arg)
br label %find
find:
%i = phi i32 [ 0, %nonblock ], [ %inext, %next ]
%done = icmp sge i32 %i, 16
br i1 %done, label %closefail, label %check
check:
%slot = getelementptr [16 x i64], ptr @LU_sock, i32 0, i32 %i
%v = load i64, ptr %slot
%free = icmp eq i64 %v, 0
br i1 %free, label %take, label %next
next:
%inext = add i32 %i, 1
br label %find
take:
%s1 = add i64 %s, 1
store i64 %s1, ptr %slot
%h = add i32 %i, 1
ret i32 %h
closefail:
%c = call i32 @closesocket(i64 %s)
br label %fail
fail:
ret i32 0
}
define i32 @lu_udp_port(i32 %h) {
entry:
%sa = alloca [16 x i8]
%len = alloca i32
store i32 16, ptr %len
%s = call i64 @lu_sock_of(i32 %h)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %none, label %ask
ask:
%r = call i32 @getsockname(i64 %s, ptr %sa, ptr %len)
%rb = icmp ne i32 %r, 0
br i1 %rb, label %none, label %read
read:
%pp = getelementptr i8, ptr %sa, i64 2
%pn = load i16, ptr %pp
%ph = call i16 @llvm.bswap.i16(i16 %pn)
%p = zext i16 %ph to i32
ret i32 %p
none:
ret i32 0
}
define i32 @lu_udp_send(i32 %h, i32 %ip, i32 %port, ptr %buf, i32 %n) {
entry:
%sa = alloca [16 x i8]
%s = call i64 @lu_sock_of(i32 %h)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %none, label %go
go:
call void @lu_addr(ptr %sa, i32 %ip, i32 %port)
%r = call i32 @sendto(i64 %s, ptr %buf, i32 %n, i32 0, ptr %sa, i32 16)
ret i32 %r
none:
ret i32 -1
}
define i32 @lu_udp_recv(i32 %h, ptr %buf, i32 %cap, ptr %ipport) {
entry:
%sa = alloca [16 x i8]
%len = alloca i32
store i32 16, ptr %len
%s = call i64 @lu_sock_of(i32 %h)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %none, label %go
go:
%r = call i32 @recvfrom(i64 %s, ptr %buf, i32 %cap, i32 0, ptr %sa, ptr %len)
%got = icmp sgt i32 %r, 0
br i1 %got, label %from, label %none
from:
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
store i32 %ah, ptr %ipport
%pp = getelementptr i8, ptr %sa, i64 2
%pn = load i16, ptr %pp
%ph = call i16 @llvm.bswap.i16(i16 %pn)
%p = zext i16 %ph to i32
%o = getelementptr i8, ptr %ipport, i64 4
store i32 %p, ptr %o
ret i32 %r
none:
ret i32 0
}
define void @lu_udp_close(i32 %h) {
entry:
%s = call i64 @lu_sock_of(i32 %h)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %done, label %go
go:
%c = call i32 @closesocket(i64 %s)
%i = sub i32 %h, 1
%slot = getelementptr [16 x i64], ptr @LU_sock, i32 0, i32 %i
store i64 0, ptr %slot
br label %done
done:
ret void
}
; addrinfo (Winsock): flags, family, socktype, protocol (i32 each), addrlen (size_t at 16),
; then canonname, addr, next (pointers at 24, 32, 40)
define i32 @lu_udp_resolve(ptr %name) {
entry:
call void @lu_start()
%hints = alloca [48 x i8]
%res = alloca ptr
store ptr null, ptr %res
call void @llvm.memset.p0.i64(ptr %hints, i8 0, i64 48, i1 false)
%fam = getelementptr i8, ptr %hints, i64 4
store i32 2, ptr %fam
%st = getelementptr i8, ptr %hints, i64 8
store i32 2, ptr %st
%r = call i32 @getaddrinfo(ptr %name, ptr null, ptr %hints, ptr %res)
%rb = icmp ne i32 %r, 0
br i1 %rb, label %none, label %read
read:
%ai = load ptr, ptr %res
%isnull = icmp eq ptr %ai, null
br i1 %isnull, label %none, label %addr
addr:
%sap = getelementptr i8, ptr %ai, i64 32
%sa = load ptr, ptr %sap
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
call void @freeaddrinfo(ptr %ai)
ret i32 %ah
none:
ret i32 0
}
define i32 @lu_udp_local_ip() {
entry:
call void @lu_start()
%sa = alloca [16 x i8]
%len = alloca i32
%s = call i64 @socket(i32 2, i32 2, i32 17)
%bad = icmp eq i64 %s, -1
br i1 %bad, label %none, label %go
go:
call void @lu_addr(ptr %sa, i32 134744072, i32 53)
%c = call i32 @connect(i64 %s, ptr %sa, i32 16)
%cb = icmp ne i32 %c, 0
br i1 %cb, label %closenone, label %ask
ask:
store i32 16, ptr %len
%r = call i32 @getsockname(i64 %s, ptr %sa, ptr %len)
%ap = getelementptr i8, ptr %sa, i64 4
%an = load i32, ptr %ap
%ah = call i32 @llvm.bswap.i32(i32 %an)
%x = call i32 @closesocket(i64 %s)
ret i32 %ah
closenone:
%y = call i32 @closesocket(i64 %s)
br label %none
none:
ret i32 0
}

View file

@ -517,6 +517,21 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
if (meth == "parse") { bare = "http_parse"; push(labels, "bytes"); push(labels, "len") }
}
# Udp.* — polled IPv4 datagrams (runtime/native/udp.ludic + udp.ll / udp_win.ll).
# Out-of-band like Http.*: the transport under a game's own netcode.
if (ns == "Udp") {
if (meth == "open") { bare = "udp_open"; push(labels, "port") }
if (meth == "port") { bare = "udp_port"; push(labels, "socket") }
if (meth == "send") { bare = "udp_send"; push(labels, "socket"); push(labels, "ip"); push(labels, "port"); push(labels, "bytes"); push(labels, "len") }
if (meth == "recv") { bare = "udp_recv"; push(labels, "socket"); push(labels, "bytes"); push(labels, "cap") }
if (meth == "from_ip") { bare = "udp_from_ip"; push(labels, "socket") }
if (meth == "from_port") { bare = "udp_from_port"; push(labels, "socket") }
if (meth == "close") { bare = "udp_close"; push(labels, "socket") }
if (meth == "resolve") { bare = "udp_resolve"; push(labels, "name") }
if (meth == "local_ip") { bare = "udp_local_ip" }
if (meth == "ip") { bare = "udp_ip"; push(labels, "text") }
if (meth == "ip_text") { bare = "udp_ip_text"; push(labels, "ip") }
}
# Phase 3: the bare reflection / networking / process builtins, namespaced.
# Each is a pure alias — the callee is rewritten to the bare name below.
if (ns == "World") {

View file

@ -252,6 +252,8 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Audio" { g_uses_audio = true }
# Http.* (#6) — any Http method splices the HTTP client runtime.
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
# Udp.* — any Udp method splices the datagram runtime (and links udp.ll / udp_win.ll).
if e.a.kind == E_ID and e.a.s == "Udp" { g_uses_udp = true }
# Gl.* — any Gl method splices the OpenGL runtime (and links the GL backend).
if e.a.kind == E_ID and e.a.s == "Gl" { g_uses_gl = true }
# Vk.* — any Vk method splices the Vulkan runtime (and links its loader)
@ -677,6 +679,7 @@ var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tw
var g_uses_fx: bool = false # Fx.sparks/number/clear -> splice fx.ludic; fx_tick each Update, fx_draw each Render
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
var g_uses_udp: bool = false # Udp.* -> splice udp.ludic; links udp.ll (macOS) / udp_win.ll + ws2_32
var g_uses_gl: bool = false # Gl.* -> splice gl.ludic (+ generated gl_api.ludic); links gl.ll + gl_thunks.ll + OpenGL
var g_uses_vk: bool = false # Vk.* -> splice vk.ludic (+ generated vk_api.ludic); links vk_thunks.ll + the platform loader
# issue #64: functions marked @System(Phase) in a prebuilt binary module — the
@ -1187,6 +1190,13 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/http.ludic")
cur_dir = saved
}
# Udp.*: splice the datagram library. The sockets are native (udp.ll / udp_win.ll, bound
# by `extern function` in udp.ludic); the address helpers are pure Ludic.
if g_uses_udp {
cur_dir = ""
do_import("runtime/native/udp.ludic")
cur_dir = saved
}
# Gl.*: splice the OpenGL surface (gl.ludic + the generated gl_api.ludic). The
# native calls are the linked GL entry points themselves; the window attach is
# is_windowed()-guarded, so a headless build renders into an offscreen context.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -273,6 +273,12 @@ entry {
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/threads_win.ll")}` }
else { cmd = `{cmd} {join_path(home, "runtime/native/threads.ll")}` }
}
# Udp.* links the socket layer: BSD sockets from libSystem on macOS, Winsock (ws2_32) on
# Windows.
if g_uses_udp {
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/udp_win.ll")} -lws2_32` }
else { cmd = `{cmd} {join_path(home, "runtime/native/udp.ll")}` }
}
if g_uses_http {
if g_target_win {
let httpw = join_path(home, "runtime/native/http_win.ll")

View file

@ -48,7 +48,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
if mode == 2 {
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -58,7 +58,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll`
let audio = `{home}runtime/native/audio.ll`
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -91,6 +91,13 @@ function http_link_flags(ll: pointer) -> pointer {
return ` {ludic_home()}runtime/native/http.ll -Wl,-needed_framework,Foundation`
}
# A program that uses Udp.* calls the lu_udp_* socket layer; link udp.ll only then, as
# `ludicc -o` does (Windows links through ludicc, with udp_win.ll and ws2_32).
function udp_link_flags(ll: pointer) -> pointer {
if not shq(`grep -q "call .*@lu_udp_" {ll}`) { return "" }
return ` {ludic_home()}runtime/native/udp.ll`
}
# A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link
# threads.ll only then, as `ludicc -o` does.
function threads_link_flags(ll: pointer) -> pointer {

View file

@ -731,6 +731,20 @@ function cmd_dev_test() -> int {
else { bad2("http.ludic via ludic build", `got [{got2}]`) }
} else { bad2("ludic build of an Http.* program", capture_line(`tail -1 {tmp_dir()}/http_lb.out`)) }
}
# Udp.*: two sockets on the loopback, a datagram each way, through ludicc and ludic build
if is_darwin() {
let udp_want = "2 2 104 105 1 2 127.0.0.1 1 0 1"
if shq(`LUDIC_HOME=. bin/ludicc examples/library/udp.ludic -o {tmp_dir()}/udp_demo > {tmp_dir()}/udp.out 2>&1`) {
let got = capture_line(`{tmp_dir()}/udp_demo < /dev/null`)
if (got == udp_want) { ok("udp.ludic (Udp.open/send/recv/from/ip - datagrams over the loopback)") }
else { bad2("udp.ludic", `got [{got}]`) }
} else { bad2("udp.ludic build", capture_line(`tail -1 {tmp_dir()}/udp.out`)) }
if shq(`LUDIC_HOME=. bin/ludic build examples/library/udp.ludic --headless -o {tmp_dir()}/udp_lb > {tmp_dir()}/udp_lb.out 2>&1`) {
let got2 = capture_line(`{tmp_dir()}/udp_lb < /dev/null`)
if (got2 == udp_want) { ok("ludic build links udp.ll for a program that uses Udp.*") }
else { bad2("udp.ludic via ludic build", `got [{got2}]`) }
} else { bad2("ludic build of a Udp.* program", capture_line(`tail -1 {tmp_dir()}/udp_lb.out`)) }
}
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("events/scoped", "2")