|
1 | 1 | import _CJavaScriptKit |
2 | 2 |
|
| 3 | +#if canImport(wasi_pthread) |
| 4 | + import wasi_pthread |
| 5 | +#else |
| 6 | + import Foundation // for pthread_t on non-wasi platforms |
| 7 | +#endif |
| 8 | + |
3 | 9 | /// `JSObject` represents an object in JavaScript and supports dynamic member lookup. |
4 | 10 | /// Any member access like `object.foo` will dynamically request the JavaScript and Swift |
5 | 11 | /// runtime bridge library for a member with the specified name in this object. |
@@ -18,9 +24,35 @@ import _CJavaScriptKit |
18 | 24 | public class JSObject: Equatable { |
19 | 25 | @_spi(JSObject_id) |
20 | 26 | public var id: JavaScriptObjectRef |
| 27 | + |
| 28 | +#if _runtime(_multithreaded) |
| 29 | + private let ownerThread: pthread_t |
| 30 | +#endif |
| 31 | + |
21 | 32 | @_spi(JSObject_id) |
22 | 33 | public init(id: JavaScriptObjectRef) { |
23 | 34 | self.id = id |
| 35 | + self.ownerThread = pthread_self() |
| 36 | + } |
| 37 | + |
| 38 | + /// Asserts that the object is being accessed from the owner thread. |
| 39 | + /// |
| 40 | + /// - Parameter hint: A string to provide additional context for debugging. |
| 41 | + /// |
| 42 | + /// NOTE: Accessing a `JSObject` from a thread other than the thread it was created on |
| 43 | + /// is a programmer error and will result in a runtime assertion failure because JavaScript |
| 44 | + /// object spaces are not shared across threads backed by Web Workers. |
| 45 | + private func assertOnOwnerThread(hint: @autoclosure () -> String) { |
| 46 | + #if _runtime(_multithreaded) |
| 47 | + precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") |
| 48 | + #endif |
| 49 | + } |
| 50 | + |
| 51 | + /// Asserts that the two objects being compared are owned by the same thread. |
| 52 | + private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) { |
| 53 | + #if _runtime(_multithreaded) |
| 54 | + precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") |
| 55 | + #endif |
24 | 56 | } |
25 | 57 |
|
26 | 58 | #if !hasFeature(Embedded) |
@@ -79,32 +111,56 @@ public class JSObject: Equatable { |
79 | 111 | /// - Parameter name: The name of this object's member to access. |
80 | 112 | /// - Returns: The value of the `name` member of this object. |
81 | 113 | public subscript(_ name: String) -> JSValue { |
82 | | - get { getJSValue(this: self, name: JSString(name)) } |
83 | | - set { setJSValue(this: self, name: JSString(name), value: newValue) } |
| 114 | + get { |
| 115 | + assertOnOwnerThread(hint: "reading '\(name)' property") |
| 116 | + return getJSValue(this: self, name: JSString(name)) |
| 117 | + } |
| 118 | + set { |
| 119 | + assertOnOwnerThread(hint: "writing '\(name)' property") |
| 120 | + setJSValue(this: self, name: JSString(name), value: newValue) |
| 121 | + } |
84 | 122 | } |
85 | 123 |
|
86 | 124 | /// Access the `name` member dynamically through JavaScript and Swift runtime bridge library. |
87 | 125 | /// - Parameter name: The name of this object's member to access. |
88 | 126 | /// - Returns: The value of the `name` member of this object. |
89 | 127 | public subscript(_ name: JSString) -> JSValue { |
90 | | - get { getJSValue(this: self, name: name) } |
91 | | - set { setJSValue(this: self, name: name, value: newValue) } |
| 128 | + get { |
| 129 | + assertOnOwnerThread(hint: "reading '<<JSString>>' property") |
| 130 | + return getJSValue(this: self, name: name) |
| 131 | + } |
| 132 | + set { |
| 133 | + assertOnOwnerThread(hint: "writing '<<JSString>>' property") |
| 134 | + setJSValue(this: self, name: name, value: newValue) |
| 135 | + } |
92 | 136 | } |
93 | 137 |
|
94 | 138 | /// Access the `index` member dynamically through JavaScript and Swift runtime bridge library. |
95 | 139 | /// - Parameter index: The index of this object's member to access. |
96 | 140 | /// - Returns: The value of the `index` member of this object. |
97 | 141 | public subscript(_ index: Int) -> JSValue { |
98 | | - get { getJSValue(this: self, index: Int32(index)) } |
99 | | - set { setJSValue(this: self, index: Int32(index), value: newValue) } |
| 142 | + get { |
| 143 | + assertOnOwnerThread(hint: "reading '\(index)' property") |
| 144 | + return getJSValue(this: self, index: Int32(index)) |
| 145 | + } |
| 146 | + set { |
| 147 | + assertOnOwnerThread(hint: "writing '\(index)' property") |
| 148 | + setJSValue(this: self, index: Int32(index), value: newValue) |
| 149 | + } |
100 | 150 | } |
101 | 151 |
|
102 | 152 | /// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library. |
103 | 153 | /// - Parameter symbol: The name of this object's member to access. |
104 | 154 | /// - Returns: The value of the `name` member of this object. |
105 | 155 | public subscript(_ name: JSSymbol) -> JSValue { |
106 | | - get { getJSValue(this: self, symbol: name) } |
107 | | - set { setJSValue(this: self, symbol: name, value: newValue) } |
| 156 | + get { |
| 157 | + assertOnOwnerThread(hint: "reading '<<JSSymbol>>' property") |
| 158 | + return getJSValue(this: self, symbol: name) |
| 159 | + } |
| 160 | + set { |
| 161 | + assertOnOwnerThread(hint: "writing '<<JSSymbol>>' property") |
| 162 | + setJSValue(this: self, symbol: name, value: newValue) |
| 163 | + } |
108 | 164 | } |
109 | 165 |
|
110 | 166 | #if !hasFeature(Embedded) |
@@ -134,7 +190,8 @@ public class JSObject: Equatable { |
134 | 190 | /// - Parameter constructor: The constructor function to check. |
135 | 191 | /// - Returns: The result of `instanceof` in the JavaScript environment. |
136 | 192 | public func isInstanceOf(_ constructor: JSFunction) -> Bool { |
137 | | - swjs_instanceof(id, constructor.id) |
| 193 | + assertOnOwnerThread(hint: "calling 'isInstanceOf'") |
| 194 | + return swjs_instanceof(id, constructor.id) |
138 | 195 | } |
139 | 196 |
|
140 | 197 | static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 |
@@ -145,21 +202,32 @@ public class JSObject: Equatable { |
145 | 202 |
|
146 | 203 | // `JSObject` storage itself is immutable, and use of `JSObject.global` from other |
147 | 204 | // threads maintains the same semantics as `globalThis` in JavaScript. |
148 | | - #if compiler(>=5.10) |
149 | | - nonisolated(unsafe) |
150 | | - static let _global = JSObject(id: _JS_Predef_Value_Global) |
| 205 | + #if _runtime(_multithreaded) |
| 206 | + @LazyThreadLocal(initialize: { |
| 207 | + return JSObject(id: _JS_Predef_Value_Global) |
| 208 | + }) |
| 209 | + private static var _global: JSObject |
151 | 210 | #else |
152 | | - static let _global = JSObject(id: _JS_Predef_Value_Global) |
| 211 | + #if compiler(>=5.10) |
| 212 | + nonisolated(unsafe) |
| 213 | + static let _global = JSObject(id: _JS_Predef_Value_Global) |
| 214 | + #else |
| 215 | + static let _global = JSObject(id: _JS_Predef_Value_Global) |
| 216 | + #endif |
153 | 217 | #endif |
154 | 218 |
|
155 | | - deinit { swjs_release(id) } |
| 219 | + deinit { |
| 220 | + assertOnOwnerThread(hint: "deinitializing") |
| 221 | + swjs_release(id) |
| 222 | + } |
156 | 223 |
|
157 | 224 | /// Returns a Boolean value indicating whether two values point to same objects. |
158 | 225 | /// |
159 | 226 | /// - Parameters: |
160 | 227 | /// - lhs: A object to compare. |
161 | 228 | /// - rhs: Another object to compare. |
162 | 229 | public static func == (lhs: JSObject, rhs: JSObject) -> Bool { |
| 230 | + assertSameOwnerThread(lhs: lhs, rhs: rhs, hint: "comparing two JSObjects for equality") |
163 | 231 | return lhs.id == rhs.id |
164 | 232 | } |
165 | 233 |
|
|