Skip to content

Commit e50ace8

Browse files
committed
refactor: rewrite setTimeout/clearTimeout in JS using internal bindings
This reverts commit 3b4478c and 06ac480.
1 parent 6e75e9e commit e50ace8

File tree

7 files changed

+151
-90
lines changed

7 files changed

+151
-90
lines changed

include/internalBinding.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace InternalBinding {
1616
extern JSFunctionSpec utils[];
17+
extern JSFunctionSpec timers[];
1718
}
1819

1920
JSObject *createInternalBindingsForNamespace(JSContext *cx, JSFunctionSpec *methodSpecs);

python/pythonmonkey/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import importlib.metadata
77
__version__= importlib.metadata.version(__name__)
88

9-
# Load the module by default to make `console`/`atob`/`btoa` globally available
9+
# Load the module by default to expose global APIs
1010
require("console")
1111
require("base64")
12+
require("timers")

python/pythonmonkey/builtin_modules/internal-binding.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,19 @@ declare function internalBinding(namespace: "utils"): {
3030
getProxyDetails<T extends object>(proxy: T): undefined | [target: T, handler: ProxyHandler<T>];
3131
};
3232

33+
declare function internalBinding(namespace: "timers"): {
34+
/**
35+
* internal binding helper for the `setTimeout` global function
36+
*
37+
* **UNSAFE**, does not perform argument type checks
38+
* @return timeoutId
39+
*/
40+
enqueueWithDelay(handler: Function, delaySeconds: number): number;
41+
42+
/**
43+
* internal binding helper for the `clearTimeout` global function
44+
*/
45+
cancelByTimeoutId(timeoutId: number): void;
46+
};
47+
3348
export = internalBinding;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @file timers.js
3+
* @author Tom Tang <[email protected]>
4+
* @date May 2023
5+
*/
6+
const internalBinding = require("internal-binding")
7+
8+
const {
9+
enqueueWithDelay,
10+
cancelByTimeoutId
11+
} = internalBinding("timers")
12+
13+
/**
14+
* Implement the `setTimeout` global function
15+
* @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout and
16+
* @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
17+
* @param {Function | string} handler
18+
* @param {number} delayMs timeout milliseconds, use value of 0 if this is omitted
19+
* @param {any[]} args additional arguments to be passed to the `handler`
20+
* @return {number} timeoutId
21+
*/
22+
function setTimeout(handler, delayMs = 0, ...args) {
23+
// Ensure the first parameter is a function
24+
if (typeof handler !== "function") {
25+
throw new TypeError("The first parameter to setTimeout() is not a function")
26+
}
27+
28+
// `setTimeout` allows passing additional arguments to the callback, as spec-ed
29+
// FIXME (Tom Tang): the spec doesn't allow additional arguments to be passed if the original `handler` is not a function
30+
const thisArg = globalThis // HTML spec requires `thisArg` to be the global object
31+
// Wrap the job function into a bound function with the given additional arguments
32+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
33+
/** @type {Function} */
34+
const boundHandler = handler.bind(thisArg, ...args)
35+
36+
// Get the delay time in seconds
37+
// JS `setTimeout` takes milliseconds, but Python takes seconds
38+
delayMs = Number(delayMs) || 0 // ensure the `delayMs` is a `number`, explicitly do type coercion
39+
if (delayMs < 0) { delayMs = 0 } // as spec-ed
40+
const delaySeconds = delayMs / 1000 // convert ms to s
41+
42+
return enqueueWithDelay(boundHandler, delaySeconds)
43+
}
44+
45+
/**
46+
* Implement the `clearTimeout` global function
47+
* @see https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout and
48+
* @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-cleartimeout
49+
* @param {number} timeoutId
50+
* @return {void}
51+
*/
52+
function clearTimeout(timeoutId) {
53+
return cancelByTimeoutId(timeoutId)
54+
}
55+
56+
if (!globalThis.setTimeout) {
57+
globalThis.setTimeout = setTimeout;
58+
}
59+
if (!globalThis.clearTimeout) {
60+
globalThis.clearTimeout = clearTimeout;
61+
}
62+
63+
exports.setTimeout = setTimeout;
64+
exports.clearTimeout = clearTimeout;

