35 KiB
bsdsocket.library — BSD Sockets on AmigaOS: Event Loops, WaitSelect, and Non-Blocking I/O
Overview
bsdsocket.library is the AmigaOS implementation of the BSD socket API, provided by third-party TCP/IP stacks (AmiTCP, Miami, Roadshow). Unlike Unix where sockets are kernel-managed file descriptors accessed via system calls, Amiga sockets live entirely in user space — each task opens its own private library base with isolated socket state. This means no context switches for socket operations, but also no memory protection between the application and the stack.
The defining feature of the Amiga socket API is WaitSelect() — a select() replacement that simultaneously waits on socket I/O and Exec signal bits (window events, timers, ARexx). A single event loop handles network, GUI, and timing without threads — a design that made responsive networked applications possible on a 7 MHz 68000.
Key constraints:
- Per-task
SocketBase— never share between tasks; each task mustOpenLibraryits own copy - Big-endian — all multi-byte network values are Motorola byte order;
htons()/htonl()are no-ops but must still be used for portability - No IPv6 — classic Amiga stacks are IPv4-only
- User-space stack — any crash in the stack process (or a misbehaving app) can corrupt socket state for the entire system
Architecture
Where bsdsocket.library Sits
flowchart TD
subgraph "Application Task"
APP["Your Code\n(main task)"]
SB["SocketBase\n(private per task)"]
APP <-->|"LVO calls\nsocket(), recv()"| SB
end
subgraph "TCP/IP Stack Process"
BSS["bsdsocket.library\n(user-space daemon)"]
TCP["TCP state machine"]
UDP["UDP"]
IP["IP / routing"]
ARP["ARP"]
SANA["SANA-II IORequest layer"]
end
subgraph "Hardware"
NIC["Network Card\n(Ethernet/PPP)"]
end
SB <-->|"IPC / shared mem\n(no syscall)"| BSS
BSS --> TCP --> IP --> ARP --> SANA
BSS --> UDP --> IP
SANA --> NIC
style SB fill:#e8f4fd,stroke:#2196f3,color:#333
style BSS fill:#fff9c4,stroke:#f9a825,color:#333
style SANA fill:#c8e6c9,stroke:#2e7d32,color:#333
The Per-Task Library Base Model
Every task that calls OpenLibrary("bsdsocket.library", ...) receives a distinct library base with its own socket descriptor table, error state, and tag configuration. There is no global fd table in kernel space. This is elegant for cooperative multitasking but fragile:
| Aspect | Unix Kernel Socket | Amiga bsdsocket.library |
|---|---|---|
| API entry | System call (trap) | Library call (JSR through LVO) |
| fd table | Per-process, kernel-managed | Per-opener, in stack process memory |
| Context switch | Yes (user→kernel→user) | No — stays in user space |
| Cross-task sharing | fd inherited on fork | Forbidden — each task needs own SocketBase |
| Protection | Memory-isolated kernel | None — stack runs in user space |
| Crash impact | Kernel panic (rare) | Stack dies, all sockets lost |
Warning
bsdsocket.libraryruns in the same address space as your application. A stray write through a bad pointer can corrupt the TCP state machine. Use Enforcer or a memory-protected emulator when debugging network code.
Per-Task Setup
Opening the Library and Configuring Error Handling
/* proto/socket.h, netinet/in.h — stack-provided headers */
#include <proto/socket.h>
#include <netinet/in.h>
struct Library *SocketBase = NULL;
LONG myErrno = 0;
BOOL initNetwork(void)
{
/* Minimum API version: 4 covers all functions used here */
SocketBase = OpenLibrary("bsdsocket.library", 4);
if (!SocketBase)
{
Printf("No TCP/IP stack running. Start AmiTCP, Miami, or Roadshow.\n");
return FALSE;
}
/* Route errno into our own variable so we can use it like Unix errno */
SocketBaseTags(
SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&myErrno,
SBTM_SETVAL(SBTC_LOGTAGPTR), (ULONG)"MyApp",
TAG_DONE);
return TRUE;
}
void cleanupNetwork(void)
{
if (SocketBase)
{
CloseLibrary(SocketBase);
SocketBase = NULL;
}
}
Caution
Never share
SocketBasebetween tasks. Each task MUSTOpenLibraryits own copy. Sharing causes socket state corruption and random crashes. This is the #1 Amiga networking bug.
Data Structures
Socket Address
/* netinet/in.h */
struct sockaddr_in {
UBYTE sin_len; /* V39+ Roadshow; 0 on older stacks */
UBYTE sin_family; /* AF_INET */
UWORD sin_port; /* Network byte order — use htons() */
struct in_addr sin_addr; /* IP address in network byte order */
UBYTE sin_zero[8]; /* Padding to match struct sockaddr */
};
struct in_addr {
ULONG s_addr; /* Big-endian 32-bit IPv4 address */
};
fd_set and timeval
/* sys/time.h */
struct timeval {
LONG tv_sec; /* seconds */
LONG tv_usec; /* microseconds */
};
/* sys/socket.h */
#define FD_SETSIZE 64
typedef struct fd_set {
ULONG fds_bits[FD_SETSIZE / 32]; /* Bitmap of descriptors */
} fd_set;
#define FD_ZERO(set) /* clear all bits */
#define FD_SET(fd, set) /* set bit for fd */
#define FD_CLR(fd, set) /* clear bit for fd */
#define FD_ISSET(fd, set) /* test bit for fd */
Note
FD_SETSIZEis typically 64 on Amiga stacks. If you need more concurrent sockets, check your stack's headers — Roadshow may support larger values. Thenfdsparameter toWaitSelectis the highest socket number + 1, not the count of sockets.
API Reference
Core Socket Lifecycle
| LVO | Function | Description |
|---|---|---|
| −30 | socket(domain, type, protocol) |
Create a socket descriptor |
| −36 | bind(sock, addr, addrlen) |
Bind to local address/port |
| −42 | listen(sock, backlog) |
Mark socket as passive listener |
| −48 | accept(sock, addr, addrlen) |
Accept incoming connection |
| −54 | connect(sock, addr, addrlen) |
Initiate outgoing connection |
| −174 | CloseSocket(sock) |
Close a socket (NOT close()) |
| −84 | shutdown(sock, how) |
Partially close (send/receive/both) |
Data Transfer
| LVO | Function | Description |
|---|---|---|
| −66 | send(sock, buf, len, flags) |
Send on connected socket |
| −60 | sendto(sock, buf, len, flags, addr, addrlen) |
Send datagram to address |
| −78 | recv(sock, buf, len, flags) |
Receive from connected socket |
| −72 | recvfrom(sock, buf, len, flags, from, fromlen) |
Receive datagram with source |
I/O Multiplexing and Control
| LVO | Function | Description |
|---|---|---|
| −180 | WaitSelect(nfds, rd, wr, ex, timeout, sigmask) |
select() + Exec signal integration |
| −186 | IoctlSocket(d, request, argp) |
Socket control (FIONBIO, FIONREAD, etc.) |
| −90 | setsockopt(...) |
Set socket options |
| −96 | getsockopt(...) |
Get socket options |
Name Resolution
| LVO | Function | Description |
|---|---|---|
| −102 | gethostbyname(name) |
Resolve hostname to IP (blocking) |
| −108 | gethostbyaddr(addr, len, type) |
Reverse DNS lookup |
| −210 | inet_addr(cp) |
Parse dotted-decimal string to in_addr |
| −216 | Inet_NtoA(in) |
Format in_addr to dotted-decimal string |
| −252 | getservbyname(name, proto) |
Resolve service name to port |
Error and Configuration
| LVO | Function | Description |
|---|---|---|
| −168 | Errno() |
Return last error code for this task |
| −270 | SocketBaseTagList(tags) |
Configure per-task behavior |
WaitSelect Deep Dive
WaitSelect() is the single most important function in Amiga network programming. It replaces both Unix select() and the need for threads in most applications.
LONG WaitSelect(LONG nfds,
fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout,
ULONG *sigmask);
The Bidirectional Sigmask
The sigmask parameter is bidirectional — a pattern unique to AmigaOS:
- On entry:
*sigmaskcontains the Exec signal bits you also want to wait for (e.g., window port signal, Ctrl-C, timer signal) - On exit:
*sigmaskcontains which of those signals actually fired
ULONG sigmask = winSig | timerSig | SIGBREAKF_CTRL_C;
LONG result = WaitSelect(maxFd + 1, &readfds, NULL, NULL, &tv, &sigmask);
/* sigmask now tells us WHICH non-socket events occurred */
if (sigmask & winSig) { /* handle window events */ }
if (sigmask & timerSig) { /* handle timer tick */ }
if (sigmask & SIGBREAKF_CTRL_C) { running = FALSE; }
Timeout Behavior and the Reinitialization Requirement
Warning
WaitSelectmay modify thestruct timevalyou pass in, decrementing it by the elapsed time (like POSIXselect). You must reinitializetimeoutbefore every call. Reusing the same struct after a timeout will eventually collapse to zero and cause busy-polling.
/* CORRECT: reinitialize timeout every iteration */
while (running)
{
struct timeval tv;
tv.tv_sec = 1; /* 1 second */
tv.tv_usec = 0;
fd_set rfds = masterReadSet; /* copy, because WaitSelect modifies */
ULONG sigmask = mySignals;
LONG n = WaitSelect(maxFd + 1, &rfds, NULL, NULL, &tv, &sigmask);
/* ... */
}
Return Value Semantics
| Return | Meaning |
|---|---|
> 0 |
Number of socket descriptors ready |
0 |
Timeout expired — no sockets ready, no signals received |
< 0 |
Error — call Errno() to get code (EINTR, EBADF, etc.) |
The fd_set Destruction Rule
Like POSIX select(), WaitSelect modifies the fd_set arguments. Only descriptors that are ready remain set. You must reinitialize or copy your fd_sets on every call.
/* WRONG: reusing the same fd_set without re-init */
FD_SET(sock, &readfds);
while (running) {
WaitSelect(sock + 1, &readfds, NULL, NULL, &tv, &sigmask);
/* After first success, readfds is modified! */
}
/* CORRECT: rebuild or copy each iteration */
while (running) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
}
Event Loop Patterns
Pattern 1: Single Socket + GUI (The Classic)
A responsive client application that handles both network data and window events without threads:
ULONG winSig = 1L << window->UserPort->mp_SigBit;
ULONG ctrlSig = SIGBREAKF_CTRL_C;
BOOL running = TRUE;
while (running)
{
struct timeval tv = { 1, 0 }; /* 1 second timeout */
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
ULONG sigmask = winSig | ctrlSig;
LONG n = WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
if (n > 0 && FD_ISSET(sock, &rfds))
{
char buf[4096];
LONG got = recv(sock, buf, sizeof(buf) - 1, 0);
if (got > 0)
{
buf[got] = '\0';
/* Process network data */
}
else if (got == 0)
{
/* Peer closed connection */
running = FALSE;
}
else /* got < 0 */
{
LONG err = Errno();
if (err != EINTR)
{
Printf("recv error: %ld\n", err);
running = FALSE;
}
}
}
if (sigmask & winSig)
{
struct IntuiMessage *imsg;
while ((imsg = (struct IntuiMessage *)GetMsg(window->UserPort)))
{
switch (imsg->Class)
{
case IDCMP_CLOSEWINDOW:
running = FALSE;
break;
/* ... other IDCMP classes ... */
}
ReplyMsg((struct Message *)imsg);
}
}
if (sigmask & ctrlSig)
{
Printf("*** Break\n");
running = FALSE;
}
}
Pattern 2: Multi-Socket Server with Dynamic Clients
A TCP server that accepts new connections and monitors all clients in one loop:
#define MAX_CLIENTS 16
struct Client {
LONG sock;
BOOL active;
} clients[MAX_CLIENTS] = {0};
LONG listenSock;
ULONG winSig, ctrlSig;
BOOL running = TRUE;
while (running)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listenSock, &rfds);
LONG maxFd = listenSock;
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (clients[i].active)
{
FD_SET(clients[i].sock, &rfds);
if (clients[i].sock > maxFd) maxFd = clients[i].sock;
}
}
struct timeval tv = { 0, 500000 }; /* 500ms */
ULONG sigmask = winSig | ctrlSig;
LONG n = WaitSelect(maxFd + 1, &rfds, NULL, NULL, &tv, &sigmask);
/* Accept new connection? */
if (n > 0 && FD_ISSET(listenSock, &rfds))
{
struct sockaddr_in clientAddr;
LONG addrLen = sizeof(clientAddr);
LONG newSock = accept(listenSock,
(struct sockaddr *)&clientAddr, &addrLen);
if (newSock >= 0)
{
int added = 0;
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (!clients[i].active)
{
clients[i].sock = newSock;
clients[i].active = TRUE;
added = 1;
break;
}
}
if (!added)
{
Printf("Server full, dropping connection\n");
CloseSocket(newSock);
}
}
}
/* Check client sockets */
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (clients[i].active && FD_ISSET(clients[i].sock, &rfds))
{
char buf[1024];
LONG got = recv(clients[i].sock, buf, sizeof(buf), 0);
if (got > 0)
{
/* Echo back */
send(clients[i].sock, buf, got, 0);
}
else
{
/* Disconnect or error */
CloseSocket(clients[i].sock);
clients[i].active = FALSE;
}
}
}
/* Handle window events / Ctrl-C ... */
}
Pattern 3: Three-Source Loop (Socket + Timer + GUI)
Add a timer.device IORequest for periodic tasks (keepalive pings, timeouts, animation frames):
/* Setup timer device (see timer.md for full details) */
struct timerequest *tr;
struct MsgPort *timerPort = CreateMsgPort();
/* OpenDevice(TIMERNAME, UNIT_MICROHZ, (struct IORequest *)tr, 0) ... */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG winSig = 1L << window->UserPort->mp_SigBit;
while (running)
{
/* Re-queue timer for next tick */
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = 16667; /* ~60 Hz */
SendIO((struct IORequest *)tr);
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
struct timeval tv = { 5, 0 }; /* 5 second safety timeout */
ULONG sigmask = winSig | timerSig | SIGBREAKF_CTRL_C;
LONG n = WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
if (n > 0 && FD_ISSET(sock, &rfds))
{
/* Handle socket data */
}
if (sigmask & timerSig)
{
WaitIO((struct IORequest *)tr); /* reclaim IORequest */
/* 60 Hz tick: update UI, send keepalive, etc. */
}
if (sigmask & winSig) { /* handle IDCMP */ }
}
Non-Blocking Async I/O
Setting Non-Blocking Mode
Use IoctlSocket() with FIONBIO to enable non-blocking I/O:
ULONG on = 1;
if (IoctlSocket(sock, FIONBIO, (char *)&on) < 0)
{
Printf("IoctlSocket(FIONBIO) failed: %ld\n", Errno());
}
Note
Non-blocking UDP sockets work reliably on all stacks. Non-blocking TCP is supported by Miami and Roadshow; early AmiTCP versions had issues with non-blocking TCP
connect(). Test on your target stack.
The Non-Blocking Connect Pattern
A blocking connect() can hang for minutes on an unreachable host. The non-blocking pattern:
/* 1. Create non-blocking socket */
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
ULONG on = 1;
IoctlSocket(sock, FIONBIO, (char *)&on);
/* 2. Initiate connection (will likely return EINPROGRESS) */
LONG rc = connect(sock, (struct sockaddr *)&sa, sizeof(sa));
if (rc < 0 && Errno() != EINPROGRESS)
{
Printf("connect failed immediately: %ld\n", Errno());
CloseSocket(sock);
return;
}
/* 3. Wait for writable with timeout */
fd_set wfds;
struct timeval tv = { 10, 0 }; /* 10 second timeout */
FD_ZERO(&wfds);
FD_SET(sock, &wfds);
rc = WaitSelect(sock + 1, NULL, &wfds, NULL, &tv, NULL);
if (rc > 0 && FD_ISSET(sock, &wfds))
{
/* 4. Check SO_ERROR to confirm success */
LONG so_err = 0;
LONG optlen = sizeof(so_err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&so_err, &optlen);
if (so_err == 0)
{
Printf("Connected!\n");
/* Optionally restore blocking mode */
ULONG off = 0;
IoctlSocket(sock, FIONBIO, (char *)&off);
}
else
{
Printf("Connection failed: %ld\n", so_err);
CloseSocket(sock);
}
}
else if (rc == 0)
{
Printf("Connection timeout\n");
CloseSocket(sock);
}
Handling EWOULDBLOCK
With non-blocking sockets, send() and recv() return -1 with EWOULDBLOCK (or EAGAIN) when the operation would block:
LONG n = recv(sock, buf, sizeof(buf), 0);
if (n < 0)
{
LONG err = Errno();
if (err == EWOULDBLOCK || err == EAGAIN)
{
/* No data ready — normal for non-blocking */
/* Will be picked up by next WaitSelect cycle */
}
else
{
/* Real error */
}
}
DNS Resolution
gethostbyname() is synchronous and blocking — it does not return until the DNS response arrives (or times out). On Amiga this typically takes 2–5 seconds per failed query.
struct hostent *he = gethostbyname("www.amiga.org");
if (he)
{
struct in_addr addr;
CopyMem(he->h_addr, &addr, sizeof(addr));
Printf("Host: %s IP: %s\n", he->h_name, Inet_NtoA(addr.s_addr));
/* Multiple addresses are common for load balancing */
char **p;
for (p = he->h_addr_list; *p; p++)
{
CopyMem(*p, &addr, sizeof(addr));
Printf(" Addr: %s\n", Inet_NtoA(addr.s_addr));
}
}
else
{
/* gethostbyname does not set errno — use h_errno equivalent
* On Amiga, check Errno() anyway; some stacks set it.
*/
Printf("DNS lookup failed (errno=%ld)\n", Errno());
}
Warning
gethostbyname()returns a pointer to a static buffer owned by the stack. The result is valid only until the next call togethostbyname()orgethostbyaddr()in the same task. Copy the address data before calling again.
Decision Guides
Blocking vs Non-Blocking vs WaitSelect
| Approach | When to Use | Caveats |
|---|---|---|
| Blocking | Simple scripts, single-connection tools | GUI freezes; no signal handling during I/O |
| Non-blocking + WaitSelect | GUI apps, servers, multi-socket code | More complex; must handle EWOULDBLOCK |
| WaitSelect only (blocking sockets) | Most Amiga applications | Simpler logic; WaitSelect tells you when data is ready |
Do I Need Threads?
Almost never on AmigaOS. WaitSelect + signals provides the same concurrency model as select() + event loop on Unix. Threads add complexity in a non-memory-protected OS where a crash in any task brings down the system.
| Scenario | Amiga Approach |
|---|---|
| Handle GUI while downloading | WaitSelect on socket + window port |
| Timeout a slow connect | Non-blocking connect + WaitSelect on writefds |
| Server with many clients | Single loop monitoring all sockets in readfds |
| Background file transfer | Separate task with its own SocketBase, communicate via MsgPort |
Historical Context & Modern Analogies
Why Amiga Went User-Space
When the Amiga launched in 1985, TCP/IP was an academic curiosity. By the early 1990s, Commodore had added no networking to the OS — the bsdsocket.library model emerged from third-party developers (AmiTCP, 1991) who had to work within the existing Exec architecture. Running the stack as a user-space process was not a design choice but a necessity: there was no kernel to extend.
Surprisingly, this had advantages:
- No syscall overhead — socket calls are simple JSR instructions
- Stack swapability — users could switch between AmiTCP, Miami, and Roadshow without rebooting (in some cases)
- No kernel panics — a stack crash doesn't bring down the OS (though it kills all network state)
And disadvantages:
- Zero protection — any program can corrupt the stack's memory
- No kernel buffer cache — data copies between stack and app add overhead
- Single-address-space fragility — the 68000 has no MMU on most models
Competitive Landscape (1992–1996)
| Platform | Networking Model | Socket API |
|---|---|---|
| Amiga | User-space stack (bsdsocket) | BSD sockets via library |
| Atari ST | No standard; STiNG / MiNT later | MiNT had BSD sockets in kernel |
| Macintosh | MacTCP (user-space) | MacTCP API (not BSD) |
| DOS | Trumpet/WATTCP packet drivers | BSD-like but fragmented |
| Windows 3.1 | Winsock 1.1 (user DLL) | BSD-compatible |
| Linux 1.0 | Kernel TCP/IP | True BSD sockets |
The Amiga's approach was most similar to MacTCP and Winsock — both user-space networking layers — but unlike those, AmigaOS had no OS vendor providing the stack. Third-party competition drove rapid innovation (Miami's GUI, Roadshow's stability) but also fragmentation.
Modern Analogies
| Amiga Concept | Modern Equivalent | Analogy Strength |
|---|---|---|
WaitSelect + signals |
epoll/kqueue + event loop |
Strong — single-threaded multiplexing |
bsdsocket.library |
libuv, libevent |
Moderate — both user-space, but libuv is a library not a daemon |
Per-task SocketBase |
Thread-local storage | Weak — TLS is per-thread, SocketBase is per-opener |
| SANA-II device | NDIS / Linux net_device | Strong — standardized driver interface |
| User-space stack | DPDK, netmap | Moderate — both bypass kernel, but DPDK is for performance |
The key insight for modern developers: Amiga network programming feels like writing an epoll-based server in C on Linux, except there's no kernel boundary. The mental model of "register descriptors with a multiplexer, then dispatch events" translates directly.
Practical Examples
Complete Non-Blocking HTTP Client with Timeout
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
struct Library *SocketBase = NULL;
LONG myErrno = 0;
LONG httpGet(const char *host, UWORD port, const char *path)
{
LONG sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return -1;
/* Non-blocking connect with 10-second timeout */
ULONG nb = 1;
IoctlSocket(sock, FIONBIO, (char *)&nb);
struct hostent *he = gethostbyname(host);
if (!he) { CloseSocket(sock); return -1; }
struct sockaddr_in sa = {0};
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
CopyMem(he->h_addr, &sa.sin_addr, he->h_length);
LONG rc = connect(sock, (struct sockaddr *)&sa, sizeof(sa));
if (rc < 0 && myErrno != EINPROGRESS)
{
CloseSocket(sock);
return -1;
}
/* Wait for connection */
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(sock, &wfds);
struct timeval tv = { 10, 0 };
rc = WaitSelect(sock + 1, NULL, &wfds, NULL, &tv, NULL);
if (rc <= 0 || !FD_ISSET(sock, &wfds))
{
CloseSocket(sock);
return -1;
}
LONG so_err = 0;
LONG optlen = sizeof(so_err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&so_err, &optlen);
if (so_err != 0) { CloseSocket(sock); return -1; }
/* Restore blocking for simple send/recv */
nb = 0;
IoctlSocket(sock, FIONBIO, (char *)&nb);
/* Send request */
char req[256];
snprintf(req, sizeof(req),
"GET %s HTTP/1.0\r\nHost: %s\r\n\r\n", path, host);
send(sock, req, strlen(req), 0);
/* Receive response */
char buf[4096];
LONG total = 0;
LONG n;
while ((n = recv(sock, buf, sizeof(buf) - 1, 0)) > 0)
{
buf[n] = '\0';
Printf("%s", buf);
total += n;
}
CloseSocket(sock);
return total;
}
int main(void)
{
SocketBase = OpenLibrary("bsdsocket.library", 4);
if (!SocketBase) {
Printf("No TCP/IP stack\n");
return 20;
}
SocketBaseTags(
SBTM_SETVAL(SBTC_ERRNOPTR(sizeof(LONG))), (ULONG)&myErrno,
TAG_DONE);
httpGet("www.example.com", 80, "/");
CloseLibrary(SocketBase);
return 0;
}
When to Use / When NOT to Use
When to Use bsdsocket.library
- Any Amiga application that communicates over TCP/IP or UDP
- GUI applications that must remain responsive during network I/O —
WaitSelectmakes this natural - Servers handling up to a few dozen concurrent connections — the single-event-loop model scales well here
- Applications integrating network I/O with IDCMP, ARexx, or timer events
When NOT to Use bsdsocket.library
- High-throughput file transfers — the user-space stack with 680x0 CPU copying can bottleneck below 1 MB/s even on Fast Ethernet. Consider SANA-II direct frame access for custom protocols.
- Real-time streaming —
WaitSelecttimer granularity and stack latency introduce jitter. For 50/60 Hz synchronized data, use UDP with carefully sized buffers or bypass TCP entirely. - IPv6 networks — classic Amiga stacks do not support IPv6. For dual-stack environments, use a NAT64 gateway or run a modern TCP/IP stack under emulation.
- Memory-constrained 512 KB systems — the TCP/IP stack itself consumes 100–300 KB. On unexpanded A500 systems, networking is impractical.
Best Practices & Antipatterns
Best Practices
- Always open
bsdsocket.libraryat version 4 or higher for full API coverage - Always configure
SBTC_ERRNOPTRso you can read errors like Unixerrno - Always use
CloseSocket(), never AmigaDOSClose()— sockets are not file handles - Always reinitialize
struct timevalbefore eachWaitSelectcall - Always copy
fd_setstructures before passing toWaitSelect; the function modifies them - Always check
SO_ERRORafter a non-blockingconnectsucceeds inwritefds - Always call
CloseSocket()on every socket beforeCloseLibrary(SocketBase) - Copy
gethostbyname()results immediately — the returned pointer is to static stack memory - Use
SIGBREAKF_CTRL_Cin yourWaitSelectsigmask for clean shutdown - Test on your target stack (AmiTCP vs Miami vs Roadshow) — behavior varies subtly
Named Antipatterns
"The Shared SocketBase" — Passing SocketBase Between Tasks
/* BAD: Task A opens, Task B uses */
struct Library *SocketBase; /* global */
/* Task A */
SocketBase = OpenLibrary("bsdsocket.library", 4);
/* Task B (separate task!) */
socket(AF_INET, SOCK_STREAM, 0); /* Crash or silent corruption */
/* CORRECT: Each task opens its own */
/* Task A */
struct Library *sbA = OpenLibrary("bsdsocket.library", 4);
/* Task B */
struct Library *sbB = OpenLibrary("bsdsocket.library", 4);
"The Stale fd_set" — Reusing fd_set After WaitSelect
/* BAD */
FD_SET(sock, &readfds);
while (running) {
WaitSelect(sock + 1, &readfds, NULL, NULL, &tv, &sigmask);
/* After first iteration, readfds is destroyed! */
}
/* CORRECT */
while (running) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
WaitSelect(sock + 1, &rfds, NULL, NULL, &tv, &sigmask);
}
"The select() Refugee" — Using close() Instead of CloseSocket()
/* BAD: Unix habit */
close(sock); /* Closes an AmigaDOS file handle, not the socket! */
/* CORRECT */
CloseSocket(sock);
"The Forgotten Timeout Reset" — Collapsing tv to Zero
/* BAD */
struct timeval tv = { 1, 0 };
while (running) {
WaitSelect(..., &tv, ...); /* tv becomes {0, 0} after first timeout */
/* Now busy-polls at 100% CPU! */
}
/* CORRECT */
while (running) {
struct timeval tv = { 1, 0 }; /* fresh each iteration */
WaitSelect(..., &tv, ...);
}
"The Phantom Hostent" — Keeping gethostbyname Result Past Next Call
/* BAD */
struct hostent *he = gethostbyname("foo.com");
/* ... later ... */
struct hostent *he2 = gethostbyname("bar.com");
/* he->h_addr is now invalid — may point to bar.com's data! */
CopyMem(he->h_addr, &addr, sizeof(addr));
/* CORRECT */
struct hostent *he = gethostbyname("foo.com");
struct in_addr addr;
CopyMem(he->h_addr, &addr, sizeof(addr)); /* copy immediately */
/* now safe to call gethostbyname again */
Pitfalls & Common Mistakes
1. Per-Task SocketBase Sharing
Symptom: Random crashes, sockets returning invalid data, Errno() returning garbage.
Cause: Two tasks share one SocketBase. The stack maintains per-opener state; when two tasks interleave calls, descriptor tables get corrupted.
Fix: Every task that does socket I/O must call OpenLibrary("bsdsocket.library", ...) independently. Use message ports, not shared library bases, for inter-task communication.
2. WaitSelect Timeout Not Reinitialized
Symptom: Application starts responsive, then after a few seconds CPU usage spikes to 100%.
Cause: POSIX select() and Amiga WaitSelect() may modify the timeout struct to show remaining time. Reusing it causes the timeout to shrink to zero, creating a busy loop.
Fix: Declare struct timeval inside the loop or reassign before each call.
3. Confusing Sigmask Directionality
Symptom: Signals are never detected, or WaitSelect returns immediately with spurious signals.
Cause: Forgetting that sigmask is both input and output. If you don't reinitialize it, stale output bits from the previous call leak into the next wait set.
Fix:
ULONG sigmask = winSig | timerSig; /* always reinitialize */
WaitSelect(..., &sigmask);
4. Big-Endian Blindness
Symptom: connect() fails with EADDRNOTAVAIL or binds to wrong port.
Cause: The 68000 is big-endian. Port numbers and IP addresses must be in network byte order. htons(80) is 0x0050 on Amiga (same as big-endian network order), but writing sa.sin_port = 80 produces 0x5000 which is port 20480.
Fix: Always use htons() / htonl() / ntohs() / ntohl(). They compile to no-ops on big-endian but make the intent explicit and keep the code portable.
5. Stack-Specific Behavior Differences
Symptom: Code works on Miami but fails on AmiTCP, or vice versa.
Cause: Early AmiTCP (v3) lacks some v4 APIs, handles non-blocking TCP differently, and has a smaller fd_set size. Roadshow has the most complete implementation but may behave differently with gethostbyname timeouts.
Fix: Open the library at the API version you need (OpenLibrary(..., 4)). Document your minimum supported stack. Test on all target configurations.
Use Cases
Real-World Software
| Software | bsdsocket Pattern | Notes |
|---|---|---|
| IBrowse | WaitSelect + IDCMP |
Single-threaded; tabs share one event loop |
| AmiFTP | Blocking + WaitSelect fallback |
Data channel uses WaitSelect; control channel often blocking |
| AmIRC | Multi-socket WaitSelect |
Server connection + DCC sends monitored together |
| Voyager | Non-blocking + WaitSelect |
Heavy use of IoctlSocket(FIONBIO) for parallel HTTP requests |
| Genesis | Stack-provided utilities | ping, traceroute, telnet use blocking sockets |
| MiamiDX | WaitSelect + ARexx |
GUI and scriptable via ARexx port in same event loop |
Integration Patterns
- HTTP client: Non-blocking
connect→WaitSelect→ blocking send/recv for simplicity - Chat client:
WaitSelecton server socket + window port + ARexx port - File server: Listener in
readfds; accepted sockets added to master set - Game netcode: UDP
recvfrominWaitSelectloop at 60 Hz with timer.device
Performance
Rough Benchmarks (68030/50 MHz, Fast RAM)
| Operation | Approximate Time | Notes |
|---|---|---|
socket() + bind() |
< 1 ms | User-space, no syscall |
connect() localhost |
1–3 ms | Stack loopback |
connect() LAN host |
5–20 ms | ARP + TCP handshake |
connect() WAN host |
50–300 ms | RTT dependent |
send() 1 KB |
0.1–0.5 ms | Memory copy into stack buffers |
recv() 1 KB |
0.1–0.5 ms | Copy from stack to app buffer |
WaitSelect wake |
< 1 ms | Signal-based, no polling |
gethostbyname() cache hit |
< 1 ms | Miami/Roadshow cache |
gethostbyname() cache miss |
2000–5000 ms | UDP DNS timeout on failure |
| Max throughput (68030/50) | ~300–500 KB/s | CPU-bound on copy + TCP stack processing |
| Max throughput (68060) | ~1–2 MB/s | Fast RAM, optimized stack (Roadshow) |
Bottlenecks
- CPU copy overhead — every
send()/recv()copies data between app and stack buffers - Stack processing — TCP checksums, segmentation, reassembly on 680x0
- SANA-II driver — Some drivers are polled; interrupt-driven drivers (X-Surf 100) perform better
- Chip RAM contention — if stack or driver buffers are in Chip RAM, Blitter/Audio/DMA steal cycles
FAQ
Q: Can I use select() instead of WaitSelect()?
A: No — Amiga stacks do not provide POSIX select(). WaitSelect is the only multiplexing primitive. It is a superset of select() (adds signal support).
Q: How many sockets can I monitor?
A: Typically 64 (FD_SETSIZE). Check your stack's sys/socket.h. For more, some stacks support larger fd_sets at compile time, or you can use multiple tasks each with their own socket set.
Q: Do I need htons() on a big-endian CPU?
A: Yes — it compiles to a no-op, but it documents intent and keeps code portable to emulators or future ports.
Q: Can I mix bsdsocket I/O with DOS file I/O in the same WaitSelect?
A: No — AmigaDOS file handles are not socket descriptors and cannot be passed to WaitSelect. Use WaitSelect for sockets and a separate signal-based mechanism for async DOS I/O, or use a background DOS task with a MsgPort.
Q: Why does my non-blocking TCP connect() return EINPROGRESS then immediately fail?
A: Some early AmiTCP versions do not support non-blocking TCP connect. Use Miami or Roadshow for reliable non-blocking behavior.
Q: How do I set socket send/receive buffer sizes?
A: setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)) and SO_RCVBUF. Not all stacks honor large values; test your target.
Q: Is Errno() thread-safe?
A: On AmigaOS "thread-safe" is less meaningful because there are no threads in the POSIX sense — only tasks. Errno() returns the error for the current task. Setting SBTC_ERRNOPTR routes errors to a per-task variable, which is task-safe.
References
- Roadshow SDK documentation: http://roadshow.apc-tcp.de/
- AmiTCP SDK: Aminet
comm/tcp/AmiTCP-SDK-4.3.lha - Genesis (free AmiTCP fork): Aminet
comm/tcp/Genesis.lha - SANA-II specification: Aminet
docs/hard/sana2.lha - NDK 3.9
proto/socket.h,netinet/in.h,sys/socket.h,sys/time.h - See also: tcp_ip_stacks.md — stack architecture and configuration
- See also: protocols.md — DNS, TCP, UDP working examples
- See also: sana2.md — SANA-II driver layer below the stack
- See also: signals.md — Exec signal fundamentals
- See also: message_ports.md — MsgPort and message passing