summaryrefslogtreecommitdiff
path: root/vendor/github.com/tetratelabs/wazero/RATIONALE.md
blob: 8d783cb4483c8ca8757054f72674f50512df7b2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
# Notable rationale of wazero

## Zero dependencies

Wazero has zero dependencies to differentiate itself from other runtimes which
have heavy impact usually due to CGO. By avoiding CGO, wazero avoids
prerequisites such as shared libraries or libc, and lets users keep features
like cross compilation.

Avoiding go.mod dependencies reduces interference on Go version support, and
size of a statically compiled binary. However, doing so brings some
responsibility into the project.

Go's native platform support is good: We don't need platform-specific code to
get monotonic time, nor do we need much work to implement certain features
needed by our compiler such as `mmap`. That said, Go does not support all
common operating systems to the same degree. For example, Go 1.18 includes
`Mprotect` on Linux and Darwin, but not FreeBSD.

The general tradeoff the project takes from a zero dependency policy is more
explicit support of platforms (in the compiler runtime), as well a larger and
more technically difficult codebase.

At some point, we may allow extensions to supply their own platform-specific
hooks. Until then, one end user impact/tradeoff is some glitches trying
untested platforms (with the Compiler runtime).

### Why do we use CGO to implement system calls on darwin?

wazero is dependency and CGO free by design. In some cases, we have code that
can optionally use CGO, but retain a fallback for when that's disabled. The only
operating system (`GOOS`) we use CGO by default in is `darwin`.