src/internalBinding.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ JSObject *createInternalBindingsForNamespace(JSContext *cx, JSFunctionSpec *meth
2323
JSObject *getInternalBindingsByNamespace(JSContext *cx, JSLinearString *namespaceStr) {
2424
if (JS_LinearStringEqualsLiteral(namespaceStr, "utils")) {
2525
return createInternalBindingsForNamespace(cx, InternalBinding::utils);
26+
} else if (JS_LinearStringEqualsLiteral(namespaceStr, "timers")) {
27+
return createInternalBindingsForNamespace(cx, InternalBinding::timers);
2628
} else { // not found
2729
return nullptr;
2830
}

src/internalBinding/timers.cc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @file timers.cc
3+
* @author Tom Tang ([email protected])
4+
* @brief Implement functions in `internalBinding("timers")`
5+
*/
6+
7+
#include "include/internalBinding.hh"
8+
#include "include/pyTypeFactory.hh"
9+
#include "include/PyEventLoop.hh"
10+
11+
#include <jsapi.h>
12+
13+
/**
14+
* See function declarations in python/pythonmonkey/builtin_modules/internal-binding.d.ts :
15+
* `declare function internalBinding(namespace: "timers")`
16+
*/
17+
18+
static bool enqueueWithDelay(JSContext *cx, unsigned argc, JS::Value *vp) {
19+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
20+
JS::HandleValue jobArgVal = args.get(0);
21+
double delaySeconds = args.get(1).toNumber();
22+
23+
// Convert to a Python function
24+
// FIXME (Tom Tang): memory leak, not free-ed
25+
JS::RootedObject *thisv = new JS::RootedObject(cx, nullptr);
26+
JS::RootedValue *jobArg = new JS::RootedValue(cx, jobArgVal);
27+
PyObject *job = pyTypeFactory(cx, thisv, jobArg)->getPyObject();
28+
29+
// Schedule job to the running Python event-loop
30+
PyEventLoop loop = PyEventLoop::getRunningLoop();
31+
if (!loop.initialized()) return false;
32+
PyEventLoop::AsyncHandle handle = loop.enqueueWithDelay(job, delaySeconds);
33+
34+
// Return the `timeoutID` to use in `clearTimeout`
35+
args.rval().setDouble((double)PyEventLoop::AsyncHandle::getUniqueId(std::move(handle)));
36+
return true;
37+
}
38+
39+
// TODO (Tom Tang): move argument checks to the JavaScript side
40+
static bool cancelByTimeoutId(JSContext *cx, unsigned argc, JS::Value *vp) {
41+
using AsyncHandle = PyEventLoop::AsyncHandle;
42+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
43+
JS::HandleValue timeoutIdArg = args.get(0);
44+
45+
args.rval().setUndefined();
46+
47+
// silently does nothing when an invalid timeoutID is passed in
48+
if (!timeoutIdArg.isInt32()) {
49+
return true;
50+
}
51+
52+
// Retrieve the AsyncHandle by `timeoutID`
53+
int32_t timeoutID = timeoutIdArg.toInt32();
54+
AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID);
55+
if (!handle) return true; // does nothing on invalid timeoutID
56+
57+
// Cancel this job on Python event-loop
58+
handle->cancel();
59+
60+
return true;
61+
}
62+
63+
JSFunctionSpec InternalBinding::timers[] = {
64+
JS_FN("enqueueWithDelay", enqueueWithDelay, /* nargs */ 2, 0),
65+
JS_FN("cancelByTimeoutId", cancelByTimeoutId, 1, 0),
66+
JS_FS_END
67+
};

src/modules/pythonmonkey/pythonmonkey.cc

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -297,90 +297,6 @@ struct PyModuleDef pythonmonkey =
297297

298298
PyObject *SpiderMonkeyError = NULL;
299299

300-
// Implement the `setTimeout` global function
301-
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
302-
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
303-
static bool setTimeout(JSContext *cx, unsigned argc, JS::Value *vp) {
304-
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
305-
306-
// Ensure the first parameter is a function
307-
// We don't support passing a `code` string to `setTimeout` (yet)
308-
JS::HandleValue jobArgVal = args.get(0);
309-
bool jobArgIsFunction = jobArgVal.isObject() && js::IsFunctionObject(&jobArgVal.toObject());
310-
if (!jobArgIsFunction) {
311-
JS_ReportErrorNumberASCII(cx, nullptr, nullptr, JSErrNum::JSMSG_NOT_FUNCTION, "The first parameter to setTimeout()");
312-
return false;
313-
}
314-
315-
// Get the function to be executed
316-
// FIXME (Tom Tang): memory leak, not free-ed
317-
JS::RootedObject *thisv = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(&args.callee())); // HTML spec requires `thisArg` to be the global object
318-
JS::RootedValue *jobArg = new JS::RootedValue(cx, jobArgVal);
319-
// `setTimeout` allows passing additional arguments to the callback, as spec-ed
320-
if (args.length() > 2) { // having additional arguments
321-
// Wrap the job function into a bound function with the given additional arguments
322-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
323-
JS::RootedVector<JS::Value> bindArgs(cx);
324-
(void)bindArgs.append(JS::ObjectValue(**thisv)); /** @todo XXXwg handle return value */
325-
for (size_t j = 2; j < args.length(); j++) {
326-
(void)bindArgs.append(args[j]); /** @todo XXXwg handle return value */
327-
}
328-
JS::RootedObject jobArgObj = JS::RootedObject(cx, &jobArgVal.toObject());
329-
JS_CallFunctionName(cx, jobArgObj, "bind", JS::HandleValueArray(bindArgs), jobArg); // jobArg = jobArg.bind(thisv, ...bindArgs)
330-
}
331-
// Convert to a Python function
332-
PyObject *job = pyTypeFactory(cx, thisv, jobArg)->getPyObject();
333-
334-
// Get the delay time
335-
// JS `setTimeout` takes milliseconds, but Python takes seconds
336-
double delayMs = 0; // use value of 0 if the delay parameter is omitted
337-
if (args.hasDefined(1)) { JS::ToNumber(cx, args[1], &delayMs); } // implicitly do type coercion to a `number`
338-
if (delayMs < 0) { delayMs = 0; } // as spec-ed
339-
double delaySeconds = delayMs / 1000; // convert ms to s
340-
341-
// Schedule job to the running Python event-loop
342-
PyEventLoop loop = PyEventLoop::getRunningLoop();
343-
if (!loop.initialized()) return false;
344-
PyEventLoop::AsyncHandle handle = loop.enqueueWithDelay(job, delaySeconds);
345-
346-
// Return the `timeoutID` to use in `clearTimeout`
347-
args.rval().setDouble((double)PyEventLoop::AsyncHandle::getUniqueId(std::move(handle)));
348-
349-
return true;
350-
}
351-
352-
// Implement the `clearTimeout` global function
353-
// https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout
354-
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-cleartimeout
355-
static bool clearTimeout(JSContext *cx, unsigned argc, JS::Value *vp) {
356-
using AsyncHandle = PyEventLoop::AsyncHandle;
357-
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
358-
JS::HandleValue timeoutIdArg = args.get(0);
359-
360-
args.rval().setUndefined();
361-
362-
// silently does nothing when an invalid timeoutID is passed in
363-
if (!timeoutIdArg.isInt32()) {
364-
return true;
365-
}
366-
367-
// Retrieve the AsyncHandle by `timeoutID`
368-
int32_t timeoutID = timeoutIdArg.toInt32();
369-
AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID);
370-
if (!handle) return true; // does nothing on invalid timeoutID
371-
372-
// Cancel this job on Python event-loop
373-
handle->cancel();
374-
375-
return true;
376-
}
377-
378-
static JSFunctionSpec jsGlobalFunctions[] = {
379-
JS_FN("setTimeout", setTimeout, /* nargs */ 2, 0),
380-
JS_FN("clearTimeout", clearTimeout, 1, 0),
381-
JS_FS_END
382-
};
383-
384300
PyMODINIT_FUNC PyInit_pythonmonkey(void)
385301
{
386302
PyDateTime_IMPORT;
@@ -425,11 +341,6 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void)
425341

426342
autoRealm = new JSAutoRealm(GLOBAL_CX, *global);
427343

428-
if (!JS_DefineFunctions(GLOBAL_CX, *global, jsGlobalFunctions)) {
429-
PyErr_SetString(SpiderMonkeyError, "Spidermonkey could not define global functions.");
430-
return NULL;
431-
}
432-
433344
JS_SetGCCallback(GLOBAL_CX, handleSharedPythonMonkeyMemory, NULL);
434345

435346
// XXX: SpiderMonkey bug???

0 commit comments

Comments
 (0)