-
Notifications
You must be signed in to change notification settings - Fork 272
/
Copy pathphpwasm-emscripten-library.js
413 lines (381 loc) · 11.7 KB
/
phpwasm-emscripten-library.js
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
/**
* This file is an Emscripten "library" file. It is included in the
* build "php-8.0.js" file and implements JavaScript functions that
* called from C code.
*
* @see https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-a-c-api-in-javascript
*/
"use strict";
const LibraryExample = {
// Emscripten dependencies:
$PHPWASM__deps: ['$allocateUTF8OnStack'],
// Functions not exposed to C but available in the generated
// JavaScript library under the PHPWASM object:
$PHPWASM: {
/**
* A utility function to get all websocket objects associated
* with an Emscripten file descriptor.
*
* @param {int} socketd Socket descriptor
* @returns WebSocket[]
*/
getAllWebSockets: function(sock) {
const webSockets = /* @__PURE__ */ new Set();
if (sock.server) {
sock.server.clients.forEach((ws) => {
webSockets.add(ws);
});
}
for (const peer of PHPWASM.getAllPeers(sock)) {
webSockets.add(peer.socket);
}
return Array.from(webSockets);
},
/**
* A utility function to get all Emscripten Peer objects
* associated with a given Emscripten file descriptor.
*
* @param {int} socketd Socket descriptor
* @returns WebSocket[]
*/
getAllPeers: function(sock) {
const peers = new Set();
if (sock.server) {
sock.pending
.filter(pending => pending.peers)
.forEach((pending) => {
for (const peer of Object.values(pending.peers)) {
peers.add(peer);
}
});
}
if (sock.peers) {
for (const peer of Object.values(sock.peers)) {
peers.add(peer);
}
}
return Array.from(peers);
},
/**
* Waits for inbound data on a websocket.
*
* @param {WebSocket} ws Websocket object
* @returns {[Promise, function]} A promise and a function to cancel the promise
*/
awaitData: function(ws) {
return PHPWASM.awaitWsEvent(ws, "message");
},
/**
* Waits for opening a websocket connection.
*
* @param {WebSocket} ws Websocket object
* @returns {[Promise, function]} A promise and a function to cancel the promise
*/
awaitConnection: function(ws) {
if (ws.OPEN === ws.readyState) {
return [Promise.resolve(), PHPWASM.noop];
}
return PHPWASM.awaitWsEvent(ws, "open");
},
/**
* Waits for closing a websocket connection.
*
* @param {WebSocket} ws Websocket object
* @returns {[Promise, function]} A promise and a function to cancel the promise
*/
awaitClose: function(ws) {
if ([ws.CLOSING, ws.CLOSED].includes(ws.readyState)) {
return [Promise.resolve(), PHPWASM.noop];
}
return PHPWASM.awaitWsEvent(ws, "close");
},
/**
* Waits for an error on a websocket connection.
*
* @param {WebSocket} ws Websocket object
* @returns {[Promise, function]} A promise and a function to cancel the promise
*/
awaitError: function(ws) {
if ([ws.CLOSING, ws.CLOSED].includes(ws.readyState)) {
return [Promise.resolve(), PHPWASM.noop];
}
return PHPWASM.awaitWsEvent(ws, "error");
},
/**
* Waits for a websocket-related event.
*
* @param {WebSocket} ws Websocket object
* @param {string} event The event to wait for.
* @returns {[Promise, function]} A promise and a function to cancel the promise
*/
awaitWsEvent: function(ws, event) {
let resolve;
const listener = () => {
resolve();
}
const promise = new Promise(function(_resolve) {
resolve = _resolve;
ws.once(event, listener);
});
const cancel = () => {
ws.removeListener(event, listener);
// Rejecting the promises bubbles up and kills the entire
// node process. Let's resolve them on the next tick instead
// to give the caller some space to unbind any handlers.
setTimeout(resolve);
};
return [promise, cancel];
},
noop: function () { },
/**
* Shims unix shutdown(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/shutdown.2.html
*
* Does not support SHUT_RD or SHUT_WR.
*
* @param {int} socketd
* @param {int} how
* @returns 0 on success, -1 on failure
*/
shutdownSocket: function (socketd, how) {
const sock = getSocketFromFD(socketd);
const peer = Object.values(sock.peers)[0];
if (!peer) {
return -1;
}
try {
peer.socket.close();
SOCKFS.websocket_sock_ops.removePeer(sock, peer);
return 0;
} catch (e) {
console.log("Socket shutdown error", e)
return -1;
}
}
},
/**
* Shims poll(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/poll.2.html
*
* The semantics don't line up exactly with poll(2) but
* the intent does. This function is called in php_pollfd_for()
* to await a websocket-related event.
*
* @param {int} socketd The socket descriptor
* @param {int} events The events to wait for
* @param {int} timeout The timeout in milliseconds
* @returns {int} 1 if any event was triggered, 0 if the timeout expired
*/
wasm_poll_socket: function(socketd, events, timeout) {
if (typeof Asyncify === 'undefined') {
return 0;
}
const POLLIN = 0x0001; /* There is data to read */
const POLLPRI = 0x0002; /* There is urgent data to read */
const POLLOUT = 0x0004; /* Writing now will not block */
const POLLERR = 0x0008; /* Error condition */
const POLLHUP = 0x0010; /* Hung up */
const POLLNVAL = 0x0020; /* Invalid request: fd not open */
return Asyncify.handleSleep((wakeUp) => {
const sock = getSocketFromFD(socketd);
if (!sock) {
wakeUp(0);
return;
}
const polls = [];
const lookingFor = new Set();
if (events & POLLIN || events & POLLPRI) {
if (sock.server) {
for (const client of sock.pending) {
if ((client.recv_queue || []).length > 0) {
wakeUp(1);
return;
}
}
} else if ((sock.recv_queue || []).length > 0) {
wakeUp(1);
return;
}
}
const webSockets = PHPWASM.getAllWebSockets(sock);
if (!webSockets.length) {
wakeUp(0);
return;
}
for (const ws of webSockets) {
if (events & POLLIN || events & POLLPRI) {
polls.push(PHPWASM.awaitData(ws));
lookingFor.add("POLLIN");
}
if (events & POLLOUT) {
polls.push(PHPWASM.awaitConnection(ws));
lookingFor.add("POLLOUT");
}
if (events & POLLHUP) {
polls.push(PHPWASM.awaitClose(ws));
lookingFor.add("POLLHUP");
}
if (events & POLLERR || events & POLLNVAL) {
polls.push(PHPWASM.awaitError(ws));
lookingFor.add("POLLERR");
}
}
if (polls.length === 0) {
console.warn("Unsupported poll event " + events + ", defaulting to setTimeout().");
setTimeout(function() {
wakeUp(0);
}, timeout);
return;
}
const promises = polls.map(([promise]) => promise);
const clearPolling = () => polls.forEach(([, clear]) => clear());
let awaken = false;
Promise.race(promises).then(function(results) {
if (!awaken) {
awaken = true;
wakeUp(1);
clearTimeout(timeoutId);
clearPolling();
}
});
const timeoutId = setTimeout(function() {
if (!awaken) {
awaken = true;
wakeUp(0);
clearPolling();
}
}, timeout);
});
},
/**
* Shims unix shutdown(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/shutdown.2.html
*
* Does not support SHUT_RD or SHUT_WR.
*
* @param {int} socketd
* @param {int} how
* @returns 0 on success, -1 on failure
*/
wasm_shutdown: function (socketd, how) {
return PHPWASM.shutdownSocket(socketd, how);
},
/**
* Shims unix close(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/close.2.html
*
* @param {int} socketd
* @returns 0 on success, -1 on failure
*/
wasm_close: function (socketd) {
return PHPWASM.shutdownSocket(socketd, 2);
},
/**
* Shims setsockopt(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/setsockopt.2.html
* The only supported options are SO_KEEPALIVE and TCP_NODELAY.
*
* Technically these options are propagated to the WebSockets proxy
* server which then sets them on the underlying TCP connection.
*
* @param {int} socketd Socket descriptor
* @param {int} level Level at which the option is defined
* @param {int} optionName The option name
* @param {int} optionValuePtr Pointer to the option value
* @param {int} optionLen The length of the option value
* @returns {int} 0 on success, -1 on failure
*/
wasm_setsockopt: function(socketd, level, optionName, optionValuePtr, optionLen) {
const optionValue = HEAPU8[optionValuePtr];
const SOL_SOCKET = 1;
const SO_KEEPALIVE = 9;
const IPPROTO_TCP = 6;
const TCP_NODELAY = 1;
const isSupported = level === SOL_SOCKET && optionName === SO_KEEPALIVE || level === IPPROTO_TCP && optionName === TCP_NODELAY;
if (!isSupported) {
console.warn(`Unsupported socket option: ${level}, ${optionName}, ${optionValue}`);
return -1;
}
const ws = PHPWASM.getAllWebSockets(socketd)[0];
if (!ws) {
return -1;
}
ws.setSocketOpt(level, optionName, optionValuePtr);
return 0;
},
/**
* Shims popen(3) functionallity:
* https://man7.org/linux/man-pages/man3/popen.3.html
*
* On Node.js, this function is implemented using child_process.spawn().
*
* In the browser, you must provide a Module['popen_to_file'] function
* that accepts a command string and popen mode (like "r" or "w") and
* returns an object with a 'path' property and an 'exitCode' property:
* * The 'path' property is the path of the file where the output of the
* command is written.
* * The 'exitCode' property is the exit code of the command.
*
* @param {int} command Command to execute
* @param {int} mode Mode to open the command in
* @param {int} exitCodePtr Pointer to the exit code
* @returns {int} File descriptor of the command output
*/
js_popen_to_file: function(command, mode, exitCodePtr) {
// Parse args
if (!command) return 1; // shell is available
const cmdstr = UTF8ToString(command);
if (!cmdstr.length) return 0; // this is what glibc seems to do (shell works test?)
const modestr = UTF8ToString(mode);
if (!modestr.length) return 0; // this is what glibc seems to do (shell works test?)
if (Module['popen_to_file']) {
const {
path,
exitCode
} = Module['popen_to_file'](cmdstr, modestr);
HEAPU8[exitCodePtr] = exitCode;
return allocateUTF8OnStack(path);
}
#if ENVIRONMENT_MAY_BE_NODE
if (ENVIRONMENT_IS_NODE) {
// Create a temporary file to read stdin from or write stdout to
const tmp = require('os').tmpdir();
const tmpFileName = 'php-process-stream';
const pipeFilePath = tmp + '/' + tmpFileName;
const cp = require('child_process');
let ret;
if (modestr === 'r') {
ret = cp.spawnSync(cmdstr, [], {
shell: true,
stdio: ["inherit", "pipe", "inherit"],
});
HEAPU8[exitCodePtr] = ret.status;
require('fs').writeFileSync(pipeFilePath, ret.stdout, {
encoding: 'utf8',
flag: 'w+',
});
} else if (modestr === 'w') {
console.error('popen mode w not implemented yet');
return _W_EXITCODE(0, 2); // 2 is SIGINT
} else {
console.error('invalid mode ' + modestr + ' (should be r or w)');
return _W_EXITCODE(0, 2); // 2 is SIGINT
}
return allocateUTF8OnStack(pipeFilePath);
}
#endif // ENVIRONMENT_MAY_BE_NODE
throw new Error(
'popen() is unsupported in the browser. Implement popen_to_file in your Module ' +
'or disable shell_exec() and similar functions via php.ini.'
);
return _W_EXITCODE(0, 2); // 2 is SIGINT
},
js_module_onMessage: function (data) {
if (Module['onMessage']) {
const dataStr = UTF8ToString(data);
Module['onMessage'](dataStr);
}
}
};
autoAddDeps(LibraryExample, '$PHPWASM');
mergeInto(LibraryManager.library, LibraryExample);