Skip to content

Commit f9785f3

Browse files
author
Adam Yost
authored
Fix for func_id rollover (justjake#94)
* 32767 functions good, 32768 functions BAD * change `magic` to uin16_t (avoids signed intereger overflow) * type magic as uint32_t, add simple test * re-enable all tests * remove missed test code * address PR issues * switch to a map of maps for fnMap * update fnId to start at min value * skip max funcID tests for debug mode * missed a flag * run prettier
1 parent 1bb27eb commit f9785f3

8 files changed

+72
-19
lines changed

c/interface.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ int QTS_BuildIsAsyncify() {
552552
// -------------------
553553
// function: C -> Host
554554
#ifdef __EMSCRIPTEN__
555-
EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, int magic_func_id), {
555+
EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, uint32_t magic_func_id), {
556556
#ifdef QTS_ASYNCIFY
557557
const asyncify = {['handleSleep'] : Asyncify.handleSleep};
558558
#else
@@ -563,7 +563,7 @@ EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueCo
563563
#endif
564564

565565
// Function: QuickJS -> C
566-
JSValue qts_call_function(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) {
566+
JSValue qts_call_function(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, uint32_t magic) {
567567
JSValue *result_ptr = qts_host_call_function(ctx, &this_val, argc, argv, magic);
568568
if (result_ptr == NULL) {
569569
return JS_UNDEFINED;
@@ -574,7 +574,7 @@ JSValue qts_call_function(JSContext *ctx, JSValueConst this_val, int argc, JSVal
574574
}
575575

576576
// Function: Host -> QuickJS
577-
JSValue *QTS_NewFunction(JSContext *ctx, int func_id, const char *name) {
577+
JSValue *QTS_NewFunction(JSContext *ctx, uint32_t func_id, const char *name) {
578578
#ifdef QTS_DEBUG_MODE
579579
char msg[500];
580580
sprintf(msg, "new_function(name: %s, magic: %d)", name, func_id);

generate.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,13 @@ function cTypeToTypescriptType(ctype: string): ParsedType {
135135
if (type === "void") {
136136
ffi = null
137137
}
138-
if (type === "double" || type === "int" || type === "size_t") {
138+
if (
139+
type === "double" ||
140+
type === "int" ||
141+
type === "size_t" ||
142+
type === "uint16_t" ||
143+
type === "uint32_t"
144+
) {
139145
ffi = "number"
140146
typescript = "number"
141147
}
@@ -237,8 +243,8 @@ function buildSyncSymbols(matches: RegExpMatchArray[]) {
237243
return filtered.map((fn) => "_" + fn.functionName)
238244
}
239245

240-
// Input: EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, int magic_func_id), {
241-
// Match: MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, int magic_func_id)
246+
// Input: EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, uint32_t magic_func_id), {
247+
// Match: MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, uint32_t magic_func_id)
242248
function buildAsyncifySymbols(matches: RegExpMatchArray[]) {
243249
const parsed = matches.map((match) => {
244250
const [, contents] = match
@@ -312,7 +318,7 @@ export function matchAll(regexp: RegExp, text: string) {
312318
// We're using .exec, which mutates the regexp by setting the .lastIndex
313319
const initialLastIndex = regexp.lastIndex
314320
const result: RegExpExecArray[] = []
315-
let match = null
321+
let match: RegExpExecArray | null = null
316322
while ((match = regexp.exec(text)) !== null) {
317323
result.push(match)
318324
}

ts/context.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ export class QuickJSContext implements LowLevelJavascriptVm<QuickJSHandle>, Disp
412412
*/
413413
newFunction(name: string, fn: VmFunctionImplementation<QuickJSHandle>): QuickJSHandle {
414414
const fnId = ++this.fnNextId
415-
this.fnMap.set(fnId, fn)
415+
this.setFunction(fnId, fn)
416416
return this.memory.heapValueHandle(this.ffi.QTS_NewFunction(this.ctx.value, fnId, name))
417417
}
418418

@@ -784,9 +784,30 @@ export class QuickJSContext implements LowLevelJavascriptVm<QuickJSHandle>, Disp
784784
}
785785

786786
/** @private */
787-
protected fnNextId = 0
787+
protected fnNextId = -32768 // min value of signed 16bit int used by Quickjs
788788
/** @private */
789-
protected fnMap = new Map<number, VmFunctionImplementation<QuickJSHandle>>()
789+
protected fnMaps = new Map<number, Map<number, VmFunctionImplementation<QuickJSHandle>>>()
790+
791+
/** @private */
792+
protected getFunction(fn_id: number): VmFunctionImplementation<QuickJSHandle> | undefined {
793+
const map_id = fn_id >> 8
794+
const fnMap = this.fnMaps.get(map_id)
795+
if (!fnMap) {
796+
return undefined
797+
}
798+
return fnMap.get(fn_id)
799+
}
800+
801+
/** @private */
802+
protected setFunction(fn_id: number, handle: VmFunctionImplementation<QuickJSHandle>) {
803+
const map_id = fn_id >> 8
804+
let fnMap = this.fnMaps.get(map_id)
805+
if (!fnMap) {
806+
fnMap = new Map<number, VmFunctionImplementation<QuickJSHandle>>()
807+
this.fnMaps.set(map_id, fnMap)
808+
}
809+
return fnMap.set(fn_id, handle)
810+
}
790811

791812
/**
792813
* @hidden
@@ -797,8 +818,9 @@ export class QuickJSContext implements LowLevelJavascriptVm<QuickJSHandle>, Disp
797818
throw new Error("QuickJSContext instance received C -> JS call with mismatched ctx")
798819
}
799820

800-
const fn = this.fnMap.get(fn_id)
821+
const fn = this.getFunction(fn_id)
801822
if (!fn) {
823+
// this "throw" is not catch-able from the TS side. could we somehow handle this higher up?
802824
throw new Error(`QuickJSContext had no callback with id ${fn_id}`)
803825
}
804826

ts/generated/emscripten-module.WASM_DEBUG_ASYNCIFY.wasm.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts/generated/emscripten-module.WASM_DEBUG_SYNC.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,8 +2359,8 @@ var tempI64;
23592359
// === Body ===
23602360

23612361
var ASM_CONSTS = {
2362-
114190: function() {return withBuiltinMalloc(function () { return allocateUTF8(Module['LSAN_OPTIONS'] || 0); });},
2363-
114287: function() {var setting = Module['printWithColors']; if (setting != null) { return setting; } else { return ENVIRONMENT_IS_NODE && process.stderr.isTTY; }}
2362+
113795: function() {return withBuiltinMalloc(function () { return allocateUTF8(Module['LSAN_OPTIONS'] || 0); });},
2363+
113892: function() {var setting = Module['printWithColors']; if (setting != null) { return setting; } else { return ENVIRONMENT_IS_NODE && process.stderr.isTTY; }}
23642364
};
23652365
function qts_host_call_function(ctx,this_ptr,argc,argv,magic_func_id){ const asyncify = undefined; return Module['callbacks']['callFunction'](asyncify, ctx, this_ptr, argc, argv, magic_func_id); }
23662366
function qts_host_interrupt_handler(rt){ const asyncify = undefined; return Module['callbacks']['shouldInterrupt'](asyncify, rt); }
-400 Bytes
Binary file not shown.

ts/generated/emscripten-module.WASM_RELEASE_ASYNCIFY.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts/quickjs.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { it, describe } from "mocha"
1414
import assert from "assert"
1515
import { isFail, VmCallResult } from "./vm-interface"
16-
import fs from "fs"
16+
import fs, { chmod } from "fs"
1717
import { QuickJSContext } from "./context"
1818
import { QuickJSAsyncContext } from "./context-asyncify"
1919
import { DEBUG_ASYNC, DEBUG_SYNC, memoizePromiseFactory, QuickJSFFI } from "./variants"
@@ -23,7 +23,7 @@ import { EitherFFI } from "./types"
2323

2424
const TEST_NO_ASYNC = Boolean(process.env.TEST_NO_ASYNC)
2525

26-
function contextTests(getContext: () => Promise<QuickJSContext>) {
26+
function contextTests(getContext: () => Promise<QuickJSContext>, isDebug = false) {
2727
let vm: QuickJSContext = undefined as any
2828
let ffi: EitherFFI = undefined as any
2929
let testId = 0
@@ -150,6 +150,31 @@ function contextTests(getContext: () => Promise<QuickJSContext>) {
150150

151151
fnHandle.dispose()
152152
})
153+
154+
it("can handle more than signed int max functions being registered", function (done) {
155+
// test for unsigned func_id impl
156+
this.timeout(30000) // we need more time to register 2^16 functions
157+
158+
if (isDebug) {
159+
this.skip() // no need to run this again, and it takes WAY too long
160+
}
161+
162+
for (let i = 0; i < Math.pow(2, 16); i++) {
163+
const funcID = i
164+
const fnHandle = vm.newFunction(`__func-${i}`, () => {
165+
return vm.newNumber(funcID)
166+
})
167+
if (i % 1024 === 0) {
168+
// spot check every 1024 funcs
169+
const res = vm.unwrapResult(vm.callFunction(fnHandle, vm.undefined))
170+
const calledFuncID = vm.dump(res)
171+
assert(calledFuncID === i)
172+
res.dispose()
173+
}
174+
fnHandle.dispose()
175+
}
176+
done()
177+
})
153178
})
154179

155180
describe("properties", () => {
@@ -921,7 +946,7 @@ describe("QuickJSContext", function () {
921946
describe("DEBUG sync module", function () {
922947
const loader = memoizePromiseFactory(() => newQuickJSWASMModule(DEBUG_SYNC))
923948
const getContext = () => loader().then((mod) => mod.newContext())
924-
contextTests.call(this, getContext)
949+
contextTests.call(this, getContext, true)
925950
})
926951
})
927952

@@ -945,7 +970,7 @@ if (!TEST_NO_ASYNC) {
945970
const getContext = () => loader().then((mod) => mod.newContext())
946971

947972
describe("sync API", () => {
948-
contextTests(getContext)
973+
contextTests(getContext, true)
949974
})
950975

951976
describe("async API", () => {

0 commit comments

Comments
 (0)