$darkmode
Practical notes for working on the codebase — entry points, the contracts the public API depends on, and sketches of how a host program could embed libmodemu2k. The build and test instructions live in README.md and tests/README.md; this file is for what those don't cover.
If you've never read the code before, this order keeps the surface manageable:
modemu2k.h** — the entire public API on one page, with docblocks. Skim this first; it answers "what can callers do."src/main.c** — ~100 lines, links against the library, shows the recommended call sequence (m2k_new → m2k_setup_* → m2k_run → m2k_free). The CLI is the simplest possible consumer.src/m2k_api.c** — the state machine. The four state-enter helpers (stepEnterCmd, stepEnterDial, stepEnterOnline, and the implicit M2K_STATE_DONE transition) and m2k_step()'s switch are the spine of the library.src/cmdlex.l** + **src/atcmd.c** — the AT command set. The lexer (Flex) produces a Cmdstat enum; atcmd.c has the per-verb handlers (atcmdH, atcmdZ, S-register ops, etc.).The internal headers src/m2k_private.h and src/m2k_ctx.h define everything that lives in the opaque m2k_t. Read them when you need to know what state the context carries; don't read them to understand the public contract.
| File | |
|---|---|
src/m2k_api.c | State machine, public-API entry points, the synchronous-vs-step wrappers |
src/cmdlex.l | Flex grammar for the AT command set (generated to cmdlex.c) |
src/atcmd.c | AT verb handlers + S-register state in Atcmd |
src/sock.c | TCP dial (blocking m2k_sockDial + non-blocking m2k_sockDialStart/Progress/Abort), listen/accept |
src/sockbuf.c / src/ttybuf.c | Buffered I/O for socket and TTY sides |
src/telopt.c | Telnet option negotiation (WILL/WONT/DO/DONT/SB) |
src/comm_program.c | Fork/exec a comm program (minicom etc.) on the PTY slave |
src/stty.c | Raw-mode terminal setup for stdin/stdout mode |
src/cmdarg.c | CLI argument parsing — only used by the binary, not the library |
src/verbose.c | Logging callback + the curl-style error buffer writer |
src/utils.c | m2k_alloc / m2k_calloc macros with call-site logging |
src/cmdlex.l matching the verb and returning a Cmdstat (or dispatching to a handler that returns one).src/atcmd.c if it's non-trivial. Handlers return 0 on success, 1 on rejection — the lexer turns that into CMDST_OK / CMDST_ERROR.tests/test_atcmd.c exercising the happy path and the range-check rejection. White-box style; you can call the handler directly.modemu2k.1.modemu2k.h with the M2K_API prefix and a docblock.src/m2k_api.c (or the topical file if it's narrowly scoped, e.g. socket-related code in sock.c).m2k_err_t code, append it to the enum. Never reorder existing values; consumers may have cached the numeric values across releases.tests/test_*.c case. White-box tests can include m2k_private.h/m2k_ctx.h to reach into context state.ChangeLog under the (in-progress) section at the top.These constraints are load-bearing for callers; breaking them silently will surprise someone.
m2k_err_t is append-only.** New codes go at the end. Existing numeric positions are part of the ABI.m2k_t instances must not share or clobber each other. No file-scope mutable state in the library. (Commit 645ab30 fixed the last remaining violation — telnet option negotiation tables — by moving them into ctx->telOpt.)m2k_t contexts from different threads as long as no single ctx is touched concurrently. There is no m2k_lock / m2k_unlock.m2k_t is opaque to consumers. They should never #include m2k_ctx.h or peek at struct layout. The test suite does, on purpose, via -DUSE_AS_TEST_LIB.M2K_API marker is required on every public function, so that a future -fvisibility=hidden build (or a Windows DLL build) keeps internals internal.These are approach sketches, not validated implementations. They describe what an integration would look like, based on reading the upstream code and
libmodemu2k's current API. Neither sketch has been built end-to-end against a real upstream tree. If you're considering doing the work, treat these as starting points for design, not as instructions.
dosemu2 currently bundles a vendored copy of the original modemu under src/plugin/modemu/ (~10 source files: modemu.c, atcmd.c, sock.c, telopt.c, etc., all wrapped in #ifdef DOSEMU). The plugin is wired into dosemu2's I/O dispatcher via ioselect.h.
In broad strokes, replacing it with libmodemu2k:
libmodemu2k (or vendor it via meson subproject).modemu_init today). Replace it with a m2k_new() + the appropriate m2k_setup_*() choice (likely m2k_setup_app_io since dosemu2 owns the serial-port emulation).m2k_get_pollfds() to learn which fds modemu2k wants watched and splice them into dosemu2's existing select/poll set. After select/poll returns, call m2k_step() with the same fd array and the reported revents.src/dosext/serial/) with m2k_write_from_app (DOS side → modem) and m2k_read_to_app (modem → DOS side).m2k_set_dtr(). The &D2 hangup-on-DTR-drop behavior is already implemented in modemu2k.The biggest unknown is whether dosemu2's main loop architecture is compatible with calling into a library that owns its own state machine. Worth a serious design conversation with dosemu2 maintainers before any code is written.
minicom currently centers everything on a single file descriptor portfd (the serial device or socket). It's referenced ~50 times in src/main.c plus appearances in dial.c, updown.c, functions.c. Read/write/select use this fd directly.
A --modem-emu mode that swaps the fd-based path for libmodemu2k:
m2k_new() + m2k_setup_app_io() at the same point where minicom would have opened portfd.src/main.c:108 and the surrounding select-loop) needs to call m2k_get_pollfds() and merge those fds with its own, instead of putting portfd in the poll set.read(portfd, buf, n) become m2k_read_to_app(ctx, buf, n, &len). Important: when an incoming socket burst fills the modem's internal TTY-bound buffer past what one read_to_app can return (host's buf is smaller than the burst), the next poll() must run with a zero timeout. None of the modem's fds will fire to wake it — the bytes are buffered inside libmodemu2k. Check m2k_has_pending_output() and force timeout=0 whenever it's true; otherwise effective throughput collapses to roughly "host buffer size per poll timeout." The same applies right after a m2k_write_from_app() keystroke: the bytes sit in the modem's tty-input buffer until the next m2k_step() flushes them to the socket, so the post-write poll should also use zero timeout for one iteration.m2k_write_from_app(ctx, buf, n, &consumed), with the M2K_ERR_WOULDBLOCK return treated as "pause output,
retry after next step."m_dtrtoggle(portfd, ...) calls map to m2k_set_dtr().The harder questions: minicom's terminal state (raw mode, hardware flow control) presumes a real TTY behind portfd. Modemu2k's embed mode side-steps that, but the existing minicom code paths that call m_sethwf / m_break / m_flush would become no-ops and need either stubs or compile-time gating.
File transfer (lrzsz) is the real architectural gap. Minicom's updown.c spawns the transfer process with dup2(portfd, 0) and dup2(portfd, 1), giving the child its stdin/stdout. With portfd sentinel'd to /dev/null in embed mode, lrzsz reads EOF and writes go nowhere — transfer times out. A real fix needs a PTY-pair bridge: allocate openpty(), give the child the slave, and have the parent pump bytes between the master and m2k_{write_from,read_to}_app — roughly 50 lines of new code. Until then, file transfer requires the standalone-modemu2k-plus-system-minicom path (the m2k-minicom helper script), which gives lrzsz a real PTY end-to-end.