* docs: rewrite FFI reference around buffer features, add worked libc examples
Update docs/driver/ffi-plan.md now that buffers carry the full byte
toolkit (string/int-array promotion, byte lvalues, range assignment,
foreach, concatenation):
- New "The buffer type does the heavy lifting" section mapping each
buffer language feature to its FFI use.
- The C-string idiom is now the two-line promotion form
(buffer b = s; b += ({ 0 })) instead of chained string_encode()
calls; string_encode stays documented for non-UTF-8 encodings, and
the doc notes that promotion never reaches inside ffi_call's args
array (the byte boundary stays explicit).
- New "Worked examples -- calling libc" section: scalars (sqrt/pow/abs),
strings in (strlen, incl. range assignment into an allocation),
strings out (getenv + ffi_peek(addr, -1) + string_decode with a NULL
check), out-parameters (frexp), structs (time/localtime via
ffi_struct_layout), callbacks (qsort with an LPC comparator), and
foreach over peeked bytes.
- Sync the reference with the implementation: add ffi_address() (was
missing from the efun listing), correct ffi_free() (zeroes bytes; GC
reclaims), ffi_peek(-1) (NUL-terminated read capped at the max
buffer size config), FFI_POINTER args accepting int addresses
(0 = NULL), pointer returns always being int addresses, the
FFI_INT/FFI_LONG aliases, ffi_load("") semantics, the valid_ffi
operation names with a sample master implementation, and the real
build default (PACKAGE_FFI ships ON; runtime denies without a
valid_ffi apply; WASM forces it off).
Every snippet is pinned verbatim by a new testsuite file,
testsuite/single/tests/efuns/ffi_doc_examples.lpc (guarded by
__PACKAGE_FFI__ and the ffi_probe availability fixture like the other
ffi tests). Verified: RelWithDebInfo build, ffi glob (21 files) and the
full LPC suite (600 files) pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT
* docs: add a local build quickstart to AGENTS.md
Record the verified Debian/Ubuntu setup so future agents don't
rediscover it one configure failure at a time: the full apt package
list (CI's packages: lines assume a GitHub runner image that
preinstalls cmake/ninja/libicu-dev), the configure/build commands, the
build/src/driver binary path, -DPACKAGE_DB=OFF as the escape hatch
when no MySQL client dev package is available, that libevent is
vendored and GTest optional, and how to run the LPC suite without a
pipe masking the driver's exit status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT
* docs: rename driver/ffi-plan.md to driver/ffi.md
The page stopped being a plan and is the package reference; name it
accordingly (site URL /driver/ffi/, sidebar label "FFI Package").
Update every reference: source comments (ffi.spec, ffi.cc, both
include/ffi.h copies), tools/ffi (README, generate.py's module
docstring and the two comment lines it emits into generated bindings,
with the committed ffi_genmath.lpc fixture updated to match), the
testsuite doc-examples pin, sidebar_meta.json + regenerated
sidebars.generated.json, and the zh-CN sidebar-label key.
Note: the old /driver/ffi-plan/ URL is not redirected (the site has no
client-redirects plugin configured).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT
* ffi: gate ffi_peek through valid_ffi("peek")
ffi_peek(address, nbytes) copies bytes from an arbitrary native
address, which makes it a process-memory disclosure primitive on its
own (and a driver crash on an unmapped address) -- yet it was the one
dangerous efun in the package not gated by valid_ffi, so any object
could call it whenever PACKAGE_FFI was compiled in, even under a
deny-all master. The other ungated efuns operate only on LPC-owned
buffers and handles (ffi_address reveals a buffer's own address, inert
without a peek/call grant) and stay ungated.
f_ffi_peek now calls check_valid_ffi("peek", address) before touching
any memory, exactly like load/symbol/prepare/callback. The testsuite
master denies a -0xDEAD sentinel address so the denial path is
testable without dereferencing anything; ffi_peek.lpc pins the exact
error. Docs updated: valid_ffi(4), ffi_peek(3), and the op list +
sample master in docs/driver/ffi.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT
* stdlib: add /std/ffi_util, FFI-boundary helpers for package_ffi callers
A pure-LPC library packaging the recurring idioms at the C boundary so
binding code stops re-deriving them:
- cstr(s) / cstr_enc(s, enc): NUL-terminated C strings (buffer
promotion for UTF-8, string_encode for other byte encodings)
- c_string(addr) / c_string_enc(addr, enc): read a returned char*,
with the NULL -> 0 convention folded in
- c_out(type_code): a zeroed out-parameter block for T*
- c_argv(strings): a NULL-terminated char*[] plus the kept-alive
per-string buffers (the lifetime footgun a library should own)
- c_field / c_field_set: struct-field access over ffi_struct_layout()
layouts
Deliberately consumed via `inherit "/std/ffi_util"` rather than the
simul_efun object: c_string() calls ffi_peek(), gated by
valid_ffi("peek", addr, caller), and inheriting keeps the consuming
object as the security principal the master sees. The docs page
(docs/stdlib/ffi_util, "FFI Utilities" in the sidebar) spells this out.
Tests: testsuite/single/tests/std/ffi_util.lpc exercises every helper
against libc (strlen with both encodings, getenv through c_string,
frexp through c_out, strtol's char **endptr through
c_out(FFI_POINTER), and structural checks on c_argv's pointer array).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SooGWcJbSWszCZ67k4iVQT
---------
Co-authored-by: Claude <noreply@anthropic.com>
4.2 KiB
| title |
|---|
| stdlib / ffi_util |
FFI utilities (/std/ffi_util)
A pure-LPC helper library for code that calls native functions through
package_ffi. It packages the recurring idioms at the C
boundary — NUL-terminated strings, returned char* pointers,
out-parameters, argv arrays, struct fields — so binding code doesn't
re-derive them. It lives in testsuite/std/ffi_util.lpc and is portable
to any FluffOS mudlib (copy the file plus include/ffi.h).
inherit "/std/ffi_util";
:::warning[Inherit it into the privileged object — don't simul-efun it]
c_string()/c_string_enc() call ffi_peek,
which the master gates via
valid_ffi("peek", addr, caller) — and the
caller the master sees is the object this library is inherited into.
Exposed as a simul-efun, every peek in the mud would run with the
simul_efun object as its security principal. Inherit the library
directly into the object(s) allowed to do FFI work instead.
:::
C strings in: cstr(), cstr_enc()
buffer cstr(string s);
buffer cstr_enc(string s, string encoding);
cstr() returns the string's raw UTF-8 bytes plus a NUL terminator —
exactly what a const char* parameter (an FFI_POINTER argument)
wants. cstr_enc() converts through
string_encode first, for C functions
that expect Latin-1 or another byte-oriented encoding. (The terminator
is a single NUL byte; wide encodings like UTF-16 need a wider
terminator — build those by hand.)
int sl = ffi_prepare(lib, "strlen", FFI_UINT64, ({ FFI_POINTER }));
ffi_call(sl, ({ cstr("café") })); // 5: UTF-8 bytes
ffi_call(sl, ({ cstr_enc("café", "latin-1") })); // 4: é is one byte
C strings out: c_string(), c_string_enc()
string c_string(int addr);
string c_string_enc(int addr, string encoding);
Read the NUL-terminated bytes at a raw foreign address (a returned
char*) and decode them into an LPC string. A NULL (0) address
yields 0 instead of an error, matching the C convention, so returned
pointers can be passed straight through:
int ge = ffi_prepare(lib, "getenv", FFI_POINTER, ({ FFI_POINTER }));
string path = c_string(ffi_call(ge, ({ cstr("PATH") }))); // 0 if unset
Out-parameters: c_out()
buffer c_out(int type_code);
A zeroed block sized for one scalar of the given FFI_* type code —
pass it where C wants T *out, then ffi_read(out, 0, code):
int fx = ffi_prepare(lib, "frexp", FFI_DOUBLE, ({ FFI_DOUBLE, FFI_POINTER }));
buffer exp = c_out(FFI_INT);
float mant = ffi_call(fx, ({ 8.0, exp })); // 0.5
int e = ffi_read(exp, 0, FFI_INT); // 4
c_out(FFI_POINTER) works for pointer out-parameters too — e.g.
strtol's char **endptr, read back with ffi_read and c_string().
Argument vectors: c_argv()
mixed *c_argv(string *args);
Builds a C char *argv[]: a NULL-terminated pointer array whose slots
hold the addresses of one cstr() buffer per string. Returns
({ pointer_array, string_buffers }) — pass element 0 as the
FFI_POINTER argument, and keep the returned array referenced for as
long as C may use the pointers: the addresses in the pointer array
die when the buffers in element 1 are collected.
mixed *av = c_argv(({ "ls", "-l" }));
ffi_call(f, ({ cstr("/bin/ls"), av[0] })); // av stays in scope
Struct fields: c_field(), c_field_set()
mixed c_field(buffer data, mixed *layout, int idx, int type_code);
void c_field_set(buffer data, mixed *layout, int idx, int type_code, mixed value);
Read/write field idx of a C struct held in data, using a layout
from ffi_struct_layout (the offset is
layout[1][idx]). With the symbolic indexes the
tools/ffi generator
emits, field access reads by name:
mixed *lay = ffi_struct_layout(STRUCT_TM_TYPES);
int year = 1900 + c_field(tm, lay, STRUCT_TM_tm_year, FFI_INT);
See also
- The FFI package reference — worked libc examples
- valid_ffi — the security gate
- Tests:
testsuite/single/tests/std/ffi_util.lpc