* docs: fully-expandable generated sidebar, replacing index.md link pages
Rework docs navigation so the sidebar expands to every page of every
reference tree, instead of terminating at generated index.md link lists:
- New docs/gen_sidebar.py (replaces gen_index.py + update_index.sh):
walks efun/, apply/, stdlib/, concepts/, driver/, cli/ and zh-CN/ and
emits sidebars.generated.json — a full Docusaurus category tree per
directory. Category landing pages are now `generated-index` card pages
(title/description/slug), so all generated index.md files are deleted.
--check mode verifies freshness; new .github/workflows/docs-sidebar.yml
runs it in CI.
- New docs/sidebar_meta.json holds curated presentation: category labels,
one-line descriptions (shown on the landing cards), explicit ordering
(driver/cli/concepts read top-down from user-facing to internals) and
per-page label overrides.
- sidebars.ts becomes a hand-authored skeleton (Getting Started, lpc/,
Historical) that splices in the generated trees.
Content reorganization (from a docs-wide review):
- Move misplaced efun pages out of efun/general: terminal/protocol efuns
(act_mxp, send_zmp, request_term_*) to interactive/, debugging efuns
(check_memory, dump_*, clear_debug_level, destructed_objects) to
internals/, shallow_inherit_list to system/.
- Delete stub duplicates superseded by complete pages elsewhere:
general/parse_{add_synonym,dump,my_rules,remove}, contrib/{shuffle,
element_of}.
Modernize key pages with MDX:
- index.mdx: landing page with a card grid linking each doc section.
- build.mdx: per-platform <Tabs> (Ubuntu/macOS/Windows/Alpine+Docker),
admonitions, VitePress [[toc]] leftover removed, stale per-platform CI
workflow links updated to the unified ci.yml.
- ffi-plan.md: GitHub-style [!CAUTION] alert converted to an admonition.
`npm run build` passes clean (onBrokenLinks: throw, no warnings).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om
* docs: serve the Chinese corpus through Docusaurus i18n
Move the zh-CN/ directory out of the default docs tree and into a proper
Docusaurus locale (i18n/zh-CN/docusaurus-plugin-content-docs/current/):
- The flat zh-CN/efun/ directory (333 pages) is re-homed to mirror the
categorized English layout (name-matched 1:1; `hash` maps to strings/
per its own frontmatter). apply/ pages map 1:1; the stray English-text
zh-CN/apply/master/view_errors.md documents a MudOS-era apply that no
longer exists in the driver and is dropped; stdlib/db/database_zh.md
becomes the i18n translation of stdlib/db/database.md; the Chinese
build guide becomes the translation of build.mdx.
- Untranslated pages automatically fall back to English content under
/zh-CN/, so the whole site is navigable in either locale from the new
navbar locale dropdown.
- Both locales share one sidebar. Generated sidebar items now carry
stable `key` fields (the directory/doc path) so translation keys are
unique (both efun/ and stdlib/ have an "Arrays" category, crypto and
strings both document `hash`). Category labels, generated-index
titles/descriptions, navbar and footer are translated in
i18n/zh-CN/...; theme UI strings come from Docusaurus' bundled
zh-Hans translations. Translated landing page at /zh-CN/.
- The "中文文档" sidebar section, the zh-CN tree in gen_sidebar.py /
sidebar_meta.json, and its slice of sidebars.generated.json are gone.
- Relative .md-file links on pages that render in both locales break
the localized build (the file->permalink map points at the localized
copy), so concepts/, the two socket_*_option pages and the config.md
generator now emit extension-less route links instead.
- zh interactive.md/objects.md get explicit slugs like their English
counterparts (a doc named after its parent directory is otherwise a
Docusaurus category-index doc, colliding with the generated-index
route).
`npm run build` builds both locales clean (onBrokenLinks: throw).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174GM2azvAHBmvyESwxm5om
---------
Co-authored-by: Claude <noreply@anthropic.com>
11 KiB
| title |
|---|
| general / socket_efuns |
socket_efuns
LPC sockets let an object open network connections directly from LPC, using the
socket_* efuns. They make it possible to write network services — telnet
bridges, inter-mud communication, external clients, and so on — entirely in the
mudlib.
Include the mode and option constants with #include <socket.h>, and the error
codes with #include <socket_err.h>.
Socket I/O is asynchronous: efuns like socket_connect() and
socket_write() return immediately, and the driver later invokes callback
functions on your object when network events occur (data arrives, the socket is
writable, the connection closes). You register these callbacks when you create,
connect, or accept a socket.
Socket modes
socket_create() takes a mode that determines the transport and how data is
framed:
| Mode | Value | Transport | Data |
|---|---|---|---|
MUD |
0 | TCP | Any LPC value except objects (arrays, mappings, etc.), serialized in the driver's save/restore format |
STREAM |
1 | TCP | Raw string data |
DATAGRAM |
2 | UDP | Raw string data |
STREAM_BINARY |
3 | TCP | Binary data as buffers |
DATAGRAM_BINARY |
4 | UDP | Binary data as buffers |
STREAM_TLS |
5 | TLS over TCP | Raw string data, encrypted |
STREAM_TLS_BINARY |
6 | TLS over TCP | Binary data, encrypted |
MUD mode is STREAM with automatic serialization of LPC values; it is more
convenient but slower and heavier than STREAM, so use it only when you need to
exchange structured data. TCP modes (MUD, STREAM, the TLS modes) are
reliable and ordered. DATAGRAM (UDP) is connectionless and unreliable — a
datagram may be silently lost with no error reported — so it is only appropriate
when occasional loss is acceptable.
Note that with STREAM, a message may arrive split across several read
callbacks (in order); the receiver must be prepared to reassemble it.
Serialization in MUD mode
MUD mode is the only mode that transfers structured LPC values, and how it does
so matters. It serializes the value with the driver's own save/restore encoding —
the same textual format produced by save_object()/restore_object() — and puts
a 4-byte length prefix in front of it; the receiving side decodes that back into
an LPC value before invoking the read callback. Arrays, mappings, and nested
combinations are supported (up to a nesting depth of 100). Objects cannot be sent
and yield EETYPENOTSUPP.
Because this encoding is specific to the LP driver family, MUD mode is really meant for communication between two LP MUDs that both understand it (historically, the inter-mud services that shipped with MudOS). A peer that is not an LP driver receives an opaque, length-prefixed blob and would have to implement a compatible decoder to use it — there is no generic adapter built in.
The other modes do not serialize structured data. STREAM and DATAGRAM
put strings on the wire as raw bytes; the *_BINARY modes send buffers as raw
bytes. Do not expect to hand an array to a STREAM socket and have it arrive as
an array: at best you get a raw, non-portable byte dump of its numeric elements,
and mappings are rejected outright. If you need to move structured data between
muds, use MUD mode; if you are speaking a byte- or line-oriented protocol
(telnet, HTTP, a custom text protocol), use STREAM and handle framing and
encoding yourself.
Return values and errors
Every socket efun returns a status. EESUCCESS (1) means success; any negative
value is an error or warning. socket_error() converts an error code to a
human-readable string. The codes are defined in <socket_err.h>; common ones
include EEADDRINUSE (port already bound), EEFDRANGE/EEBADF (bad
descriptor), EESECURITY (a security check failed), and the flow-control codes
EECALLBACK, EEWOULDBLOCK, and EEALREADY described below.
socket_create() and socket_accept() are the exceptions: on success they
return a non-negative socket descriptor rather than EESUCCESS.
Creating a socket
socket_create(int mode, string|function read_callback, void|string|function close_callback) returns a socket descriptor (>= 0) or a negative error.
Sockets are a finite resource, ultimately bounded by the process's file-descriptor limit, so always close sockets you are finished with. When an object is destructed, its sockets are closed automatically. Each open socket has a unique descriptor; a common idiom is to use it as a key into a mapping of per-socket state.
Client/server model
Connection-oriented modes use the client/server model. The server creates a socket, binds it to a well-known port, and listens for connections. The client creates a socket and connects to that port. A port is an integer from 1 to 65535; ports below 1024 are typically reserved, so mudlib services usually use 1024–65535.
Server: bind, listen, accept
#include <socket.h>
#include <socket_err.h>
int listen_fd;
void create() {
listen_fd = socket_create(STREAM, "read_callback", "close_callback");
if (listen_fd < 0) {
write("socket_create: " + socket_error(listen_fd) + "\n");
return;
}
int err = socket_bind(listen_fd, 12345);
if (err != EESUCCESS) {
write("socket_bind: " + socket_error(err) + "\n");
socket_close(listen_fd);
return;
}
err = socket_listen(listen_fd, "listen_callback");
if (err != EESUCCESS) {
write("socket_listen: " + socket_error(err) + "\n");
socket_close(listen_fd);
}
}
// Called when a client connects. Accept it to obtain a new socket
// dedicated to that connection; the listening socket keeps listening.
void listen_callback(int fd) {
int ns = socket_accept(fd, "read_callback", "write_callback");
if (ns < 0)
write("socket_accept: " + socket_error(ns) + "\n");
}
void read_callback(int fd, mixed data) {
socket_write(fd, "You said: " + data);
}
void write_callback(int fd) {
// The socket is ready to accept more data (see Flow control).
}
void close_callback(int fd) {
// The peer closed the connection.
}
socket_bind()'s port may be 0, which asks the system to pick any free port —
useful for clients, which do not care which local port they use. Binding a port
already in use fails with EEADDRINUSE.
socket_accept() returns a new descriptor for the accepted connection; the
listening socket is used only to accept, never to transfer data.
Client: connect
The connection target is a single string of "address port". socket_connect()
returns immediately; the outcome arrives later via a callback.
#include <socket.h>
#include <socket_err.h>
int fd;
void create() {
fd = socket_create(STREAM, "read_callback", "close_callback");
if (fd < 0) {
write("socket_create: " + socket_error(fd) + "\n");
return;
}
int err = socket_connect(fd, "138.96.19.14 12345",
"read_callback", "write_callback");
if (err != EESUCCESS) {
write("socket_connect: " + socket_error(err) + "\n");
socket_close(fd);
}
}
void write_callback(int fd) {
socket_write(fd, "hello");
}
void read_callback(int fd, mixed data) {
write("Received: " + data + "\n");
}
If socket_connect() returns EESUCCESS, exactly one of three things will
happen later: the read callback fires (data arrived), the write callback fires
(the connection is up and writable), or the close callback fires (the connection
failed or was refused). If it returns an error, no callback will fire.
Callbacks
Callbacks may be given as function names (strings) or function pointers. Their signatures are:
void read_callback(int fd, mixed data); // STREAM / MUD / TLS
void read_callback(int fd, mixed data, string addr); // DATAGRAM modes
void listen_callback(int fd);
void write_callback(int fd);
void close_callback(int fd);
For MUD mode, data may be any LPC type that was sent; it is the receiver's
responsibility to validate it. For DATAGRAM modes, the sender's address is
passed as a third argument.
Flow control
A computer can generate data far faster than a network can send it, so each socket has a limited send buffer. When it fills, the socket is flow controlled and you must stop writing until it drains. The rule is simple: after a write reports the buffer is full, wait for the write callback before sending more.
socket_write() communicates this through its return value:
EESUCCESS— the data was sent (or buffered with room to spare); you may keep writing.EECALLBACK— the data was buffered but the buffer is now full; stop writing until the write callback fires.EEWOULDBLOCK— the data was not buffered; the write callback must fire before you retry. Prefer acall_out()to retry, giving the system a chance to recover.EEALREADY— you wrote while already flow controlled; the data was not buffered. A correctly written application should never see this.
A client is flow controlled until its first write callback, so it must wait for that callback before its first write. A server may write as soon as it has accepted the connection.
Security
Socket use is gated by two mechanisms.
First, the master object's
valid_socket() apply is consulted for
every socket operation. It receives the calling object, the operation name, and
an info array ({ fd, owner, address, port }), and returns 1 to allow or 0 to
deny. If the apply does not exist, access is denied. A permissive
implementation looks like:
int valid_socket(object caller, string operation, mixed *info) {
return 1;
}
Second, each socket is owned by the object that created it. Socket efuns
compare the caller against the owner and abort if they differ, so one object
cannot operate on another's sockets. A violation of either check returns
EESECURITY. Ownership can be transferred deliberately with socket_release()
and socket_acquire().