quickjs-emscripten
Advanced tools
Comparing version 0.1.2 to 0.2.0
@@ -40,4 +40,8 @@ /// <reference types="emscripten" /> | ||
*/ | ||
export declare type QTS_C_To_HostCallbackFuncPointer = Pointer<'C_To_HostCallbackFuncPointer'>; | ||
export declare type QTS_C_To_HostCallbackFuncPointer = Pointer<'C_To_HostCallbackFunc'>; | ||
/** | ||
* Used internally for C-to-Javascript interrupt handlers. | ||
*/ | ||
export declare type QTS_C_To_HostInterruptFuncPointer = Pointer<'C_To_HostInterruptFunc'>; | ||
/** | ||
* Low-level FFI bindings to QuickJS's Emscripten module. | ||
@@ -57,2 +61,5 @@ * See instead [[QuickJSVm]], the public Javascript interface exposed by this | ||
QTS_NewError: (ctx: JSContextPointer) => JSValuePointer; | ||
QTS_SetInterruptCallback: (cb: QTS_C_To_HostInterruptFuncPointer) => void; | ||
QTS_RuntimeEnableInterruptHandler: (rt: JSRuntimePointer) => void; | ||
QTS_RuntimeDisableInterruptHandler: (rt: JSRuntimePointer) => void; | ||
QTS_GetUndefined: () => JSValueConstPointer; | ||
@@ -59,0 +66,0 @@ QTS_NewRuntime: () => JSRuntimePointer; |
@@ -19,2 +19,5 @@ "use strict"; | ||
this.QTS_NewError = this.module.cwrap("QTS_NewError", "number", ["number"]); | ||
this.QTS_SetInterruptCallback = this.module.cwrap("QTS_SetInterruptCallback", null, ["number"]); | ||
this.QTS_RuntimeEnableInterruptHandler = this.module.cwrap("QTS_RuntimeEnableInterruptHandler", null, ["number"]); | ||
this.QTS_RuntimeDisableInterruptHandler = this.module.cwrap("QTS_RuntimeDisableInterruptHandler", null, ["number"]); | ||
this.QTS_GetUndefined = this.module.cwrap("QTS_GetUndefined", "number", []); | ||
@@ -21,0 +24,0 @@ this.QTS_NewRuntime = this.module.cwrap("QTS_NewRuntime", "number", []); |
@@ -5,3 +5,11 @@ import { QuickJSFFI, JSContextPointer, JSValuePointer, JSRuntimePointer, JSValueConstPointer } from './ffi'; | ||
declare type CToHostCallbackFunctionImplementation = (ctx: JSContextPointer, this_ptr: JSValueConstPointer, argc: number, argv: JSValueConstPointer, fn_data_ptr: JSValueConstPointer) => JSValuePointer; | ||
declare type CToHostInterruptImplementation = (rt: JSRuntimePointer) => 0 | 1; | ||
/** | ||
* Determines if a VM's execution should be interrupted. | ||
* | ||
* Return `true` to interrupt JS execution. | ||
* Return `false` or `undefined` to continue JS execution. | ||
*/ | ||
export declare type ShouldInterruptHandler = (vm: QuickJSVm) => boolean | undefined; | ||
/** | ||
* A lifetime prevents access to a value after the lifetime has been | ||
@@ -175,6 +183,9 @@ * [[dispose]]ed. | ||
* | ||
* *Note: this does not protect against infinite loops*. | ||
* *Note*: to protect against infinite loops, provide an interrupt handler to | ||
* [[setShouldInterruptHandler]]. You can use [[shouldInterruptAfterDeadline]] to | ||
* create a time-based deadline. | ||
* | ||
* @returns The last statement's value. If the code threw, result `error` be | ||
* a handle to the exception. | ||
* @returns The last statement's value. If the code threw, result `error` will be | ||
* a handle to the exception. If execution was interrupted, the error will | ||
* have name `InternalError` and message `interrupted`. | ||
*/ | ||
@@ -192,6 +203,20 @@ evalCode(code: string): VmCallResult<QuickJSHandle>; | ||
unwrapResult(result: VmCallResult<QuickJSHandle>): QuickJSHandle; | ||
private interruptHandler; | ||
/** | ||
* Set a callback which is regularly called by the QuickJS engine when it is | ||
* executing code. This callback can be used to implement an execution | ||
* timeout. | ||
* | ||
* The interrupt handler can be removed with [[removeShouldInterruptHandler]]. | ||
*/ | ||
setShouldInterruptHandler(cb: ShouldInterruptHandler): void; | ||
/** | ||
* Remove the interrupt handler, if any. | ||
* See [[setShouldInterruptHandler]]. | ||
*/ | ||
removeShouldInterruptHandler(): void; | ||
/** | ||
* Dispose of this VM's underlying resources. | ||
* | ||
* @throws If Calling this method without disposing of all created handles | ||
* @throws Calling this method without disposing of all created handles | ||
* will result in an error. | ||
@@ -206,2 +231,4 @@ */ | ||
cToHostCallbackFunction: CToHostCallbackFunctionImplementation; | ||
/** @hidden */ | ||
cToHostInterrupt: CToHostInterruptImplementation; | ||
private assertOwned; | ||
@@ -254,2 +281,12 @@ private freeJSValue; | ||
/** | ||
* Options for [[QuickJS.evalCode]]. | ||
*/ | ||
export interface QuickJSEvalOptions { | ||
/** | ||
* Interrupt evaluation if `shouldInterrupt` returns `true`. | ||
* See [[shouldInterruptAfterDeadline]]. | ||
*/ | ||
shouldInterrupt?: ShouldInterruptHandler; | ||
} | ||
/** | ||
* QuickJS presents a Javascript interface to QuickJS, a Javascript interpreter that | ||
@@ -269,2 +306,3 @@ * supports ES2019. | ||
private vmMap; | ||
private rtMap; | ||
private module; | ||
@@ -281,17 +319,30 @@ constructor(); | ||
* One-off evaluate code without needing to create a VM. | ||
* The result is coerced to a native Javascript value using JSON | ||
* serialization, so values unsupported by JSON will be dropped. | ||
* | ||
* To protect against infinite loops, use the `shouldInterrupt` option. The | ||
* [[shouldInterruptAfterDeadline]] function will create a time-based deadline. | ||
* | ||
* If you need more control over how the code executes, create a | ||
* [[QuickJSVm]] instance and use its [[QuickJSVm.evalCode]] method. | ||
* | ||
* *Note: this does not protect against infinite loops.* | ||
* @returns The result is coerced to a native Javascript value using JSON | ||
* serialization, so properties and values unsupported by JSON will be dropped. | ||
* | ||
* @throws If `code` throws during evaluation, the exception will be | ||
* converted into a Javascript value and throw. | ||
* converted into a native Javascript value and thrown. | ||
* | ||
* @throws if `options.shouldInterrupt` interrupted execution, will throw a Error | ||
* with name `"InternalError"` and message `"interrupted"`. | ||
*/ | ||
evalCode(code: string): unknown; | ||
evalCode(code: string, options?: QuickJSEvalOptions): unknown; | ||
private cToHostCallbackFunction; | ||
private cToHostInterrupt; | ||
} | ||
/** | ||
* Returns an interrupt handler that interrupts Javascript execution after a deadline time. | ||
* | ||
* @param deadline - Interrupt execution if it's still running after this time. | ||
* Number values are compared against `Date.now()` | ||
*/ | ||
export declare function shouldInterruptAfterDeadline(deadline: Date | number): ShouldInterruptHandler; | ||
/** | ||
* This is the top-level entrypoint for the quickjs-emscripten library. | ||
@@ -298,0 +349,0 @@ * Get the root QuickJS API. |
@@ -223,2 +223,13 @@ "use strict"; | ||
}; | ||
/** @hidden */ | ||
this.cToHostInterrupt = function (rt) { | ||
if (rt !== _this.rt.value) { | ||
throw new Error('QuickJSVm instance received C -> JS interrupt with mismatched rt'); | ||
} | ||
var fn = _this.interruptHandler; | ||
if (!fn) { | ||
throw new Error('QuickJSVm had no interrupt handler'); | ||
} | ||
return fn(_this) ? 1 : 0; | ||
}; | ||
this.freeJSValue = function (ptr) { | ||
@@ -436,6 +447,9 @@ _this.ffi.QTS_FreeValuePointer(_this.ctx.value, ptr); | ||
* | ||
* *Note: this does not protect against infinite loops*. | ||
* *Note*: to protect against infinite loops, provide an interrupt handler to | ||
* [[setShouldInterruptHandler]]. You can use [[shouldInterruptAfterDeadline]] to | ||
* create a time-based deadline. | ||
* | ||
* @returns The last statement's value. If the code threw, result `error` be | ||
* a handle to the exception. | ||
* @returns The last statement's value. If the code threw, result `error` will be | ||
* a handle to the exception. If execution was interrupted, the error will | ||
* have name `InternalError` and message `interrupted`. | ||
*/ | ||
@@ -496,5 +510,29 @@ QuickJSVm.prototype.evalCode = function (code) { | ||
/** | ||
* Set a callback which is regularly called by the QuickJS engine when it is | ||
* executing code. This callback can be used to implement an execution | ||
* timeout. | ||
* | ||
* The interrupt handler can be removed with [[removeShouldInterruptHandler]]. | ||
*/ | ||
QuickJSVm.prototype.setShouldInterruptHandler = function (cb) { | ||
var prevInterruptHandler = this.interruptHandler; | ||
this.interruptHandler = cb; | ||
if (!prevInterruptHandler) { | ||
this.ffi.QTS_RuntimeEnableInterruptHandler(this.rt.value); | ||
} | ||
}; | ||
/** | ||
* Remove the interrupt handler, if any. | ||
* See [[setShouldInterruptHandler]]. | ||
*/ | ||
QuickJSVm.prototype.removeShouldInterruptHandler = function () { | ||
if (this.interruptHandler) { | ||
this.ffi.QTS_RuntimeDisableInterruptHandler(this.rt.value); | ||
this.interruptHandler = undefined; | ||
} | ||
}; | ||
/** | ||
* Dispose of this VM's underlying resources. | ||
* | ||
* @throws If Calling this method without disposing of all created handles | ||
* @throws Calling this method without disposing of all created handles | ||
* will result in an error. | ||
@@ -572,2 +610,3 @@ */ | ||
this.vmMap = new Map(); | ||
this.rtMap = new Map(); | ||
this.module = QuickJSModule; | ||
@@ -589,2 +628,15 @@ // We need to send this into C-land | ||
}; | ||
this.cToHostInterrupt = function (rt) { | ||
try { | ||
var vm = _this.rtMap.get(rt); | ||
if (!vm) { | ||
throw new Error("QuickJSVm(rt = " + rt + ") not found for C interrupt"); | ||
} | ||
return vm.cToHostInterrupt(rt); | ||
} | ||
catch (error) { | ||
console.error('[C to host interrupt: returning error]', error); | ||
return 1; | ||
} | ||
}; | ||
if (!isReady) { | ||
@@ -601,3 +653,3 @@ throw new Error('QuickJS WASM module not initialized. Either wait for `ready` or use getQuickJS()'); | ||
var intType = 'i'; | ||
var wasmTypes = [ | ||
var functionCallbackWasmTypes = [ | ||
pointerType, | ||
@@ -610,4 +662,10 @@ pointerType, | ||
]; | ||
var fp = this.module.addFunction(this.cToHostCallbackFunction, wasmTypes.join('')); | ||
this.ffi.QTS_SetHostCallback(fp); | ||
var funcCallbackFp = this.module.addFunction(this.cToHostCallbackFunction, functionCallbackWasmTypes.join('')); | ||
this.ffi.QTS_SetHostCallback(funcCallbackFp); | ||
var interruptCallbackWasmTypes = [ | ||
intType, | ||
pointerType, | ||
]; | ||
var interruptCallbackFp = this.module.addFunction(this.cToHostInterrupt, interruptCallbackWasmTypes.join('')); | ||
this.ffi.QTS_SetInterruptCallback(interruptCallbackFp); | ||
} | ||
@@ -622,3 +680,6 @@ /** | ||
var _this = this; | ||
var rt = new Lifetime(this.ffi.QTS_NewRuntime(), function (rt_ptr) { return _this.ffi.QTS_FreeRuntime(rt_ptr); }); | ||
var rt = new Lifetime(this.ffi.QTS_NewRuntime(), function (rt_ptr) { | ||
_this.rtMap.delete(rt_ptr); | ||
_this.ffi.QTS_FreeRuntime(rt_ptr); | ||
}); | ||
var ctx = new Lifetime(this.ffi.QTS_NewContext(rt.value), function (ctx_ptr) { | ||
@@ -635,2 +696,3 @@ _this.vmMap.delete(ctx_ptr); | ||
this.vmMap.set(ctx.value, vm); | ||
this.rtMap.set(rt.value, vm); | ||
return vm; | ||
@@ -640,15 +702,24 @@ }; | ||
* One-off evaluate code without needing to create a VM. | ||
* The result is coerced to a native Javascript value using JSON | ||
* serialization, so values unsupported by JSON will be dropped. | ||
* | ||
* To protect against infinite loops, use the `shouldInterrupt` option. The | ||
* [[shouldInterruptAfterDeadline]] function will create a time-based deadline. | ||
* | ||
* If you need more control over how the code executes, create a | ||
* [[QuickJSVm]] instance and use its [[QuickJSVm.evalCode]] method. | ||
* | ||
* *Note: this does not protect against infinite loops.* | ||
* @returns The result is coerced to a native Javascript value using JSON | ||
* serialization, so properties and values unsupported by JSON will be dropped. | ||
* | ||
* @throws If `code` throws during evaluation, the exception will be | ||
* converted into a Javascript value and throw. | ||
* converted into a native Javascript value and thrown. | ||
* | ||
* @throws if `options.shouldInterrupt` interrupted execution, will throw a Error | ||
* with name `"InternalError"` and message `"interrupted"`. | ||
*/ | ||
QuickJS.prototype.evalCode = function (code) { | ||
QuickJS.prototype.evalCode = function (code, options) { | ||
if (options === void 0) { options = {}; } | ||
var vm = this.createVm(); | ||
if (options.shouldInterrupt) { | ||
vm.setShouldInterruptHandler(options.shouldInterrupt); | ||
} | ||
var result = vm.evalCode(code); | ||
@@ -669,2 +740,15 @@ if (result.error) { | ||
exports.QuickJS = QuickJS; | ||
/** | ||
* Returns an interrupt handler that interrupts Javascript execution after a deadline time. | ||
* | ||
* @param deadline - Interrupt execution if it's still running after this time. | ||
* Number values are compared against `Date.now()` | ||
*/ | ||
function shouldInterruptAfterDeadline(deadline) { | ||
var deadlineAsNumber = typeof deadline === 'number' ? deadline : deadline.getTime(); | ||
return function () { | ||
return Date.now() > deadlineAsNumber; | ||
}; | ||
} | ||
exports.shouldInterruptAfterDeadline = shouldInterruptAfterDeadline; | ||
var singleton = undefined; | ||
@@ -671,0 +755,0 @@ /** |
@@ -267,2 +267,44 @@ "use strict"; | ||
}); | ||
mocha_1.describe('interrupt handler', function () { | ||
mocha_1.it('is called with the expected VM', function () { | ||
var calls = 0; | ||
var interruptHandler = function (interruptVm) { | ||
assert_1.default.strictEqual(interruptVm, vm, 'ShouldInterruptHandler callback VM is the vm'); | ||
calls++; | ||
return false; | ||
}; | ||
vm.setShouldInterruptHandler(interruptHandler); | ||
vm.unwrapResult(vm.evalCode('1 + 1')).dispose(); | ||
assert_1.default(calls > 0, 'interruptHandler called at least once'); | ||
}); | ||
mocha_1.it('interrupts infinite loop execution', function () { | ||
var calls = 0; | ||
var interruptHandler = function (interruptVm) { | ||
if (calls > 10) { | ||
return true; | ||
} | ||
calls++; | ||
return false; | ||
}; | ||
vm.setShouldInterruptHandler(interruptHandler); | ||
var result = vm.evalCode('i = 0; while (1) { i++ }'); | ||
// Make sure we actually got to interrupt the loop. | ||
var iHandle = vm.getProp(vm.global, 'i'); | ||
var i = vm.getNumber(iHandle); | ||
iHandle.dispose(); | ||
assert_1.default(i > 10, 'incremented i'); | ||
assert_1.default(i > calls, 'incremented i more than called the interrupt handler'); | ||
// console.log('Javascript loop iterrations:', i, 'interrupt handler calls:', calls) | ||
if (result.error) { | ||
var errorJson = vm.dump(result.error); | ||
result.error.dispose(); | ||
assert_1.default.equal(errorJson.name, 'InternalError'); | ||
assert_1.default.equal(errorJson.message, 'interrupted'); | ||
} | ||
else { | ||
result.value.dispose(); | ||
assert_1.default.fail('Should have returned an interrupt error'); | ||
} | ||
}); | ||
}); | ||
return [2 /*return*/]; | ||
@@ -269,0 +311,0 @@ }); |
{ | ||
"name": "quickjs-emscripten", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"main": "dist/quickjs.js", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -49,11 +49,12 @@ # quickjs-emscripten | ||
```typescript | ||
import { getQuickJS } from 'quickjs-emscripten' | ||
import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten' | ||
getQuickJS.then(QuickJS => { | ||
console.log(QuickJS.evalCode('1 + 1')) | ||
const result = QuickJS.evalCode('1 + 1', { | ||
shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 1000) | ||
}) | ||
console.log(result) | ||
}) | ||
``` | ||
_Note: this will not protect you from infinite loops._ | ||
### Interfacing with the interpreter | ||
@@ -111,3 +112,2 @@ | ||
- quickjs-emscripten only exposes a small subset of the QuickJS APIs. Add more QuickJS bindings! | ||
- Expose the QuickJS interpreter execution hooks to protect against infinite loops. | ||
- Expose tools for object and array iteration and creation. | ||
@@ -114,0 +114,0 @@ - Stretch goals: class support, an event emitter bridge implementation |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
1222748
6212
2
0
0