Documentation: https://retrocorelabs.github.io/ndmonlib/ - one page per MON call (manual text, parameters, examples, ndmonlib status and handler notes), rebuilt on every push to main.
Production-grade emulation of SINTRAN III MON operating system calls for Norsk Data ND-series emulators.
ndmonlib is a portable, callback-based MON (Monitor Call) subsystem for emulating SINTRAN III operating system calls. It registers 234 MON calls (62 implemented, 172 still stubs - see MON Calls Status) so that ND-series emulators can run real SINTRAN binary programs (:DOM files, kernels, utilities).
Key Features:
- 234 registered MON calls (62 implemented) covering file I/O, terminal I/O, device operations, process control, and system information
- Architecture-agnostic design — Single code base works for ND-100, ND-500, and other architectures via callbacks
- O(1) dispatch — Hash table registry for fast MON call throughput
- Comprehensive testing — Unit tests for core logic, integration tests for MON variants
- Auto-generated documentation — docs/mon-implementation-status.md lists every registered call with its implementation status
- SINTRAN semantics — File system (own-dir fallback to SYSTEM), path resolution, error codes match real SINTRAN
- Automatic unimplemented detection — Reports which MON calls your program needs
- No external dependencies — Portable C11, uses only libc and host file I/O
cd ~/repos/ndmonlib
mkdir build && cd build
cmake ..
make
ctest-
Add as git submodule:
cd ~/repos/nd500x git submodule add ../ndmonlib external/ndmonlib
-
Implement architecture callbacks (e.g.,
src/cpu/nd500_mon_callbacks.c):#include <ndmon/mon.h> static uint32_t nd500_mon_read_word(void* cpu_ptr, uint32_t addr) { Nd500Cpu* cpu = (Nd500Cpu*)cpu_ptr; return nd500_mmu_read_word(cpu->mmu, addr); } void nd500_mon_init_callbacks(MonContext* ctx, Nd500Cpu* cpu) { ctx->cpu = cpu; ctx->read_word = nd500_mon_read_word; ctx->write_word = nd500_mon_write_word; // ... set all callbacks }
-
Dispatch MON calls from your CPU:
if (is_mon_call(instruction)) { MonContext ctx = {0}; nd500_mon_init_callbacks(&ctx, cpu); MonResult result = mon_dispatch(&ctx, mon_number, arg_addresses); if (ctx.halt_requested) stop_execution(); if (ctx.wait_requested) handle_input_wait(&ctx); }
See docs/INTEGRATION.md for detailed instructions.
Your Emulator (ND-500, ND-100, etc.)
│
├─ Instruction: CALLG segment 31, address XXB
│
├─ CPU calls: mon_dispatch(&ctx, mon_number, arg_addresses)
│
├─ Callbacks: ctx.read_word(), ctx.set_i1(), etc.
│ (you implement these for your architecture)
│
└─ Handler executes via callbacks (no direct CPU/MMU access)
Result: file opened, data read, process halted, etc.
234 registered MON calls (from src/core/mon_registry.c), grouped by function as in chapter 2 of SINTRAN III Monitor Calls (ND-860228.2 EN):
| Group | Manual section | Example | Count |
|---|---|---|---|
| File Operations | 2.4 | 1B INBT | 48 |
| Input and Output Monitor Calls | 2.5 | 3B ECHOM | 29 |
| Monitor Calls for Terminal Handling | 2.6 | 16B MGTTY | 14 |
| Monitor Calls for Printer Handling | 2.7 | 234B SPEFI | 1 |
| Monitor Calls for Error Handling | 2.8 | 56B PASET | 9 |
| File System Operations | 2.9 | 41B ROBJE | 21 |
| RT Program Execution | 2.10 | 11B TIME | 28 |
| Device Handling | 2.11 | 31B EXIOX | 14 |
| Segment Administration | 2.12 | 33B ALTON | 23 |
| Data Communication | 2.13 | 200B XMSG | 4 |
| Monitor Calls for Internal Use | 2.14 | 45B GTYPR | 3 |
| Commonly-Used Monitor Calls | 2.3 | 0B LEAVE | 3 |
| Not grouped in manual | - | 34B ALTOFF | 37 |
| Total | - | - | 234 |
The manual lists some calls in more than one section. Here each call is counted once, in the first section it appears in (section 2.3 Commonly-Used Monitor Calls is checked last), so the rows add up to the total. Example = lowest MON number in the group. Generated by tools/generate_mon_status.py - do not edit by hand.
See docs/mon-implementation-status.md for the complete per-call listing.
Implementation completeness (from the authoritative registration table in
src/core/mon_registry.c — regenerate with tools/generate_mon_status.py):
✅ VALIDATED (tested, fully working) — 45 handlers (19%)
🔶 IN_PROGRESS (partial, may need fixes) — 17 handlers ( 7%)
❌ NOT_IMPLEMENTED (stubs only) — 172 handlers (74%)
────────────────────────────────────────────────────────
Total registered 234 handlers
Most MON calls are still auto-generated stubs: the dispatcher returns "not implemented" for them without calling the handler. See the full per-call breakdown in docs/mon-implementation-status.md (auto-generated; machine-readable copy in metadata/mon_status.json).
Check which MON calls your program needs:
# Run your program
./build/bin/nd500x --debug < myprogram.dom
# Unimplemented MON calls are logged automatically:
# [MON] ERROR: 412B not implemented (call #1 from PC=0x12345678)
# After execution, get a summary
mon_report_unimplemented_usage();
# Shows: MON 412B — called 3 times
# MON 413B — called 1 timeFor detailed status of each MON call: docs/mon-implementation-status.md
# 1. Program fails with unimplemented MON
# [MON] ERROR: 412B not implemented at PC=0x12345678
# 2. Look up the MON call
grep '`412B`' docs/mon-implementation-status.md
# Shows: name, status, handler file size, handler symbol
# 3. Implement it
# See: docs/CARVING.md (step-by-step guide)
# 4. Add tests and update metadata
python3 tools/generate_mon_status.py # Regenerate status docs and README tablesFor in-depth guide: docs/CARVING.md
cd build
ctest # All tests
ctest -R dispatcher -V # One test, with its PASS/FAIL lines
# Tests: dispatcher, params, file_table, mon_handlers| Document | Purpose |
|---|---|
| docs/CARVING.md | How to identify and implement missing MON calls |
| docs/INTEGRATION.md | Step-by-step integration guide |
| docs/mon-implementation-status.md | Auto-generated: implemented-vs-stub audit (validated/in-progress/stub counts, mismatches) |
python3 tools/generate_mon_status.py
# Reads: src/core/mon_registry.c + src/handlers/*.c
# + metadata/mon_function_groups.json
# Writes: metadata/mon_status.json, docs/mon-implementation-status.md,
# and the function-group table in README.md
python3 tools/generate_mon_status.py --check
# Exit 1 if a handler's registered status disagrees with its source
NDINSIGHT_DIR=<NDInsight repo root> python3 tools/generate_mon_status.py --extract-groups
# Re-read the function groups from the SINTRAN III Monitor Calls manual
# (Developer/MON/Monitor Calls.md in the NDInsight repo)- ND-500 emulator (nd500x) —
external/ndmonlibas submodule - ND-100 emulator (nd100x) — Same submodule, different callbacks
Both share identical dispatcher and handlers; only callbacks differ per architecture.
- Dispatcher: O(1) hash table lookup
- Parameter access: O(1) per parameter
- File operations: Host filesystem speed
- Overall: Millions of MON calls/second on modern hardware
- C Standard: C11 (libc only, no external dependencies)
- Platforms: Linux, Windows, macOS, WebAssembly (Emscripten)
- CPU Architectures: ND-100, ND-500, and others via callbacks
MIT License — See LICENSE for details.
Lines of Code: 17,410 (handlers + core)
MON calls: 234 registered (45 validated, 17 in progress, 172 stubs)
Tests: 4 programs (dispatcher, params, file_table, mon_handlers); coverage not measured
Build Time: <1 second (native)
Latest Release: v1.0 | Stability: Production | Last Updated: 2026-07-23