Skip to content

Commit 42af47a

Browse files
committed
Added failable funtions/constructors/getters/setters
1 parent a17b5ea commit 42af47a

File tree

9 files changed

+205
-29
lines changed

9 files changed

+205
-29
lines changed

Sources/Substrata/Internals/Callbacks.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,20 @@ internal func typedConstruct(context: JSContextRef?, this: JSValue, magic: Int32
5959
JS_FreeAtom(context.ref, instanceAtom)
6060

6161
if classType.waitingToAttach == nil {
62-
instance.construct(args: args)
62+
do {
63+
try instance.construct(args: args)
64+
} catch {
65+
// Clean up the resources we allocated
66+
result.free(context)
67+
ptr.deallocate()
68+
69+
let jsError = JSError.from(error)
70+
if let errorValue = jsError.toJSValue(context: context) {
71+
return JS_Throw(context.ref, errorValue)
72+
}
73+
74+
return JS_Throw(context.ref, JS_NewError(context.ref))
75+
}
6376
}
6477

6578
// set up our instance properties ...

Sources/Substrata/Internals/Context.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ internal class JSContext {
7070
}
7171
}
7272

73+
extension JSContext {
74+
func throwError(_ error: Error) -> JSValue {
75+
let jsError = JSError.from(error)
76+
if let errorValue = jsError.toJSValue(context: self) {
77+
return JS_Throw(self.ref, errorValue)
78+
}
79+
return JS_Throw(self.ref, JS_NewError(self.ref))
80+
}
81+
}
82+
7383
extension JSContext {
7484
func newContextClassID() -> Int32 {
7585
return exportLock.perform {

Sources/Substrata/Internals/Conversion.swift

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,42 @@ import SubstrataQuickJS
1111
// MARK: - Call conversion funcs
1212

1313
internal func returnJSValueRef(context: JSContext, function: JSFunctionDefinition, args: [JSConvertible]) -> JSValue {
14-
// make sure we're consistent with our types.
15-
// nil = undefined in js.
16-
// nsnull = null in js.
17-
var result = JSValue.undefined
18-
let v = function(args) as? JSInternalConvertible
19-
if let v = v?.toJSValue(context: context) {
20-
result = v
14+
do {
15+
let v = try function(args) as? JSInternalConvertible
16+
if let v = v?.toJSValue(context: context) {
17+
return v
18+
}
19+
return JSValue.undefined
20+
} catch {
21+
return context.throwError(error)
2122
}
22-
23-
return result
2423
}
2524

2625
internal func returnJSValueRef(context: JSContext, function: JSPropertyGetterDefinition) -> JSValue {
2726
// make sure we're consistent with our types.
2827
// nil = undefined in js.
2928
// nsnull = null in js.
30-
var result = JSValue.undefined
31-
let v = function() as? JSInternalConvertible
32-
if let v = v?.toJSValue(context: context) {
33-
result = v
29+
do {
30+
let v = try function() as? JSInternalConvertible
31+
if let v = v?.toJSValue(context: context) {
32+
return v
33+
}
34+
return JSValue.undefined
35+
} catch {
36+
return context.throwError(error)
3437
}
35-
36-
return result
3738
}
3839

3940
internal func returnJSValueRef(context: JSContext, function: JSPropertySetterDefinition, arg: JSConvertible?) -> JSValue {
4041
// make sure we're consistent with our types.
4142
// nil = undefined in js.
4243
// nsnull = null in js.
43-
function(arg)
44-
return JSValue.undefined
44+
do {
45+
try function(arg)
46+
return JSValue.undefined
47+
} catch {
48+
return context.throwError(error)
49+
}
4550
}
4651

4752
internal func jsArgsToTypes(context: JSContext?, argc: Int32, argv: UnsafeMutablePointer<JSValue>?) -> [JSConvertible] {
@@ -610,11 +615,45 @@ public final class JSError: JSConvertible, JSInternalConvertible {
610615
}
611616

612617
internal func toJSValue(context: JSContext) -> JSValue? {
613-
// it's not expected that JS errors are created in native
614-
// and flow back into JS.
615-
return nil
618+
// Create the base error object
619+
let errorValue = JS_NewError(context.ref)
620+
621+
// Set the message if we have it
622+
if let message = self.message {
623+
let messageAtom = JS_NewAtom(context.ref, "message")
624+
let messageValue = JS_NewString(context.ref, message)
625+
JS_SetProperty(context.ref, errorValue, messageAtom, messageValue)
626+
JS_FreeAtom(context.ref, messageAtom)
627+
}
628+
629+
// Set the name if we have it
630+
if let name = self.name {
631+
let nameAtom = JS_NewAtom(context.ref, "name")
632+
let nameValue = JS_NewString(context.ref, name)
633+
JS_SetProperty(context.ref, errorValue, nameAtom, nameValue)
634+
JS_FreeAtom(context.ref, nameAtom)
635+
}
636+
637+
// Set the cause if we have it
638+
if let cause = self.cause {
639+
let causeAtom = JS_NewAtom(context.ref, "cause")
640+
let causeValue = JS_NewString(context.ref, cause)
641+
JS_SetProperty(context.ref, errorValue, causeAtom, causeValue)
642+
JS_FreeAtom(context.ref, causeAtom)
643+
}
644+
645+
// Set the stack if we have it
646+
if let stack = self.stack {
647+
let stackAtom = JS_NewAtom(context.ref, "stack")
648+
let stackValue = JS_NewString(context.ref, stack)
649+
JS_SetProperty(context.ref, errorValue, stackAtom, stackValue)
650+
JS_FreeAtom(context.ref, stackAtom)
651+
}
652+
653+
return errorValue
616654
}
617655

656+
618657
public var string: String {
619658
return """
620659
Javascript Error:
@@ -624,6 +663,10 @@ public final class JSError: JSConvertible, JSInternalConvertible {
624663
"""
625664
}
626665

666+
static func from(_ error: Error) -> JSError {
667+
return JSError(message: error.localizedDescription)
668+
}
669+
627670
internal init(value: JSValue, context: JSContext) {
628671
name = Self.value(for: "name", object: value, context: context)
629672
message = Self.value(for: "message", object: value, context: context)
@@ -632,6 +675,13 @@ public final class JSError: JSConvertible, JSInternalConvertible {
632675
stack = Self.value(for: "stack", object: value, context: context)
633676
}
634677

678+
internal init(name: String? = "Native Code Exception", message: String?, cause: String? = nil, stack: String? = nil) {
679+
self.name = name
680+
self.message = message
681+
self.cause = cause
682+
self.stack = stack
683+
}
684+
635685
internal let name: String?
636686
internal let message: String?
637687
internal let cause: String?

Sources/Substrata/Internals/InternalTypes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal class JSClassInfo {
4040

4141
internal class JSClassInstanceInfo {
4242
let classID: JSClassID
43-
weak var instance: JSExport?
43+
var instance: JSExport?
4444
let type: JSExport.Type
4545

4646
init(type: JSExport.Type, classID: JSClassID, instance: JSExport?) {

Sources/Substrata/Types.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ extension JSConvertible {
3030
}
3131
}
3232

33-
public typealias JSFunctionDefinition = ([JSConvertible?]) -> JSConvertible?
33+
public typealias JSFunctionDefinition = ([JSConvertible?]) throws -> JSConvertible?
3434

3535
public protocol JSStatic {
3636
static func staticInit()
3737
}
3838

39-
public typealias JSPropertyGetterDefinition = () -> JSConvertible?
40-
public typealias JSPropertySetterDefinition = (JSConvertible?) -> Void
39+
public typealias JSPropertyGetterDefinition = () throws -> JSConvertible?
40+
public typealias JSPropertySetterDefinition = (JSConvertible?) throws -> Void
4141

4242
public class JSProperty {
4343
internal let getter: JSPropertyGetterDefinition
@@ -109,7 +109,7 @@ open class JSExport {
109109
}
110110
let wrappedFunction: JSFunctionDefinition = { [weak self] args in
111111
guard self != nil else { return nil }
112-
return function(args) // function captures 'self' strongly, but our wrapper holds it weakly
112+
return try function(args) // function captures 'self' strongly, but our wrapper holds it weakly
113113
}
114114

115115
_exportedMethods[named] = wrappedFunction
@@ -135,5 +135,5 @@ open class JSExport {
135135

136136
// Overrides
137137
public required init() {}
138-
open func construct(args: [JSConvertible?]) {}
138+
open func construct(args: [JSConvertible?]) throws {}
139139
}

Tests/SubstrataTests/ConversionTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,55 @@ final class ConversionTests: XCTestCase {
1818
// Put teardown code here. This method is called after the invocation of each test method in the class.
1919
EdgeFunctionJS.reset()
2020
}
21+
22+
func testNativeThrowingConstructor() throws {
23+
let engine = JSEngine()
24+
var exceptionHit = false
25+
engine.exceptionHandler = { error in
26+
exceptionHit = true
27+
print(error.jsDescription())
28+
}
29+
30+
engine.export(type: ThrowingConstructorJS.self, className:"ThrowingConstructor")
31+
32+
// test constructor
33+
let r = engine.evaluate(script: "let x = new ThrowingConstructor(true);")
34+
XCTAssertNotNil(r)
35+
XCTAssertTrue(exceptionHit)
36+
}
37+
38+
func testNativeThrowingOtherStuff() throws {
39+
let engine = JSEngine()
40+
var exceptionHit = false
41+
engine.exceptionHandler = { error in
42+
exceptionHit = true
43+
print(error.jsDescription())
44+
}
45+
46+
engine.export(type: ThrowingConstructorJS.self, className: "ThrowingConstructor")
47+
48+
// test function
49+
exceptionHit = false
50+
var r = engine.evaluate(script: "let y = new ThrowingConstructor(false);")
51+
XCTAssertNil(r)
52+
let b = engine.evaluate(script: "y.noThrowFunc()")?.typed(as: Int.self)
53+
XCTAssertEqual(b, 3)
54+
r = engine.evaluate(script: "y.throwFunc()")
55+
XCTAssertNotNil(r)
56+
XCTAssertTrue(exceptionHit)
57+
58+
// test getter
59+
exceptionHit = false
60+
r = engine.evaluate(script: "y.throwProp")
61+
XCTAssertNotNil(r)
62+
XCTAssertTrue(exceptionHit)
63+
64+
// test setter
65+
exceptionHit = false
66+
r = engine.evaluate(script: "y.throwProp = 5")
67+
XCTAssertNotNil(r)
68+
XCTAssertTrue(exceptionHit)
69+
}
2170

2271
func testMassConversionOut() throws {
2372
let engine = JSEngine()

Tests/SubstrataTests/SubstrataTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ class MyJSClass: JSExport, JSStatic {
5959
super.init()
6060

6161
exportMethod(named: "test", function: test)
62+
exportMethod(named: "test2", function: { args in
63+
return 3
64+
})
6265

6366
exportProperty(named: "myInstanceProperty", getter: {
6467
return self.myInstanceProperty
@@ -207,6 +210,18 @@ final class SubstrataTests: XCTestCase {
207210
XCTAssertEqual(result, 1337)
208211
}
209212

213+
func testExportInstanceMethod() {
214+
let engine = JSEngine()
215+
216+
engine.export(type: MyJSClass.self, className: "MyJSClass")
217+
engine.evaluate(script: "let x = new MyJSClass()")
218+
var result = engine.evaluate(script: "x.test()")?.typed(as: Int.self)
219+
XCTAssertEqual(result, 42)
220+
221+
result = engine.evaluate(script: "x.test2()")?.typed(as: Int.self)
222+
XCTAssertEqual(result, 3)
223+
}
224+
210225
func testStaticProperties() {
211226
let engine = JSEngine()
212227
engine.export(type: MyJSClass.self, className: "MyJSClass")

Tests/SubstrataTests/Support/Mocks.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,45 @@
88
import Foundation
99
@testable import Substrata
1010

11+
class ThrowingConstructorJS: JSExport, JSStatic {
12+
static func staticInit() {
13+
14+
}
15+
16+
enum ErrorTest: String, Error {
17+
case constructor
18+
case function
19+
case getter
20+
case setter
21+
}
22+
23+
required init() {
24+
super.init()
25+
26+
exportMethod(named: "noThrowFunc") { args in
27+
print("noThrowFunc")
28+
return 3
29+
}
30+
31+
exportMethod(named: "throwFunc") { args in
32+
print("throwFunc")
33+
throw ErrorTest.function
34+
}
35+
36+
exportProperty(named: "throwProp") {
37+
throw ErrorTest.getter
38+
} setter: { value in
39+
throw ErrorTest.setter
40+
}
41+
}
42+
43+
override func construct(args: [JSConvertible?]) throws {
44+
if let shouldThrow = args.first as? Bool, shouldThrow {
45+
throw ErrorTest.constructor
46+
}
47+
}
48+
}
49+
1150
class EdgeFunctionJS: JSExport, JSStatic {
1251
static var myStaticBool: Bool? = true
1352
var myBool: Bool? = false

Tests/SubstrataTests/TypeTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ final class TypeTests: XCTestCase {
9191
let r = engine.evaluate(script: "Error('test')")?.typed(as: JSError.self)!
9292
XCTAssertNotNil(r)
9393
let out = r!.toJSValue(context: engine.context)
94-
// we won't make an error on the native side.
95-
XCTAssertNil(out)
94+
// we WILL make an error on the native side.
95+
XCTAssertNotNil(out)
9696
}
9797

9898
func testInt() throws {

0 commit comments

Comments
 (0)