From 42af47ae3558dd774e386c55d550721d8e196d53 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 8 May 2025 12:23:52 -0700 Subject: [PATCH] Added failable funtions/constructors/getters/setters --- Sources/Substrata/Internals/Callbacks.swift | 15 +++- Sources/Substrata/Internals/Context.swift | 10 +++ Sources/Substrata/Internals/Conversion.swift | 90 ++++++++++++++----- .../Substrata/Internals/InternalTypes.swift | 2 +- Sources/Substrata/Types.swift | 10 +-- Tests/SubstrataTests/ConversionTests.swift | 49 ++++++++++ Tests/SubstrataTests/SubstrataTests.swift | 15 ++++ Tests/SubstrataTests/Support/Mocks.swift | 39 ++++++++ Tests/SubstrataTests/TypeTests.swift | 4 +- 9 files changed, 205 insertions(+), 29 deletions(-) diff --git a/Sources/Substrata/Internals/Callbacks.swift b/Sources/Substrata/Internals/Callbacks.swift index fa2ac53..147c87d 100644 --- a/Sources/Substrata/Internals/Callbacks.swift +++ b/Sources/Substrata/Internals/Callbacks.swift @@ -59,7 +59,20 @@ internal func typedConstruct(context: JSContextRef?, this: JSValue, magic: Int32 JS_FreeAtom(context.ref, instanceAtom) if classType.waitingToAttach == nil { - instance.construct(args: args) + do { + try instance.construct(args: args) + } catch { + // Clean up the resources we allocated + result.free(context) + ptr.deallocate() + + let jsError = JSError.from(error) + if let errorValue = jsError.toJSValue(context: context) { + return JS_Throw(context.ref, errorValue) + } + + return JS_Throw(context.ref, JS_NewError(context.ref)) + } } // set up our instance properties ... diff --git a/Sources/Substrata/Internals/Context.swift b/Sources/Substrata/Internals/Context.swift index 8071dae..0e14503 100644 --- a/Sources/Substrata/Internals/Context.swift +++ b/Sources/Substrata/Internals/Context.swift @@ -70,6 +70,16 @@ internal class JSContext { } } +extension JSContext { + func throwError(_ error: Error) -> JSValue { + let jsError = JSError.from(error) + if let errorValue = jsError.toJSValue(context: self) { + return JS_Throw(self.ref, errorValue) + } + return JS_Throw(self.ref, JS_NewError(self.ref)) + } +} + extension JSContext { func newContextClassID() -> Int32 { return exportLock.perform { diff --git a/Sources/Substrata/Internals/Conversion.swift b/Sources/Substrata/Internals/Conversion.swift index f46e6d4..5dd14d3 100644 --- a/Sources/Substrata/Internals/Conversion.swift +++ b/Sources/Substrata/Internals/Conversion.swift @@ -11,37 +11,42 @@ import SubstrataQuickJS // MARK: - Call conversion funcs internal func returnJSValueRef(context: JSContext, function: JSFunctionDefinition, args: [JSConvertible]) -> JSValue { - // make sure we're consistent with our types. - // nil = undefined in js. - // nsnull = null in js. - var result = JSValue.undefined - let v = function(args) as? JSInternalConvertible - if let v = v?.toJSValue(context: context) { - result = v + do { + let v = try function(args) as? JSInternalConvertible + if let v = v?.toJSValue(context: context) { + return v + } + return JSValue.undefined + } catch { + return context.throwError(error) } - - return result } internal func returnJSValueRef(context: JSContext, function: JSPropertyGetterDefinition) -> JSValue { // make sure we're consistent with our types. // nil = undefined in js. // nsnull = null in js. - var result = JSValue.undefined - let v = function() as? JSInternalConvertible - if let v = v?.toJSValue(context: context) { - result = v + do { + let v = try function() as? JSInternalConvertible + if let v = v?.toJSValue(context: context) { + return v + } + return JSValue.undefined + } catch { + return context.throwError(error) } - - return result } internal func returnJSValueRef(context: JSContext, function: JSPropertySetterDefinition, arg: JSConvertible?) -> JSValue { // make sure we're consistent with our types. // nil = undefined in js. // nsnull = null in js. - function(arg) - return JSValue.undefined + do { + try function(arg) + return JSValue.undefined + } catch { + return context.throwError(error) + } } internal func jsArgsToTypes(context: JSContext?, argc: Int32, argv: UnsafeMutablePointer?) -> [JSConvertible] { @@ -610,11 +615,45 @@ public final class JSError: JSConvertible, JSInternalConvertible { } internal func toJSValue(context: JSContext) -> JSValue? { - // it's not expected that JS errors are created in native - // and flow back into JS. - return nil + // Create the base error object + let errorValue = JS_NewError(context.ref) + + // Set the message if we have it + if let message = self.message { + let messageAtom = JS_NewAtom(context.ref, "message") + let messageValue = JS_NewString(context.ref, message) + JS_SetProperty(context.ref, errorValue, messageAtom, messageValue) + JS_FreeAtom(context.ref, messageAtom) + } + + // Set the name if we have it + if let name = self.name { + let nameAtom = JS_NewAtom(context.ref, "name") + let nameValue = JS_NewString(context.ref, name) + JS_SetProperty(context.ref, errorValue, nameAtom, nameValue) + JS_FreeAtom(context.ref, nameAtom) + } + + // Set the cause if we have it + if let cause = self.cause { + let causeAtom = JS_NewAtom(context.ref, "cause") + let causeValue = JS_NewString(context.ref, cause) + JS_SetProperty(context.ref, errorValue, causeAtom, causeValue) + JS_FreeAtom(context.ref, causeAtom) + } + + // Set the stack if we have it + if let stack = self.stack { + let stackAtom = JS_NewAtom(context.ref, "stack") + let stackValue = JS_NewString(context.ref, stack) + JS_SetProperty(context.ref, errorValue, stackAtom, stackValue) + JS_FreeAtom(context.ref, stackAtom) + } + + return errorValue } + public var string: String { return """ Javascript Error: @@ -624,6 +663,10 @@ public final class JSError: JSConvertible, JSInternalConvertible { """ } + static func from(_ error: Error) -> JSError { + return JSError(message: error.localizedDescription) + } + internal init(value: JSValue, context: JSContext) { name = Self.value(for: "name", object: value, context: context) message = Self.value(for: "message", object: value, context: context) @@ -632,6 +675,13 @@ public final class JSError: JSConvertible, JSInternalConvertible { stack = Self.value(for: "stack", object: value, context: context) } + internal init(name: String? = "Native Code Exception", message: String?, cause: String? = nil, stack: String? = nil) { + self.name = name + self.message = message + self.cause = cause + self.stack = stack + } + internal let name: String? internal let message: String? internal let cause: String? diff --git a/Sources/Substrata/Internals/InternalTypes.swift b/Sources/Substrata/Internals/InternalTypes.swift index 6113ce0..ac9ee26 100644 --- a/Sources/Substrata/Internals/InternalTypes.swift +++ b/Sources/Substrata/Internals/InternalTypes.swift @@ -40,7 +40,7 @@ internal class JSClassInfo { internal class JSClassInstanceInfo { let classID: JSClassID - weak var instance: JSExport? + var instance: JSExport? let type: JSExport.Type init(type: JSExport.Type, classID: JSClassID, instance: JSExport?) { diff --git a/Sources/Substrata/Types.swift b/Sources/Substrata/Types.swift index 40c51bf..c5e2df2 100644 --- a/Sources/Substrata/Types.swift +++ b/Sources/Substrata/Types.swift @@ -30,14 +30,14 @@ extension JSConvertible { } } -public typealias JSFunctionDefinition = ([JSConvertible?]) -> JSConvertible? +public typealias JSFunctionDefinition = ([JSConvertible?]) throws -> JSConvertible? public protocol JSStatic { static func staticInit() } -public typealias JSPropertyGetterDefinition = () -> JSConvertible? -public typealias JSPropertySetterDefinition = (JSConvertible?) -> Void +public typealias JSPropertyGetterDefinition = () throws -> JSConvertible? +public typealias JSPropertySetterDefinition = (JSConvertible?) throws -> Void public class JSProperty { internal let getter: JSPropertyGetterDefinition @@ -109,7 +109,7 @@ open class JSExport { } let wrappedFunction: JSFunctionDefinition = { [weak self] args in guard self != nil else { return nil } - return function(args) // function captures 'self' strongly, but our wrapper holds it weakly + return try function(args) // function captures 'self' strongly, but our wrapper holds it weakly } _exportedMethods[named] = wrappedFunction @@ -135,5 +135,5 @@ open class JSExport { // Overrides public required init() {} - open func construct(args: [JSConvertible?]) {} + open func construct(args: [JSConvertible?]) throws {} } diff --git a/Tests/SubstrataTests/ConversionTests.swift b/Tests/SubstrataTests/ConversionTests.swift index 76b063e..457ff1a 100644 --- a/Tests/SubstrataTests/ConversionTests.swift +++ b/Tests/SubstrataTests/ConversionTests.swift @@ -18,6 +18,55 @@ final class ConversionTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. EdgeFunctionJS.reset() } + + func testNativeThrowingConstructor() throws { + let engine = JSEngine() + var exceptionHit = false + engine.exceptionHandler = { error in + exceptionHit = true + print(error.jsDescription()) + } + + engine.export(type: ThrowingConstructorJS.self, className:"ThrowingConstructor") + + // test constructor + let r = engine.evaluate(script: "let x = new ThrowingConstructor(true);") + XCTAssertNotNil(r) + XCTAssertTrue(exceptionHit) + } + + func testNativeThrowingOtherStuff() throws { + let engine = JSEngine() + var exceptionHit = false + engine.exceptionHandler = { error in + exceptionHit = true + print(error.jsDescription()) + } + + engine.export(type: ThrowingConstructorJS.self, className: "ThrowingConstructor") + + // test function + exceptionHit = false + var r = engine.evaluate(script: "let y = new ThrowingConstructor(false);") + XCTAssertNil(r) + let b = engine.evaluate(script: "y.noThrowFunc()")?.typed(as: Int.self) + XCTAssertEqual(b, 3) + r = engine.evaluate(script: "y.throwFunc()") + XCTAssertNotNil(r) + XCTAssertTrue(exceptionHit) + + // test getter + exceptionHit = false + r = engine.evaluate(script: "y.throwProp") + XCTAssertNotNil(r) + XCTAssertTrue(exceptionHit) + + // test setter + exceptionHit = false + r = engine.evaluate(script: "y.throwProp = 5") + XCTAssertNotNil(r) + XCTAssertTrue(exceptionHit) + } func testMassConversionOut() throws { let engine = JSEngine() diff --git a/Tests/SubstrataTests/SubstrataTests.swift b/Tests/SubstrataTests/SubstrataTests.swift index ce07be1..b82f31f 100644 --- a/Tests/SubstrataTests/SubstrataTests.swift +++ b/Tests/SubstrataTests/SubstrataTests.swift @@ -59,6 +59,9 @@ class MyJSClass: JSExport, JSStatic { super.init() exportMethod(named: "test", function: test) + exportMethod(named: "test2", function: { args in + return 3 + }) exportProperty(named: "myInstanceProperty", getter: { return self.myInstanceProperty @@ -207,6 +210,18 @@ final class SubstrataTests: XCTestCase { XCTAssertEqual(result, 1337) } + func testExportInstanceMethod() { + let engine = JSEngine() + + engine.export(type: MyJSClass.self, className: "MyJSClass") + engine.evaluate(script: "let x = new MyJSClass()") + var result = engine.evaluate(script: "x.test()")?.typed(as: Int.self) + XCTAssertEqual(result, 42) + + result = engine.evaluate(script: "x.test2()")?.typed(as: Int.self) + XCTAssertEqual(result, 3) + } + func testStaticProperties() { let engine = JSEngine() engine.export(type: MyJSClass.self, className: "MyJSClass") diff --git a/Tests/SubstrataTests/Support/Mocks.swift b/Tests/SubstrataTests/Support/Mocks.swift index 8894b4a..2159aee 100644 --- a/Tests/SubstrataTests/Support/Mocks.swift +++ b/Tests/SubstrataTests/Support/Mocks.swift @@ -8,6 +8,45 @@ import Foundation @testable import Substrata +class ThrowingConstructorJS: JSExport, JSStatic { + static func staticInit() { + + } + + enum ErrorTest: String, Error { + case constructor + case function + case getter + case setter + } + + required init() { + super.init() + + exportMethod(named: "noThrowFunc") { args in + print("noThrowFunc") + return 3 + } + + exportMethod(named: "throwFunc") { args in + print("throwFunc") + throw ErrorTest.function + } + + exportProperty(named: "throwProp") { + throw ErrorTest.getter + } setter: { value in + throw ErrorTest.setter + } + } + + override func construct(args: [JSConvertible?]) throws { + if let shouldThrow = args.first as? Bool, shouldThrow { + throw ErrorTest.constructor + } + } +} + class EdgeFunctionJS: JSExport, JSStatic { static var myStaticBool: Bool? = true var myBool: Bool? = false diff --git a/Tests/SubstrataTests/TypeTests.swift b/Tests/SubstrataTests/TypeTests.swift index 36b1676..fb192e5 100644 --- a/Tests/SubstrataTests/TypeTests.swift +++ b/Tests/SubstrataTests/TypeTests.swift @@ -91,8 +91,8 @@ final class TypeTests: XCTestCase { let r = engine.evaluate(script: "Error('test')")?.typed(as: JSError.self)! XCTAssertNotNil(r) let out = r!.toJSValue(context: engine.context) - // we won't make an error on the native side. - XCTAssertNil(out) + // we WILL make an error on the native side. + XCTAssertNotNil(out) } func testInt() throws {