Skip to content

Commit 6ae7311

Browse files
pedrovgsj-f1
andauthored
Improve error messages when JS code throws exceptions (#173)
* Remove a not neede try/catch when evaluating JSFunction values so we can improve error reporting by not hiding the information we get form the JS crash * Modify JSThrowingFunction to be based on the safe version of _call_function_with_this and _call_function instead of the unsafe version we created * Update swjs_call_function_unsafe to notify wasm when there is an exception Co-authored-by: Jed Fox <git@jedfox.com> * Modify swjs_call_function_with_this_unsafe to notify WASM there was an exception as Jed suggested * Recover the already written test cases for JSThrowingFunction and create a new test case for the new functionality * Rename unsafe fuctions to no_catch and simplify invokeNonThrowingJSFunction implementation * Bump swjs_library_version version Co-authored-by: Jed Fox <git@jedfox.com>
1 parent b6b7b98 commit 6ae7311

File tree

10 files changed

+236
-14
lines changed

10 files changed

+236
-14
lines changed

IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift

+6
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ func expectThrow<T>(_ body: @autoclosure () throws -> T, file: StaticString = #f
103103
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
104104
}
105105

106+
func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error {
107+
JSObject.global.callThrowingClosure.function!(JSClosure { _ in
108+
body()
109+
return .undefined
110+
})
111+
}
106112
func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
107113
switch value {
108114
case .some: return

IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+40-1
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,6 @@ try test("Exception") {
700700
// }
701701
// ```
702702
//
703-
704703
let globalObject1 = JSObject.global.globalObject1
705704
let prop_9: JSValue = globalObject1.prop_9
706705

@@ -735,6 +734,46 @@ try test("Exception") {
735734
try expectNotNil(errorObject3)
736735
}
737736

737+
try test("Unhandled Exception") {
738+
// ```js
739+
// global.globalObject1 = {
740+
// ...
741+
// prop_9: {
742+
// func1: function () {
743+
// throw new Error();
744+
// },
745+
// func2: function () {
746+
// throw "String Error";
747+
// },
748+
// func3: function () {
749+
// throw 3.0
750+
// },
751+
// },
752+
// ...
753+
// }
754+
// ```
755+
//
756+
757+
let globalObject1 = JSObject.global.globalObject1
758+
let prop_9: JSValue = globalObject1.prop_9
759+
760+
// MARK: Throwing method calls
761+
let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() }
762+
try expectEqual(error1 is JSValue, true)
763+
let errorObject = JSError(from: error1 as! JSValue)
764+
try expectNotNil(errorObject)
765+
766+
let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() }
767+
try expectEqual(error2 is JSValue, true)
768+
let errorString = try expectString(error2 as! JSValue)
769+
try expectEqual(errorString, "String Error")
770+
771+
let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() }
772+
try expectEqual(error3 is JSValue, true)
773+
let errorNumber = try expectNumber(error3 as! JSValue)
774+
try expectEqual(errorNumber, 3.0)
775+
}
776+
738777
/// If WebAssembly.Memory is not accessed correctly (i.e. creating a new view each time),
739778
/// this test will fail with `TypeError: Cannot perform Construct on a detached ArrayBuffer`,
740779
/// since asking to grow memory will detach the backing ArrayBuffer.

IntegrationTests/bin/primary-tests.js

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ global.Animal = function (name, age, isCat) {
8282
}
8383
};
8484

85+
global.callThrowingClosure = (c) => {
86+
try {
87+
c()
88+
} catch (error) {
89+
return error
90+
}
91+
}
92+
8593
const { startWasiTask } = require("../lib");
8694

