Hacker News new | past | comments | ask | show | jobs | submit login

Excited for threads, sockets, futexes, but not excited for fork, signals and setjmp. They didn't include POSIX stuff like shared memory, select(2), unix sockets, and had to draw a line somewhere. Why include error-prone cruft?



A few points there:

* classical shared memory (mapped into the main address space) is very hard to do in the context of Webassembly, especially in a performant way. Not impossible, but complicated. It would be valuable to add, but there are different approaches as well (like sharing additional Webassembly-level memories between instances, for example)

* WASI already has a concept similar to select

* unix sockets would be a pretty straight-forward addition

> Why include error-prone cruft?

A big goal here is to make existing software compile to Webassembly, with only a relatively small amount of changes.

For writing new software the direction of WASI (going towards the fine-grained capability model) is a very valid approach. But that won't get you compatibility with pretty much all software in existence.

And there is space to explore both directions.


Plain fork (as opposed to vfork) isn’t really error-prone, it’s arguably less error-prone than threads. (It forces bad resource accounting, like most CoW mechanisms, but that’s an orthogonal issue.) Signals and setjmp... have their problems, admittedly, but on a restricted VM like WASM are AFAICT unimplementable on top of other things. So if you want unwinding, you’ve got to have an equivalent to setjmp (or throw / catch) if you want asynchronous interrupts, you’ve got to have an equivalent to signals.


> So if you want unwinding, you’ve got to have an equivalent to setjmp (or throw / catch)

Unwinding is a somewhat cleaner interface than setjmp/longjmp, and it's a useful primitive in its own right, especially if you can attach a notion of a stack trace to the unwind primitive. At a low-level language primitive level, unwind can be seen essentially as a combination of a mechanism to have two distinct exit points from a function call (the normal and exceptional return addresses) and an intrinsic that causes the function to return via its exceptional return address instead of the normal return address.

> if you want asynchronous interrupts, you’ve got to have an equivalent to signals.

... not really. There's a few different kind of signals. Synchronous processor signals (e.g., SIGSEGV, SIGFPE) could be supported via something akin to unwind rather than signal handlers (and this is essentially how they work on Windows). The asynchronous events can instead be supported by basically having some primitive notion of an event loop, dropping those signals as events in the event loop. Signals already interoperate pretty sketchily with event loops, and many signal handlers for asynchronous events already tend to boil down to "just dump this as an event in the event loop", so you're left with a pretty sketchy work around to do what you actually wanted to do.

Interfaces like fork and POSIX signals are pretty notorious for being generally wrong ways to do what you actually wanted to do; if you're making a new OS ABI, there's absolutely no reason to include them, especially because they can be hard to emulate on some OSes, e.g., Windows or Fuchsia (which lacks POSIX signals altogether).


>> So if you want unwinding, you’ve got to have an equivalent to setjmp (or throw / catch)

> Unwinding is a somewhat cleaner interface than setjmp/longjmp, and it's a useful primitive in its own right

I think the distinction is becoming subtle enough that we need to define our terms here. I’d distinguish three mutually interexpressible (is that a word?) systems:

(1) Marks (for lack of a better term): C. There’s an object that designates an activation (jmp_buf) that you can ask for (setjmp), unwind to (longjmp), and must not let escape. Upside: disjoint error filters do not require cross-ABI cooperation. Downside: finalizers do.

(2) Panics: Forth ’94, Lua, Go. There is a singular dynamically scoped recovery point where you can unwind and pass some data, which it will be able to rethrow further if it wishes. Upside: finalizers do not require cross-ABI cooperation (unless you hardwire backtraces). Downside: disjoint error filters do.

(3) Exceptions: C++, Java, arguably Common Lisp THROW/CATCH as a degenerate (no-subtyping) case. There is a single system of errors mandated by the platform (or language, as the case may be) and a stack of handlers of two types: catch, associated with an error type, which stops propagation of its subtypes and runs user code; finally, not associated with anything, which suspends propagation of anything, runs user code, then resumes propagation. Upside: implements the semantics of C++ directly in the system. Downside: anybody who wants anything else will have to fight the system.

(4) Filters: Win32 SEH (both old- and new-style), Itanium ABI (IIUC). Basically (3) but a catch-type handler is matched by running user code, either in the middle of an unwind (simpler) or before it (does not destroy stack traces). Upside: you get some degree of language interop compared to (3). Downside: the interop is still anemic compared to the complexity.

