-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 32 arexx polish plan
Part of the Amiga port design log.
Three additions to the existing ARexx surface (amiga.rexx_send,
amiga.rexx_open/recv/reply from the Phase 18 ARexx surface):
import amiga
# (1) Cheap existence check.
if amiga.rexx_exists("IBROWSE"):
rc, html = amiga.rexx_send("IBROWSE", "QUERY ITEM=URL")
# (2) Public-port enumeration.
for port in amiga.rexx_list():
print(port)
# (3) Persistent client -- caches the reply MsgPort across sends so a
# tight loop doesn't pay CreateMsgPort/DeleteMsgPort per call.
with amiga.RexxClient("DOPUS.1") as ib:
ib.send("LISTER NEW")
rc, result = ib.send("LISTER QUERY 1 PATH", check=False)The first two are tiny wrappers around FindPort / SysBase->PortList
walking. The third amortises the CreateMsgPort / DeleteMsgPort
cost across many sends — invisible for a single call, real when
driving a host (DOpus, IBrowse, YAM, ...) in a tight loop.
-
amiga.rexx_exists(name)→bool. Thin wrapper aroundFindPort(name)withForbid()/Permit()braces. -
amiga.rexx_list()→list[str]. WalksSysBase->PortList, returning every public port'sln_Name. -
amiga.RexxClient(host)class:- Opens a reply
MsgPortin__init__; reuses it across calls. -
.send(command, check=True)returns the same shape asamiga.rexx(host, command). -
.close()/__exit__/__del__tear down the reply port. - Tracked in an at-exit cleanup chain so a forgotten close doesn't leak the MsgPort on process exit.
- Opens a reply
- Migrating the existing
amiga.rexx_*surface into anamiga.rexx.*sub-module — breaking change for callers and the inbound server side (Phase 18) is already deeply embedded in the module flat namespace - Reshaping the one-shot
amiga.rexx_send/amiga.rexxhelpers to use the client class internally — the per-call MsgPort cost is invisible for a single send; the polish is for tight loops - Async send (fire-and-forget without waiting for reply) — easy add later but not in scope for Phase 32
- Bidirectional Port objects (a single object that's both a server and a client) — separate phase if needed
-
rexxsyslib.library(already opened lazily by Phase 18amiga.rexx_send)
Five new entry points alongside the Phase 18 ARexx surface:
| Entry | Returns |
|---|---|
amiga.rexx_exists(name) |
bool (Forbid-fenced FindPort) |
amiga.rexx_list() |
list[str] (Forbid-fenced walk of SysBase->PortList) |
amiga.RexxClient(host) |
persistent client; .send(cmd, check=True), context manager, __del__ cleanup |
amiga.rexx_client_open / _close / _send
|
low-level primitives that back the class |
The class's reply MsgPort is registered in a 16-slot static
chain at construction. amiga_rexx_shutdown (already wired into
main.c's cleanup path) walks the chain on process exit and
DeleteMsgPorts anything still live, so a script that forgot
close() doesn't leak the port. The send body was factored out
of amiga_rexx_send_fn into amiga_rexx_send_via_port so the
one-shot and persistent paths share the wire format / Ctrl+C
latch / argstring handling.
This section is the step-by-step ship plan — how to chunk the work into landable PRs. The design above answers what and why; this answers how.
Step 1: rexx_exists + rexx_list (one-shot helpers)
↓
Step 2: RexxClient (persistent client)
↓
Step 3: docs + smoke
| # | Step | Output | On-target smoke |
|---|---|---|---|
| 1 |
amiga.rexx_exists(name) → bool (FindPort with Forbid/Permit). amiga.rexx_list() → list[str] (walks SysBase->PortList). |
Two new entries in amiga_module_globals_table. |
From the REPL: amiga.rexx_exists("MICROPYTHON.1") after amiga.rexx_open() returns True; amiga.rexx_list() returns a list containing our own port. |
| 2 |
amiga.rexx_client_open() / _close(h) / _send(h, host, cmd) C primitives (factor out the body of amiga_rexx_send_fn so the client reuses it). Frozen RexxClient class in amiga.py wraps them with __enter__ / __exit__ / __del__ / .send(check=True/False). |
Three new C entries; new Python class; cleanup chain wired into amiga_rexx_shutdown so a forgotten close doesn't leak the MsgPort on exit. |
From the REPL: open RexxClient("MICROPYTHON.1") (which talks to our own inbound port via Phase 18), .send("noop") returns (rc, result). With-block exits cleanly. |
| 3 | Docs flip, manifest verification, arg-shape tests. |
docs/amiga.md Phase 32 → ✅, docs/amiga-testing.md gains an ARexx-polish subsection. |
vamos-runnable arg-shape / module-import test; on-target reality check noted as interactive. |
Each step is small: Step 1 is ~40 LOC C, Step 2 is ~120 LOC C + ~40 LOC Python, Step 3 is paperwork.
-
Two new C entries in
modamiga.c, both gated on the existingMICROPY_PY_AMIGAblock:static mp_obj_t amiga_rexx_exists(mp_obj_t name_obj) { const char *name = mp_obj_str_get_str(name_obj); Forbid(); bool ok = (FindPort((CONST_STRPTR)name) != NULL); Permit(); return mp_obj_new_bool(ok); } static MP_DEFINE_CONST_FUN_OBJ_1(amiga_rexx_exists_obj, amiga_rexx_exists); static mp_obj_t amiga_rexx_list(void) { mp_obj_t list = mp_obj_new_list(0, NULL); Forbid(); struct List *plist = &SysBase->PortList; struct Node *n; for (n = plist->lh_Head; n->ln_Succ != NULL; n = n->ln_Succ) { if (n->ln_Name != NULL) { mp_obj_list_append(list, mp_obj_new_str(n->ln_Name, strlen(n->ln_Name))); } } Permit(); return list; } static MP_DEFINE_CONST_FUN_OBJ_0(amiga_rexx_list_obj, amiga_rexx_list);
-
Registered in
amiga_module_globals_tablealongside the otherrexx_*entries:{ MP_ROM_QSTR(MP_QSTR_rexx_exists), MP_ROM_PTR(&amiga_rexx_exists_obj) }, { MP_ROM_QSTR(MP_QSTR_rexx_list), MP_ROM_PTR(&amiga_rexx_list_obj) },
-
Forbid()/Permit()braces so the port list can't mutate mid-walk.Forbidis light enough that wrapping the whole list walk is fine; we're allocating Python objects inside the Forbid window, which is borderline but the readline / vstr GC paths used bymp_obj_new_strdon't issue any wait/Forbid- unsafe calls. (Phase 18's existingamiga_rexx_open_fnalready does Python allocs inside a Forbid block, so this matches the precedent.) - Empty list (no public ports — degenerate case, doesn't happen on
a real boot) returns
[]rather than raising.
REPL on target (Amiberry):
>>> import amiga
>>> amiga.rexx_exists("WORKBENCH")
True
>>> amiga.rexx_exists("DOES_NOT_EXIST")
False
>>> sorted(amiga.rexx_list())
['MICROPYTHON.1', 'WORKBENCH', ...]Under vamos: WORKBENCH doesn't exist (no Intuition); rexx_list()
returns whatever stubs vamos publishes. The vamos smoke just
checks the entries are callable and return the right shapes.
Refactor the one-shot send path (amiga_rexx_send_fn) so the
core "wrap command in argstring + PutMsg + Wait" loop becomes a
helper that takes an externally-owned reply port:
static mp_obj_t amiga_rexx_send_via_port(struct MsgPort *reply,
const char *host,
mp_buffer_info_t *cmd_bi);amiga_rexx_send_fn becomes a thin wrapper that CreateMsgPort's,
calls the helper, then DeleteMsgPort's.
Three new entry points:
| Function | Signature | Behaviour |
|---|---|---|
amiga.rexx_client_open() |
() -> int |
CreateMsgPort(); registers in the at-exit chain; returns the address as an int handle. |
amiga.rexx_client_close(h) |
(int) -> None |
Unregisters from the chain, DeleteMsgPort(h). Tolerates h == 0. |
amiga.rexx_client_send(h, host, cmd) |
(int, str, bytes) -> (int, bytes | None) |
Sends cmd to host using the cached reply port at h. Same return shape as amiga.rexx_send. |
The at-exit chain ensures a forgotten close doesn't leak the
MsgPort on process exit; same pattern as the existing
amiga_rexx_shutdown().
class RexxClient:
"""Persistent ARexx client.
Holds an open reply MsgPort across many sends. Use when
driving a host (DOpus, IBrowse, ...) in a tight loop. For a
single send, `amiga.rexx_send()` or `amiga.rexx()` is enough.
"""
__slots__ = ("_host", "_handle")
def __init__(self, host):
self._host = host
self._handle = _c.rexx_client_open()
def send(self, command, check=True):
if not self._handle:
raise ValueError("RexxClient: closed")
if isinstance(command, str):
command = command.encode("latin-1")
rc, result = _c.rexx_client_send(self._handle, self._host, command)
if check and rc != 0:
raise OSError(rc, "rexx %r at %s rc=%d" % (command, self._host, rc))
if check:
return result if result is not None else b""
return (rc, result)
@property
def host(self):
return self._host
def close(self):
if self._handle:
_c.rexx_client_close(self._handle)
self._handle = 0
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
def __del__(self):
try:
self.close()
except Exception:
pass
def __repr__(self):
state = "closed" if not self._handle else "open"
return "<RexxClient host=%r %s>" % (self._host, state)REPL on target:
>>> from amiga import RexxClient, rexx_open, rexx_port_name
>>> rexx_open() # so we have a port to talk to
'MICROPYTHON.1'
>>> with RexxClient("MICROPYTHON.1") as c:
... # ... server-side serve loop would handle this
... pass
>>> # tear-down on __exit__ is the testFor a real-world smoke, drive a host that's actually up — DOpus
or YAM — and confirm a tight loop of 100 sends is noticeably
faster than 100 calls through amiga.rexx_send.
-
docs/amiga.mdPhase 32 → ✅; section gains a "Status — done" block listing the three new entries and noting the cleanup-chain registration. -
docs/amiga-testing.mdgains an ARexx-polish subsection under Amiberry, with theRexxClientREPL example. -
tests/ports/amiga/test_rexx_polish.py— vamos-runnable:-
amiga.rexx_exists("nonsense_no_such_port")returnsFalse -
amiga.rexx_list()returns alist[str] -
RexxClientinstantiates, has.send/.close/__enter__ -
RexxClient.sendon a closed instance raisesValueError
-
All three shipped variants include the additions —
rexxsyslib.library is part of any Workbench install and there's
no external SDK. Per-variant text-segment growth: ~600 bytes.
-
No Forbid leaks. Both
rexx_existsandrexx_listopen Forbid blocks; the bodies match the precedent inamiga_rexx_open_fn.RexxClient.senddoes not Forbid — it justPutMsg+Waitlikeamiga_rexx_send_fn. -
At-exit cleanup chain. Open
RexxClientinstances register in a linked list rooted inmodamiga.c.amiga_rexx_shutdown(already called bymain.c) walks the chain and closes each port. So a script that opens a client, faults, and never hits__del__still has itsMsgPortreclaimed on exit. -
check=Truedefault. Matches the existingamiga.rexxhelper — easier to compose with normal Python control flow than the lower-levelamiga.rexx_sendtuple shape. -
Ctrl+Cduring.sendis deferred — same rationale as Phase 18'samiga_rexx_send_fn: abandoning the wait mid-flight would have the hostPutMsginto a freed reply port. The client's reply port isWait-ed on synchronously and Ctrl+C is latched, raised after the reply lands.
ports/amiga/modamiga.c — five new C entries
(rexx_exists, rexx_list,
rexx_client_open/close/send);
amiga_rexx_send_via_port helper
ports/amiga/modules/amiga.py — RexxClient Python facade
tests/ports/amiga/test_rexx_polish.py — vamos arg-shape smoke
Variants: all three. ~1.4 KB text per variant.