2026-09-02 - (/loop) Bulk port batch 3: the standard streams, real recursive locks, and the C++ runtime
Two carried-over items cleared and the C++ batch done. Eighteen entries, and the biggest thing in it is a decision about what not to write.
documented 711 needed, 434 missing -> 711 needed, 416 missing
The standard streams, as data (carried over)
_Stdout and _Stderr are objects
holding a FILE *, not functions, so nothing could
fill them lazily - there is no call to hook. New
orbistoun-libc/src/streams.rs fills them once, from the
worker, after the data imports are published and before the guest is
entered.
What goes in them is not an invented FILE layout:
fprintf already routes a stream by asking
orbistoun_fs::open::wrapped_descriptor which descriptor it
stands for, and wrap_descriptor mints exactly that handle -
the same mechanism fdopen uses. So the standard streams are
two wrapped descriptors, 1 and 2, and every existing path that writes to
a wrapped stream works on them unchanged.
This is a real behavioural gain rather than a box ticked: until now
fprintf(stdout, ...) and fprintf(stderr, ...)
both fell through to the host's error stream, so a
guest's output could not be told from its diagnostics.
The runtime's recursive locks, done properly (carried over)
_Lockfilelock / _Unlockfilelock /
_Locksyslock / _Unlocksyslock, in
orbistoun-libc/src/locks.rs: owner thread, depth, and a
condition variable. Batch 2 deferred these rather than write the no-op,
and the tests say why - one thread may take a lock twice and must
release it twice, and a release by a thread that does not hold
it is ignored, because obeying it would free a lock its real
owner still believes it holds. A no-op implementation does exactly that
on every call, and looks perfectly correct until two threads share a
FILE.
The two tables are kept separate so a FILE * that
happens to equal a small lock index cannot collide with it.
The C++ runtime, and the line drawn through it (D473)
Written: the allocation forms
operator new(nothrow_t),
operator new(align_val_t),
operator delete(align_val_t), and
std::get_new_handler - which answers null,
and that is the correct answer rather than a placeholder: the standard's
initial handler is a null pointer, and nothing here imports
set_new_handler.
Stopped, deliberately: std::terminate,
__cxa_pure_virtual, _Unwind_Resume, and the
runtime's seven throw helpers. All are [[noreturn]], and
there is no unwinder. Returning would resume the guest at an instruction
its own compiler proved unreachable, holding the half-built object that
provoked the throw - a silent wrong continuation. Stopping cannot
deliver the exception to a catch, and says so, naming the
exception and its message. One stop_for for all of them, so
none can quietly grow a return. D473 has the full
argument.
Left unimplemented: __cxa_throw and its
family, __gxx_personality_v0, the _ZTV*
vtables, _ZSt4cout/_ZSt4cerr, the locale and
facet objects. The __cxa_* calls are not all
[[noreturn]], so the stopping argument does not extend to
them; the vtables and stream objects are data needing real C++ object
layouts, which is a different kind of work from writing a function. An
unimplemented import is already loud, which is the right state for
these.
State
clippy --tests clean, fmt clean, orbistoun-libc (114)
and orbistoun-worker tests pass, identity scan clean, nothing
committed.
Next: batch 4 (stdio), batch 5 (the real
coshf/expf math family, not the runtime's
_F* internals), batch 6 (conversions incl.
strtoumax, plus a locale batch for
wcstombs/wcsrtombs). Still pending:
setenv, and _Thrd_id/_Thrd_join
which belong in orbistoun-kernel beside the
_Mtx_*/_Cnd_* family. Then the libScePosix
281, where the DELEGATED table in
orbistoun-posix should be checked first - many may already
exist and need only declaring.