(4a) Declarative (4): Win32 SEH (new-style), Itanium ABI. Upside: security (?), speed (?). Downside: uninvolved code (C, JIT) needs to get involved; better get your stack maps right the first time (*cough* Itanium’s 30% size overhead *cough*).

To me, only (1) and (2) sound like real options on the VM level. If what you mean by “unwinding” instead of longjmp is (2) instead of (1), I’d say the advantages and disadvantages here are basically mirror images of one another and so far I can’t see a reason to prefer either. If you mean to replace (1) with something else, please elaborate—my options (3) and (4) are somewhat fuzzy and possibly nonexhaustive.

> especially if you can attach a notion of a stack trace to the unwind primitive.

As a registered Conditionist[1,2] who thinks all systems of non-resumable exceptions suck, I take exception to this (pun not intended). More directly, if you’re going to turn SIGFPE or SIGILL or SIGTRAP into exceptions, the debugger / crash recorder / etc. needs a stack trace available on an ununwound stack, and those tools are the only consumers of a stack trace in non-insane production code anyway.

See also the part about the stack map format in (4a).

So far, then, I feel that the stack trace tie-in is just not a good idea.

> At a low-level language primitive level, unwind can be seen [as] a mechanism to have [...] normal and exceptional return addresses[.]

I’m unaware of double-barrelled CPS being used as anything but a theoretical tool, but it would be interesting to see. [This is the part that made me think you meant (2) by “unwinding”, as it fits this model most naturally.]

>> if you want asynchronous interrupts, you’ve got to have an equivalent to signals.

> ... not really. There's a few different kind of signals. Synchronous processor signals (e.g., SIGSEGV, SIGFPE) could be supported via something akin to unwind rather than signal handlers (and this is essentially how they work on Windows).

Which is why I said “asynchronous” :) (in part as a reference to VMS / NT APCs). But OK, let’s have a digression: I agree that SEH is better than POSIX signals for synchronous processor traps, with two caveats:

- Whatever you replace a SIGILL handler with must be able to resume instead of unwinding, as the use case might be instruction or syscall emulation (SEH can do this, but your proposal doesn’t mention it);

- Whatever you replace a SIGSEGV handler with must also be able to resume and will probably find the memory location more interesting than the call stack (this is a problem with POSIX signals[3] that Linux solves with userfaultfd, and also a problem with SEH which Windows solves with an inferior hack[4]).

(End of digression.)

> The asynchronous events can instead be supported by basically having some primitive notion of an event loop, dropping those signals as events in the event loop.

I don’t expect I want to live in your proposed world—in an event loop, the flow controls you—but I probably want to see this world in more detail first.

Your mental model for async notifications also seems to be an I/O-bound program that wants to have the environment notify it about I/O. So had mine been—and then I wrote a CPU emulator, which is by necessity CPU-bound and cannot (non-hackily) afford to check for events with a syscall but still wants to hear about them, and suddenly SIGIO was looking mightily attractive.

This is niche. It is also solvable by having a second thread run select() and set the same flag my SIGIO handler currently uses and the emulation loop polls. A case where you wanted to gracefully terminate a thread, though (a case that I have admittedly not yet encountered, and that is even more niche), would require nothing less than an actual signal.

Again, niche. I’m not at all advocating for using signals everywhere POSIX does. But it’s not hard to think of cases where nothing will do but a notification that can actually interrupt your instructions and not just system calls. Maybe such cases are too obscure to handle (Java certainly seems unhappy with asynchronous thread termination), but “YAGNI signals” is a different argument from “signals evil”.

> Signals already interoperate pretty sketchily with event loops

AFAICS this is a problem of Unix getting select() and threads too late and the retrofit being subpar, not an inherent flaw. (And once again, I entirely agree that most POSIX signals should be turned into other kinds of notifications... It’s just that not all of them can.)

> Interfaces like fork and POSIX signals are pretty notorious for being generally wrong ways to do what you actually wanted to do [...].

Signals, see above. My argument about fork was very different: I don’t like it; I just don’t think it‘s guilty of GGP‘s specific charge of being error-prone—certainly not more so than shared-memory concurrency.

[1] https://news.ycombinator.com/item?id=31196046

[2] https://news.ycombinator.com/item?id=33983418

[3] https://sourceware.org/legacy-ml/libc-alpha/2018-03/msg00214...

[4] http://bytepointer.com/resources/pietrek_vectored_exception_...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: