Skip to content

Improve error messages when JS code throws exceptions #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ func expectThrow<T>(_ body: @autoclosure () throws -> T, file: StaticString = #f
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}

func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error {
JSObject.global.callThrowingClosure.function!(JSClosure { _ in
body()
return .undefined
})
}
func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
switch value {
case .some: return
Expand Down
41 changes: 40 additions & 1 deletion IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,6 @@ try test("Exception") {
// }
// ```
//

let globalObject1 = JSObject.global.globalObject1
let prop_9: JSValue = globalObject1.prop_9

Expand Down Expand Up @@ -735,6 +734,46 @@ try test("Exception") {
try expectNotNil(errorObject3)
}

try test("Unhandled Exception") {
// ```js
// global.globalObject1 = {
// ...
// prop_9: {
// func1: function () {
// throw new Error();
// },
// func2: function () {
// throw "String Error";
// },
// func3: function () {
// throw 3.0
// },
// },
// ...
// }
// ```
//

let globalObject1 = JSObject.global.globalObject1
let prop_9: JSValue = globalObject1.prop_9

// MARK: Throwing method calls
let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() }
try expectEqual(error1 is JSValue, true)
let errorObject = JSError(from: error1 as! JSValue)
try expectNotNil(errorObject)

let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() }
try expectEqual(error2 is JSValue, true)
let errorString = try expectString(error2 as! JSValue)
try expectEqual(errorString, "String Error")

let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() }
try expectEqual(error3 is JSValue, true)
let errorNumber = try expectNumber(error3 as! JSValue)
try expectEqual(errorNumber, 3.0)
}

/// If WebAssembly.Memory is not accessed correctly (i.e. creating a new view each time),
/// this test will fail with `TypeError: Cannot perform Construct on a detached ArrayBuffer`,
/// since asking to grow memory will detach the backing ArrayBuffer.
Expand Down
8 changes: 8 additions & 0 deletions IntegrationTests/bin/primary-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ global.Animal = function (name, age, isCat) {
}
};

global.callThrowingClosure = (c) => {
try {
c()
} catch (error) {
return error
}
}

const { startWasiTask } = require("../lib");

startWasiTask("./dist/PrimaryTests.wasm").catch((err) => {
Expand Down
79 changes: 79 additions & 0 deletions Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,44 @@ export class SwiftRuntime {
this.memory
);
},
swjs_call_function_unsafe: (
ref: ref,
argv: pointer,
argc: number,
kind_ptr: pointer,
payload1_ptr: pointer,
payload2_ptr: pointer
) => {
const func = this.memory.getObject(ref);
let isException = true;
try {
const result = Reflect.apply(
func,
undefined,
JSValue.decodeArray(argv, argc, this.memory)
);
JSValue.write(
result,
kind_ptr,
payload1_ptr,
payload2_ptr,
false,
this.memory
);
isException = false;
} finally {
if (isException) {
JSValue.write(
undefined,
kind_ptr,
payload1_ptr,
payload2_ptr,
true,
this.memory
);
}
}
},

swjs_call_function_with_this: (
obj_ref: ref,
Expand Down Expand Up @@ -265,6 +303,47 @@ export class SwiftRuntime {
this.memory
);
},

swjs_call_function_with_this_unsafe: (
obj_ref: ref,
func_ref: ref,
argv: pointer,
argc: number,
kind_ptr: pointer,
payload1_ptr: pointer,
payload2_ptr: pointer
) => {
const obj = this.memory.getObject(obj_ref);
const func = this.memory.getObject(func_ref);
let isException = true;
try {
const result = Reflect.apply(
func,
obj,
JSValue.decodeArray(argv, argc, this.memory)
);
JSValue.write(
result,
kind_ptr,
payload1_ptr,
payload2_ptr,
false,
this.memory
);
isException = false
} finally {
if (isException) {
JSValue.write(
undefined,
kind_ptr,
payload1_ptr,
payload2_ptr,
true,
this.memory
);
}
}
},
swjs_call_new: (ref: ref, argv: pointer, argc: number) => {
const constructor = this.memory.getObject(ref);
const instance = Reflect.construct(
Expand Down
17 changes: 17 additions & 0 deletions Runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export interface ImportedFunctions {
payload1_ptr: pointer,
payload2_ptr: pointer
): void;
swjs_call_function_unsafe(
ref: number,
argv: pointer,
argc: number,
kind_ptr: pointer,
payload1_ptr: pointer,
payload2_ptr: pointer
): void;
swjs_call_function_with_this(
obj_ref: ref,
func_ref: ref,
Expand All @@ -67,6 +75,15 @@ export interface ImportedFunctions {
payload1_ptr: pointer,
payload2_ptr: pointer
): void;
swjs_call_function_with_this_unsafe(
obj_ref: ref,
func_ref: ref,
argv: pointer,
argc: number,
kind_ptr: pointer,
payload1_ptr: pointer,
payload2_ptr: pointer
): void;
swjs_call_new(ref: number, argv: pointer, argc: number): number;
swjs_call_throwing_new(
ref: number,
Expand Down
9 changes: 5 additions & 4 deletions Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class JSFunction: JSObject {
/// - Returns: The result of this call.
@discardableResult
public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue {
try! invokeJSFunction(self, arguments: arguments, this: this)
try! invokeNonThrowingJSFunction(self, arguments: arguments, this: this)
}

/// A variadic arguments version of `callAsFunction`.
Expand Down Expand Up @@ -84,7 +84,7 @@ public class JSFunction: JSObject {
}
}

internal func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
let (result, isException) = arguments.withRawJSValues { rawValues in
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in
let argv = bufferPointer.baseAddress
Expand All @@ -93,15 +93,16 @@ internal func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJS
var payload1 = JavaScriptPayload1()
var payload2 = JavaScriptPayload2()
if let thisId = this?.id {
_call_function_with_this(thisId,
_call_function_with_this_unsafe(thisId,
jsFunc.id, argv, Int32(argc),
&kindAndFlags, &payload1, &payload2)
} else {
_call_function(
_call_function_unsafe(
jsFunc.id, argv, Int32(argc),
&kindAndFlags, &payload1, &payload2
)
}
assert(!kindAndFlags.isException)
let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2)
return (result.jsValue(), kindAndFlags.isException)
}
Expand Down
28 changes: 28 additions & 0 deletions Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,31 @@ public class JSThrowingFunction {
try new(arguments: arguments)
}
}

private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
let (result, isException) = arguments.withRawJSValues { rawValues in
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in
let argv = bufferPointer.baseAddress
let argc = bufferPointer.count
var kindAndFlags = JavaScriptValueKindAndFlags()
var payload1 = JavaScriptPayload1()
var payload2 = JavaScriptPayload2()
if let thisId = this?.id {
_call_function_with_this(thisId,
jsFunc.id, argv, Int32(argc),
&kindAndFlags, &payload1, &payload2)
} else {
_call_function(
jsFunc.id, argv, Int32(argc),
&kindAndFlags, &payload1, &payload2
)
}
let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2)
return (result.jsValue(), kindAndFlags.isException)
}
}
if isException {
throw result
}
return result
}
15 changes: 15 additions & 0 deletions Sources/JavaScriptKit/XcodeSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ import _CJavaScriptKit
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }
func _call_function_unsafe(
_: JavaScriptObjectRef,
_: UnsafePointer<RawJSValue>!, _: Int32,
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }
func _call_function_with_this(
_: JavaScriptObjectRef,
_: JavaScriptObjectRef,
Expand All @@ -61,6 +68,14 @@ import _CJavaScriptKit
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }
func _call_function_with_this_unsafe(
_: JavaScriptObjectRef,
_: JavaScriptObjectRef,
_: UnsafePointer<RawJSValue>!, _: Int32,
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
_: UnsafeMutablePointer<JavaScriptPayload1>!,
_: UnsafeMutablePointer<JavaScriptPayload2>!
) { fatalError() }
func _call_new(
_: JavaScriptObjectRef,
_: UnsafePointer<RawJSValue>!, _: Int32
Expand Down
33 changes: 33 additions & 0 deletions Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ extern void _call_function(const JavaScriptObjectRef ref, const RawJSValue *argv
JavaScriptPayload1 *result_payload1,
JavaScriptPayload2 *result_payload2);

/// `_call_function` calls JavaScript function with given arguments list without capturing any exception
///
/// @param ref The target JavaScript function to call.
/// @param argv A list of `RawJSValue` arguments to apply.
/// @param argc The length of `argv``.
/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception.
/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception.
/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception.
__attribute__((__import_module__("javascript_kit"),
__import_name__("swjs_call_function_unsafe")))
extern void _call_function_unsafe(const JavaScriptObjectRef ref, const RawJSValue *argv,
const int argc, JavaScriptValueKindAndFlags *result_kind,
JavaScriptPayload1 *result_payload1,
JavaScriptPayload2 *result_payload2);

/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this`.
///
/// @param _this The value of `this` provided for the call to `func_ref`.
Expand All @@ -188,6 +203,24 @@ extern void _call_function_with_this(const JavaScriptObjectRef _this,
JavaScriptPayload1 *result_payload1,
JavaScriptPayload2 *result_payload2);

/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this` without capturing any exception.
///
/// @param _this The value of `this` provided for the call to `func_ref`.
/// @param func_ref The target JavaScript function to call.
/// @param argv A list of `RawJSValue` arguments to apply.
/// @param argc The length of `argv``.
/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception.
/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception.
/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception.
__attribute__((__import_module__("javascript_kit"),
__import_name__("swjs_call_function_with_this_unsafe")))
extern void _call_function_with_this_unsafe(const JavaScriptObjectRef _this,
const JavaScriptObjectRef func_ref,
const RawJSValue *argv, const int argc,
JavaScriptValueKindAndFlags *result_kind,
JavaScriptPayload1 *result_payload1,
JavaScriptPayload2 *result_payload2);

/// `_call_new` calls JavaScript object constructor with given arguments list.
///
/// @param ref The target JavaScript constructor to call.
Expand Down