8795
startWasiTask("./dist/PrimaryTests.wasm").catch((err) => {

Runtime/src/index.ts

+80-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class SwiftRuntime {
1414
private _instance: WebAssembly.Instance | null;
1515
private _memory: Memory | null;
1616
private _closureDeallocator: SwiftClosureDeallocator | null;
17-
private version: number = 705;
17+
private version: number = 706;
1818

1919
private textDecoder = new TextDecoder("utf-8");
2020
private textEncoder = new TextEncoder(); // Only support utf-8
@@ -226,6 +226,44 @@ export class SwiftRuntime {
226226
this.memory
227227
);
228228
},
229+
swjs_call_function_no_catch: (
230+
ref: ref,
231+
argv: pointer,
232+
argc: number,
233+
kind_ptr: pointer,
234+
payload1_ptr: pointer,
235+
payload2_ptr: pointer
236+
) => {
237+
const func = this.memory.getObject(ref);
238+
let isException = true;
239+
try {
240+
const result = Reflect.apply(
241+
func,
242+
undefined,
243+
JSValue.decodeArray(argv, argc, this.memory)
244+
);
245+
JSValue.write(
246+
result,
247+
kind_ptr,
248+
payload1_ptr,
249+
payload2_ptr,
250+
false,
251+
this.memory
252+
);
253+
isException = false;
254+
} finally {
255+
if (isException) {
256+
JSValue.write(
257+
undefined,
258+
kind_ptr,
259+
payload1_ptr,
260+
payload2_ptr,
261+
true,
262+
this.memory
263+
);
264+
}
265+
}
266+
},
229267

230268
swjs_call_function_with_this: (
231269
obj_ref: ref,
@@ -265,6 +303,47 @@ export class SwiftRuntime {
265303
this.memory
266304
);
267305
},
306+
307+
swjs_call_function_with_this_no_catch: (
308+
obj_ref: ref,
309+
func_ref: ref,
310+
argv: pointer,
311+
argc: number,
312+
kind_ptr: pointer,
313+
payload1_ptr: pointer,
314+
payload2_ptr: pointer
315+
) => {
316+
const obj = this.memory.getObject(obj_ref);
317+
const func = this.memory.getObject(func_ref);
318+
let isException = true;
319+
try {
320+
const result = Reflect.apply(
321+
func,
322+
obj,
323+
JSValue.decodeArray(argv, argc, this.memory)
324+
);
325+
JSValue.write(
326+
result,
327+
kind_ptr,
328+
payload1_ptr,
329+
payload2_ptr,
330+
false,
331+
this.memory
332+
);
333+
isException = false
334+
} finally {
335+
if (isException) {
336+
JSValue.write(
337+
undefined,
338+
kind_ptr,
339+
payload1_ptr,
340+
payload2_ptr,
341+
true,
342+
this.memory
343+
);
344+
}
345+
}
346+
},
268347
swjs_call_new: (ref: ref, argv: pointer, argc: number) => {
269348
const constructor = this.memory.getObject(ref);
270349
const instance = Reflect.construct(

Runtime/src/types.ts

+17
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ export interface ImportedFunctions {
5858
payload1_ptr: pointer,
5959
payload2_ptr: pointer
6060
): void;
61+
swjs_call_function_no_catch(
62+
ref: number,
63+
argv: pointer,
64+
argc: number,
65+
kind_ptr: pointer,
66+
payload1_ptr: pointer,
67+
payload2_ptr: pointer
68+
): void;
6169
swjs_call_function_with_this(
6270
obj_ref: ref,
6371
func_ref: ref,
@@ -67,6 +75,15 @@ export interface ImportedFunctions {
6775
payload1_ptr: pointer,
6876
payload2_ptr: pointer
6977
): void;
78+
swjs_call_function_with_this_no_catch(
79+
obj_ref: ref,
80+
func_ref: ref,
81+
argv: pointer,
82+
argc: number,
83+
kind_ptr: pointer,
84+
payload1_ptr: pointer,
85+
payload2_ptr: pointer
86+
): void;
7087
swjs_call_new(ref: number, argv: pointer, argc: number): number;
7188
swjs_call_throwing_new(
7289
ref: number,

Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift

+8-11
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class JSFunction: JSObject {
1919
/// - Returns: The result of this call.
2020
@discardableResult
2121
public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue {
22-
try! invokeJSFunction(self, arguments: arguments, this: this)
22+
invokeNonThrowingJSFunction(self, arguments: arguments, this: this)
2323
}
2424

2525
/// A variadic arguments version of `callAsFunction`.
@@ -84,30 +84,27 @@ public class JSFunction: JSObject {
8484
}
8585
}
8686

87-
internal func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
88-
let (result, isException) = arguments.withRawJSValues { rawValues in
89-
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in
87+
private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue {
88+
arguments.withRawJSValues { rawValues in
89+
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in
9090
let argv = bufferPointer.baseAddress
9191
let argc = bufferPointer.count
9292
var kindAndFlags = JavaScriptValueKindAndFlags()
9393
var payload1 = JavaScriptPayload1()
9494
var payload2 = JavaScriptPayload2()
9595
if let thisId = this?.id {
96-
_call_function_with_this(thisId,
96+
_call_function_with_this_no_catch(thisId,
9797
jsFunc.id, argv, Int32(argc),
9898
&kindAndFlags, &payload1, &payload2)
9999
} else {
100-
_call_function(
100+
_call_function_no_catch(
101101
jsFunc.id, argv, Int32(argc),
102102
&kindAndFlags, &payload1, &payload2
103103
)
104104
}
105+
assert(!kindAndFlags.isException)
105106
let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2)
106-
return (result.jsValue(), kindAndFlags.isException)
107+
return result.jsValue()
107108
}
108109
}
109-
if isException {
110-
throw result
111-
}
112-
return result
113110
}

Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift

+28
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,31 @@ public class JSThrowingFunction {
6262
try new(arguments: arguments)
6363
}
6464
}
65+
66+
private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
67+
let (result, isException) = arguments.withRawJSValues { rawValues in
68+
rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in
69+
let argv = bufferPointer.baseAddress
70+
let argc = bufferPointer.count
71+
var kindAndFlags = JavaScriptValueKindAndFlags()
72+
var payload1 = JavaScriptPayload1()
73+
var payload2 = JavaScriptPayload2()
74+
if let thisId = this?.id {
75+
_call_function_with_this(thisId,
76+
jsFunc.id, argv, Int32(argc),
77+
&kindAndFlags, &payload1, &payload2)
78+
} else {
79+
_call_function(
80+
jsFunc.id, argv, Int32(argc),
81+
&kindAndFlags, &payload1, &payload2
82+
)
83+
}
84+
let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2)
85+
return (result.jsValue(), kindAndFlags.isException)
86+
}
87+
}
88+
if isException {
89+
throw result
90+
}
91+
return result
92+
}

Sources/JavaScriptKit/XcodeSupport.swift

+15
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ import _CJavaScriptKit
5353
_: UnsafeMutablePointer<JavaScriptPayload1>!,
5454
_: UnsafeMutablePointer<JavaScriptPayload2>!
5555
) { fatalError() }
56+
func _call_function_no_catch(
57+
_: JavaScriptObjectRef,
58+
_: UnsafePointer<RawJSValue>!, _: Int32,
59+
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
60+
_: UnsafeMutablePointer<JavaScriptPayload1>!,
61+
_: UnsafeMutablePointer<JavaScriptPayload2>!
62+
) { fatalError() }
5663
func _call_function_with_this(
5764
_: JavaScriptObjectRef,
5865
_: JavaScriptObjectRef,
@@ -61,6 +68,14 @@ import _CJavaScriptKit
6168
_: UnsafeMutablePointer<JavaScriptPayload1>!,
6269
_: UnsafeMutablePointer<JavaScriptPayload2>!
6370
) { fatalError() }
71+
func _call_function_with_this_no_catch(
72+
_: JavaScriptObjectRef,
73+
_: JavaScriptObjectRef,
74+
_: UnsafePointer<RawJSValue>!, _: Int32,
75+
_: UnsafeMutablePointer<JavaScriptValueKindAndFlags>!,
76+
_: UnsafeMutablePointer<JavaScriptPayload1>!,
77+
_: UnsafeMutablePointer<JavaScriptPayload2>!
78+
) { fatalError() }
6479
func _call_new(
6580
_: JavaScriptObjectRef,
6681
_: UnsafePointer<RawJSValue>!, _: Int32

Sources/_CJavaScriptKit/_CJavaScriptKit.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) {
3636
/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`.
3737
__attribute__((export_name("swjs_library_version")))
3838
int swjs_library_version(void) {
39-
return 705;
39+
return 706;
4040
}
4141

4242
int _library_features(void);

Sources/_CJavaScriptKit/include/_CJavaScriptKit.h

+33
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ extern void _call_function(const JavaScriptObjectRef ref, const RawJSValue *argv
170170
JavaScriptPayload1 *result_payload1,
171171
JavaScriptPayload2 *result_payload2);
172172

173+
/// `_call_function` calls JavaScript function with given arguments list without capturing any exception
174+
///
175+
/// @param ref The target JavaScript function to call.
176+
/// @param argv A list of `RawJSValue` arguments to apply.
177+
/// @param argc The length of `argv``.
178+
/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception.
179+
/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception.
180+
/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception.
181+
__attribute__((__import_module__("javascript_kit"),
182+
__import_name__("swjs_call_function_no_catch")))
183+
extern void _call_function_no_catch(const JavaScriptObjectRef ref, const RawJSValue *argv,
184+
const int argc, JavaScriptValueKindAndFlags *result_kind,
185+
JavaScriptPayload1 *result_payload1,
186+
JavaScriptPayload2 *result_payload2);
187+
173188
/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this`.
174189
///
175190
/// @param _this The value of `this` provided for the call to `func_ref`.
@@ -188,6 +203,24 @@ extern void _call_function_with_this(const JavaScriptObjectRef _this,
188203
JavaScriptPayload1 *result_payload1,
189204
JavaScriptPayload2 *result_payload2);
190205

206+
/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this` without capturing any exception.
207+
///
208+
/// @param _this The value of `this` provided for the call to `func_ref`.
209+
/// @param func_ref The target JavaScript function to call.
210+
/// @param argv A list of `RawJSValue` arguments to apply.
211+
/// @param argc The length of `argv``.
212+
/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception.
213+
/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception.
214+
/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception.
215+
__attribute__((__import_module__("javascript_kit"),
216+
__import_name__("swjs_call_function_with_this_no_catch")))
217+
extern void _call_function_with_this_no_catch(const JavaScriptObjectRef _this,
218+
const JavaScriptObjectRef func_ref,
219+
const RawJSValue *argv, const int argc,
220+
JavaScriptValueKindAndFlags *result_kind,
221+
JavaScriptPayload1 *result_payload1,
222+
JavaScriptPayload2 *result_payload2);
223+
191224
/// `_call_new` calls JavaScript object constructor with given arguments list.
192225
///
193226
/// @param ref The target JavaScript constructor to call.

0 commit comments

Comments
 (0)