feat(sterni/blipqn): reuse address and socket for multiple Sends

This is achieved by storing the resources we need to acquire for
interacting with the flipdot (socket fd and addrinfo struct) in a
`struct flipdot` that is dynamically allocated and treated as an opaque
pointer object via the BQN FFI.

To make sure these resources are released correctly, we only provide a
lisp style WithFlipdot to the user which takes care of acquiring and
releasing the `struct flipdot`. This works even if an error occurs in
the function the user provides thanks to _defer_. I'm not sure if
calling it _defer_ is right since Go's error handling works differently,
so defer really is deferred execution in a sense which doesn't really
fit what we're doing here. The closest is probably Haskell's bracket,
but that name references it's triadic nature which doesn't fit our
implementation.

Change-Id: Iff65d277a448dbe0c6ac93e816ece5ab6fa10190
Reviewed-on: https://cl.tvl.fyi/c/depot/+/13011
Reviewed-by: sterni <sternenseemann@systemli.org>
Tested-by: BuildkiteCI
This commit is contained in:
sterni 2025-01-15 20:46:32 +01:00
parent 1027e21eee
commit a9e121380b
3 changed files with 78 additions and 34 deletions

View file

@ -1,20 +1,42 @@
MakeFlipdot⇐
WithFlipdot⇐
# - host (char*) argument:
# - Assume char is i8 (safe even if it's actually u8 if
# ASCII). We can't let •FFI check this for us since CBQN doesn't support
# specifying c7. We do this in MakeFlipdot
# - Needs to be zero-terminated, we also do this in MakeFlipdot.
# - bitmap_len (size_t) argument: assume size_t is at least u32.
ffi_send_to_flipdot "libflipdot.so" •FFI "i8""send_to_flipdot""*i8:c8""u16""*u8""u32"
t {
bool "i8"
# needs bound checking in wrapper (no c7 in CBQN) and zero byte appended
charp "*i8:c8"
# assume size_t is at least 32bit
size "u32"
# used for struct flipdot *
hdl "*"
}
ffi_flipdot_open "libflipdot.so" •FFI t.hdl"flipdot_open"t.charp"u16"
ffi_flipdot_close "libflipdot.so" •FFI """flipdot_close"t.hdl
ffi_flipdot_send "libflipdot.so" •FFI t.bool"flipdot_send"t.hdl"*u8"t.size
Compact +´( × 2)˘(8)
MakeFlipdot {
hostport 𝕊 wh:
"Total pixel count must be divisible by 8" ! 0=8|w×h
hostportwh:
"Hostname must be ASCII" ! ´(@+127)host
"Total pixel count must be divisible by 8" ! 0=8|w×h
hdl FFI_flipdot_open (host@)port
Close {𝕤 FFI_flipdot_close hdl}
shape hw
empty shape0
Send {"sendto(2) error"!FFI_send_to_flipdot (host@)port() Compact 𝕩}
Send {FFI_flipdot_send hdl() Compact 𝕩}
}
# Inspired by Go's defer. Will execute 𝔽 after 𝔾 (passing 𝕨 and 𝕩 to each)
# even if an error occurs. If an error occurred, _defer_ will cause an
# assertion failure with the error message after executing 𝔾. This looses
# the original error location, though.
_defer_ {
# At least in CBQN, •CurrentError may fail for namespace related reasons
sr (1𝔾)(0(•CurrentError("_defer_: Unknown Error occurred"˙))) 𝕩 𝔽 𝕩 rr!s;
(𝕨𝔽) _𝕣_ (𝕨𝔾) 𝕩
}
WithFlipdot {{𝕩.Close @} _defer_ 𝕏 MakeFlipdot 𝕨}

View file

@ -1,6 +1,6 @@
#!/usr/bin/env BQN
MakeFlipdot •Import "../lib/"("bin" ((-)) •path) "blipqn.bqn"
WithFlipdot •Import "../lib/"("bin" ((-)) •path) "blipqn.bqn"
examples >
"random"{𝕩.Send 𝕩.shape •rand.Range 2},
@ -19,11 +19,9 @@ opts ← {
hostportwidthheightexample •args
example
addr host•ParseFloat port
dim •ParseFloat¨ widthheight
cfg host•ParseFloat¨ portwidthheight
}
action examples (() ˜1)< opts.example
flip opts.addr MakeFlipdot opts.dim
Action flip
opts.cfg WithFlipdot action

View file

@ -1,9 +1,9 @@
#define _DEFAULT_SOURCE // see getnameinfo(3), for NI_MAX*
#define _POSIX_C_SOURCE 200112L // see getaddrinfo(3)
#include <assert.h>
#include <limits.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
@ -19,29 +19,53 @@ int resolve_addr(char *host, char *port, struct addrinfo **addrs) {
return getaddrinfo(host, port, &hints, addrs);
}
// Send given bytes[len] to the host:port via UDP, returns 1 if all bytes
// where sent and no locally detectable errors occurred.
int8_t send_to_flipdot(char *host, in_port_t port_number, uint8_t *bitmap,
size_t bitmap_len) {
struct flipdot {
int sockfd;
struct addrinfo *addrs;
};
// Assumes all pointers in struct flipdot are not NULL which should be the case
// for any struct returned by flipdot_open().
void flipdot_close(struct flipdot *flipdot) {
freeaddrinfo(flipdot->addrs);
close(flipdot->sockfd);
free(flipdot);
}
// Returns NULL if some error occurred. Note that errno isn't necessarily set.
struct flipdot *flipdot_open(char *host, in_port_t port_number) {
char port[NI_MAXSERV];
int sockfd = -1;
ssize_t sent = 0;
struct addrinfo *addrs = NULL;
struct flipdot *flipdot = malloc(sizeof(struct flipdot));
if (flipdot == NULL) goto error;
if (snprintf(port, sizeof port, "%d", port_number) < 0) goto error;
memset(flipdot, 0, sizeof(struct flipdot));
flipdot->sockfd = -1;
flipdot->addrs = NULL;
if (resolve_addr(host, port, &addrs) != 0) goto error;
if (snprintf(port, sizeof(port), "%d", port_number) < 0) goto error;
if (resolve_addr(host, port, &flipdot->addrs) != 0) goto error;
sockfd = socket(addrs->ai_family, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) goto error;
flipdot->sockfd = socket(flipdot->addrs->ai_family, SOCK_DGRAM, IPPROTO_UDP);
if (flipdot->sockfd < 0) goto error;
sent =
sendto(sockfd, bitmap, bitmap_len, 0, addrs->ai_addr, addrs->ai_addrlen);
if (sent != (ssize_t)bitmap_len) goto error;
return flipdot;
error:
if (addrs != NULL) freeaddrinfo(addrs);
if (sockfd >= 0) close(sockfd);
if (flipdot != NULL) {
if (flipdot->sockfd >= 0) close(flipdot->sockfd);
if (flipdot->addrs != NULL) freeaddrinfo(flipdot->addrs);
free(flipdot);
}
return NULL;
}
// Send given bytes[len] to the given flipdot, returns 1 if all bytes were sent
// and no locally detectable errors occurred, 0 otherwise.
int8_t flipdot_send(struct flipdot *flipdot, uint8_t *bitmap,
size_t bitmap_len) {
ssize_t sent = sendto(flipdot->sockfd, bitmap, bitmap_len, 0,
flipdot->addrs->ai_addr, flipdot->addrs->ai_addrlen);
return (sent == (ssize_t)bitmap_len);
}