@amritk/nish-x86_64-linux 0.30.0 → 0.32.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.
- package/bin/nish +0 -0
- package/docs/AI.md +66 -8
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -0
- package/runtime/core/host.mjs +9 -0
- package/runtime/nish.d.ts +48 -2
- package/runtime/nish.h +9 -1
- package/runtime/nish.mjs +36 -0
- package/runtime/runtime-net.c +32 -3
- package/runtime/runtime-os.c +33 -1
- package/runtime/runtime-region.c +15 -3
- package/runtime/runtime.c +9 -2
- package/std/README.md +9 -7
- package/std/crypto/bcrypt.ts +152 -101
- package/std/net/{http-client.ts → fetch.ts} +17 -17
- package/std/net/handler.ts +1 -1
- package/std/net/{http1-client.ts → http1-client-codec.ts} +6 -6
- package/std/net/{http1.ts → http1-codec.ts} +2 -2
- package/std/net/http1-server.ts +4 -4
- package/std/net/outbound.ts +2 -2
- package/std/net/postgres.ts +383 -49
- package/std/net/prefork.ts +149 -13
- package/std/net/router.ts +1 -1
- package/std/net/upstream.ts +3 -3
- package/std/net/websocket.ts +5 -5
- package/std/region.ts +95 -0
- package/std/sort.ts +178 -0
package/bin/nish
CHANGED
|
Binary file
|
package/docs/AI.md
CHANGED
|
@@ -49,7 +49,7 @@ stdout, nothing on stderr, every field 1-based with `endLine`/`endColumn`
|
|
|
49
49
|
exclusive:
|
|
50
50
|
|
|
51
51
|
```json
|
|
52
|
-
{"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill)"}
|
|
52
|
+
{"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill, sort)"}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
- **Key on `code`, never on `message`.** A code is a promise: `NL2249` means
|
|
@@ -159,7 +159,7 @@ rejects. This table is the highest-value part of the page.
|
|
|
159
159
|
| `x == y` | `Loose equality is forbidden` | `x === y` |
|
|
160
160
|
| `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
|
|
161
161
|
| `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
|
|
162
|
-
| `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.
|
|
162
|
+
| `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill, sort) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
|
|
163
163
|
| `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
|
|
164
164
|
| `a?.b`, `a ?? b` | `?.` only in a chain that ends in `??`; `??` only on a `T \| null`, a chain or `m.get(k)` | `a?.b ?? d`, `a ?? d`, or `if (a !== null)` first |
|
|
165
165
|
| `x as T`, `<T>x`, `x satisfies T` | `Unsupported expression in Phase 1: AsExpression` (to `any` or `unknown`: `` Type assertion to `any` is forbidden ``); `x!` is a syntax error | there are no casts; `implements` is the only widening |
|
|
@@ -852,7 +852,8 @@ export const main = (): i32 => {
|
|
|
852
852
|
(`` The arrow reads `k`, a `let` of the function it is written in: ... bind the value to a `const` before the call ``),
|
|
853
853
|
read `this` (bind `const self = this`), or read a function parameter of its
|
|
854
854
|
function. An arrow inside another arrow, or given to a method, a
|
|
855
|
-
constructor, `expose` or `exposeWith`, captures nothing.
|
|
855
|
+
constructor, `expose` or `exposeWith`, captures nothing; `xs.sort(cmp)`'s
|
|
856
|
+
comparator is the exception, and captures as an arrow argument does.
|
|
856
857
|
- **The argument's type must match exactly**: `(x: i32) => i64` for a
|
|
857
858
|
`(x: i32) => i32` parameter is refused. A generic function cannot be passed
|
|
858
859
|
by name — write `(x) => identity(x)`.
|
|
@@ -2138,7 +2139,12 @@ there is no `undefined` to return), `a.indexOf(v)`, `a.join(sep)` — **`join` i
|
|
|
2138
2139
|
`string[]` only** — and, on an array of numbers only, `dst.set(src[, offset])`
|
|
2139
2140
|
(one `memmove`; a range past the end panics) and `a.fill(v[, start[, end]])`
|
|
2140
2141
|
(ends clamped and counted back from the end when negative, as in JavaScript;
|
|
2141
|
-
never panics). Both are statements.
|
|
2142
|
+
never panics). Both are statements. `a.sort(cmp)` sorts in place, stably, and
|
|
2143
|
+
answers `a` — the same order Node gives; the comparator is required (`a.sort()`
|
|
2144
|
+
is refused, since JavaScript would compare strings), a readonly array and
|
|
2145
|
+
`process.argv` are refused, `a - b` panics on `i32` values a full range apart
|
|
2146
|
+
(compare with `a > b ? 1 : a < b ? -1 : 0` there), and the comparator may read the caller's `const` locals and parameters but not
|
|
2147
|
+
a `let`, as any arrow argument may. That is every array method there is. A
|
|
2142
2148
|
window into a buffer is `(buf, offset, length)`; there is no view type.
|
|
2143
2149
|
|
|
2144
2150
|
**Strings.** `s.length` (**bytes**), `s.charCodeAt(i)` (the byte, bounds-checked),
|
|
@@ -2222,6 +2228,13 @@ system call either way. `setIdlePriority()` moves the program to Linux's `SCHED_
|
|
|
2222
2228
|
`main`, never in a task. An unprivileged process can never leave `SCHED_IDLE`
|
|
2223
2229
|
and an idle one can starve on a saturated machine, so keep a normal-priority
|
|
2224
2230
|
fallback. Neither exists on wasm; both throw under Node.
|
|
2231
|
+
`setCpuAffinity(cpu)` pins the process to one CPU, narrow-only: a CPU outside
|
|
2232
|
+
the mask it already has is `-22`; `-95` on Darwin and under Node, refused on
|
|
2233
|
+
wasm. `nish/net/prefork`'s `PreforkConfig` takes `pin = true` to pin worker
|
|
2234
|
+
`k` to CPU `k` (more workers than CPUs panics), and `listenPort` (with
|
|
2235
|
+
`listenHost`, `listenBacklog`, `listenFlags` 0 or 2) to have the supervisor
|
|
2236
|
+
bind one `SO_REUSEPORT` listener per worker, steered on Linux by the receiving
|
|
2237
|
+
CPU when pinned; a worker takes its own with `preforkListener(process.argv)`.
|
|
2225
2238
|
|
|
2226
2239
|
**Sockets** (`nish:net`, or the same names as globals). `tcpListen(host, port,
|
|
2227
2240
|
backlog)`, `tcpAccept(fd, peer)`, `netRead(fd, buf, off, len)`,
|
|
@@ -2236,10 +2249,10 @@ wait for it to be writable, then `connectResult(fd)` is 0 or the failure. Buffer
|
|
|
2236
2249
|
are `u8[]` only; an address is 18 bytes of one (IPv4 as `::ffff:a.b.c.d`, then
|
|
2237
2250
|
the port big-endian); hosts are numeric literals; to reach a name, resolve it first with
|
|
2238
2251
|
`nish/net/dns` (`DnsClient` on your loop, or the blocking `resolveHost`).
|
|
2239
|
-
To fetch a URL from a CLI, skip all of this: `nish/net/
|
|
2240
|
-
`
|
|
2252
|
+
To fetch a URL from a CLI, skip all of this: `nish/net/fetch`'s
|
|
2253
|
+
`fetchSync(new HttpRequest("GET", url), new HttpOptions())` blocks until the
|
|
2241
2254
|
response's head is in and answers it with `body.readAll(limit)`, or null on a
|
|
2242
|
-
failure (
|
|
2255
|
+
failure (a `FetchSyncClient` keeps the connection and says why in `error`).
|
|
2243
2256
|
`netRead` / `netWrite` panic on a range outside the buffer.
|
|
2244
2257
|
`tcpListenFlags(host, port, backlog, flags)` is `tcpListen` with flag 1
|
|
2245
2258
|
`SO_REUSEPORT`, so several listeners, in one process or many, share a port (Linux
|
|
@@ -2596,6 +2609,46 @@ returns. To give memory back sooner, scope it with `using a = arena()`;
|
|
|
2596
2609
|
`Arena.release` and `Arena.reset` are gone, and a call to either is an unknown
|
|
2597
2610
|
builtin (NL2244).
|
|
2598
2611
|
|
|
2612
|
+
### Regions: `nish:region`
|
|
2613
|
+
|
|
2614
|
+
**For memory a long-running program frees itself — per connection, per cache
|
|
2615
|
+
entry — use a region.** `region(f)` / `regionWith(arg, f)` make one and run
|
|
2616
|
+
`f` in it, and what `f` answers is the root; `enter(r, f)` /
|
|
2617
|
+
`enterWith(r, arg, f)` run `f(root)` / `f(root, arg)` in it again; `free(r)`
|
|
2618
|
+
gives all of it back. `f` is a named function or an arrow that captures
|
|
2619
|
+
nothing. `Region<T>` is a number, not a pointer.
|
|
2620
|
+
|
|
2621
|
+
```ts nish:ok
|
|
2622
|
+
import { Region, enter, free, region } from "nish:region";
|
|
2623
|
+
|
|
2624
|
+
class Session {
|
|
2625
|
+
hits: i32 = 0;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
const open = (): Session => new Session();
|
|
2629
|
+
const touch = (s: Session): i32 => {
|
|
2630
|
+
s.hits = s.hits + 1;
|
|
2631
|
+
return s.hits;
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2634
|
+
export const main = (): i32 => {
|
|
2635
|
+
const r: Region<Session> = region(open);
|
|
2636
|
+
console.log(enter(r, touch));
|
|
2637
|
+
free(r);
|
|
2638
|
+
return 0;
|
|
2639
|
+
};
|
|
2640
|
+
```
|
|
2641
|
+
|
|
2642
|
+
The checker refuses every way region memory could outlive `free`, and every way
|
|
2643
|
+
outer memory could be pointed at from inside: only numbers, booleans, enums and
|
|
2644
|
+
`Region` handles cross. `enter` answers a scalar or a handle (NL2611), `f` may
|
|
2645
|
+
not store region memory into `arg` or other outer memory (NL2609) nor outer
|
|
2646
|
+
memory into the region (NL2610), nor `push`/`set`/`add` to an outer collection
|
|
2647
|
+
(NL2615). A handle stays on its thread (NL2612, NL2613, NL2351), is never a
|
|
2648
|
+
`using` (NL2387) and is never `new Region` (NL2618). A bad handle, a double
|
|
2649
|
+
`free` or a nested `enter` of one region stops the program. `--emit-ts` refuses
|
|
2650
|
+
the module (NL4015).
|
|
2651
|
+
|
|
2599
2652
|
## Recipes for what is missing
|
|
2600
2653
|
|
|
2601
2654
|
**Iterate and transform** — there is no `map`, and no function to pass it:
|
|
@@ -2705,7 +2758,12 @@ parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
|
|
|
2705
2758
|
element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
|
|
2706
2759
|
windows of a buffer. An element refused for room answers `false` and leaves
|
|
2707
2760
|
the array open: end it and send the rest in the next statement. Every other
|
|
2708
|
-
`param...` call and `bindEnd()` panic until `arrayEnd()`.
|
|
2761
|
+
`param...` call and `bindEnd()` panic until `arrayEnd()`. For a long
|
|
2762
|
+
`int4[]` or `int8[]`, `paramInt4Array` / `paramInt8Array` (same window) or
|
|
2763
|
+
`binaryArrayStart(23 or 20)`, `binaryArrayInt` / `binaryArrayNull`,
|
|
2764
|
+
`arrayEnd()` send it in binary, at a tenth of the client CPU; the element type
|
|
2765
|
+
must be the statement's (`int4[]` to an `int8[]` parameter is SQLSTATE
|
|
2766
|
+
`42804`), and an `int4` element out of range panics. (No block, as for
|
|
2709
2767
|
`nish/text`; `tests/postgres/live` runs it against a server, and the user
|
|
2710
2768
|
guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
|
|
2711
2769
|
walk-through.)
|
package/package.json
CHANGED
package/runtime/core/host.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare function readSignal(fd: i32): i32;
|
|
|
82
82
|
/** These two have no reading under Node: each throws, naming itself. */
|
|
83
83
|
export declare function setNice(n: i32): i32;
|
|
84
84
|
export declare function setIdlePriority(): i32;
|
|
85
|
+
/** -95: there is no affinity call under Node. */
|
|
86
|
+
export declare function setCpuAffinity(cpu: i32): i32;
|
|
85
87
|
/** What each `nish:net` function is under the core: a throw naming `name`. */
|
|
86
88
|
export declare function noNetReading(name: string): never;
|
|
87
89
|
/** What `readStdin`, `waitStdin`, `setRawMode` and `setTerminalRestore` are under the core: a throw naming `name`. */
|
package/runtime/core/host.mjs
CHANGED
|
@@ -335,6 +335,15 @@ export function setIdlePriority() {
|
|
|
335
335
|
return noSchedulingReading("setIdlePriority");
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* `setCpuAffinity(cpu)` answers -95, the native build's answer on a host with
|
|
340
|
+
* no affinity call: unlike the two above it has a defined "not supported"
|
|
341
|
+
* value, so a program that pins where it can runs unpinned here.
|
|
342
|
+
*/
|
|
343
|
+
export function setCpuAffinity() {
|
|
344
|
+
return -95;
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
/**
|
|
339
348
|
* `signalFd()` and `readSignal(fd)` have no faithful reading under Node, and
|
|
340
349
|
* these say so rather than answer something else. Node delivers a signal to
|
package/runtime/nish.d.ts
CHANGED
|
@@ -378,6 +378,14 @@ declare function setNice(n: i32): i32;
|
|
|
378
378
|
* normal-priority fallback for work that must finish. Throws under Node.
|
|
379
379
|
*/
|
|
380
380
|
declare function setIdlePriority(): i32;
|
|
381
|
+
/**
|
|
382
|
+
* Pin this process to CPU `cpu` alone (`sched_setaffinity`): 0, or a negative
|
|
383
|
+
* errno. Narrow-only: a CPU outside the mask the process already has (an
|
|
384
|
+
* operator's `taskset` or cpuset), a negative one, or one past the kernel's
|
|
385
|
+
* set size is -22 with no system call. -95 where the host has no affinity
|
|
386
|
+
* call (Darwin, and under Node).
|
|
387
|
+
*/
|
|
388
|
+
declare function setCpuAffinity(cpu: i32): i32;
|
|
381
389
|
/** One environment variable, or `null` when it is unset (an empty value is a set variable). */
|
|
382
390
|
declare function getenv(name: string): string | null;
|
|
383
391
|
/**
|
|
@@ -422,7 +430,13 @@ declare function netAddress(out: u8[], host: string, port: i32): i32;
|
|
|
422
430
|
declare function netLocalPort(fd: i32): i32;
|
|
423
431
|
/** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
|
|
424
432
|
declare function tcpListen(host: string, port: i32, backlog: i32): i32;
|
|
425
|
-
/**
|
|
433
|
+
/**
|
|
434
|
+
* `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
|
|
435
|
+
* each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
|
|
436
|
+
* connection goes to the group's listener whose bind order is the index of the
|
|
437
|
+
* CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
|
|
438
|
+
* is -22.
|
|
439
|
+
*/
|
|
426
440
|
declare function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
|
|
427
441
|
/** The next connection's descriptor, its address in `peer`; -11 when none waits. */
|
|
428
442
|
declare function tcpAccept(fd: i32, peer: u8[]): i32;
|
|
@@ -667,6 +681,8 @@ declare module "nish:process" {
|
|
|
667
681
|
* on a saturated machine, so a caller keeps a normal-priority fallback.
|
|
668
682
|
*/
|
|
669
683
|
export function setIdlePriority(): i32;
|
|
684
|
+
/** Pin this process to CPU `cpu` alone: 0, -22 for a CPU outside the current mask (narrow-only), or -95 off Linux. */
|
|
685
|
+
export function setCpuAffinity(cpu: i32): i32;
|
|
670
686
|
/**
|
|
671
687
|
* A monotonic clock in nanoseconds. The origin is arbitrary, so only the
|
|
672
688
|
* difference between two reads is meaningful.
|
|
@@ -693,7 +709,13 @@ declare module "nish:net" {
|
|
|
693
709
|
export function netLocalPort(fd: i32): i32;
|
|
694
710
|
/** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
|
|
695
711
|
export function tcpListen(host: string, port: i32, backlog: i32): i32;
|
|
696
|
-
/**
|
|
712
|
+
/**
|
|
713
|
+
* `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
|
|
714
|
+
* each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
|
|
715
|
+
* connection goes to the group's listener whose bind order is the index of the
|
|
716
|
+
* CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
|
|
717
|
+
* is -22.
|
|
718
|
+
*/
|
|
697
719
|
export function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
|
|
698
720
|
/** The next connection's descriptor, its address in `peer`; -11 when none waits. */
|
|
699
721
|
export function tcpAccept(fd: i32, peer: u8[]): i32;
|
|
@@ -936,6 +958,30 @@ declare module "nish:secret" {
|
|
|
936
958
|
export function wipe(target: object): void;
|
|
937
959
|
}
|
|
938
960
|
|
|
961
|
+
// `nish:region` has source behind it, `std/region.ts`, but a program imports it
|
|
962
|
+
// by this name. The class has a private constructor, so `tsc` refuses
|
|
963
|
+
// `new Region()` as `nish` does (NL2618). What `nish` refuses about the
|
|
964
|
+
// functions a region runs — keeping its memory outside it, growing outer
|
|
965
|
+
// storage, reaching threads — is flow, which `tsc` cannot see
|
|
966
|
+
// (docs/LANGUAGE.md -> Regions).
|
|
967
|
+
declare module "nish:region" {
|
|
968
|
+
/** A handle to a region whose root is a `T`: 64 bits, not a pointer. */
|
|
969
|
+
export class Region<T> {
|
|
970
|
+
private constructor();
|
|
971
|
+
private readonly root: T;
|
|
972
|
+
}
|
|
973
|
+
/** A new region whose root is what `f` answers; everything `f` allocates is in it. */
|
|
974
|
+
export function region<T>(f: () => T): Region<T>;
|
|
975
|
+
/** `region` with one outside argument for `f`, which `f` may read but not keep. */
|
|
976
|
+
export function regionWith<A, T>(arg: A, f: (arg: A) => T): Region<T>;
|
|
977
|
+
/** `f(root)`, allocating in the region. What `f` answers is a scalar or a handle. */
|
|
978
|
+
export function enter<T, R>(r: Region<T>, f: (root: T) => R): R;
|
|
979
|
+
/** `f(root, arg)`, under `enter`'s rules; `f` may not keep or grow `arg`. */
|
|
980
|
+
export function enterWith<T, A, R>(r: Region<T>, arg: A, f: (root: T, arg: A) => R): R;
|
|
981
|
+
/** Give the region's memory back. A freed handle used again stops the program. */
|
|
982
|
+
export function free<T>(r: Region<T>): void;
|
|
983
|
+
}
|
|
984
|
+
|
|
939
985
|
// ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
|
|
940
986
|
|
|
941
987
|
declare const Arena: {
|
package/runtime/nish.h
CHANGED
|
@@ -577,6 +577,11 @@ int32_t nish_set_nice(int32_t n);
|
|
|
577
577
|
* negative errno; -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. An
|
|
578
578
|
* unprivileged process cannot leave the policy again. */
|
|
579
579
|
int32_t nish_set_idle_priority(void);
|
|
580
|
+
/* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone
|
|
581
|
+
* (`sched_setaffinity`, Linux), 0 or a negative errno. Narrow-only: a CPU not
|
|
582
|
+
* in the current mask, negative, or past `CPU_SETSIZE` is -22 (-EINVAL) with
|
|
583
|
+
* no system call. -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. */
|
|
584
|
+
int32_t nish_set_cpu_affinity(int32_t cpu);
|
|
580
585
|
|
|
581
586
|
/* ---- The environment (WP19 R1) ------------------------------------------
|
|
582
587
|
* `getenv(name)`: the value of environment variable `name`, copied into the
|
|
@@ -679,7 +684,10 @@ bool nish_is_executable(const nish_str *path);
|
|
|
679
684
|
* `nish_tcp_listen(host, port, backlog)`: `SO_REUSEADDR`, and `::` hears
|
|
680
685
|
* both families, or IPv4 alone on a kernel without IPv6.
|
|
681
686
|
* `nish_tcp_listen_flags(host, port, backlog, flags)`: the same listener,
|
|
682
|
-
* with `SO_REUSEPORT` for flag 1
|
|
687
|
+
* with `SO_REUSEPORT` for flag 1, `TCP_NODELAY` on each accepted socket for
|
|
688
|
+
* flag 2, and, for flag 4 with flag 1, a reuseport program steering each new
|
|
689
|
+
* connection to the listener bound at the index of the CPU that received it
|
|
690
|
+
* (Linux; -95 elsewhere). Any other bit, or 4 without 1, is -22.
|
|
683
691
|
* `nish_tcp_accept(fd, peer)`: a new descriptor and the peer's address.
|
|
684
692
|
* `nish_net_read` / `nish_net_write(fd, buf, off, len)`: bytes `[off, off +
|
|
685
693
|
* len)` of `buf`, which the compiled call has range-checked; a write to a
|
package/runtime/nish.mjs
CHANGED
|
@@ -121,6 +121,9 @@ for (const name of ["setNice", "setIdlePriority"]) {
|
|
|
121
121
|
throw new Error(`\`${name}\` has no reading under Node: it changes how the kernel schedules the native program`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
// `setCpuAffinity` answers -95, as on Darwin: Node has no affinity call, and
|
|
125
|
+
// "not supported" is a value a caller already branches on.
|
|
126
|
+
provide("setCpuAffinity", shim.setCpuAffinity);
|
|
124
127
|
provide("getenv", shim.getenv);
|
|
125
128
|
provide("statMtimeSync", shim.statMtimeSync);
|
|
126
129
|
provide("signalFd", shim.signalFd);
|
|
@@ -328,11 +331,41 @@ provide("arena", shim.arena);
|
|
|
328
331
|
// zero-fills it. `nish:unsafe` and `nish:term` are each answered by a module
|
|
329
332
|
// that hands back the globals installed above: its functions are globals of this module either
|
|
330
333
|
// way, and the import is what the compiler requires a program to write.
|
|
334
|
+
// `nish:region` is answered by a module of its own, below.
|
|
331
335
|
// `nish:simd` is `runtime/core.mjs`, whose vectors are typed arrays made once
|
|
332
336
|
// and never written after, so `--emit-ts` output and a program run as it was
|
|
333
337
|
// written share one implementation.
|
|
334
338
|
const UNSAFE_EXPORTS = ["uncheckedGet", "uncheckedSet", "wrappingAdd", "wrappingSub", "wrappingMul"];
|
|
335
339
|
const unsafeSource = UNSAFE_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
|
|
340
|
+
// `nish:region` has no allocator to swap under Node: a region is an entry in a
|
|
341
|
+
// table, keyed by its handle (a BigInt, as every `i64` is here), `enter` runs
|
|
342
|
+
// its function on the root, and `free` drops the entry. The handle checks the
|
|
343
|
+
// native runtime makes are made here too, so a freed handle or a second
|
|
344
|
+
// `enter` throws where the native program stops.
|
|
345
|
+
const regionSource = `
|
|
346
|
+
const roots = new Map();
|
|
347
|
+
const entered = new Set();
|
|
348
|
+
let next = 0n;
|
|
349
|
+
const rootOf = (r) => {
|
|
350
|
+
if (!roots.has(r)) throw new Error("nish:region: the region is freed or was never made");
|
|
351
|
+
if (entered.has(r)) throw new Error("nish:region: the region is entered already");
|
|
352
|
+
return roots.get(r);
|
|
353
|
+
};
|
|
354
|
+
const run = (r, f, arg, withArg) => {
|
|
355
|
+
const root = rootOf(r);
|
|
356
|
+
entered.add(r);
|
|
357
|
+
try { return withArg ? f(root, arg) : f(root); } finally { entered.delete(r); }
|
|
358
|
+
};
|
|
359
|
+
export class Region {}
|
|
360
|
+
export const region = (f) => { next += 1n; roots.set(next, f()); return next; };
|
|
361
|
+
export const regionWith = (arg, f) => { next += 1n; roots.set(next, f(arg)); return next; };
|
|
362
|
+
export const enter = (r, f) => run(r, f, undefined, false);
|
|
363
|
+
export const enterWith = (r, arg, f) => run(r, f, arg, true);
|
|
364
|
+
export const free = (r) => {
|
|
365
|
+
if (entered.has(r)) throw new Error("nish:region: the region is freed while it is entered");
|
|
366
|
+
if (!roots.delete(r)) throw new Error("nish:region: the region is freed or was never made");
|
|
367
|
+
};
|
|
368
|
+
`;
|
|
336
369
|
const TERM_EXPORTS = ["readStdin", "waitStdin", "isTerminal", "terminalSize", "setRawMode", "setTerminalRestore"];
|
|
337
370
|
const termSource = TERM_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
|
|
338
371
|
registerHooks({
|
|
@@ -346,6 +379,9 @@ registerHooks({
|
|
|
346
379
|
if (specifier === "nish:simd") {
|
|
347
380
|
return next(new URL("./core.mjs", import.meta.url).href, context);
|
|
348
381
|
}
|
|
382
|
+
if (specifier === "nish:region") {
|
|
383
|
+
return { url: `data:text/javascript,${encodeURIComponent(regionSource)}`, shortCircuit: true };
|
|
384
|
+
}
|
|
349
385
|
if (specifier === "nish:term") {
|
|
350
386
|
return { url: `data:text/javascript,${encodeURIComponent(termSource)}`, shortCircuit: true };
|
|
351
387
|
}
|
package/runtime/runtime-net.c
CHANGED
|
@@ -73,6 +73,7 @@ typedef int nish_net_unused;
|
|
|
73
73
|
#include <sys/un.h>
|
|
74
74
|
#include <unistd.h>
|
|
75
75
|
#if defined(__linux__)
|
|
76
|
+
#include <linux/filter.h>
|
|
76
77
|
#include <netinet/udp.h>
|
|
77
78
|
#include <sys/epoll.h>
|
|
78
79
|
/* glibc before 2.29 names neither; the numbers are the kernel's ABI. */
|
|
@@ -82,6 +83,10 @@ typedef int nish_net_unused;
|
|
|
82
83
|
#ifndef UDP_GRO
|
|
83
84
|
#define UDP_GRO 104
|
|
84
85
|
#endif
|
|
86
|
+
/* Linux 4.5's; a header older than the kernel may not name it. */
|
|
87
|
+
#ifndef SO_ATTACH_REUSEPORT_CBPF
|
|
88
|
+
#define SO_ATTACH_REUSEPORT_CBPF 51
|
|
89
|
+
#endif
|
|
85
90
|
#else
|
|
86
91
|
#include <sys/event.h>
|
|
87
92
|
#include <sys/time.h>
|
|
@@ -243,7 +248,23 @@ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int3
|
|
|
243
248
|
}
|
|
244
249
|
nish_sockaddr s;
|
|
245
250
|
socklen_t n = nish_net_sockaddr(&s, a, port, v4);
|
|
246
|
-
|
|
251
|
+
#if defined(__linux__)
|
|
252
|
+
/* `tcpListenFlags` flag 4's program, two instructions, the kernel
|
|
253
|
+
selftest's: load the receiving CPU, return it. It is the reuseport
|
|
254
|
+
group's, so attaching it from every listener installs the same program
|
|
255
|
+
each time, and it goes on after `listen`, once the socket is in its
|
|
256
|
+
group. Only a stream listener's `opts` can hold 4. */
|
|
257
|
+
static struct sock_filter cpu[] = {
|
|
258
|
+
{BPF_LD | BPF_W | BPF_ABS, 0, 0, (uint32_t)(SKF_AD_OFF + SKF_AD_CPU)},
|
|
259
|
+
{BPF_RET | BPF_A, 0, 0, 0},
|
|
260
|
+
};
|
|
261
|
+
static const struct sock_fprog steer = {2, cpu};
|
|
262
|
+
#endif
|
|
263
|
+
if (bad != 0 || bind(fd, &s.sa, n) != 0 || (type == SOCK_STREAM && listen(fd, arg) != 0)
|
|
264
|
+
#if defined(__linux__)
|
|
265
|
+
|| ((opts & 4) && setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, &steer, sizeof steer) != 0)
|
|
266
|
+
#endif
|
|
267
|
+
) {
|
|
247
268
|
int e = errno;
|
|
248
269
|
close(fd);
|
|
249
270
|
return nish_net_err(e);
|
|
@@ -262,9 +283,17 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
|
|
|
262
283
|
`SO_REUSEPORT` for flag 1, so several listeners of one uid share the port
|
|
263
284
|
(Linux spreads new connections across them; Darwin hands them all to one),
|
|
264
285
|
and `TCP_NODELAY` for flag 2, which the listener carries so that
|
|
265
|
-
`tcpAccept` sets it on each connection it takes.
|
|
286
|
+
`tcpAccept` sets it on each connection it takes. Flag 4, which needs flag
|
|
287
|
+
1, steers by CPU: a reuseport program (`SO_ATTACH_REUSEPORT_CBPF`) hands a
|
|
288
|
+
new connection to the group's listener whose index is the CPU that received
|
|
289
|
+
it, the index being the order the group's sockets were bound in, and the
|
|
290
|
+
kernel falls back to its hash when there is no such listener. Linux alone
|
|
291
|
+
has it; elsewhere flag 4 is -95. Any other bit, or 4 without 1, is -22. */
|
|
266
292
|
int32_t nish_tcp_listen_flags(const nish_str *host, int32_t port, int32_t backlog, int32_t flags) {
|
|
267
|
-
if ((uint32_t)flags >
|
|
293
|
+
if ((uint32_t)flags > 7 || (flags & 5) == 4) return -22;
|
|
294
|
+
#if !defined(__linux__)
|
|
295
|
+
if (flags & 4) return -95;
|
|
296
|
+
#endif
|
|
268
297
|
return nish_net_bound(host, port, SOCK_STREAM, backlog, flags);
|
|
269
298
|
}
|
|
270
299
|
|
package/runtime/runtime-os.c
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* from drifting from the contract it is published under.
|
|
27
27
|
*/
|
|
28
28
|
#if defined(__linux__) && !defined(_GNU_SOURCE)
|
|
29
|
-
#define _GNU_SOURCE /* `sched_getaffinity` and `CPU_COUNT`, for
|
|
29
|
+
#define _GNU_SOURCE /* `sched_getaffinity`, `sched_setaffinity` and `CPU_COUNT`, for
|
|
30
|
+
`nish_cpu_online` and `nish_set_cpu_affinity` */
|
|
30
31
|
#endif
|
|
31
32
|
#define _POSIX_C_SOURCE 200809L
|
|
32
33
|
/* `realpath` is XSI rather than POSIX base, so _POSIX_C_SOURCE alone does not
|
|
@@ -860,6 +861,37 @@ int32_t nish_set_idle_priority(void) {
|
|
|
860
861
|
#endif
|
|
861
862
|
}
|
|
862
863
|
|
|
864
|
+
/* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone, 0 or a
|
|
865
|
+
negative errno. Narrow-only: `cpu` must be in the mask the process already
|
|
866
|
+
has (`sched_getaffinity`), so a pin never escapes an operator's `taskset` or
|
|
867
|
+
cpuset; a CPU outside it, a negative one, or one past `CPU_SETSIZE` is
|
|
868
|
+
-EINVAL with no `sched_setaffinity` call. Linux alone has the call; Darwin
|
|
869
|
+
answers -95 as `setIdlePriority` does, WASI -ENOSYS. Like the nice value,
|
|
870
|
+
the mask is the calling thread's and every thread started after it. */
|
|
871
|
+
int32_t nish_set_cpu_affinity(int32_t cpu) {
|
|
872
|
+
#if defined(__linux__)
|
|
873
|
+
/* The mask as words: the set to pin is the word holding `cpu` with only its
|
|
874
|
+
bit, the words below it zero, and the kernel zero-fills a shorter mask.
|
|
875
|
+
`sched_getaffinity` fails only on a bad pid or size, neither of which
|
|
876
|
+
this call can pass, so its failure shares the range check's answer. */
|
|
877
|
+
unsigned long m[CPU_SETSIZE / (8 * sizeof(unsigned long))];
|
|
878
|
+
uint32_t w = (uint32_t)cpu / (8 * sizeof m[0]);
|
|
879
|
+
unsigned long bit = 1UL << ((uint32_t)cpu % (8 * sizeof m[0]));
|
|
880
|
+
if ((uint32_t)cpu >= CPU_SETSIZE || sched_getaffinity(0, sizeof m, (cpu_set_t *)m) != 0 ||
|
|
881
|
+
!(m[w] & bit))
|
|
882
|
+
return -EINVAL;
|
|
883
|
+
for (uint32_t i = 0; i < w; i++) m[i] = 0;
|
|
884
|
+
m[w] = bit;
|
|
885
|
+
return sched_setaffinity(0, (w + 1) * sizeof m[0], (cpu_set_t *)m) == 0 ? 0 : -errno;
|
|
886
|
+
#elif defined(__wasi__)
|
|
887
|
+
(void)cpu;
|
|
888
|
+
return -ENOSYS;
|
|
889
|
+
#else
|
|
890
|
+
(void)cpu;
|
|
891
|
+
return -95;
|
|
892
|
+
#endif
|
|
893
|
+
}
|
|
894
|
+
|
|
863
895
|
/* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
|
|
864
896
|
|
|
865
897
|
**Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons
|
package/runtime/runtime-region.c
CHANGED
|
@@ -173,14 +173,26 @@ uint64_t nish_region_new(void) {
|
|
|
173
173
|
s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
|
|
174
174
|
nish_region_free_list = s->outer;
|
|
175
175
|
} else {
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
/* Each panic lets go of the lock first: an atexit handler, or another
|
|
177
|
+
thread while the exit is on its way, may still make or free a region
|
|
178
|
+
(docs/security/runtime.md, RT-46). */
|
|
179
|
+
if (nish_region_used == NISH_REGION_SLOTS) {
|
|
180
|
+
nish_region_unlock();
|
|
181
|
+
nish_region_die(NISH_REGION_FULL);
|
|
182
|
+
}
|
|
183
|
+
/* The slot is counted once its page is there: a failed page leaves the
|
|
184
|
+
count as it was (RT-46). */
|
|
185
|
+
index = nish_region_used;
|
|
178
186
|
nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
|
|
179
187
|
if (!*page) {
|
|
180
188
|
nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
|
|
181
|
-
if (!p)
|
|
189
|
+
if (!p) {
|
|
190
|
+
nish_region_unlock();
|
|
191
|
+
nish_region_die(NISH_REGION_OOM);
|
|
192
|
+
}
|
|
182
193
|
__atomic_store_n(page, p, __ATOMIC_RELEASE);
|
|
183
194
|
}
|
|
195
|
+
nish_region_used = index + 1;
|
|
184
196
|
s = &(*page)[index % NISH_REGION_PAGE];
|
|
185
197
|
__atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
|
|
186
198
|
}
|
package/runtime/runtime.c
CHANGED
|
@@ -180,8 +180,15 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
|
|
|
180
180
|
this file still links alone. */
|
|
181
181
|
NISH_TLS uint64_t nish_region_chunk;
|
|
182
182
|
|
|
183
|
+
/* The most a region's next chunk grows to: past it the chunks stop doubling,
|
|
184
|
+
so a region that keeps allocating never asks malloc for one block of
|
|
185
|
+
gigabytes when smaller ones would do (docs/security/runtime.md, RT-47). A
|
|
186
|
+
single allocation larger than this still gets a chunk of its own size. */
|
|
187
|
+
#define NISH_REGION_CHUNK_MAX ((uint64_t)1 << 20)
|
|
188
|
+
|
|
183
189
|
/* Slow path (size 8-byte rounded): push a chunk (>= 64 KB, or a region's
|
|
184
|
-
next size), bump from it. A region's chunks double from its first
|
|
190
|
+
next size), bump from it. A region's chunks double from its first, up to
|
|
191
|
+
NISH_REGION_CHUNK_MAX. */
|
|
185
192
|
void *nish_arena_grow(uint64_t size) {
|
|
186
193
|
#ifdef NISH_ASAN
|
|
187
194
|
/* An exact chunk leaves off == cap, so the inline bump never fits again and
|
|
@@ -191,7 +198,7 @@ void *nish_arena_grow(uint64_t size) {
|
|
|
191
198
|
uint64_t next = nish_region_chunk;
|
|
192
199
|
uint64_t floor = next ? next : 65536;
|
|
193
200
|
uint64_t cap = size > floor ? size : floor;
|
|
194
|
-
if (next) nish_region_chunk = cap * 2;
|
|
201
|
+
if (next) nish_region_chunk = cap < NISH_REGION_CHUNK_MAX / 2 ? cap * 2 : NISH_REGION_CHUNK_MAX;
|
|
195
202
|
#endif
|
|
196
203
|
/* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
|
|
197
204
|
and malloc would silently take the low half of a chunk nobody can address. */
|