Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/wapython
Path: blob/main/core/kernel/src/wasm/posix/socket.ts
1070 views
1
/*
2
This is an implementation of POSIX Sockets.
3
4
Of course, much of the code for this is written in zig in the posix-node package
5
in this file packages/posix-node/src/socket.zig
6
7
8
RELATED WORK: I wrote most of this, then searched and found it is solving
9
a similar problem to emscripten's "Full POSIX Sockets over WebSocket Proxy Server":
10
11
- https://emscripten.org/docs/porting/networking.html#full-posix-sockets-over-websocket-proxy-server
12
- https://github.com/emscripten-core/emscripten/tree/main/tools/websocket_to_posix_proxy
13
- https://github.com/emscripten-core/emscripten/pull/7670 (interesting discussion)
14
15
16
Of course, the architecture of the CoWasm solution is massively different.
17
It looks like Emscripten's is a full multithreaded standalone C++ program
18
for proxying many network connections from (presumably) many clients.
19
In contrast, CoWasm's solution is partly written in zig as a node extension
20
and integrated with Javascript. It can be used standalone with other Javascript
21
projects having nothing to do with cowasm, by using the posix-node package,
22
so there is potential for more community feedback and development.
23
We would likely have to have one Javascript worker/thread per client, but
24
this could be integrated with accessing other resources on the server.
25
It's also nice that CoWasm also can operate with exactly the same code
26
in a mode that doesn't involving proxying to a backend server, with
27
everything in the same process (which is all that is implemented here
28
right now!), since that should be fast and good for automated testing.
29
*/
30
31
import Errno from "./errno";
32
import {
33
nativeToWasmFamily,
34
wasmToNativeFamily,
35
wasmToNativeSocktype,
36
sendSockaddr,
37
} from "./netdb";
38
import constants from "./constants";
39
import { constants as wasi_constants, SOCKET_DEFAULT_RIGHTS } from "wasi-js";
40
import debug from "debug";
41
42
const log = debug("posix:socket");
43
44
export default function socket({
45
callFunction,
46
posix,
47
recv,
48
wasi,
49
send,
50
memory,
51
}) {
52
function sendNativeSockaddr(sockaddr, ptr: number) {
53
sendSockaddr(
54
send,
55
memory,
56
ptr,
57
nativeToWasmFamily(posix, sockaddr.sa_family),
58
sockaddr.sa_len,
59
sockaddr.sa_data
60
);
61
}
62
63
function getSockaddr(sockaddrPtr: number, address_len: number) {
64
const sa_family = wasmToNativeFamily(
65
posix,
66
callFunction("recv_sockaddr_sa_family", sockaddrPtr)
67
);
68
const sa_len = address_len - 2;
69
const sa_data = recv.buffer(
70
callFunction("recv_sockaddr_sa_data", sockaddrPtr),
71
sa_len
72
);
73
for (let i = sa_len; i < sa_len; i++) {
74
sa_data[i] = 0;
75
}
76
return { sa_family, sa_len, sa_data };
77
}
78
79
function native_fd(virtual_fd: number): number {
80
const data = wasi.FD_MAP.get(virtual_fd);
81
if (data == null) {
82
throw Error(`invalid fd=${virtual_fd}`);
83
}
84
return data.real;
85
}
86
87
function getSocktype(virtual_fd: number): number {
88
const data = wasi.FD_MAP.get(virtual_fd);
89
if (data == null) {
90
throw Error(`unknown sock type for fd=${virtual_fd}`);
91
}
92
return data.socktype;
93
}
94
95
// Convert flags from wasi to native. (Right now it looks
96
// like only MSG_WAITALL is different.)
97
function native_flags(wasi_flags: number): number {
98
let flags = 0;
99
for (const name of [
100
"MSG_OOB",
101
"MSG_PEEK",
102
"MSG_WAITALL",
103
"MSG_DONTROUTE",
104
]) {
105
if (wasi_flags & constants[name]) {
106
flags |= posix.constants[name];
107
}
108
}
109
return flags;
110
}
111
112
function native_level(level: number): number {
113
if (level == constants.SOL_SOCKET) {
114
return posix.constants.SOL_SOCKET;
115
}
116
return level;
117
}
118
119
function native_option_name(option_name: number): number {
120
for (const name in constants) {
121
if (name.startsWith("SO_") && option_name == constants[name]) {
122
const x = posix.constants[name];
123
if (x == null) {
124
throw Error(
125
`unsupported option name "${name}" -- defined in WebAssembly but not natively`
126
);
127
}
128
return x;
129
}
130
}
131
throw Error(`unknown option name ${option_name}`);
132
}
133
134
function createWasiFd(native_fd: number, socktype: number): number {
135
// TODO: I'm starting the socket fd's at value over 1000 entirely because
136
// if wstart at the default smallest possible when doing
137
// "python-wasm -m pip" it crashes, since the fd=4 gets assigned
138
// to some socket for a moment, then freed and then 4 gets used
139
// for a directory (maybe at the same time), and this somehow
140
// confuses things. Maybe there is a bug somewhere in WASI or Python.
141
// For now we just workaround it by putting the socket fd's
142
// way out of reach of the normal file fd's.
143
const wasi_fd = wasi.getUnusedFileDescriptor(1000);
144
wasi.FD_MAP.set(wasi_fd, {
145
real: native_fd,
146
rights: {
147
base: SOCKET_DEFAULT_RIGHTS,
148
inheriting: BigInt(0),
149
},
150
filetype: wasi_constants.WASI_FILETYPE_SOCKET_STREAM,
151
socktype, // constants.SOCK_STREAM (tcp) or constants.SOCK_DGRAM (udp)
152
});
153
return wasi_fd;
154
}
155
156
return {
157
socket(family: number, socktype: number, protocol: number): number {
158
if (posix.socket == null) {
159
throw Errno("ENOTSUP");
160
}
161
log("socket", { family, socktype, protocol });
162
163
const familyNative = wasmToNativeFamily(posix, family);
164
165
let inheritable;
166
if (constants.SOCK_CLOEXEC & socktype) {
167
// SOCK_CLOEXEC is defined on Linux and WASI (weirdly) but not MacOS,
168
// so we manually implement it to avoid weird hacks in C code.
169
socktype &= ~constants.SOCK_CLOEXEC; // remove it
170
inheritable = false; // below we will do what SOCK_CLOEXEC would do manually.
171
} else {
172
inheritable = true;
173
}
174
175
const socktypeNative = wasmToNativeSocktype(posix, socktype);
176
177
// TODO? I don't know how to translate this or if it is necessary.
178
const protocolNative = protocol;
179
180
const native_fd = posix.socket(
181
familyNative,
182
socktypeNative,
183
protocolNative
184
);
185
186
if (!inheritable) {
187
posix.set_inheritable(native_fd, inheritable);
188
}
189
return createWasiFd(native_fd, socktype);
190
},
191
192
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
193
bind(socket: number, sockaddrPtr: number, address_len: number): number {
194
log("bind", socket);
195
if (posix.bind == null) {
196
throw Errno("ENOTSUP");
197
}
198
const sockaddr = getSockaddr(sockaddrPtr, address_len);
199
log("bind: address", sockaddr);
200
posix.bind(native_fd(socket), sockaddr);
201
202
return 0;
203
},
204
205
connect(socket: number, sockaddrPtr: number, address_len: number): number {
206
if (posix.connect == null) {
207
throw Errno("ENOTSUP");
208
}
209
const sockaddr = getSockaddr(sockaddrPtr, address_len);
210
log("connect", { socket, sockaddr, address_len });
211
posix.connect(native_fd(socket), sockaddr);
212
return 0;
213
},
214
215
/*
216
int getsockname(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
217
*/
218
getsockname(
219
socket: number,
220
sockaddrPtr: number,
221
addressLenPtr: number
222
): number {
223
if (posix.getsockname == null) {
224
throw Errno("ENOTSUP");
225
}
226
log("getsockname", socket);
227
const sockaddr = posix.getsockname(native_fd(socket));
228
sendNativeSockaddr(sockaddr, sockaddrPtr);
229
send.u32(addressLenPtr, sockaddr.sa_len);
230
return 0;
231
},
232
233
/*
234
int getpeername(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
235
*/
236
getpeername(
237
socket: number,
238
sockaddrPtr: number,
239
addressLenPtr: number
240
): number {
241
log("getpeername", socket);
242
const sockaddr = posix.getpeername(native_fd(socket));
243
sendNativeSockaddr(sockaddr, sockaddrPtr);
244
send.u32(addressLenPtr, sockaddr.sa_len);
245
return 0;
246
},
247
248
/*
249
ssize_t recv(int socket, void *buffer, size_t length, int flags);
250
251
NOTE: send and recv are less efficient than they might otherwise
252
be due to a lot of extra copying of data to/from dynamically allocated
253
Buffers. Probably the cost of calling to Javascript at all exceeds
254
this though.
255
*/
256
recv(
257
socket: number,
258
bufPtr: number,
259
length: number,
260
flags: number
261
): number {
262
log("recv", { socket, bufPtr, length, flags });
263
if (posix.recv == null) {
264
throw Errno("ENOTSUP");
265
}
266
const buffer = Buffer.alloc(length);
267
const bytesReceived = posix.recv(
268
native_fd(socket),
269
buffer,
270
native_flags(flags)
271
);
272
//log("recv got ", { buffer, bytesReceived });
273
send.buffer(buffer, bufPtr);
274
return bytesReceived;
275
},
276
277
/*
278
TODO:
279
ssize_t
280
recvfrom(int socket, void *buffer, size_t length, int flags,
281
struct sockaddr *address, socklen_t *address_len);
282
*/
283
recvfrom(
284
socket: number,
285
bufPtr: number,
286
length: number,
287
flags: number,
288
sockaddrPtr: number,
289
sockaddrLenPtr: number
290
): number {
291
log("recvfrom", {
292
socket,
293
bufPtr,
294
length,
295
flags,
296
sockaddrPtr,
297
sockaddrLenPtr,
298
});
299
if (posix.recvfrom == null) {
300
throw Errno("ENOTSUP");
301
}
302
const buffer = Buffer.alloc(length);
303
const { bytesReceived, sockaddr } = posix.recvfrom(
304
native_fd(socket),
305
buffer,
306
native_flags(flags)
307
);
308
log("recvfrom got ", { buffer, bytesReceived, sockaddr });
309
send.buffer(buffer, bufPtr);
310
sendNativeSockaddr(sockaddr, sockaddrPtr);
311
send.u32(sockaddrLenPtr, sockaddr.sa_len);
312
return bytesReceived;
313
},
314
315
/*
316
ssize_t send(int socket, const void *buffer, size_t length, int flags);
317
*/
318
send(
319
socket: number,
320
bufPtr: number,
321
length: number,
322
flags: number
323
): number {
324
log("send", { socket, bufPtr, length, flags });
325
if (posix.send == null) {
326
throw Errno("ENOTSUP");
327
}
328
const buffer = recv.buffer(bufPtr, length);
329
return posix.send(native_fd(socket), buffer, native_flags(flags));
330
},
331
332
/*
333
TODO:
334
335
ssize_t
336
sendto(int socket, const void *buffer, size_t length, int flags,
337
const struct sockaddr *dest_addr, socklen_t dest_len);
338
*/
339
sendto(
340
socket: number,
341
bufPtr: number,
342
length: number,
343
flags: number,
344
addressPtr: number,
345
addressLen: number
346
): number {
347
log("sendto", {
348
socket,
349
bufPtr,
350
length,
351
flags,
352
addressPtr,
353
addressLen,
354
});
355
if (posix.sendto == null) {
356
throw Errno("ENOTSUP");
357
}
358
const buffer = Buffer.alloc(length);
359
const destination = getSockaddr(addressPtr, addressLen);
360
const bytesSent = posix.sendto(
361
native_fd(socket),
362
buffer,
363
native_flags(flags),
364
destination
365
);
366
367
log("sendto sent ", bytesSent);
368
return bytesSent;
369
},
370
371
/*
372
int shutdown(int socket, int how);
373
*/
374
shutdown(socket: number, how: number): number {
375
log("shutdown", { socket, how });
376
if (posix.shutdown == null) {
377
throw Errno("ENOTSUP");
378
}
379
let real_how = -1;
380
for (const name of ["SHUT_RD", "SHUT_WR", "SHUT_RDWR"]) {
381
if (how == constants[name]) {
382
real_how = posix.constants[name];
383
break;
384
}
385
}
386
if (real_how == -1) {
387
throw Errno("EINVAL");
388
}
389
posix.shutdown(native_fd(socket), real_how);
390
return 0;
391
},
392
393
/*
394
listen – listen for connections on a socket
395
396
int listen(int socket, int backlog);
397
*/
398
listen(socket: number, backlog: number): number {
399
log("listen", { socket, backlog });
400
if (posix.listen == null) {
401
throw Errno("ENOTSUP");
402
}
403
return posix.listen(native_fd(socket), backlog);
404
},
405
406
/*
407
accept – accept a connection on a socket
408
int accept(int socket, struct sockaddr *address, socklen_t *address_len);
409
*/
410
accept(socket: number, sockaddrPtr: number, socklenPtr: number): number {
411
log("accept", { socket });
412
if (posix.accept == null) {
413
throw Errno("ENOTSUP");
414
}
415
const { fd, sockaddr } = posix.accept(native_fd(socket));
416
sendNativeSockaddr(sockaddr, sockaddrPtr);
417
send.u32(socklenPtr, sockaddr.sa_len);
418
log("accept got back ", { fd, sockaddr });
419
return createWasiFd(fd, getSocktype(socket));
420
},
421
422
/*
423
int getsockopt(int socket, int level, int option_name, void *option_value,
424
socklen_t *option_len);
425
426
TODO: This is of very limited value right now since the result
427
native, hence just wrong, and it's so opaque I don't know how to convert
428
it back in general. That said, I did socktype as a special case properly.
429
*/
430
getsockopt(
431
socket: number,
432
level: number,
433
option_name: number,
434
option_value_ptr: number,
435
option_len_ptr: number
436
): number {
437
log("getsockopt", {
438
socket,
439
level,
440
option_name,
441
option_value_ptr,
442
option_len_ptr,
443
});
444
445
if (level == constants.SOL_SOCKET && option_name == constants.SO_TYPE) {
446
// special case we can handle easily -- getting the type of a socket.
447
const socktype = getSocktype(socket);
448
const ab = new ArrayBuffer(4);
449
const dv = new DataView(ab);
450
dv.setUint32(0, socktype, true);
451
const option = Buffer.from(ab);
452
send.buffer(option, option_value_ptr);
453
send.i32(option_len_ptr, option.length);
454
return 0;
455
}
456
457
if (posix.getsockopt == null) {
458
throw Errno("ENOTSUP");
459
}
460
const option = posix.getsockopt(
461
native_fd(socket),
462
native_level(level),
463
native_option_name(option_name),
464
recv.i32(option_len_ptr)
465
);
466
send.buffer(option, option_value_ptr);
467
send.i32(option_len_ptr, option.length);
468
return 0;
469
},
470
471
/*
472
int setsockopt(int socket, int level, int option_name, const void *option_value,
473
socklen_t option_len);
474
*/
475
setsockopt(
476
socket: number,
477
level: number,
478
option_name: number,
479
option_value_ptr: number,
480
option_len: number
481
): number {
482
log("setsockopt", {
483
socket,
484
level,
485
option_name,
486
option_value_ptr,
487
option_len,
488
});
489
if (posix.setsockopt == null) {
490
throw Errno("ENOTSUP");
491
}
492
const option = recv.buffer(option_value_ptr, option_len);
493
posix.setsockopt(
494
native_fd(socket),
495
native_level(level),
496
native_option_name(option_name),
497
option
498
);
499
return 0;
500
},
501
502
pollSocket(
503
socket: number,
504
type: "read" | "write",
505
timeout_ms: number
506
): number {
507
//log("pollForSocket", { socket, type, timeout_ms });
508
//return 0;
509
// TODO: The code below doesn't work properly -- it ALWAYS waits for the full timeout_ms,
510
// which is very annoying in practice, e.g., making pip hang 15s before each operation.
511
// Thus we replace the timeout with the min of the timeout and 250ms for now.
512
if (posix.pollSocket == null) {
513
return 0; // stub
514
// return wasi_constants.WASI_ENOSYS;
515
}
516
posix.pollSocket(
517
native_fd(socket),
518
type == "read" ? constants.POLLIN : constants.POLLOUT,
519
Math.min(250, timeout_ms)
520
);
521
return 0;
522
},
523
};
524
}
525
526