Skip to content

Commit d4e0ee8

Browse files
Restrict the use of ThreadLocal to immortal storage only
1 parent 49b207a commit d4e0ee8

File tree

3 files changed

+43
-35
lines changed

3 files changed

+43
-35
lines changed

Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

+4-13
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,10 @@ public class JSObject: Equatable {
206206

207207
// `JSObject` storage itself is immutable, and use of `JSObject.global` from other
208208
// threads maintains the same semantics as `globalThis` in JavaScript.
209-
#if compiler(>=6.1) && _runtime(_multithreaded)
210-
@LazyThreadLocal(initialize: {
211-
return JSObject(id: _JS_Predef_Value_Global)
212-
})
213-
private static var _global: JSObject
214-
#else
215-
#if compiler(>=5.10)
216-
nonisolated(unsafe)
217-
static let _global = JSObject(id: _JS_Predef_Value_Global)
218-
#else
219-
static let _global = JSObject(id: _JS_Predef_Value_Global)
220-
#endif
221-
#endif
209+
@LazyThreadLocal(initialize: {
210+
return JSObject(id: _JS_Predef_Value_Global)
211+
})
212+
private static var _global: JSObject
222213

223214
deinit {
224215
assertOnOwnerThread(hint: "deinitializing")

Sources/JavaScriptKit/ThreadLocal.swift

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if compiler(>=6.1) && _runtime(_multithreaded)
21
#if canImport(wasi_pthread)
32
import wasi_pthread
43
#elseif canImport(Darwin)
@@ -9,8 +8,14 @@ import Glibc
98
#error("Unsupported platform")
109
#endif
1110

11+
/// A property wrapper that provides thread-local storage for a value.
12+
///
13+
/// The value is stored in a thread-local variable, which is a separate copy for each thread.
1214
@propertyWrapper
1315
final class ThreadLocal<Value>: Sendable {
16+
#if compiler(>=6.1) && _runtime(_multithreaded)
17+
/// The wrapped value stored in the thread-local storage.
18+
/// The initial value is `nil` for each thread.
1419
var wrappedValue: Value? {
1520
get {
1621
guard let pointer = pthread_getspecific(key) else {
@@ -34,6 +39,8 @@ final class ThreadLocal<Value>: Sendable {
3439
private let fromPointer: @Sendable (UnsafeMutableRawPointer) -> Value
3540
private let release: @Sendable (UnsafeMutableRawPointer) -> Void
3641

42+
/// A constructor that requires `Value` to be `AnyObject` to be
43+
/// able to store the value directly in the thread-local storage.
3744
init() where Value: AnyObject {
3845
var key = pthread_key_t()
3946
pthread_key_create(&key, nil)
@@ -43,13 +50,15 @@ final class ThreadLocal<Value>: Sendable {
4350
self.release = { Unmanaged<Value>.fromOpaque($0).release() }
4451
}
4552

46-
class Box {
53+
private class Box {
4754
let value: Value
4855
init(_ value: Value) {
4956
self.value = value
5057
}
5158
}
5259

60+
/// A constructor that doesn't require `Value` to be `AnyObject` but
61+
/// boxing the value in heap-allocated memory.
5362
init(boxing _: Void) {
5463
var key = pthread_key_t()
5564
pthread_key_create(&key, nil)
@@ -65,15 +74,26 @@ final class ThreadLocal<Value>: Sendable {
6574
}
6675
self.release = { Unmanaged<Box>.fromOpaque($0).release() }
6776
}
77+
#else
78+
// Fallback implementation for platforms that don't support pthread
79+
80+
var wrappedValue: Value?
81+
82+
init() where Value: AnyObject {
83+
wrappedValue = nil
84+
}
85+
init(boxing _: Void) {
86+
wrappedValue = nil
87+
}
88+
#endif
6889

6990
deinit {
70-
if let oldPointer = pthread_getspecific(key) {
71-
release(oldPointer)
72-
}
73-
pthread_key_delete(key)
91+
preconditionFailure("ThreadLocal can only be used as an immortal storage, cannot be deallocated")
7492
}
7593
}
7694

95+
/// A property wrapper that lazily initializes a thread-local value
96+
/// for each thread that accesses the value.
7797
@propertyWrapper
7898
final class LazyThreadLocal<Value>: Sendable {
7999
private let storage: ThreadLocal<Value>
@@ -99,5 +119,3 @@ final class LazyThreadLocal<Value>: Sendable {
99119
self.initialValue = initialize
100120
}
101121
}
102-
103-
#endif

Tests/JavaScriptKitTests/ThreadLocalTests.swift

+13-14
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,40 @@ final class ThreadLocalTests: XCTestCase {
1010
func testLeak() throws {
1111
struct Check {
1212
@ThreadLocal
13-
var value: MyHeapObject?
13+
static var value: MyHeapObject?
1414
@ThreadLocal(boxing: ())
15-
var value2: MyStruct?
15+
static var value2: MyStruct?
1616
}
1717
weak var weakObject: MyHeapObject?
1818
do {
1919
let object = MyHeapObject()
2020
weakObject = object
21-
let check = Check()
22-
check.value = object
23-
XCTAssertNotNil(check.value)
24-
XCTAssertTrue(check.value === object)
21+
Check.value = object
22+
XCTAssertNotNil(Check.value)
23+
XCTAssertTrue(Check.value === object)
24+
Check.value = nil
2525
}
2626
XCTAssertNil(weakObject)
2727

2828
weak var weakObject2: MyHeapObject?
2929
do {
3030
let object = MyHeapObject()
3131
weakObject2 = object
32-
let check = Check()
33-
check.value2 = MyStruct(object: object)
34-
XCTAssertNotNil(check.value2)
35-
XCTAssertTrue(check.value2!.object === object)
32+
Check.value2 = MyStruct(object: object)
33+
XCTAssertNotNil(Check.value2)
34+
XCTAssertTrue(Check.value2!.object === object)
35+
Check.value2 = nil
3636
}
3737
XCTAssertNil(weakObject2)
3838
}
3939

4040
func testLazyThreadLocal() throws {
4141
struct Check {
4242
@LazyThreadLocal(initialize: { MyHeapObject() })
43-
var value: MyHeapObject
43+
static var value: MyHeapObject
4444
}
45-
let check = Check()
46-
let object1 = check.value
47-
let object2 = check.value
45+
let object1 = Check.value
46+
let object2 = Check.value
4847
XCTAssertTrue(object1 === object2)
4948
}
5049
}

0 commit comments

Comments
 (0)