@amritk/nish-x86_64-linux 0.17.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +231 -20
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +1376 -42
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1412 -70
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -0,0 +1,366 @@
1
+ /* Nish runtime, the copy-at-join half: how a string or an array of scalars
2
+ * crosses from one thread's arena to another's, and how a `Mutex` keeps the
3
+ * strings stored through its guard.
4
+ *
5
+ * A translation unit of its own rather than more of runtime-parallel.c, for
6
+ * the reason that file's header gives for being one: each file carries its own
7
+ * measured `.text*` ceiling in tests/runtime-budgets.js, and this one would not
8
+ * fit in what runtime-parallel.c had left (RUNTIME_CROSS_TEXT_BUDGET says how
9
+ * much it needs). Apart, a program that divides a range of numbers links none
10
+ * of this, and section GC drops what a program that crosses no string calls.
11
+ *
12
+ * A task's or a chunk's arena is rewound once its job is done
13
+ * (`nish_pool_drain`, and the release the compiler emits after a task or a
14
+ * chunk returns), so a string or an array it answers, sends or stores through
15
+ * a guard cannot stay where it was made. `nish_cross_out` runs on the thread
16
+ * that made the value: one that was bumped in its arena since `mark` (0: at
17
+ * any time) is copied into a block of its own, outside every arena, and
18
+ * answered with its low bit set. Any other value -- a literal, or one the
19
+ * parent made, which outlives the join -- is answered as it is, so a task that
20
+ * answers its parent's array answers that array, as it does under Node.
21
+ * `nish_cross_send` is a channel's: it copies every string, which costs the
22
+ * string's length and nothing that grows with the sender's arena.
23
+ * `nish_cross_in` runs on the thread that takes the value: a tagged one is
24
+ * copied into its arena and its block freed, or, when it is large, its block
25
+ * becomes a chunk of that arena, so every block is either freed by the one
26
+ * `cross_in` its value meets or freed with the arena that adopted it. A map's
27
+ * elements do not go one block each: a chunk packs them into blocks of its
28
+ * own (`nish_cross_keep`), which the caller adopts whole, or copies in and
29
+ * frees when they hold little (`nish_cross_adopt`). Arena blocks,
30
+ * literals and the copies here are all 8-aligned, which is what keeps the low
31
+ * bit free for the tag.
32
+ *
33
+ * `esize` is 0 for a string and the element's size for an array of scalars,
34
+ * which holds no pointer, so one flat copy of its header and its elements is
35
+ * the whole value. A panic ends the process, and every block with it.
36
+ *
37
+ * It compiles in both configurations, as runtime-parallel.c does: a task run
38
+ * on the calling thread at the join is rewound all the same, so the copies are
39
+ * needed whether or not there are threads; only the guard set's lock is
40
+ * `-DNISH_THREADS` alone. */
41
+ #include "nish.h"
42
+
43
+ #include <stddef.h>
44
+ #include <stdlib.h>
45
+ #include <string.h>
46
+ #include <unistd.h>
47
+
48
+ #if defined(NISH_THREADS) && !defined(__wasi__) && !defined(__wasm__)
49
+ #define NISH_CROSS_LOCKED 1
50
+ #include <pthread.h>
51
+ #endif
52
+
53
+ /* runtime.c's `nish_chunk`, which nish.h leaves opaque (`nish_arena.chunks` is
54
+ * a `void *` there). runtime.c asserts the same three facts of its own
55
+ * definition, so a change to either side fails to compile until the other
56
+ * follows. */
57
+ typedef struct nish_xchunk { struct nish_xchunk *next; uint64_t cap; } nish_xchunk;
58
+ _Static_assert(sizeof(nish_xchunk) == 16, "runtime.c's nish_chunk: { next, cap }, the bytes after it");
59
+ _Static_assert(offsetof(nish_xchunk, next) == 0, "runtime.c's nish_chunk: next first");
60
+ _Static_assert(offsetof(nish_xchunk, cap) == 8, "runtime.c's nish_chunk: cap second");
61
+
62
+ /* The end of the program when a copy cannot be allocated: nothing that
63
+ * crosses has a way to go on without the memory. */
64
+ static void nish_cross_oom(void) {
65
+ nish_panic_claim();
66
+ (void)!write(2, "nish: out of memory\n", 20);
67
+ _exit(1);
68
+ }
69
+
70
+ static void *nish_cross_block(uint64_t size) {
71
+ void *b = size <= (uint64_t)SIZE_MAX ? malloc((size_t)size) : 0;
72
+ if (!b) nish_cross_oom();
73
+ return b;
74
+ }
75
+
76
+ /* Whether `p` was bumped since `mark`: one walk of the chunks, for a single
77
+ * value. */
78
+ static int nish_since(uint64_t mark, uintptr_t p) {
79
+ for (const nish_xchunk *c = (const nish_xchunk *)nish_arena.chunks; c; c = c->next) {
80
+ uintptr_t base = (uintptr_t)(c + 1), end = base + c->cap;
81
+ int marked = mark >= base && mark <= end;
82
+ if (p >= (marked ? (uintptr_t)mark : base) && p < end) return 1;
83
+ if (marked) return 0;
84
+ }
85
+ return 0;
86
+ }
87
+
88
+ /* The bytes `v` takes, rounded to 8: a string's header, bytes and NUL, or an
89
+ * array's header and elements. */
90
+ static uint64_t nish_cross_size(const void *v, uint64_t esize) {
91
+ uint64_t n = esize ? sizeof(nish_array) + ((const nish_array *)v)->len * esize
92
+ : 8 + ((const nish_str *)v)->len + 1;
93
+ return (n + 7) & ~(uint64_t)7;
94
+ }
95
+
96
+ /* `v` copied to `at`, flat (an array's elements follow its header). */
97
+ static void nish_cross_put(char *at, const void *v, uint64_t esize) {
98
+ if (esize == 0) {
99
+ memcpy(at, v, (size_t)(8 + ((const nish_str *)v)->len + 1));
100
+ } else {
101
+ const nish_array *a = (const nish_array *)v;
102
+ nish_array *c = (nish_array *)at;
103
+ c->len = c->cap = a->len;
104
+ c->data = (char *)(c + 1);
105
+ if (a->len) memcpy(c->data, a->data, (size_t)(a->len * esize));
106
+ }
107
+ }
108
+
109
+ /* `n` bytes after a chunk header, so the block can become a chunk of the arena
110
+ * that takes it: runtime.c's `nish_free_until` frees it as it frees one
111
+ * `nish_arena_grow` made. */
112
+ static nish_xchunk *nish_cross_chunk(uint64_t n) {
113
+ nish_xchunk *c = (nish_xchunk *)nish_cross_block(sizeof *c + n);
114
+ c->cap = n;
115
+ return c;
116
+ }
117
+
118
+ /* Links the block `c` into this thread's arena as its newest chunk, and bumps
119
+ * from it, full to its `cap`. A chunk grown here would
120
+ * sit in the same place, so a release, a keep and a reset treat these as they
121
+ * treat it: whatever rewinds the arena past this point frees them. */
122
+ static void nish_cross_link(nish_xchunk *c) {
123
+ c->next = (nish_xchunk *)nish_arena.chunks;
124
+ nish_arena = (struct nish_arena){(char *)(c + 1), c->cap, c->cap, c};
125
+ }
126
+
127
+ /* `v` in a block of its own, tagged. */
128
+ static void *nish_cross_copy(const void *v, uint64_t esize) {
129
+ nish_xchunk *c = nish_cross_chunk(nish_cross_size(v, esize));
130
+ nish_cross_put((char *)(c + 1), v, esize);
131
+ return (void *)((uintptr_t)(c + 1) | 1);
132
+ }
133
+
134
+ void *nish_cross_out(void *v, uint64_t esize, uint64_t mark) {
135
+ return v && nish_since(mark, (uintptr_t)v) ? nish_cross_copy(v, esize) : v;
136
+ }
137
+
138
+ void *nish_cross_send(void *s) { return nish_cross_copy(s, 0); }
139
+
140
+ /* The transfer state of the chunk this thread is running
141
+ * (`nish_cross_begin`): the list its blocks go on, the block being filled and
142
+ * how much of it is, and the state it hides, which `nish_cross_end` restores
143
+ * -- a map nested in a chunk's body runs its own chunks inside it. A map's
144
+ * sequential path begins with no list, which hides the outer state without
145
+ * pushing one, so its elements are not packed into the outer chunk's block. */
146
+ typedef struct nish_xfer {
147
+ void **list;
148
+ nish_xchunk *block;
149
+ uint64_t off;
150
+ struct nish_xfer *prev;
151
+ } nish_xfer;
152
+ _Static_assert(sizeof(nish_xfer) == (sizeof(void *) == 8 ? 32 : 24),
153
+ "src/emit-parallel.ts allocas the state as { i8*, i8*, i64, i8* }");
154
+ static NISH_TLS nish_xfer *nish_xfer_top;
155
+
156
+ void nish_cross_begin(void *state, void **list) {
157
+ nish_xfer *x = (nish_xfer *)state;
158
+ *x = (nish_xfer){list, 0, 0, nish_xfer_top};
159
+ nish_xfer_top = list ? x : 0;
160
+ }
161
+
162
+ /* The block being filled records how much of it is, as its `cap`, so the
163
+ * join reads what each chunk kept and an adopted block is full where it ends. */
164
+ void nish_cross_end(void *state) {
165
+ nish_xfer *x = (nish_xfer *)state;
166
+ if (x->block) x->block->cap = x->off;
167
+ nish_xfer_top = x->prev;
168
+ }
169
+
170
+ /* `nish_arena_release(mark)` for an element's rewind, but keeping the newest
171
+ * chunk when the body grew it past `mark`'s: a mark at the end of a full chunk
172
+ * would otherwise have every element's body malloc a fresh chunk and its
173
+ * release free it again. Only a 64 KB one is kept, emptied, in front of
174
+ * `mark`'s chunk, with those between freed; an older mark still finds its own
175
+ * chunk below it, and what `mark`'s chunk had left is given up once. A mark
176
+ * in no chunk below the newest leaves the arena as it was, as the release
177
+ * does. */
178
+ static void nish_rewind(uint64_t mark) {
179
+ struct nish_arena *a = &nish_arena;
180
+ nish_xchunk *top = a->chunks;
181
+ char *buf = (char *)(top + 1);
182
+ if (mark && top && top->cap == 65536 && mark - (uintptr_t)buf > 65536) {
183
+ a->chunks = top->next;
184
+ nish_arena_release(mark);
185
+ if (a->buf != buf) {
186
+ top->next = a->chunks;
187
+ *a = (struct nish_arena){ buf, 0, 65536, top };
188
+ } else
189
+ a->chunks = top;
190
+ return;
191
+ }
192
+ nish_arena_release(mark);
193
+ }
194
+
195
+ /* A map's or a `parallelFrom`'s element, kept as its chunk loop rewinds the
196
+ * arena to `mark`, the bump position before the body was called: everything
197
+ * else the body bumped is dead once it has answered, so a chunk holds one
198
+ * element's temporaries at a time rather than all of its elements'. A value
199
+ * made since `mark` is copied, in a chunk of a divided region, to the end of
200
+ * the chunk's transfer block -- a new one, pushed on the region's list when it
201
+ * does not fit, of 4 KB doubling to 64 KB or of the value's size, so what a
202
+ * chunk leaves unused is bounded by what it keeps -- and is answered there;
203
+ * the caller adopts the blocks at the join (`nish_cross_adopt`). On a map's
204
+ * sequential path, where no state is pushed, it goes out to a block of its own
205
+ * and back in below `mark`. One made before `mark` is answered as it is. */
206
+ void *nish_cross_keep(void *v, uint64_t esize, uint64_t mark) {
207
+ nish_xfer *x = nish_xfer_top;
208
+ if (v && nish_since(mark, (uintptr_t)v)) {
209
+ if (!x) {
210
+ v = nish_cross_copy(v, esize);
211
+ } else {
212
+ uint64_t n = nish_cross_size(v, esize);
213
+ if (!x->block || x->block->cap - x->off < n) {
214
+ uint64_t want = x->block && x->block->cap < 32768 ? x->block->cap * 2 : x->block ? 65536 : 4096;
215
+ x->block = nish_cross_chunk(n > want ? n : want);
216
+ x->block->next = __atomic_exchange_n((nish_xchunk **)x->list, x->block, __ATOMIC_RELAXED);
217
+ x->off = 0;
218
+ }
219
+ char *at = (char *)(x->block + 1) + x->off;
220
+ x->off += n;
221
+ nish_cross_put(at, v, esize);
222
+ v = at;
223
+ }
224
+ }
225
+ nish_rewind(mark);
226
+ return nish_cross_in(v, esize);
227
+ }
228
+
229
+ /* A value of 64 KB or more that does not fit what is left of the current
230
+ * chunk is adopted rather than copied: `nish_arena_grow` would have made a
231
+ * chunk of exactly its size for the copy, and the block is that chunk
232
+ * already. A smaller one is copied, so a run of small values never makes a
233
+ * chunk each, which a release would walk. */
234
+ void *nish_cross_in(void *p, uint64_t esize) {
235
+ if (!((uintptr_t)p & 1)) return p;
236
+ void *v = (void *)((uintptr_t)p & ~(uintptr_t)1);
237
+ nish_xchunk *c = (nish_xchunk *)v - 1;
238
+ uint64_t n = c->cap;
239
+ if (n >= 65536 && n > nish_arena.cap - nish_arena.off) {
240
+ nish_cross_link(c);
241
+ return v;
242
+ }
243
+ char *r = (char *)nish_alloc_struct(n);
244
+ nish_cross_put(r, v, esize);
245
+ free(c);
246
+ return r;
247
+ }
248
+
249
+ /* Once every chunk is done, on the thread that called the region, with `dst`
250
+ * the array the region wrote. When the blocks the chunks filled hold 64 KB or
251
+ * more, they become the newest chunks of its arena (`nish_cross_link`), so
252
+ * the elements in them stay where they are, and the cost is one link per
253
+ * block rather than a copy per element. Less is copied, element by element,
254
+ * into the arena and the blocks freed, as `nish_cross_in` copies a small
255
+ * value: an adopted block leaves the arena's current chunk behind, and a
256
+ * region run in a loop would leave one per run. */
257
+ void nish_cross_adopt(void *list, void *dst, uint64_t esize) {
258
+ nish_xchunk *first = (nish_xchunk *)list, *c, *next;
259
+ uint64_t kept = 0;
260
+ for (c = first; c; c = c->next) kept += c->cap;
261
+ if (kept >= 65536) {
262
+ for (c = first; c; c = next) {
263
+ next = c->next;
264
+ nish_cross_link(c);
265
+ }
266
+ return;
267
+ }
268
+ if (!first) return;
269
+ nish_array *a = (nish_array *)dst;
270
+ void **e = (void **)a->data;
271
+ for (uint64_t i = 0; i < a->len; i++) {
272
+ for (c = first; c; c = c->next) {
273
+ if ((uintptr_t)e[i] - (uintptr_t)(c + 1) < c->cap) {
274
+ char *r = (char *)nish_alloc_struct(nish_cross_size(e[i], esize));
275
+ nish_cross_put(r, e[i], esize);
276
+ e[i] = r;
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ for (c = first; c; c = next) {
282
+ next = c->next;
283
+ free(c);
284
+ }
285
+ }
286
+
287
+ /* ---- A string a guard stores ----
288
+ *
289
+ * `g.value = s` keeps `s` in memory the `Mutex` owns, since the task that
290
+ * stores it may have made it in its arena: `nish_guard_keep` copies it into a
291
+ * block of its own and answers the copy, and frees `old`, the string the slot
292
+ * held, when it is a copy made here; `g.value = null` into a `string | null`
293
+ * slot frees that copy and keeps nothing. A read that keeps the string past the
294
+ * lock's block copies it into the reader's arena (the compiler calls
295
+ * `nish_str_new`), so nothing outside the lock points at a block, and the
296
+ * next store frees it. The copies are recorded in one set, `nish_kept`, which
297
+ * is how `old` is told apart from the string `new Mutex` was given or a
298
+ * literal: those are never freed. A block still held when its `Mutex` is
299
+ * dropped stays allocated until the process ends -- one per string slot of
300
+ * the `Mutex`, not one per store. The set is one for the process, behind one
301
+ * lock of its own, since two `Mutex`es are locked by two tasks at once; it is
302
+ * taken only by a store, so stores through two different guards wait on each
303
+ * other for the moment it is held. The set holds each block's header, the
304
+ * pointer `malloc` answered, so a leak checker counts a block a dropped `Mutex`
305
+ * left as reachable rather than as possibly lost. */
306
+ static struct {
307
+ #ifdef NISH_CROSS_LOCKED
308
+ pthread_mutex_t mu;
309
+ #endif
310
+ uintptr_t *slots; /* open addressing; 0 empty, 1 a removed entry */
311
+ uint64_t cap, used;
312
+ } nish_kept = {
313
+ #ifdef NISH_CROSS_LOCKED
314
+ PTHREAD_MUTEX_INITIALIZER,
315
+ #endif
316
+ 0, 0, 0};
317
+
318
+ static uint64_t nish_kept_at(uintptr_t p) { return (uint64_t)((p >> 3) * 0x9E3779B97F4A7C15ull); }
319
+
320
+ static void nish_kept_put(uintptr_t p) {
321
+ if ((nish_kept.used + 1) * 2 > nish_kept.cap) {
322
+ uintptr_t *old = nish_kept.slots;
323
+ uint64_t n = nish_kept.cap;
324
+ nish_kept.cap = n ? n * 2 : 64;
325
+ nish_kept.slots = (uintptr_t *)calloc((size_t)nish_kept.cap, sizeof(uintptr_t));
326
+ if (!nish_kept.slots) nish_cross_oom();
327
+ nish_kept.used = 0;
328
+ for (uint64_t i = 0; i < n; i++)
329
+ if (old[i] > 1) nish_kept_put(old[i]);
330
+ free(old);
331
+ }
332
+ uint64_t i = nish_kept_at(p) & (nish_kept.cap - 1);
333
+ while (nish_kept.slots[i] > 1) i = (i + 1) & (nish_kept.cap - 1);
334
+ if (nish_kept.slots[i] == 0) nish_kept.used++;
335
+ nish_kept.slots[i] = p;
336
+ }
337
+
338
+ /* Whether `p` is in the set, taking it out when it is. */
339
+ static int nish_kept_take(uintptr_t p) {
340
+ if (!nish_kept.cap) return 0;
341
+ uint64_t i = nish_kept_at(p) & (nish_kept.cap - 1);
342
+ for (; nish_kept.slots[i] != 0; i = (i + 1) & (nish_kept.cap - 1)) {
343
+ if (nish_kept.slots[i] == p) {
344
+ nish_kept.slots[i] = 1;
345
+ return 1;
346
+ }
347
+ }
348
+ return 0;
349
+ }
350
+
351
+ /* `old` is null when the slot is a `string | null` that held none, and `s`
352
+ * is null for a `null` stored into one: the slot's copy is freed and nothing
353
+ * is kept in its place, so the store answers null. */
354
+ void *nish_guard_keep(void *old, const void *s) {
355
+ void *b = s ? (void *)((uintptr_t)nish_cross_copy(s, 0) & ~(uintptr_t)1) : 0;
356
+ #ifdef NISH_CROSS_LOCKED
357
+ pthread_mutex_lock(&nish_kept.mu);
358
+ #endif
359
+ int mine = old && nish_kept_take((uintptr_t)((nish_xchunk *)old - 1));
360
+ if (b) nish_kept_put((uintptr_t)((nish_xchunk *)b - 1));
361
+ #ifdef NISH_CROSS_LOCKED
362
+ pthread_mutex_unlock(&nish_kept.mu);
363
+ #endif
364
+ if (mine) free((nish_xchunk *)old - 1);
365
+ return b;
366
+ }
@@ -5,12 +5,12 @@
5
5
  * before it trusts one.
6
6
  *
7
7
  * A translation unit of its own for the reason runtime-os.c is one: each file
8
- * carries its own measured `.text*` ceiling in tests/run.js. runtime-os.c had
9
- * 143 bytes of its ceiling left when these arrived, the four measure more than
10
- * that, and a ceiling is not raised to make room (docs/MASTER_PLAN.md §2 has
11
- * the numbers). Section GC still means a program that calls none of them pays
12
- * for none of them, and scripts/build.sh pairs this file with runtime.c like
13
- * the other two halves, so a link line still names one runtime.
8
+ * carries its own measured `.text*` ceiling in tests/runtime-budgets.js.
9
+ * runtime-os.c had 143 bytes of its ceiling left when these arrived, the
10
+ * four measure more than that, and a ceiling is not raised to make room
11
+ * (docs/MASTER_PLAN.md §2 has the numbers). Section GC still means a program that
12
+ * calls none of them pays for none of them, and scripts/build.sh pairs this file
13
+ * with runtime.c like the other two halves, so a link line still names one runtime.
14
14
  *
15
15
  * Every function here is platform code, and the two platforms differ in three
16
16
  * places: `getrandom` on Linux and `getentropy` elsewhere, `st_mtim`
@@ -57,6 +57,7 @@ double nish_date_now(void) {
57
57
  }
58
58
 
59
59
  static NISH_COLD void nish_entropy_fail(uint64_t asked) {
60
+ nish_panic_claim();
60
61
  if (asked > 65536) {
61
62
  dprintf(2, "crypto.getRandomValues: %llu bytes asked for, and one call fills at most 65536\n",
62
63
  (unsigned long long)asked);
@@ -5,9 +5,9 @@
5
5
  * `spawnSync` starts inherits none of them.
6
6
  *
7
7
  * A translation unit of its own for the reason runtime-host.c is one: each
8
- * file carries its own measured `.text*` ceiling in tests/run.js, and sockets
9
- * are a surface that grows, which would push another unit past its ceiling
10
- * rather than into a new one. Section GC
8
+ * file carries its own measured `.text*` ceiling in tests/runtime-budgets.js,
9
+ * and sockets are a surface that grows, which would push another unit past its
10
+ * ceiling rather than into a new one. Section GC
11
11
  * means a program that calls none of these pays for none of them, and
12
12
  * scripts/build.sh pairs this file with runtime.c like the other halves, so a
13
13
  * link line still names one runtime.
@@ -39,7 +39,8 @@
39
39
  * receive leaves `meta` at 0, 0, and a `meta[1]` of 0 is also what a real
40
40
  * Not-ECT datagram reads as, so a program learns that Darwin has no ECN from
41
41
  * the send, never from the receive. The Darwin branch is compiled by CI's Darwin bootstrap
42
- * rows and run by nothing. A WASI build has none of this (the checker refuses
42
+ * rows and run by its `runtime-net (macos-latest)` job: tests/runtime-test.c and
43
+ * the `net_` sections of tests/run.js, on a mac. A WASI build has none of this (the checker refuses
43
44
  * every `nish:net` call under a wasm target), so there the file is empty.
44
45
  */
45
46
  #if defined(__wasi__) || defined(__wasm__)
@@ -54,6 +55,7 @@ typedef int nish_net_unused;
54
55
  #include <errno.h>
55
56
  #include <fcntl.h>
56
57
  #include <netinet/in.h>
58
+ #include <netinet/tcp.h>
57
59
  #include <stdint.h>
58
60
  #include <string.h>
59
61
  #include <sys/socket.h>
@@ -188,10 +190,11 @@ int32_t nish_net_local_port(int32_t fd) {
188
190
  IPv4 host is an `AF_INET` socket; any other is `AF_INET6` with
189
191
  `IPV6_V6ONLY` off, so `::` hears both families. A stream gets
190
192
  `SO_REUSEADDR` and `arg` its backlog; a datagram socket's `arg` is
191
- `udpBind`'s flags. Every option is set before the bind, which is where
192
- `SO_REUSEPORT` has to be, and where `UDP_GRO` has to be for the first
193
- datagram to be coalesced. */
194
- static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int32_t arg) {
193
+ `udpBind`'s flags. `opts` is a stream's `tcpListenFlags` word: bit 1 asks
194
+ for `SO_REUSEPORT`, bit 2 for `TCP_NODELAY`. Every option is set before the
195
+ bind, which is where `SO_REUSEPORT` has to be, and where `UDP_GRO` has to be
196
+ for the first datagram to be coalesced. */
197
+ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int32_t arg, int32_t opts) {
195
198
  unsigned char a[16];
196
199
  if ((uint32_t)port > 65535 || !nish_net_parse(host, a)) return -22;
197
200
  int v4 = memcmp(a, nish_mapped_prefix, 12) == 0;
@@ -209,11 +212,11 @@ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int3
209
212
  /* Both families at once on an IPv6 socket; on an IPv4 one this fails with
210
213
  `ENOPROTOOPT` and changes nothing, which is cheaper than asking. */
211
214
  setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof zero);
212
- int bad = 0;
215
+ int bad = (opts & 1) ? setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one) : 0;
213
216
  if (type == SOCK_STREAM) {
214
217
  setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
218
+ if (opts & 2) bad |= setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
215
219
  } else {
216
- if (arg & 1) bad = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
217
220
  #if defined(__linux__)
218
221
  if (arg & 2) bad |= setsockopt(fd, IPPROTO_UDP, UDP_GRO, &one, sizeof one);
219
222
  /* ECN is always read. An IPv6 socket needs both: a datagram from an IPv4
@@ -236,7 +239,17 @@ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int3
236
239
  so a restarted server rebinds a port whose old connections are still in
237
240
  TIME_WAIT. */
238
241
  int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
239
- return nish_net_bound(host, port, SOCK_STREAM, backlog);
242
+ return nish_net_bound(host, port, SOCK_STREAM, backlog, 0);
243
+ }
244
+
245
+ /* `tcpListenFlags(host, port, backlog, flags)`: `tcpListen`'s socket, with
246
+ `SO_REUSEPORT` for flag 1, so several listeners of one uid share the port
247
+ (Linux spreads new connections across them; Darwin hands them all to one),
248
+ and `TCP_NODELAY` for flag 2, which the listener carries so that
249
+ `tcpAccept` sets it on each connection it takes. Any other bit is -22. */
250
+ int32_t nish_tcp_listen_flags(const nish_str *host, int32_t port, int32_t backlog, int32_t flags) {
251
+ if ((uint32_t)flags > 3) return -22;
252
+ return nish_net_bound(host, port, SOCK_STREAM, backlog, flags);
240
253
  }
241
254
 
242
255
  /* A socket address of `n` bytes into the caller's 18-byte form. An IPv4 peer
@@ -264,7 +277,18 @@ static void nish_net_put(nish_array *out, const nish_sockaddr *s, socklen_t n) {
264
277
  /* `tcpAccept(fd, peer)`: the next connection as a descriptor of its own,
265
278
  non-blocking and close-on-exec, with the peer's address written into
266
279
  `peer`; -11 when none is waiting. A `peer` shorter than 18 bytes is -22
267
- before anything is accepted, so no connection is taken and lost. */
280
+ before anything is accepted, so no connection is taken and lost.
281
+
282
+ A listener opened with `tcpListenFlags` flag 2 holds `TCP_NODELAY`, and the
283
+ connection gets it here. Linux copies the option to an accepted socket,
284
+ but that is not a rule other systems keep, so it is read off the listener
285
+ and set on the connection everywhere. The listener is the record of the
286
+ flag, rather than a table beside the descriptors, so it holds for a
287
+ descriptor however the program came by it, and a closed listener's number
288
+ reused leaves nothing stale behind. A listener that cannot answer (not
289
+ TCP) has no Nagle to turn off; a connection on which the set fails is
290
+ closed and its failure answered, so a flag-2 listener never hands out a
291
+ socket without it. */
268
292
  int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
269
293
  if (peer->len < NISH_ADDR_BYTES) return -22;
270
294
  nish_sockaddr s;
@@ -275,6 +299,14 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
275
299
  int c = nish_net_setup(accept(fd, &s.sa, &n));
276
300
  #endif
277
301
  if (c < 0) return nish_net_fail();
302
+ int nodelay = 0;
303
+ socklen_t size = sizeof nodelay;
304
+ if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, &size) == 0 && nodelay != 0 &&
305
+ setsockopt(c, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof nodelay) != 0) {
306
+ int e = errno;
307
+ close(c);
308
+ return nish_net_err(e);
309
+ }
278
310
  nish_net_put(peer, &s, n);
279
311
  return c;
280
312
  }
@@ -348,7 +380,7 @@ int32_t nish_udp_bind(const nish_str *host, int32_t port, int32_t flags) {
348
380
  #if !defined(__linux__)
349
381
  if (flags & 2) return -95;
350
382
  #endif
351
- return nish_net_bound(host, port, SOCK_DGRAM, flags);
383
+ return nish_net_bound(host, port, SOCK_DGRAM, flags, flags & 1);
352
384
  }
353
385
 
354
386
  /* Room for the two control messages either direction carries: a segment size
@@ -503,11 +535,15 @@ static int nish_poll_change(int32_t loop, int32_t fd, int filter, int flags, int
503
535
 
504
536
  /* Add, modify or remove `fd` in `loop`. epoll does each in one call, with
505
537
  the token in `data.u32`. kqueue has a filter per direction rather than a
506
- set of events, so a change is the filters deleted and then the ones asked
507
- for added, each carrying the token in `udata`. Deleting a filter that was
508
- never added is not an error, except to a removal that finds neither, which
509
- answers epoll's ENOENT; a modification adds what it asks for either way, so
510
- a descriptor added watching nothing, which has no filter, can be modified. */
538
+ set of events, so the answers epoll gives are built from filters: a
539
+ descriptor in the loop has at least one, a disabled read filter standing for
540
+ one added watching nothing; an add first touches both filters with no flags,
541
+ which answers ENOENT for a filter not there, and is EEXIST when either is;
542
+ a modification or a removal deletes both, and is ENOENT when neither was
543
+ there; a modification then adds the ones asked for, each carrying the token
544
+ in `udata`. A touch rewrites the filter's `udata`, and the add touches both
545
+ filters, so the add that answers -17 leaves the descriptor watched for its
546
+ old events, both ways, under the token it gave rather than the old one. */
511
547
  static int32_t nish_poll_ctl(int32_t loop, int op, int32_t fd, int32_t events, int32_t token) {
512
548
  if ((uint32_t)events > 3) return -22;
513
549
  #if defined(__linux__)
@@ -517,14 +553,23 @@ static int32_t nish_poll_ctl(int32_t loop, int op, int32_t fd, int32_t events, i
517
553
  e.data.u32 = (uint32_t)token;
518
554
  return epoll_ctl(loop, op, fd, &e) != 0 ? nish_net_fail() : 0;
519
555
  #else
520
- int e = 0;
521
- if (op != NISH_POLL_ADD) {
522
- int r = nish_poll_change(loop, fd, EVFILT_READ, EV_DELETE, 0);
523
- int w = nish_poll_change(loop, fd, EVFILT_WRITE, EV_DELETE, 0);
524
- if (op == NISH_POLL_REMOVE) return r != 0 && w != 0 ? nish_net_err(r) : 0;
556
+ int r, w;
557
+ if (op == NISH_POLL_ADD) {
558
+ r = nish_poll_change(loop, fd, EVFILT_READ, 0, token);
559
+ w = nish_poll_change(loop, fd, EVFILT_WRITE, 0, token);
560
+ if (r == 0 || w == 0) return -17;
561
+ if (r != ENOENT) return nish_net_err(r);
562
+ if (w != ENOENT) return nish_net_err(w);
563
+ } else {
564
+ r = nish_poll_change(loop, fd, EVFILT_READ, EV_DELETE, 0);
565
+ w = nish_poll_change(loop, fd, EVFILT_WRITE, EV_DELETE, 0);
566
+ if (r != 0 && w != 0) return nish_net_err(r);
567
+ if (op == NISH_POLL_REMOVE) return 0;
525
568
  }
569
+ int e = 0;
526
570
  if (events & 1) e = nish_poll_change(loop, fd, EVFILT_READ, EV_ADD | EV_ENABLE, token);
527
571
  if (e == 0 && (events & 2)) e = nish_poll_change(loop, fd, EVFILT_WRITE, EV_ADD | EV_ENABLE, token);
572
+ if (events == 0) e = nish_poll_change(loop, fd, EVFILT_READ, EV_ADD | EV_DISABLE, token);
528
573
  return e != 0 ? nish_net_err(e) : 0;
529
574
  #endif
530
575
  }
@@ -549,8 +594,7 @@ int32_t nish_poll_remove(int32_t loop, int32_t fd) { return nish_poll_ctl(loop,
549
594
  `NISH_POLL_CHUNK`. A signal that interrupts the wait answers 0, as a
550
595
  timeout does, rather than waiting again past the deadline; the program's
551
596
  loop comes round and, if it watches `signalFd()`, finds the signal ready.
552
- On Darwin a descriptor both readable and writable is two pairs with the
553
- same token, one per filter. A `ready` shorter than 2 is -22. */
597
+ A `ready` shorter than 2 is -22. */
554
598
  int32_t nish_poll_wait(int32_t loop, nish_array *ready, int32_t timeout_ms) {
555
599
  uint64_t room = ready->len / 2;
556
600
  if (room == 0) return -22;
@@ -560,22 +604,37 @@ int32_t nish_poll_wait(int32_t loop, nish_array *ready, int32_t timeout_ms) {
560
604
  struct epoll_event e[NISH_POLL_CHUNK];
561
605
  /* epoll waits forever for any negative timeout, as the language does. */
562
606
  int n = epoll_wait(loop, e, max, timeout_ms);
563
- #else
564
- struct kevent e[NISH_POLL_CHUNK];
565
- struct timespec t = {timeout_ms / 1000, (long)(timeout_ms % 1000) * 1000000};
566
- int n = kevent(loop, NULL, 0, e, max, timeout_ms < 0 ? NULL : &t);
567
- #endif
568
607
  if (n < 0) return errno == EINTR ? 0 : nish_net_fail();
569
608
  for (int i = 0; i < n; i++) {
570
- #if defined(__linux__)
571
609
  uint32_t got = e[i].events;
572
610
  out[2 * i] = (int32_t)e[i].data.u32;
573
611
  out[2 * i + 1] = ((got & EPOLLIN) ? 1 : 0) | ((got & EPOLLOUT) ? 2 : 0) | ((got & (EPOLLHUP | EPOLLERR)) ? 4 : 0);
612
+ }
613
+ return n;
574
614
  #else
575
- out[2 * i] = (int32_t)(intptr_t)e[i].udata;
576
- out[2 * i + 1] = (e[i].filter == EVFILT_READ ? 1 : 2) | ((e[i].flags & (EV_EOF | EV_ERROR)) ? 4 : 0);
577
- #endif
615
+ /* A record per filter, so a descriptor ready both ways is two, folded here
616
+ into one pair as epoll's one event is. Twice the room is asked for, so a
617
+ full `ready` is not cut short by the folding; a descriptor that finds no
618
+ pair free is still ready at the next wait. */
619
+ struct kevent e[NISH_POLL_CHUNK];
620
+ struct timespec t = {timeout_ms / 1000, (long)(timeout_ms % 1000) * 1000000};
621
+ int got = kevent(loop, NULL, 0, e, 2 * max < NISH_POLL_CHUNK ? 2 * max : NISH_POLL_CHUNK, timeout_ms < 0 ? NULL : &t);
622
+ if (got < 0) return errno == EINTR ? 0 : nish_net_fail();
623
+ int n = 0;
624
+ for (int i = 0; i < got; i++) {
625
+ int32_t bits = (e[i].filter == EVFILT_READ ? 1 : 2) | ((e[i].flags & (EV_EOF | EV_ERROR)) ? 4 : 0);
626
+ int k = 0;
627
+ while (k < n && e[k].ident != e[i].ident) k++;
628
+ if (k == n) {
629
+ if (n == max) continue;
630
+ e[n] = e[i];
631
+ out[2 * n] = (int32_t)(intptr_t)e[i].udata;
632
+ out[2 * n + 1] = 0;
633
+ n++;
634
+ }
635
+ out[2 * k + 1] |= bits;
578
636
  }
579
637
  return n;
638
+ #endif
580
639
  }
581
640
  #endif