Unlike other operating systems, regardless of `CGO_ENABLED`, Go always uses
"CGO" mechanisms in the runtime layer of `darwin`. This is explained in
[Statically linked binaries on Mac OS X](https://developer.apple.com/library/archive/qa/qa1118/_index.html#//apple_ref/doc/uid/DTS10001666):

> Apple does not support statically linked binaries on Mac OS X. A statically
> linked binary assumes binary compatibility at the kernel system call
> interface, and we do not make any guarantees on that front. Rather, we strive
> to ensure binary compatibility in each dynamically linked system library and
> framework.

This plays to our advantage for system calls that aren't yet exposed in the Go
standard library, notably `futimens` for nanosecond-precision timestamp
manipulation.

### Why not x/sys

Going beyond Go's SDK limitations can be accomplished with their [x/sys library](https://pkg.go.dev/golang.org/x/sys/unix).
For example, this includes `zsyscall_freebsd_amd64.go` missing from the Go SDK.

However, like all dependencies, x/sys is a source of conflict. For example,
x/sys had to be in order to upgrade to Go 1.18.

If we depended on x/sys, we could get more precise functionality needed for
features such as clocks or more platform support for the compiler runtime.

That said, formally supporting an operating system may still require testing as
even use of x/sys can require platform-specifics. For example, [mmap-go](https://github.com/edsrzf/mmap-go)
uses x/sys, but also mentions limitations, some not surmountable with x/sys
alone.

Regardless, we may at some point introduce a separate go.mod for users to use
x/sys as a platform plugin without forcing all users to maintain that
dependency.

## Project structure

wazero uses internal packages extensively to balance API compatibility desires for end users with the need to safely
share internals between compilers.

End-user packages include `wazero`, with `Config` structs, `api`, with shared types, and the built-in `wasi` library.
Everything else is internal.

We put the main program for wazero into a directory of the same name to match conventions used in `go install`,
notably the name of the folder becomes the binary name. We chose to use `cmd/wazero` as it is common practice
and less surprising than `wazero/wazero`.

### Internal packages

Most code in wazero is internal, and it is acknowledged that this prevents external implementation of facets such as
compilers or decoding. It also prevents splitting this code into separate repositories, resulting in a larger monorepo.
This also adds work as more code needs to be centrally reviewed.

However, the alternative is neither secure nor viable. To allow external implementation would require exporting symbols
public, such as the `CodeSection`, which can easily create bugs. Moreover, there's a high drift risk for any attempt at
external implementations, compounded not just by wazero's code organization, but also the fast moving Wasm and WASI
specifications.

For example, implementing a compiler correctly requires expertise in Wasm, Golang and assembly. This requires deep
insight into how internals are meant to be structured and the various tiers of testing required for `wazero` to result
in a high quality experience. Even if someone had these skills, supporting external code would introduce variables which
are constants in the central one. Supporting an external codebase is harder on the project team, and could starve time
from the already large burden on the central codebase.

The tradeoffs of internal packages are a larger codebase and responsibility to implement all standard features. It also
implies thinking about extension more as forking is not viable for reasons above also. The primary mitigation of these
realities are friendly OSS licensing, high rigor and a collaborative spirit which aim to make contribution in the shared
codebase productive.

### Avoiding cyclic dependencies

wazero shares constants and interfaces with internal code by a sharing pattern described below:
* shared interfaces and constants go in one package under root: `api`.
* user APIs and structs depend on `api` and go into the root package `wazero`.
  * e.g. `InstantiateModule` -> `/wasm.go` depends on the type `api.Module`.
* implementation code can also depend on `api` in a corresponding package under `/internal`.
  * Ex  package `wasm` -> `/internal/wasm/*.go` and can depend on the type `api.Module`.

The above guarantees no cyclic dependencies at the cost of having to re-define symbols that exist in both packages.
For example, if `wasm.Store` is a type the user needs access to, it is narrowed by a cover type in the `wazero`:

```go
type runtime struct {
	s *wasm.Store
}
```

This is not as bad as it sounds as mutations are only available via configuration. This means exported functions are
limited to only a few functions.

### Avoiding security bugs

In order to avoid security flaws such as code insertion, nothing in the public API is permitted to write directly to any
mutable symbol in the internal package. For example, the package `api` is shared with internal code. To ensure
immutability, the `api` package cannot contain any mutable public symbol, such as a slice or a struct with an exported
field.

In practice, this means shared functionality like memory mutation need to be implemented by interfaces.

Here are some examples:
* `api.Memory` protects access by exposing functions like `WriteFloat64Le` instead of exporting a buffer (`[]byte`).
* There is no exported symbol for the `[]byte` representing the `CodeSection`

Besides security, this practice prevents other bugs and allows centralization of validation logic such as decoding Wasm.

## API Design

### Why is `context.Context` inconsistent?

It may seem strange that only certain API have an initial `context.Context`
parameter. We originally had a `context.Context` for anything that might be
traced, but it turned out to be only useful for lifecycle and host functions.

For instruction-scoped aspects like memory updates, a context parameter is too
fine-grained and also invisible in practice. For example, most users will use
the compiler engine, and its memory, global or table access will never use go's
context.

### Why does `api.ValueType` map to uint64?

WebAssembly allows functions to be defined either by the guest or the host,
with signatures expressed as WebAssembly types. For example, `i32` is a 32-bit
type which might be interpreted as signed. Function signatures can have zero or
more parameters or results even if WebAssembly 1.0 allows up to one result.

The guest can export functions, so that the host can call it. In the case of
wazero, the host is Go and an exported function can be called via
`api.Function`. `api.Function` allows users to supply parameters and read
results as a slice of uint64. For example, if there are no results, an empty
slice is returned. The user can learn the signature via `FunctionDescription`,
which returns the `api.ValueType` corresponding to each parameter or result.
`api.ValueType` defines the mapping of WebAssembly types to `uint64` values for
reason described in this section. The special case of `v128` is also mentioned
below.

wazero maps each value type to a uint64 values because it holds the largest
type in WebAssembly 1.0 (i64). A slice allows you to express empty (e.g. a
nullary signature), for example a start function.

Here's an example of calling a function, noting this syntax works for both a
signature `(param i32 i32) (result i32)` and `(param i64 i64) (result i64)`
```go
x, y := uint64(1), uint64(2)
results, err := mod.ExportedFunction("add").Call(ctx, x, y)
if err != nil {
	log.Panicln(err)
}
fmt.Printf("%d + %d = %d\n", x, y, results[0])
```

WebAssembly does not define an encoding strategy for host defined parameters or
results. This means the encoding rules above are defined by wazero instead. To
address this, we clarified mapping both in `api.ValueType` and added helper
functions like `api.EncodeF64`. This allows users conversions typical in Go
programming, and utilities to avoid ambiguity and edge cases around casting.

Alternatively, we could have defined a byte buffer based approach and a binary
encoding of value types in and out. For example, an empty byte slice would mean
no values, while a non-empty could use a binary encoding for supported values.
This could work, but it is more difficult for the normal case of i32 and i64.
It also shares a struggle with the current approach, which is that value types
were added after WebAssembly 1.0 and not all of them have an encoding. More on
this below.

In summary, wazero chose an approach for signature mapping because there was
none, and the one we chose biases towards simplicity with integers and handles
the rest with documentation and utilities.

#### Post 1.0 value types

Value types added after WebAssembly 1.0 stressed the current model, as some
have no encoding or are larger than 64 bits. While problematic, these value
types are not commonly used in exported (extern) functions. However, some
decisions were made and detailed below.

For example `externref` has no guest representation. wazero chose to map
references to uint64 as that's the largest value needed to encode a pointer on
supported platforms. While there are two reference types, `externref` and
`functype`, the latter is an internal detail of function tables, and the former
is rarely if ever used in function signatures as of the end of 2022.

The only value larger than 64 bits is used for SIMD (`v128`). Vectorizing via
host functions is not used as of the end of 2022. Even if it were, it would be
inefficient vs guest vectorization due to host function overhead. In other
words, the `v128` value type is unlikely to be in an exported function
signature. That it requires two uint64 values to encode is an internal detail
and not worth changing the exported function interface `api.Function`, as doing
so would break all users.

### Interfaces, not structs

All exported types in public packages, regardless of configuration vs runtime, are interfaces. The primary benefits are
internal flexibility and avoiding people accidentally mis-initializing by instantiating the types on their own vs using
the `NewXxx` constructor functions. In other words, there's less support load when things can't be done incorrectly.

Here's an example:
```go
rt := &RuntimeConfig{} // not initialized properly (fields are nil which shouldn't be)
rt := RuntimeConfig{} // not initialized properly (should be a pointer)
rt := wazero.NewRuntimeConfig() // initialized properly
```

There are a few drawbacks to this, notably some work for maintainers.
* Interfaces are decoupled from the structs implementing them, which means the signature has to be repeated twice.
* Interfaces have to be documented and guarded at time of use, that 3rd party implementations aren't supported.
* As of Golang 1.21, interfaces are still [not well supported](https://github.com/golang/go/issues/5860) in godoc.

## Config

wazero configures scopes such as Runtime and Module using `XxxConfig` types. For example, `RuntimeConfig` configures
`Runtime` and `ModuleConfig` configure `Module` (instantiation). In all cases, config types begin defaults and can be
customized by a user, e.g., selecting features or a module name override.

### Why don't we make each configuration setting return an error?
No config types create resources that would need to be closed, nor do they return errors on use. This helps reduce
resource leaks, and makes chaining easier. It makes it possible to parse configuration (ex by parsing yaml) independent
of validating it.

Instead of:
```
cfg, err = cfg.WithFS(fs)
if err != nil {
  return err
}
cfg, err = cfg.WithName(name)
if err != nil {
  return err
}
mod, err = rt.InstantiateModuleWithConfig(ctx, code, cfg)
if err != nil {
  return err
}
```

There's only one call site to handle errors:
```
cfg = cfg.WithFS(fs).WithName(name)
mod, err = rt.InstantiateModuleWithConfig(ctx, code, cfg)
if err != nil {
  return err
}
```

This allows users one place to look for errors, and also the benefit that if anything internally opens a resource, but
errs, there's nothing they need to close. In other words, users don't need to track which resources need closing on
partial error, as that is handled internally by the only code that can read configuration fields.

### Why are configuration immutable?
While it seems certain scopes like `Runtime` won't repeat within a process, they do, possibly in different goroutines.
For example, some users create a new runtime for each module, and some re-use the same base module configuration with
only small updates (ex the name) for each instantiation. Making configuration immutable allows them to be safely used in
any goroutine.

Since config are immutable, changes apply via return val, similar to `append` in a slice.

For example, both of these are the same sort of error:
```go
append(slice, element) // bug as only the return value has the updated slice.
cfg.WithName(next) // bug as only the return value has the updated name.
```

Here's an example of correct use: re-assigning explicitly or via chaining.
```go
cfg = cfg.WithName(name) // explicit

mod, err = rt.InstantiateModuleWithConfig(ctx, code, cfg.WithName(name)) // implicit
if err != nil {
  return err
}
```

### Why aren't configuration assigned with option types?
The option pattern is a familiar one in Go. For example, someone defines a type `func (x X) err` and uses it to update
the target. For example, you could imagine wazero could choose to make `ModuleConfig` from options vs chaining fields.

Ex instead of:
```go
type ModuleConfig interface {
	WithName(string) ModuleConfig
	WithFS(fs.FS) ModuleConfig
}

struct moduleConfig {
	name string
	fs fs.FS
}

func (c *moduleConfig) WithName(name string) ModuleConfig {
    ret := *c // copy
    ret.name = name
    return &ret
}

func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
    ret := *c // copy
    ret.setFS("/", fs)
    return &ret
}

config := r.NewModuleConfig().WithFS(fs)
configDerived := config.WithName("name")
```

An option function could be defined, then refactor each config method into an name prefixed option function:
```go
type ModuleConfig interface {
}
struct moduleConfig {
    name string
    fs fs.FS
}

type ModuleConfigOption func(c *moduleConfig)

func ModuleConfigName(name string) ModuleConfigOption {
    return func(c *moduleConfig) {
        c.name = name
	}
}

func ModuleConfigFS(fs fs.FS) ModuleConfigOption {
    return func(c *moduleConfig) {
        c.fs = fs
    }
}

func (r *runtime) NewModuleConfig(opts ...ModuleConfigOption) ModuleConfig {
	ret := newModuleConfig() // defaults
    for _, opt := range opts {
        opt(&ret.config)
    }
    return ret
}

func (c *moduleConfig) WithOptions(opts ...ModuleConfigOption) ModuleConfig {
    ret := *c // copy base config
    for _, opt := range opts {
        opt(&ret.config)
    }
    return ret
}

config := r.NewModuleConfig(ModuleConfigFS(fs))
configDerived := config.WithOptions(ModuleConfigName("name"))
```

wazero took the path of the former design primarily due to:
* interfaces provide natural namespaces for their methods, which is more direct than functions with name prefixes.
* parsing config into function callbacks is more direct vs parsing config into a slice of functions to do the same.
* in either case derived config is needed and the options pattern is more awkward to achieve that.

There are other reasons such as test and debug being simpler without options: the above list is constrained to conserve
space. It is accepted that the options pattern is common in Go, which is the main reason for documenting this decision.

### Why aren't config types deeply structured?
wazero's configuration types cover the two main scopes of WebAssembly use:
* `RuntimeConfig`: This is the broadest scope, so applies also to compilation
  and instantiation. e.g. This controls the WebAssembly Specification Version.
* `ModuleConfig`: This affects modules instantiated after compilation and what
  resources are allowed. e.g. This defines how or if STDOUT is captured. This
  also allows sub-configuration of `FSConfig`.

These default to a flat definition each, with lazy sub-configuration only after
proven to be necessary. A flat structure is easier to work with and is also
easy to discover. Unlike the option pattern described earlier, more
configuration in the interface doesn't taint the package namespace, only
`ModuleConfig`.

We default to a flat structure to encourage simplicity. If we eagerly broke out
all possible configurations into sub-types (e.g. ClockConfig), it would be hard
to notice configuration sprawl. By keeping the config flat, it is easy to see
the cognitive load we may be adding to our users.

In other words, discomfort adding more configuration is a feature, not a bug.
We should only add new configuration rarely, and before doing so, ensure it
will be used. In fact, this is why we support using context fields for
experimental configuration. By letting users practice, we can find out if a
configuration was a good idea or not before committing to it, and potentially
sprawling our types.

In reflection, this approach worked well for the nearly 1.5 year period leading
to version 1.0. We've only had to create a single sub-configuration, `FSConfig`,
and it was well understood why when it occurred.

## Why does `ModuleConfig.WithStartFunctions` default to `_start`?

We formerly had functions like `StartWASICommand` that would verify
preconditions and start WASI's `_start` command. However, this caused confusion
because both many languages compiled a WASI dependency, and many did so
inconsistently.

The conflict is that exported functions need to use features the language
runtime provides, such as garbage collection. There's a "chicken-egg problem"
where `_start` needs to complete in order for exported behavior to work.

For example, unlike `GOOS=wasip1` in Go 1.21, TinyGo's "wasi" target supports
function exports. So, the only way to use FFI style is via the "wasi" target.
Not explicitly calling `_start` before an ABI such as wapc-go, would crash, due
to setup not happening (e.g. to implement `panic`). Other embedders such as
Envoy also called `_start` for the same reason. To avoid a common problem for
users unaware of WASI, and also to simplify normal use of WASI (e.g. `main`),
we added `_start` to `ModuleConfig.WithStartFunctions`.

In cases of multiple initializers, such as in wapc-go, users can override this
to add the others *after* `_start`. Users who want to explicitly control
`_start`, such as some of our unit tests, can clear the start functions and
remove it.

This decision was made in 2022, and holds true in 2023, even with the
introduction of "wasix". It holds because "wasix" is backwards compatible with
"wasip1". In the future, there will be other ways to start applications, and
may not be backwards compatible with "wasip1".

Most notably WASI "Preview 2" is not implemented in a way compatible with
wasip1. Its start function is likely to be different, and defined in the
wasi-cli "world". When the design settles, and it is implemented by compilers,
wazero will attempt to support "wasip2". However, it won't do so in a way that
breaks existing compilers.

In other words, we won't remove `_start` if "wasip2" continues a path of an
alternate function name. If we did, we'd break existing users despite our
compatibility promise saying we don't. The most likely case is that when we
build-in something incompatible with "wasip1", that start function will be
added to the start functions list in addition to `_start`.

See http://wasix.org
See https://github.com/WebAssembly/wasi-cli

## Runtime == Engine+Store
wazero defines a single user-type which combines the specification concept of `Store` with the unspecified `Engine`
which manages them.

### Why not multi-store?
Multi-store isn't supported as the extra tier complicates lifecycle and locking. Moreover, in practice it is unusual for
there to be an engine that has multiple stores which have multiple modules. More often, it is the case that there is
either 1 engine with 1 store and multiple modules, or 1 engine with many stores, each having 1 non-host module. In worst
case, a user can use multiple runtimes until "multi-store" is better understood.

If later, we have demand for multiple stores, that can be accomplished by overload. e.g. `Runtime.InstantiateInStore` or
`Runtime.Store(name) Store`.

## Exit

### Why do we only return a `sys.ExitError` on a non-zero exit code?

It is reasonable to think an exit error should be returned, even if the code is
success (zero). Even on success, the module is no longer functional. For
example, function exports would error later. However, wazero does not. The only
time `sys.ExitError` is on error (non-zero).

This decision was to improve performance and ergonomics for guests that both
use WASI (have a `_start` function), and also allow custom exports.
Specifically, Rust, TinyGo and normal wasi-libc, don't exit the module during
`_start`. If they did, it would invalidate their function exports. This means
it is unlikely most compilers will change this behavior.

`GOOS=waspi1` from Go 1.21 does exit during `_start`. However, it doesn't
support other exports besides `_start`, and `_start` is not defined to be
called multiple times anyway.

Since `sys.ExitError` is not always returned, we added `Module.IsClosed` for
defensive checks. This helps integrators avoid calling functions which will
always fail.

### Why panic with `sys.ExitError` after a host function exits?

Currently, the only portable way to stop processing code is via panic. For
example, WebAssembly "trap" instructions, such as divide by zero, are
implemented via panic. This ensures code isn't executed after it.

When code reaches the WASI `proc_exit` instruction, we need to stop processing.
Regardless of the exit code, any code invoked after exit would be in an
inconsistent state. This is likely why unreachable instructions are sometimes
inserted after exit: https://github.com/emscripten-core/emscripten/issues/12322

## WASI

Unfortunately, (WASI Snapshot Preview 1)[https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md] is not formally defined enough, and has APIs with ambiguous semantics.
This section describes how Wazero interprets and implements the semantics of several WASI APIs that may be interpreted differently by different wasm runtimes.
Those APIs may affect the portability of a WASI application.

### Why don't we attempt to pass wasi-testsuite on user-defined `fs.FS`?

While most cases work fine on an `os.File` based implementation, we won't
promise wasi-testsuite compatibility on user defined wrappers of `os.DirFS`.
The only option for real systems is to use our `sysfs.FS`.

There are a lot of areas where windows behaves differently, despite the
`os.File` abstraction. This goes well beyond file locking concerns (e.g.
`EBUSY` errors on open files). For example, errors like `ACCESS_DENIED` aren't
properly mapped to `EPERM`. There are trickier parts too. `FileInfo.Sys()`
doesn't return enough information to build inodes needed for WASI. To rebuild
them requires the full path to the underlying file, not just its directory
name, and there's no way for us to get that information. At one point we tried,
but in practice things became tangled and functionality such as read-only
wrappers became untenable. Finally, there are version-specific behaviors which
are difficult to maintain even in our own code. For example, go 1.20 opens
files in a different way than versions before it.

### Why aren't WASI rules enforced?

The [snapshot-01](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) version of WASI has a
number of rules for a "command module", but only the memory export rule is enforced. If a "_start" function exists, it
is enforced to be the correct signature and succeed, but the export itself isn't enforced. It follows that this means
exports are not required to be contained to a "_start" function invocation. Finally, the "__indirect_function_table"
export is also not enforced.

The reason for the exceptions are that implementations aren't following the rules. For example, TinyGo doesn't export
"__indirect_function_table", so crashing on this would make wazero unable to run TinyGo modules. Similarly, modules
loaded by wapc-go don't always define a "_start" function. Since "snapshot-01" is not a proper version, and certainly
not a W3C recommendation, there's no sense in breaking users over matters like this.

### Why is I/O configuration not coupled to WASI?

WebAssembly System Interfaces (WASI) is a formalization of a practice that can be done anyway: Define a host function to
access a system interface, such as writing to STDOUT. WASI stalled at snapshot-01 and as of early 2023, is being
rewritten entirely.

This instability implies a need to transition between WASI specs, which places wazero in a position that requires
decoupling. For example, if code uses two different functions to call `fd_write`, the underlying configuration must be
centralized and decoupled. Otherwise, calls using the same file descriptor number will end up writing to different
places.

In short, wazero defined system configuration in `ModuleConfig`, not a WASI type. This allows end-users to switch from
one spec to another with minimal impact. This has other helpful benefits, as centralized resources are simpler to close
coherently (ex via `Module.Close`).

In reflection, this worked well as more ABI became usable in wazero.

### Background on `ModuleConfig` design

WebAssembly 1.0 (20191205) specifies some aspects to control isolation between modules ([sandboxing](https://en.wikipedia.org/wiki/Sandbox_(computer_security))).
For example, `wasm.Memory` has size constraints and each instance of it is isolated from each other. While `wasm.Memory`
can be shared, by exporting it, it is not exported by default. In fact a WebAssembly Module (Wasm) has no memory by
default.

While memory is defined in WebAssembly 1.0 (20191205), many aspects are not. Let's use an example of `exec.Cmd` as for
example, a WebAssembly System Interfaces (WASI) command is implemented as a module with a `_start` function, and in many
ways acts similar to a process with a `main` function.

To capture "hello world" written to the console (stdout a.k.a. file descriptor 1) in `exec.Cmd`, you would set the
`Stdout` field accordingly, perhaps to a buffer. In WebAssembly 1.0 (20191205), the only way to perform something like
this is via a host function (ex `HostModuleFunctionBuilder`) and internally copy memory corresponding to that string
to a buffer.

WASI implements system interfaces with host functions. Concretely, to write to console, a WASI command `Module` imports
"fd_write" from "wasi_snapshot_preview1" and calls it with the `fd` parameter set to 1 (STDOUT).

The [snapshot-01](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) version of WASI has no
means to declare configuration, although its function definitions imply configuration for example if fd 1 should exist,
and if so where should it write. Moreover, snapshot-01 was last updated in late 2020 and the specification is being
completely rewritten as of early 2022. This means WASI as defined by "snapshot-01" will not clarify aspects like which
file descriptors are required. While it is possible a subsequent version may, it is too early to tell as no version of
WASI has reached a stage near W3C recommendation. Even if it did, module authors are not required to only use WASI to
write to console, as they can define their own host functions, such as they did before WASI existed.

wazero aims to serve Go developers as a primary function, and help them transition between WASI specifications. In
order to do this, we have to allow top-level configuration. To ensure isolation by default, `ModuleConfig` has WithXXX
that override defaults to no-op or empty. One `ModuleConfig` instance is used regardless of how many times the same WASI
functions are imported. The nil defaults allow safe concurrency in these situations, as well lower the cost when they
are never used. Finally, a one-to-one mapping with `Module` allows the module to close the `ModuleConfig` instead of
confusing users with another API to close.

Naming, defaults and validation rules of aspects like `STDIN` and `Environ` are intentionally similar to other Go
libraries such as `exec.Cmd` or `syscall.SetEnv`, and differences called out where helpful. For example, there's no goal
to emulate any operating system primitive specific to Windows (such as a 'c:\' drive). Moreover, certain defaults
working with real system calls are neither relevant nor safe to inherit: For example, `exec.Cmd` defaults to read STDIN
from a real file descriptor ("/dev/null"). Defaulting to this, vs reading `io.EOF`, would be unsafe as it can exhaust
file descriptors if resources aren't managed properly. In other words, blind copying of defaults isn't wise as it can
violate isolation or endanger the embedding process. In summary, we try to be similar to normal Go code, but often need
act differently and document `ModuleConfig` is more about emulating, not necessarily performing real system calls.

## File systems

### Motivation on `sys.FS`

The `sys.FS` abstraction in wazero was created because of limitations in
`fs.FS`, and `fs.File` in Go. Compilers targeting `wasip1` may access
functionality that writes new files. The ability to overcome this was requested
even before wazero was named this, via issue #21 in March 2021.

A month later, golang/go#45757 was raised by someone else on the same topic. As
of July 2023, this has not resolved to a writeable file system abstraction.

Over the next year more use cases accumulated, consolidated in March 2022 into
#390. This closed in January 2023 with a milestone of providing more
functionality, limited to users giving a real directory. This didn't yet expose
a file abstraction for general purpose use. Internally, this used `os.File`.
However, a wasm module instance is a virtual machine. Only supporting `os.File`
breaks sand-boxing use cases. Moreover, `os.File` is not an interface. Even
though this abstracts functionality, it does allow interception use cases.

Hence, a few days later in January 2023, we had more issues asking to expose an
abstraction, #1013 and later #1532, on use cases like masking access to files.
In other words, the use case requests never stopped, and aren't solved by
exposing only real files.

In summary, the primary motivation for exposing a replacement for `fs.FS` and
`fs.File` was around repetitive use case requests for years, around
interception and the ability to create new files, both virtual and real files.
While some use cases are solved with real files, not all are. Regardless, an
interface approach is necessary to ensure users can intercept I/O operations.

### Why doesn't `sys.File` have a `Fd()` method?

There are many features we could expose. We could make File expose underlying
file descriptors in case they are supported, for integration of system calls
that accept multiple ones, namely `poll` for multiplexing. This special case is
described in a subsequent section.

As noted above, users have been asking for a file abstraction for over two
years, and a common answer was to wait. Making users wait is a problem,
especially so long. Good reasons to make people wait are stabilization. Edge
case features are not a great reason to hold abstractions from users.

Another reason is implementation difficulty. Go did not attempt to abstract
file descriptors. For example, unlike `fs.ReadFile` there is no `fs.FdFile`
interface. Most likely, this is because file descriptors are an implementation
detail of common features. Programming languages, including Go, do not require
end users to know about file descriptors. Types such as `fs.File` can be used
without any knowledge of them. Implementations may or may not have file
descriptors. For example, in Go, `os.DirFS` has underlying file descriptors
while `embed.FS` does not.

Despite this, some may want to expose a non-standard interface because
`os.File` has `Fd() uintptr` to return a file descriptor. Mainly, this is
handy to integrate with `syscall` package functions (on `GOOS` values that
declare them). Notice, though that `uintptr` is unsafe and not an abstraction.
Close inspection will find some `os.File` types internally use `poll.FD`
instead, yet this is not possible to use abstractly because that type is not
exposed. For example, `plan9` uses a different type than `poll.FD`. In other
words, even in real files, `Fd()` is not wholly portable, despite it being
useful on many operating systems with the `syscall` package.

The reasons above, why Go doesn't abstract `FdFile` interface are a subset of
reasons why `sys.File` does not. If we exposed `File.Fd()` we not only would
have to declare all the edge cases that Go describes including impact of
finalizers, we would have to describe these in terms of virtualized files.
Then, we would have to reason with this value vs our existing virtualized
`sys.FileTable`, mapping whatever type we return to keys in that table, also
in consideration of garbage collection impact. The combination of issues like
this could lead down a path of not implementing a file system abstraction at
all, and instead a weak key mapped abstraction of the `syscall` package. Once
we finished with all the edge cases, we would have lost context of the original
reason why we started.. simply to allow file write access!

When wazero attempts to do more than what the Go programming language team, it
has to be carefully evaluated, to:
* Be possible to implement at least for `os.File` backed files
* Not be confusing or cognitively hard for virtual file systems and normal use.
* Affordable: custom code is solely the responsible by the core team, a much
  smaller group of individuals than who maintain the Go programming language.

Due to problems well known in Go, consideration of the end users who constantly
ask for basic file system functionality, and the difficulty virtualizing file
descriptors at multiple levels, we don't expose `Fd()` and likely won't ever
expose `Fd()` on `sys.File`.

### Why does `sys.File` have a `Poll()` method, while `sys.FS` does not?

wazero exposes `File.Poll` which allows one-at-a-time poll use cases,
requested by multiple users. This not only includes abstract tests such as
Go 1.21 `GOOS=wasip1`, but real use cases including python and container2wasm
repls, as well listen sockets. The main use cases is non-blocking poll on a
single file. Being a single file, this has no risk of problems such as
head-of-line blocking, even when emulated.

The main use case of multi-poll are bidirectional network services, something
not used in `GOOS=wasip1` standard libraries, but could be in the future.
Moving forward without a multi-poller allows wazero to expose its file system
abstraction instead of continuing to hold back it back for edge cases. We'll
continue discussion below regardless, as rationale was requested.

You can loop through multiple `sys.File`, using `File.Poll` to see if an event
is ready, but there is a head-of-line blocking problem. If a long timeout is
used, bad luck could have a file that has nothing to read or write before one
that does. This could cause more blocking than necessary, even if you could
poll the others just after with a zero timeout. What's worse than this is if
unlimited blocking was used (`timeout=-1`). The host implementations could use
goroutines to avoid this, but interrupting a "forever" poll is problematic. All
of these are reasons to consider a multi-poll API, but do not require exporting
`File.Fd()`.

Should multi-poll becomes critical, `sys.FS` could expose a `Poll` function
like below, despite it being the non-portable, complicated if possible to
implement on all platforms and virtual file systems.
```go
ready, errno := fs.Poll([]sys.PollFile{{f1, sys.POLLIN}, {f2, sys.POLLOUT}}, timeoutMillis)
```

A real filesystem could handle this by using an approach like the internal
`unix.Poll` function in Go, passing file descriptors on unix platforms, or
returning `sys.ENOSYS` for unsupported operating systems. Implementation for
virtual files could have a strategy around timeout to avoid the worst case of
head-of-line blocking (unlimited timeout).

Let's remember that when designing abstractions, it is not best to add an
interface for everything. Certainly, Go doesn't, as evidenced by them not
exposing `poll.FD` in `os.File`! Such a multi-poll could be limited to
built-in filesystems in the wazero repository, avoiding complexity of trying to
support and test this abstractly. This would still permit multiplexing for CLI
users, and also permit single file polling as exists now.

### Why doesn't wazero implement the working directory?

An early design of wazero's API included a `WithWorkDirFS` which allowed
control over which file a relative path such as "./config.yml" resolved to,
independent of the root file system. This intended to help separate concerns
like mutability of files, but it didn't work and was removed.

Compilers that target wasm act differently with regard to the working
directory. For example, wasi-libc, used by TinyGo,
tracks working directory changes in compiled wasm instead: initially "/" until
code calls `chdir`. Zig assumes the first pre-opened file descriptor is the
working directory.

The only place wazero can standardize a layered concern is via a host function.
Since WASI doesn't use host functions to track the working directory, we can't
standardize the storage and initial value of it.

Meanwhile, code may be able to affect the working directory by compiling
`chdir` into their main function, using an argument or ENV for the initial
value (possibly `PWD`). Those unable to control the compiled code should only
use absolute paths in configuration.

See
* https://github.com/golang/go/blob/go1.20/src/syscall/fs_js.go#L324
* https://github.com/WebAssembly/wasi-libc/pull/214#issue-673090117
* https://github.com/ziglang/zig/blob/53a9ee699a35a3d245ab6d1dac1f0687a4dcb42c/src/main.zig#L32

### Why ignore the error returned by io.Reader when n > 1?

Per https://pkg.go.dev/io#Reader, if we receive an error, any bytes read should
be processed first. At the syscall abstraction (`fd_read`), the caller is the
processor, so we can't process the bytes inline and also return the error (as
`EIO`).

Let's assume we want to return the bytes read on error to the caller. This
implies we at least temporarily ignore the error alongside them. The choice
remaining is whether to persist the error returned with the read until a
possible next call, or ignore the error.

If we persist an error returned, it would be coupled to a file descriptor, but
effectively it is boolean as this case coerces to `EIO`. If we track a "last
error" on a file descriptor, it could be complicated for a couple reasons
including whether the error is transient or permanent, or if the error would
apply to any FD operation, or just read. Finally, there may never be a
subsequent read as perhaps the bytes leading up to the error are enough to
satisfy the processor.

This decision boils down to whether or not to track an error bit per file
descriptor or not. If not, the assumption is that a subsequent operation would
also error, this time without reading any bytes.

The current opinion is to go with the simplest path, which is to return the
bytes read and ignore the error the there were any. Assume a subsequent
operation will err if it needs to. This helps reduce the complexity of the code
in wazero and also accommodates the scenario where the bytes read are enough to
satisfy its processor.

### File descriptor allocation strategy

File descriptor allocation currently uses a strategy similar the one implemented
by unix systems: when opening a file, the lowest unused number is picked.

The WASI standard documents that programs cannot expect that file descriptor
numbers will be allocated with a lowest-first strategy, and they should instead
assume the values will be random. Since _random_ is a very imprecise concept in
computers, we technically satisfying the implementation with the descriptor
allocation strategy we use in Wazero. We could imagine adding more _randomness_
to the descriptor selection process, however this should never be used as a
security measure to prevent applications from guessing the next file number so
there are no strong incentives to complicate the logic.

### Why does `FSConfig.WithDirMount` not match behaviour with `os.DirFS`?

It may seem that we should require any feature that seems like a standard
library in Go, to behave the same way as the standard library. Doing so would
present least surprise to Go developers. In the case of how we handle
filesystems, we break from that as it is incompatible with the expectations of
WASI, the most commonly implemented filesystem ABI.

The main reason is that `os.DirFS` is a virtual filesystem abstraction while
WASI is an abstraction over syscalls. For example, the signature of `fs.Open`
does not permit use of flags. This creates conflict on what default behaviors
to take when Go implemented `os.DirFS`. On the other hand, `path_open` can pass
flags, and in fact tests require them to be honored in specific ways.

This conflict requires us to choose what to be more compatible with, and which
type of user to surprise the least. We assume there will be more developers
compiling code to wasm than developers of custom filesystem plugins, and those
compiling code to wasm will be better served if we are compatible with WASI.
Hence on conflict, we prefer WASI behavior vs the behavior of `os.DirFS`.

See https://github.com/WebAssembly/wasi-testsuite
See https://github.com/golang/go/issues/58141

## Why is our `Readdir` function more like Go's `os.File` than POSIX `readdir`?

At one point we attempted to move from a bulk `Readdir` function to something
more like the POSIX `DIR` struct, exposing functions like `telldir`, `seekdir`
and `readdir`. However, we chose the design more like `os.File.Readdir`,
because it performs and fits wasip1 better.

### wasip1/wasix

`fd_readdir` in wasip1 (and so also wasix) is like `getdents` in Linux, not
`readdir` in POSIX. `getdents` is more like Go's `os.File.Readdir`.

We currently have an internal type `sys.DirentCache` which only is used by
wasip1 or wasix. When `HostModuleBuilder` adds support for instantiation state,
we could move this to the `wasi_snapshot_preview1` package. Meanwhile, all
filesystem code is internal anyway, so this special-case is acceptable.

### wasip2

`directory-entry-stream` in wasi-filesystem preview2 is defined in component
model, not an ABI, but in wasmtime it is a consuming iterator. A consuming
iterator is easy to support with anything (like `Readdir(1)`), even if it is
inefficient as you can neither bulk read nor skip. The implementation of the
preview1 adapter (uses preview2) confirms this. They use a dirent cache similar
in some ways to our `sysfs.DirentCache`. As there is no seek concept in
preview2, they interpret the cookie as numeric and read on repeat entries when
a cache wasn't available. Note: we currently do not skip-read like this as it
risks buffering large directories, and no user has requested entries before the
cache, yet.

Regardless, wasip2 is not complete until the end of 2023. We can defer design
discussion until after it is stable and after the reference impl wasmtime
implements it.

See
 * https://github.com/WebAssembly/wasi-filesystem/blob/ef9fc87c07323a6827632edeb6a7388b31266c8e/example-world.md#directory_entry_stream
 * https://github.com/bytecodealliance/wasmtime/blob/b741f7c79d72492d17ab8a29c8ffe4687715938e/crates/wasi/src/preview2/preview2/filesystem.rs#L286-L296
 * https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L2131-L2137
 * https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L936

### wasip3

`directory-entry-stream` is documented to change significantly in wasip3 moving
from synchronous to synchronous streams. This is dramatically different than
POSIX `readdir` which is synchronous.

Regardless, wasip3 is not complete until after wasip2, which means 2024 or
later. We can defer design discussion until after it is stable and after the
reference impl wasmtime implements it.

See
 * https://github.com/WebAssembly/WASI/blob/ddfe3d1dda5d1473f37ecebc552ae20ce5fd319a/docs/WitInWasi.md#Streams
 * https://docs.google.com/presentation/d/1MNVOZ8hdofO3tI0szg_i-Yoy0N2QPU2C--LzVuoGSlE/edit#slide=id.g1270ef7d5b6_0_662

### How do we implement `Pread` with an `fs.File`?

`ReadAt` is the Go equivalent to `pread`: it does not affect, and is not
affected by, the underlying file offset. Unfortunately, `io.ReaderAt` is not
implemented by all `fs.File`. For example, as of Go 1.19, `embed.openFile` does
not.

The initial implementation of `fd_pread` instead used `Seek`. To avoid a
regression, we fall back to `io.Seeker` when `io.ReaderAt` is not supported.

This requires obtaining the initial file offset, seeking to the intended read
offset, and resetting the file offset the initial state. If this final seek
fails, the file offset is left in an undefined state. This is not thread-safe.

While seeking per read seems expensive, the common case of `embed.openFile` is
only accessing a single int64 field, which is cheap.

### Pre-opened files

WASI includes `fd_prestat_get` and `fd_prestat_dir_name` functions used to
learn any directory paths for file descriptors open at initialization time.

For example, `__wasilibc_register_preopened_fd` scans any file descriptors past
STDERR (1) and invokes `fd_prestat_dir_name` to learn any path prefixes they
correspond to. Zig's `preopensAlloc` does similar. These pre-open functions are
not used again after initialization.

wazero supports stdio pre-opens followed by any mounts e.g `.:/`. The guest
path is a directory and its name, e.g. "/" is returned by `fd_prestat_dir_name`
for file descriptor 3 (STDERR+1). The first longest match wins on multiple
pre-opens, which allows a path like "/tmp" to match regardless of order vs "/".

See
 * https://github.com/WebAssembly/wasi-libc/blob/a02298043ff551ce1157bc2ee7ab74c3bffe7144/libc-bottom-half/sources/preopens.c
 * https://github.com/ziglang/zig/blob/9cb06f3b8bf9ea6b5e5307711bc97328762d6a1d/lib/std/fs/wasi.zig#L50-L53

### fd_prestat_dir_name

`fd_prestat_dir_name` is a WASI function to return the path of the pre-opened
directory of a file descriptor. It has the following three parameters, and the
third `path_len` has ambiguous semantics.

* `fd`: a file descriptor
* `path`: the offset for the result path
* `path_len`: In wazero, `FdPrestatDirName` writes the result path string to
  `path` offset for the exact length of `path_len`.

Wasmer considers `path_len` to be the maximum length instead of the exact
length that should be written.
See https://github.com/wasmerio/wasmer/blob/3463c51268ed551933392a4063bd4f8e7498b0f6/lib/wasi/src/syscalls/mod.rs#L764

The semantics in wazero follows that of wasmtime.
See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6ab543e8c3f3b238/crates/wasi-common/src/snapshots/preview_1.rs#L578-L582

Their semantics match when `path_len` == the length of `path`, so in practice
this difference won't matter match.

## fd_readdir

### Why does "wasi_snapshot_preview1" require dot entries when POSIX does not?

In October 2019, WASI project knew requiring dot entries ("." and "..") was not
documented in preview1, not required by POSIX and problematic to synthesize.
For example, Windows runtimes backed by `FindNextFileW` could not return these.
A year later, the tag representing WASI preview 1 (`snapshot-01`) was made.
This did not include the requested change of making dot entries optional.

The `phases/snapshot/docs.md` document was altered in subsequent years in
significant ways, often in lock-step with wasmtime or wasi-libc. In January
2022, `sock_accept` was added to `phases/snapshot/docs.md`, a document later
renamed to later renamed to `legacy/preview1/docs.md`.

As a result, the ABI and behavior remained unstable: The `snapshot-01` tag was
not an effective basis of portability. A test suite was requested well before
this tag, in April 2019. Meanwhile, compliance had no meaning. Developers had
to track changes to the latest doc, while clarifying with wasi-libc or wasmtime
behavior. This lack of stability could have permitted a fix to the dot entries
problem, just as it permitted changes desired by other users.

In November 2022, the wasi-testsuite project began and started solidifying
expectations. This quickly led to changes in runtimes and the spec doc. WASI
began importing tests from wasmtime as required behaviors for all runtimes.
Some changes implied changes to wasi-libc. For example, `readdir` began to
imply inode fan-outs, which caused performance regressions. Most notably a
test merged in January required dot entries. Tests were merged without running
against any runtime, and even when run ad-hoc only against Linux. Hence,
portability issues mentioned over three years earlier did not trigger any
failure until wazero (which tests Windows) noticed.

In the same month, wazero requested to revert this change primarily because
Go does not return them from `os.ReadDir`, and materializing them is
complicated due to tests also requiring inodes. Moreover, they are discarded by
not just Go, but other common programming languages. This was rejected by the
WASI lead for preview1, but considered for the completely different ABI named
preview2.

In February 2023, the WASI chair declared that new rule requiring preview1 to
return dot entries "was decided by the subgroup as a whole", citing meeting
notes. According to these notes, the WASI lead stated incorrectly that POSIX
conformance required returning dot entries, something it explicitly says are
optional. In other words, he said filtering them out would make Preview1
non-conforming, and asked if anyone objects to this. The co-chair was noted to
say "Because there are existing P1 programs, we shouldn’t make changes like
this." No other were recorded to say anything.

In summary, preview1 was changed retrospectively to require dot entries and
preview2 was changed to require their absence. This rule was reverse engineered
from wasmtime tests, and affirmed on two false premises:

* POSIX compliance requires dot entries
  * POSIX literally says these are optional
* WASI cannot make changes because there are existing P1 programs.
  * Changes to Preview 1 happened before and after this topic.

As of June 2023, wasi-testsuite still only runs on Linux, so compliance of this
rule on Windows is left to runtimes to decide to validate. The preview2 adapter
uses fake cookies zero and one to refer to dot dirents, uses a real inode for
the dot(".") entry and zero inode for dot-dot("..").

See https://github.com/WebAssembly/wasi-filesystem/issues/3
See https://github.com/WebAssembly/WASI/tree/snapshot-01
See https://github.com/WebAssembly/WASI/issues/9
See https://github.com/WebAssembly/WASI/pull/458
See https://github.com/WebAssembly/wasi-testsuite/pull/32
See https://github.com/WebAssembly/wasi-libc/pull/345
See https://github.com/WebAssembly/wasi-testsuite/issues/52
See https://github.com/WebAssembly/WASI/pull/516
See https://github.com/WebAssembly/meetings/blob/main/wasi/2023/WASI-02-09.md#should-preview1-fd_readdir-filter-out--and-
See https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1026-L1041

### Why are dot (".") and dot-dot ("..") entries problematic?

When reading a directory, dot (".") and dot-dot ("..") entries are problematic.
For example, Go does not return them from `os.ReadDir`, and materializing them
is complicated (at least dot-dot is).

A directory entry has stat information in it. The stat information includes
inode which is used for comparing file equivalence. In the simple case of dot,
we could materialize a special entry to expose the same info as stat on the fd
would return. However, doing this and not doing dot-dot would cause confusion,
and dot-dot is far more tricky. To back-fill inode information about a parent
directory would be costly and subtle. For example, the pre-open (mount) of the
directory may be different than its logical parent. This is easy to understand
when considering the common case of mounting "/" and "/tmp" as pre-opens. To
implement ".." from "/tmp" requires information from a separate pre-open, this
includes state to even know the difference. There are easier edge cases as
well, such as the decision to not return ".." from a root path. In any case,
this should start to explain that faking entries when underlying stdlib doesn't
return them is tricky and requires quite a lot of state.

Another issue is around the `Dirent.Off` value of a directory entry, sometimes
called a "cookie" in Linux man pagers. When the host operating system or
library function does not return dot entries, to support functions such as
`seekdir`, you still need a value for `Dirent.Off`. Naively, you can synthesize
these by choosing sequential offsets zero and one. However, POSIX strictly says
offsets should be treated opaquely. The backing filesystem could use these to
represent real entries. For example, a directory with one entry could use zero
as the `Dirent.Off` value. If you also used zero for the "." dirent, there
would be a clash. This means if you synthesize `Dirent.Off` for any entry, you
need to synthesize this value for all entries. In practice, the simplest way is
using an incrementing number, such as done in the WASI preview2 adapter.

Working around these issues causes expense to all users of wazero, so we'd
then look to see if that would be justified or not. However, the most common
compilers involved in end user questions, as of early 2023 are TinyGo, Rust and
Zig. All of these compile code which ignores dot and dot-dot entries. In other
words, faking these entries would not only cost our codebase with complexity,
but it would also add unnecessary overhead as the values aren't commonly used.

The final reason why we might do this, is an end users or a specification
requiring us to. As of early 2023, no end user has raised concern over Go and
by extension wazero not returning dot and dot-dot. The snapshot-01 spec of WASI
does not mention anything on this point. Also, POSIX has the following to say,
which summarizes to "these are optional"

> The readdir() function shall not return directory entries containing empty names. If entries for dot or dot-dot exist, one entry shall be returned for dot and one entry shall be returned for dot-dot; otherwise, they shall not be returned.

Unfortunately, as described above, the WASI project decided in early 2023 to
require dot entries in both the spec and the wasi-testsuite. For only this
reason, wazero adds overhead to synthesize dot entries despite it being
unnecessary for most users.

See https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
See https://github.com/golang/go/blob/go1.20/src/os/dir_unix.go#L108-L110
See https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1026-L1041

### Why don't we pre-populate an inode for the dot-dot ("..") entry?

We only populate an inode for dot (".") because wasi-testsuite requires it, and
we likely already have it (because we cache it). We could attempt to populate
one for dot-dot (".."), but chose not to.

Firstly, wasi-testsuite does not require the inode of dot-dot, possibly because
the wasip2 adapter doesn't populate it (but we don't really know why).

The only other reason to populate it would be to avoid wasi-libc's stat fanout
when it is missing. However, wasi-libc explicitly doesn't fan-out to lstat on
the ".." entry on a zero ino.

Fetching dot-dot's inode despite the above not only doesn't help wasi-libc, but
it also hurts languages that don't use it, such as Go. These languages would
pay a stat syscall penalty even if they don't need the inode. In fact, Go
discards both dot entries!

In summary, there are no significant upsides in attempting to pre-fetch
dot-dot's inode, and there are downsides to doing it anyway.

See
 * https://github.com/WebAssembly/wasi-libc/blob/bd950eb128bff337153de217b11270f948d04bb4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L87-L94
 * https://github.com/WebAssembly/wasi-testsuite/blob/main/tests/rust/src/bin/fd_readdir.rs#L108
 * https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1037

### Why don't we require inodes to be non-zero?

We don't require a non-zero value for `Dirent.Ino` because doing so can prevent
a real one from resolving later via `Stat_t.Ino`.

We define `Ino` like `d_ino` in POSIX which doesn't special-case zero. It can
be zero for a few reasons:

* The file is not a regular file or directory.
* The underlying filesystem does not support inodes. e.g. embed:fs
* A directory doesn't include inodes, but a later stat can. e.g. Windows
* The backend is based on wasi-filesystem (a.k.a wasip2), which has
  `directory_entry.inode` optional, and might remove it entirely.

There are other downsides to returning a zero inode in widely used compilers:

* File equivalence utilities, like `os.SameFile` will not work.
* wasi-libc's `wasip1` mode will call `lstat` and attempt to retrieve a
  non-zero value (unless the entry is named "..").

A new compiler may accidentally skip a `Dirent` with a zero `Ino` if emulating
a non-POSIX function and re-using `Dirent.Ino` for `d_fileno`.

* Linux `getdents` doesn't define `d_fileno` must be non-zero
* BSD `getdirentries` is implementation specific. For example, OpenBSD will
  return dirents with a zero `d_fileno`, but Darwin will skip them.

The above shouldn't be a problem, even in the case of BSD, because `wasip1` is
defined more in terms of `getdents` than `getdirentries`. The bottom half of
either should treat `wasip1` (or any similar ABI such as wasix or wasip2) as a
different operating system and either use different logic that doesn't skip, or
synthesize a fake non-zero `d_fileno` when `d_ino` is zero.

However, this has been a problem. Go's `syscall.ParseDirent` utility is shared
for all `GOOS=unix`. For simplicity, this abstracts `direntIno` with data from
`d_fileno` or `d_ino`, and drops if either are zero, even if `d_fileno` is the
only field with zero explicitly defined. This led to a change to special case
`GOOS=wasip1` as otherwise virtual files would be unconditionally skipped.

In practice, this problem is rather unique due to so many compilers relying on
wasi-libc, which tolerates a zero inode. For example, while issues were
reported about the performance regression when wasi-libc began doing a fan-out
on zero `Dirent.Ino`, no issues were reported about dirents being dropped as a
result.

In summary, rather than complicating implementation and forcing non-zero inodes
for a rare case, we permit zero. We instead document this topic thoroughly, so
that emerging compilers can re-use the research and reference it on conflict.
We also document that `Ino` should be non-zero, so that users implementing that
field will attempt to get it.

See
 * https://github.com/WebAssembly/wasi-filesystem/pull/81
 * https://github.com/WebAssembly/wasi-libc/blob/bd950eb128bff337153de217b11270f948d04bb4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L87-L94
 * https://linux.die.net/man/3/getdents
 * https://www.unix.com/man-page/osx/2/getdirentries/
 * https://man.openbsd.org/OpenBSD-5.4/getdirentries.2
 * https://github.com/golang/go/blob/go1.20/src/syscall/dirent.go#L60-L102
 * https://go-review.googlesource.com/c/go/+/507915

## sys.Walltime and Nanotime

The `sys` package has two function types, `Walltime` and `Nanotime` for real
and monotonic clock exports. The naming matches conventions used in Go.

```go
func time_now() (sec int64, nsec int32, mono int64) {
	sec, nsec = walltime()
	return sec, nsec, nanotime()
}
```

Splitting functions for wall and clock time allow implementations to choose
whether to implement the clock once (as in Go), or split them out.

Each can be configured with a `ClockResolution`, although is it usually
incorrect as detailed in a sub-heading below. The only reason for exposing this
is to satisfy WASI:

See https://github.com/WebAssembly/wasi-clocks

### Why default to fake time?

WebAssembly has an implicit design pattern of capabilities based security. By
defaulting to a fake time, we reduce the chance of timing attacks, at the cost
of requiring configuration to opt-into real clocks.

See https://gruss.cc/files/fantastictimers.pdf for an example attacks.

### Why does fake time increase on reading?

Both the fake nanotime and walltime increase by 1ms on reading. Particularly in
the case of nanotime, this prevents spinning.

### Why not `time.Clock`?

wazero can't use `time.Clock` as a plugin for clock implementation as it is
only substitutable with build flags (`faketime`) and conflates wall and
monotonic time in the same call.

Go's `time.Clock` was added monotonic time after the fact. For portability with
prior APIs, a decision was made to combine readings into the same API call.

See https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md

WebAssembly time imports do not have the same concern. In fact even Go's
imports for clocks split walltime from nanotime readings.

See https://github.com/golang/go/blob/go1.20/misc/wasm/wasm_exec.js#L243-L255

Finally, Go's clock is not an interface. WebAssembly users who want determinism
or security need to be able to substitute an alternative clock implementation
from the host process one.

### `ClockResolution`

A clock's resolution is hardware and OS dependent so requires a system call to retrieve an accurate value.
Go does not provide a function for getting resolution, so without CGO we don't have an easy way to get an actual
value. For now, we return fixed values of 1us for realtime and 1ns for monotonic, assuming that realtime clocks are
often lower precision than monotonic clocks. In the future, this could be improved by having OS+arch specific assembly
to make syscalls.

For example, Go implements time.Now for linux-amd64 with this [assembly](https://github.com/golang/go/blob/go1.20/src/runtime/time_linux_amd64.s).
Because retrieving resolution is not generally called often, unlike getting time, it could be appropriate to only
implement the fallback logic that does not use VDSO (executing syscalls in user mode). The syscall for clock_getres
is 229 and should be usable. https://pkg.go.dev/syscall#pkg-constants.

If implementing similar for Windows, [mingw](https://github.com/mirror/mingw-w64/blob/6a0e9165008f731bccadfc41a59719cf7c8efc02/mingw-w64-libraries/winpthreads/src/clock.c#L77
) is often a good source to find the Windows API calls that correspond
to a POSIX method.

Writing assembly would allow making syscalls without CGO, but comes with the cost that it will require implementations
across many combinations of OS and architecture.

## sys.Nanosleep

All major programming languages have a `sleep` mechanism to block for a
duration. Sleep is typically implemented by a WASI `poll_oneoff` relative clock
subscription.

For example, the below ends up calling `wasi_snapshot_preview1.poll_oneoff`:

```zig
const std = @import("std");
pub fn main() !void {
    std.time.sleep(std.time.ns_per_s * 5);
}
```

Besides Zig, this is also the case with TinyGo (`-target=wasi`) and Rust
(`--target wasm32-wasi`).

We decided to expose `sys.Nanosleep` to allow overriding the implementation
used in the common case, even if it isn't used by Go, because this gives an
easy and efficient closure over a common program function. We also documented
`sys.Nanotime` to warn users that some compilers don't optimize sleep.

## sys.Osyield

We expose `sys.Osyield`, to allow users to control the behavior of WASI's
`sched_yield` without a new build of wazero. This is mainly for parity with
all other related features which we allow users to implement, including
`sys.Nanosleep`. Unlike others, we don't provide an out-of-box implementation
primarily because it will cause performance problems when accessed.

For example, the below implementation uses CGO, which might result in a 1us
delay per invocation depending on the platform.

See https://github.com/golang/go/issues/19409#issuecomment-284788196
```go
//go:noescape
//go:linkname osyield runtime.osyield
func osyield()
```

In practice, a request to customize this is unlikely to happen until other
thread based functions are implemented. That said, as of early 2023, there are
a few signs of implementation interest and cross-referencing:

See https://github.com/WebAssembly/stack-switching/discussions/38
See https://github.com/WebAssembly/wasi-threads#what-can-be-skipped
See https://slinkydeveloper.com/Kubernetes-controllers-A-New-Hope/

## sys.Stat_t

We expose `stat` information as `sys.Stat_t`, like `syscall.Stat_t` except
defined without build constraints. For example, you can use `sys.Stat_t` on
`GOOS=windows` which doesn't define `syscall.Stat_t`.

The first use case of this is to return inodes from `fs.FileInfo` without
relying on platform-specifics. For example, a user could return `*sys.Stat_t`
from `info.Sys()` and define a non-zero inode for a virtual file, or map a
real inode to a virtual one.

Notable choices per field are listed below, where `sys.Stat_t` is unlike
`syscall.Stat_t` on `GOOS=linux`, or needs clarification. One common issue
not repeated below is that numeric fields are 64-bit when at least one platform
defines it that large. Also, zero values are equivalent to nil or absent.

* `Dev` and `Ino` (`Inode`) are both defined unsigned as they are defined
  opaque, and most `syscall.Stat_t` also defined them unsigned. There are
  separate sections in this document discussing the impact of zero in `Ino`.
* `Mode` is defined as a `fs.FileMode` even though that is not defined in POSIX
  and will not map to all possible values. This is because the current use is
  WASI, which doesn't define any types or features not already supported. By
  using `fs.FileMode`, we can re-use routine experience in Go.
* `NLink` is unsigned because it is defined that way in `syscall.Stat_t`: there
  can never be less than zero links to a file. We suggest defaulting to 1 in
  conversions when information is not knowable because at least that many links
  exist.
* `Size` is signed because it is defined that way in `syscall.Stat_t`: while
  regular files and directories will always be non-negative, irregular files
  are possibly negative or not defined. Notably sparse files are known to
  return negative values.
* `Atim`, `Mtim` and `Ctim` are signed because they are defined that way in
  `syscall.Stat_t`: Negative values are time before 1970. The resolution is
  nanosecond because that's the maximum resolution currently supported in Go.

### Why do we use `sys.EpochNanos` instead of `time.Time` or similar?

To simplify documentation, we defined a type alias `sys.EpochNanos` for int64.
`time.Time` is a data structure, and we could have used this for
`syscall.Stat_t` time values. The most important reason we do not is conversion
penalty deriving time from common types.

The most common ABI used in `wasip2`. This, and compatible ABI such as `wasix`,
encode timestamps in memory as a 64-bit number. If we used `time.Time`, we
would have to convert an underlying type like `syscall.Timespec` to `time.Time`
only to later have to call `.UnixNano()` to convert it back to a 64-bit number.

In the future, the component model module "wasi-filesystem" may represent stat
timestamps with a type shared with "wasi-clocks", abstractly structured similar
to `time.Time`. However, component model intentionally does not define an ABI.
It is likely that the canonical ABI for timestamp will be in two parts, but it
is not required for it to be intermediately represented this way. A utility
like `syscall.NsecToTimespec` could split an int64 so that it could be written
to memory as 96 bytes (int64, int32), without allocating a struct.

Finally, some may confuse epoch nanoseconds with 32-bit epoch seconds. While
32-bit epoch seconds has "The year 2038" problem, epoch nanoseconds has
"The Year 2262" problem, which is even less concerning for this library. If
the Go programming language and wazero exist in the 2200's, we can make a major
version increment to adjust the `sys.EpochNanos` approach. Meanwhile, we have
faster code.

## poll_oneoff

`poll_oneoff` is a WASI API for waiting for I/O events on multiple handles.
It is conceptually similar to the POSIX `poll(2)` syscall.
The name is not `poll`, because it references [“the fact that this function is not efficient
when used repeatedly with the same large set of handles”][poll_oneoff].

We chose to support this API in a handful of cases that work for regular files
and standard input. We currently do not support other types of file descriptors such
as socket handles.

### Clock Subscriptions

As detailed above in [sys.Nanosleep](#sysnanosleep), `poll_oneoff` handles
relative clock subscriptions. In our implementation we use `sys.Nanosleep()`
for this purpose in most cases, except when polling for interactive input
from `os.Stdin` (see more details below).

### FdRead and FdWrite Subscriptions

When subscribing a file descriptor (except `Stdin`) for reads or writes,
the implementation will generally return immediately with success, unless
the file descriptor is unknown. The file descriptor is not checked further
for new incoming data. Any timeout is cancelled, and the API call is able
to return, unless there are subscriptions to `Stdin`: these are handled
separately.

### FdRead and FdWrite Subscription to Stdin

Subscribing `Stdin` for reads (writes make no sense and cause an error),
requires extra care: wazero allows to configure a custom reader for `Stdin`.

In general, if a custom reader is found, the behavior will be the same
as for regular file descriptors: data is assumed to be present and
a success is written back to the result buffer.

However, if the reader is detected to read from `os.Stdin`,
a special code path is followed, invoking `sysfs.poll()`.

`sysfs.poll()` is a wrapper for `poll(2)` on POSIX systems,
and it is emulated on Windows.

### Poll on POSIX

On POSIX systems, `poll(2)` allows to wait for incoming data on a file
descriptor, and block until either data becomes available or the timeout
expires.

Usage of `syfs.poll()` is currently only reserved for standard input, because

1. it is really only necessary to handle interactive input: otherwise,
   there is no way in Go to peek from Standard Input without actually
   reading (and thus consuming) from it;

2. if `Stdin` is connected to a pipe, it is ok in most cases to return
   with success immediately;

3. `syfs.poll()` is currently a blocking call, irrespective of goroutines,
   because the underlying syscall is; thus, it is better to limit its usage.

So, if the subscription is for `os.Stdin` and the handle is detected
to correspond to an interactive session, then `sysfs.poll()` will be
invoked with a the `Stdin` handle *and* the timeout.

This also means that in this specific case, the timeout is uninterruptible,
unless data becomes available on `Stdin` itself.

### Select on Windows

On Windows `sysfs.poll()` cannot be delegated to a single
syscall, because there is no single syscall to handle sockets,
pipes and regular files.

Instead, we emulate its behavior for the cases that are currently
of interest.

- For regular files, we _always_ report them as ready, as
[most operating systems do anyway][async-io-windows].

- For pipes, we invoke [`PeekNamedPipe`][peeknamedpipe]
for each file handle we detect is a pipe open for reading.
We currently ignore pipes open for writing.

- Notably, we include also support for sockets using the [WinSock
implementation of `poll`][wsapoll], but instead
of relying on the timeout argument of the `WSAPoll` function,
we set a 0-duration timeout so that it behaves like a peek.

This way, we can check for regular files all at once,
at the beginning of the function, then we poll pipes and
sockets periodically using a cancellable `time.Tick`,
which plays nicely with the rest of the Go runtime.

### Impact of blocking

Because this is a blocking syscall, it will also block the carrier thread of
the goroutine, preventing any means to support context cancellation directly.

There are ways to obviate this issue. We outline here one idea, that is however
not currently implemented. A common approach to support context cancellation is
to add a signal file descriptor to the set, e.g. the read-end of a pipe or an
eventfd on Linux. When the context is canceled, we may unblock a Select call by
writing to the fd, causing it to return immediately. This however requires to
do a bit of housekeeping to hide the "special" FD from the end-user.

[poll_oneoff]: https://github.com/WebAssembly/wasi-poll#why-is-the-function-called-poll_oneoff
[async-io-windows]: https://tinyclouds.org/iocp_links
[peeknamedpipe]: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
[wsapoll]: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll

## Signed encoding of integer global constant initializers

wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For
example, there is no signed integer [value type](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0).

To get at the problem, let's use an example.
```
(global (export "start_epoch") i64 (i64.const 1620216263544))
```

In both signed and unsigned LEB128 encoding, this value is the same bit pattern. The problem is that some numbers are
not. For example, 16256 is `807f` encoded as unsigned, but `80ff00` encoded as signed.

While the specification mentions uninterpreted integers are in abstract [unsigned values](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A0),
the binary encoding is clear that they are encoded [signed](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A4).

For consistency, we go with signed encoding in the special case of global constant initializers.

## Implementation limitations

WebAssembly 1.0 (20191205) specification allows runtimes to [limit certain aspects of Wasm module or execution](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#a2-implementation-limitations).

wazero limitations are imposed pragmatically and described below.

### Number of functions in a module

The possible number of function instances in [a module](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#module-instances%E2%91%A0) is not specified in the WebAssembly specifications since [`funcaddr`](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-funcaddr) corresponding to a function instance in a store can be arbitrary number.
wazero limits the maximum function instances to 2^27 as even that number would occupy 1GB in function pointers.

That is because not only we _believe_ that all use cases are fine with the limitation, but also we have no way to test wazero runtimes under these unusual circumstances.

### Number of function types in a store

There's no limitation on the number of function types in [a store](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0) according to the spec. In wazero implementation, we assign each function type to a unique ID, and choose to use `uint32` to represent the IDs.
Therefore the maximum number of function types a store can have is limited to 2^27 as even that number would occupy 512MB just to reference the function types.

This is due to the same reason for the limitation on the number of functions above.

### Number of values on the stack in a function

While the the spec does not clarify a limitation of function stack values, wazero limits this to 2^27 = 134,217,728.
The reason is that we internally represent all the values as 64-bit integers regardless of its types (including f32, f64), and 2^27 values means
1 GiB = (2^30). 1 GiB is the reasonable for most applications [as we see a Goroutine has 250 MB as a limit on the stack for 32-bit arch](https://github.com/golang/go/blob/go1.20/src/runtime/proc.go#L152-L159), considering that WebAssembly is (currently) 32-bit environment.

All the functions are statically analyzed at module instantiation phase, and if a function can potentially reach this limit, an error is returned.

### Number of globals in a module

Theoretically, a module can declare globals (including imports) up to 2^32 times. However, wazero limits this to  2^27(134,217,728) per module.
That is because internally we store globals in a slice with pointer types (meaning 8 bytes on 64-bit platforms), and therefore 2^27 globals
means that we have 1 GiB size of slice which seems large enough for most applications.

### Number of tables in a module

While the the spec says that a module can have up to 2^32 tables, wazero limits this to 2^27 = 134,217,728.
One of the reasons is even that number would occupy 1GB in the pointers tables alone. Not only that, we access tables slice by
table index by using 32-bit signed offset in the compiler implementation, which means that the table index of 2^27 can reach 2^27 * 8 (pointer size on 64-bit machines) = 2^30 offsets in bytes.

We _believe_ that all use cases are fine with the limitation, but also note that we have no way to test wazero runtimes under these unusual circumstances.

If a module reaches this limit, an error is returned at the compilation phase.

## Compiler engine implementation

### Why it's safe to execute runtime-generated machine codes against async Goroutine preemption

Goroutine preemption is the mechanism of the Go runtime to switch goroutines contexts on an OS thread.
There are two types of preemption: cooperative preemption and async preemption. The former happens, for example,
when making a function call, and it is not an issue for our runtime-generated functions as they do not make
direct function calls to Go-implemented functions. On the other hand, the latter, async preemption, can be problematic
since it tries to interrupt the execution of Goroutine at any point of function, and manipulates CPU register states.

Fortunately, our runtime-generated machine codes do not need to take the async preemption into account.
All the assembly codes are entered via the trampoline implemented as Go Assembler Function (e.g. [arch_amd64.s](./arch_amd64.s)),
and as of Go 1.20, these assembler functions are considered as _unsafe_ for async preemption:
- https://github.com/golang/go/blob/go1.20rc1/src/runtime/preempt.go#L406-L407
- https://github.com/golang/go/blob/9f0234214473dfb785a5ad84a8fc62a6a395cbc3/src/runtime/traceback.go#L227

From the Go runtime point of view, the execution of runtime-generated machine codes is considered as a part of
that trampoline function. Therefore, runtime-generated machine code is also correctly considered unsafe for async preemption.

## Why context cancellation is handled in Go code rather than native code

Since [wazero v1.0.0-pre.9](https://github.com/tetratelabs/wazero/releases/tag/v1.0.0-pre.9), the runtime
supports integration with Go contexts to interrupt execution after a timeout, or in response to explicit cancellation.
This support is internally implemented as a special opcode `builtinFunctionCheckExitCode` that triggers the execution of
a Go function (`ModuleInstance.FailIfClosed`) that atomically checks a sentinel value at strategic points in the code.

[It _is indeed_ possible to check the sentinel value directly, without leaving the native world][native_check], thus sparing some cycles;
however, because native code never preempts (see section above), this may lead to a state where the other goroutines
never get the chance to run, and thus never get the chance to set the sentinel value; effectively preventing
cancellation from taking place.

[native_check]: https://github.com/tetratelabs/wazero/issues/1409

## Golang patterns

### Hammer tests
Code that uses concurrency primitives, such as locks or atomics, should include "hammer tests", which run large loops
inside a bounded amount of goroutines, run by half that many `GOMAXPROCS`. These are named consistently "hammer", so
they are easy to find. The name inherits from some existing tests in [golang/go](https://github.com/golang/go/search?q=hammer&type=code).

Here is an annotated description of the key pieces of a hammer test:
1. `P` declares the count of goroutines to use, defaulting to 8 or 4 if `testing.Short`.
   * Half this amount are the cores used, and 4 is less than a modern laptop's CPU. This allows multiple "hammer" tests to run in parallel.
2. `N` declares the scale of work (loop) per goroutine, defaulting to value that finishes in ~0.1s on a modern laptop.
   * When in doubt, try 1000 or 100 if `testing.Short`
   * Remember, there are multiple hammer tests and CI nodes are slow. Slower tests hurt feedback loops.
3. `defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(P/2))` makes goroutines switch cores, testing visibility of shared data.
4. To ensure goroutines execute at the same time, block them with `sync.WaitGroup`, initialized to `Add(P)`.
   * `sync.WaitGroup` internally uses `runtime_Semacquire` not available in any other library.
   * `sync.WaitGroup.Add` with a negative value can unblock many goroutines at the same time, e.g. without a for loop.
5. Track goroutines progress via `finished := make(chan int)` where each goroutine in `P` defers `finished <- 1`.
   1. Tests use `require.XXX`, so `recover()` into `t.Fail` in a `defer` function before `finished <- 1`.
      * This makes it easier to spot larger concurrency problems as you see each failure, not just the first.
   2. After the `defer` function, await unblocked, then run the stateful function `N` times in a normal loop.
      * This loop should trigger shared state problems as locks or atomics are contended by `P` goroutines.
6. After all `P` goroutines launch, atomically release all of them with `WaitGroup.Add(-P)`.
7. Block the runner on goroutine completion, by (`<-finished`) for each `P`.
8. When all goroutines complete, `return` if `t.Failed()`, otherwise perform follow-up state checks.

This is implemented in wazero in [hammer.go](internal/testing/hammer/hammer.go)

### Lock-free, cross-goroutine observations of updates

How to achieve cross-goroutine reads of a variable are not explicitly defined in https://go.dev/ref/mem. wazero uses
atomics to implement this following unofficial practice. For example, a `Close` operation can be guarded to happen only
once via compare-and-swap (CAS) against a zero value. When we use this pattern, we consistently use atomics to both
read and update the same numeric field.

In lieu of formal documentation, we infer this pattern works from other sources (besides tests):
 * `sync.WaitGroup` by definition must support calling `Add` from other goroutines. Internally, it uses atomics.
 * rsc in golang/go#5045 writes "atomics guarantee sequential consistency among the atomic variables".

See https://github.com/golang/go/blob/go1.20/src/sync/waitgroup.go#L64
See https://github.com/golang/go/issues/5045#issuecomment-252730563
See https://www.youtube.com/watch?v=VmrEG-3bWyM