Skip to content

Commit a4395d5

Browse files
authored
Support Swift 6 (#21)
* Support Swift 6 beta * fix integration tests * Fix macro warnings
1 parent ff515f2 commit a4395d5

23 files changed

+227
-113
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1510"
3+
LastUpgradeVersion = "1600"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"
7-
buildImplicitDependencies = "YES">
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
89
<BuildActionEntries>
910
<BuildActionEntry
1011
buildForTesting = "YES"
@@ -48,20 +49,6 @@
4849
ReferencedContainer = "container:">
4950
</BuildableReference>
5051
</BuildActionEntry>
51-
<BuildActionEntry
52-
buildForTesting = "YES"
53-
buildForRunning = "YES"
54-
buildForProfiling = "NO"
55-
buildForArchiving = "NO"
56-
buildForAnalyzing = "YES">
57-
<BuildableReference
58-
BuildableIdentifier = "primary"
59-
BlueprintIdentifier = "NodeJSCTests"
60-
BuildableName = "NodeJSCTests"
61-
BlueprintName = "NodeJSCTests"
62-
ReferencedContainer = "container:">
63-
</BuildableReference>
64-
</BuildActionEntry>
6552
</BuildActionEntries>
6653
</BuildAction>
6754
<TestAction
@@ -92,7 +79,6 @@
9279
buildConfiguration = "Debug"
9380
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
9481
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
95-
enableASanStackUseAfterReturn = "YES"
9682
launchStyle = "0"
9783
useCustomWorkingDirectory = "NO"
9884
ignoresPersistentStateOnLaunch = "NO"

[email protected]

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// swift-tools-version:6.0
2+
3+
import PackageDescription
4+
import CompilerPluginSupport
5+
import Foundation
6+
7+
let buildDynamic = ProcessInfo.processInfo.environment["NODE_SWIFT_BUILD_DYNAMIC"] == "1"
8+
9+
let package = Package(
10+
name: "node-swift",
11+
platforms: [
12+
.macOS(.v10_15), .iOS(.v13),
13+
],
14+
products: [
15+
.library(
16+
name: "NodeAPI",
17+
type: buildDynamic ? .dynamic : nil,
18+
targets: ["NodeAPI"]
19+
),
20+
.library(
21+
name: "NodeJSC",
22+
targets: ["NodeJSC"]
23+
),
24+
.library(
25+
name: "NodeModuleSupport",
26+
targets: ["NodeModuleSupport"]
27+
),
28+
],
29+
dependencies: [
30+
.package(url: "https://github.com/apple/swift-syntax.git", "509.0.0"..."600.0.0-prerelease"),
31+
],
32+
targets: [
33+
.systemLibrary(name: "CNodeAPI"),
34+
.target(
35+
name: "CNodeJSC",
36+
linkerSettings: [
37+
.linkedFramework("JavaScriptCore"),
38+
]
39+
),
40+
.target(
41+
name: "NodeJSC",
42+
dependencies: [
43+
"CNodeJSC",
44+
"NodeAPI",
45+
]
46+
),
47+
.target(name: "CNodeAPISupport"),
48+
.macro(
49+
name: "NodeAPIMacros",
50+
dependencies: [
51+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
52+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
53+
]
54+
),
55+
.target(
56+
name: "NodeAPI",
57+
dependencies: ["CNodeAPI", "CNodeAPISupport", "NodeAPIMacros"]
58+
),
59+
.target(
60+
name: "NodeModuleSupport",
61+
dependencies: ["CNodeAPI"]
62+
),
63+
.testTarget(
64+
name: "NodeJSCTests",
65+
dependencies: ["NodeJSC", "NodeAPI"]
66+
),
67+
],
68+
swiftLanguageVersions: [.v5, .v6],
69+
cxxLanguageStandard: .cxx17
70+
)

Sources/NodeAPI/NodeActor.swift

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import CNodeAPISupport
44

55
extension NodeContext {
66
// if we're on a node thread, run `action` on it
7-
static func runOnActor<T>(_ action: @NodeActor () throws -> T) rethrows -> T? {
7+
static func runOnActor<T>(_ action: @NodeActor @Sendable () throws -> T) rethrows -> T? {
88
guard NodeContext.hasCurrent else { return nil }
99
return try NodeActor.unsafeAssumeIsolated(action)
1010
}
@@ -27,34 +27,54 @@ extension UnownedJob {
2727

2828
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2929
private final class NodeExecutor: SerialExecutor {
30-
func enqueue(_ job: UnownedJob) {
31-
// We want to access `job`'s task-local storage. To do so,
32-
// this temporarily swaps ResumeTask for our own function.
33-
// Then, swift_job_run is called, which sets the active task to
34-
// the receiver and invokes its ResumeTask. We then execute the
35-
// given closure, allowing us to grab task-local values. Finally,
36-
// we "suspend" the task and return ResumeTask to its old value.
37-
//
38-
// on Darwin we can instead replace the "current task" thread-local
39-
// (key 103) temporarily, but that isn't portable.
40-
//
41-
// This is sort of like inserting a "work(); await Task.yield()" block
42-
// at the top of the task, since when a Task awaits it similarly changes
43-
// the Resume function and suspends. Note that we can assume that this
44-
// is a Task and not a basic Job, because Executor.enqueue is only
45-
// called from swift_task_enqueue.
46-
let target = job.asCurrent { NodeActor.target }
47-
48-
guard let q = target?.queue else {
49-
nodeFatalError("There is no target NodeAsyncQueue associated with this Task")
30+
private let schedulerQueue = DispatchQueue(label: "NodeExecutorScheduler")
31+
32+
fileprivate init() {
33+
if #unavailable(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0) {
34+
// checkExecutor isn't respected prior to these OS versions, so
35+
// we end up with a lot of false alarms. Disable unexpected executor
36+
// checking to suppress this.
37+
setenv("SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL", "0", 1)
5038
}
39+
}
5140

52-
let ref = asUnownedSerialExecutor()
41+
func enqueue(_ job: UnownedJob) {
42+
schedulerQueue.async {
43+
// We want to access `job`'s task-local storage. To do so,
44+
// this temporarily swaps ResumeTask for our own function.
45+
// Then, swift_job_run is called, which sets the active task to
46+
// the receiver and invokes its ResumeTask. We then execute the
47+
// given closure, allowing us to grab task-local values. Finally,
48+
// we "suspend" the task and return ResumeTask to its old value.
49+
//
50+
// on Darwin we can instead replace the "current task" thread-local
51+
// (key 103) temporarily, but that isn't portable.
52+
//
53+
// This is sort of like inserting a "work(); await Task.yield()" block
54+
// at the top of the task, since when a Task awaits it similarly changes
55+
// the Resume function and suspends. Note that we can assume that this
56+
// is a Task and not a basic Job, because Executor.enqueue is only
57+
// called from swift_task_enqueue.
58+
//
59+
// Regarding `schedulerQueue.async`:
60+
// Pre Swift 6.0 we didn't need a scheduler queue as enqueue would always
61+
// run on the global queue. However, Swift 6 introduces optimizations in
62+
// Task dispatch that allow tasks to be enqueued more efficiently, including
63+
// that Task.init avoids a hop when possible. This, however, interferes with
64+
// our `job.asCurrent` code because `asCurrent` relies on there being no already-
65+
// running task (swift_job_run doesn't play well with nesting, it's possible but
66+
// requires more private APIs, cf [swift_task_startOnMainActor][1]). The simplest
67+
// solution is to hop onto our own queue for scheduling.
68+
//
69+
// [1]: https://github.com/apple/swift/blob/876c056153554f93b89dfd134794a05426ee789a/stdlib/public/Concurrency/Task.cpp#L1739
70+
let target = job.asCurrent { NodeActor.target }
71+
72+
guard let q = target?.queue else {
73+
nodeFatalError("There is no target NodeAsyncQueue associated with this Task")
74+
}
75+
76+
let ref = self.asUnownedSerialExecutor()
5377

54-
if q.instanceID == NodeContext.runOnActor({ try? Node.instanceID() }) {
55-
// if we're already on the right thread, skip a hop
56-
job.runSynchronously(on: ref)
57-
} else {
5878
do {
5979
try q.run { job.runSynchronously(on: ref) }
6080
} catch {
@@ -66,6 +86,10 @@ private final class NodeExecutor: SerialExecutor {
6686
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
6787
.init(ordinary: self)
6888
}
89+
90+
func checkIsolated() {
91+
// TODO: crash if we're not on a Node thread
92+
}
6993
}
7094

7195
// This isn't *actually* a single global actor. Rather, its associated
@@ -81,9 +105,9 @@ private final class NodeExecutor: SerialExecutor {
81105

82106
@TaskLocal static var target: NodeAsyncQueue.Handle?
83107

84-
private nonisolated let _unownedExecutor = NodeExecutor()
108+
private nonisolated let executor = NodeExecutor()
85109
public nonisolated var unownedExecutor: UnownedSerialExecutor {
86-
_unownedExecutor.asUnownedSerialExecutor()
110+
executor.asUnownedSerialExecutor()
87111
}
88112

89113
public static func run<T: Sendable>(resultType: T.Type = T.self, body: @NodeActor @Sendable () throws -> T) async rethrows -> T {
@@ -92,14 +116,14 @@ private final class NodeExecutor: SerialExecutor {
92116
}
93117

94118
extension NodeActor {
95-
public static func unsafeAssumeIsolated<T>(_ action: @NodeActor () throws -> T) rethrows -> T {
119+
public static func unsafeAssumeIsolated<T>(_ action: @NodeActor @Sendable () throws -> T) rethrows -> T {
96120
try withoutActuallyEscaping(action) {
97121
try unsafeBitCast($0, to: (() throws -> T).self)()
98122
}
99123
}
100124

101125
public static func assumeIsolated<T>(
102-
_ action: @NodeActor () throws -> T,
126+
_ action: @NodeActor @Sendable () throws -> T,
103127
file: StaticString = #fileID,
104128
line: UInt = #line
105129
) rethrows -> T {

Sources/NodeAPI/NodeArrayBuffer.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ public struct NodeDataDeallocator: Sendable {
1919

2020
typealias Hint = Box<(NodeDataDeallocator, UnsafeMutableRawBufferPointer)>
2121

22-
func cBufFinalizer(_ rawEnv: napi_env!, _: UnsafeMutableRawPointer!, hint: UnsafeMutableRawPointer!) {
23-
NodeContext.withUnsafeEntrypoint(rawEnv) { _ in
24-
let (deallocator, bytes) = Unmanaged<Hint>.fromOpaque(hint).takeRetainedValue().value
25-
deallocator.action(bytes)
26-
}
27-
}
28-
2922
public final class NodeArrayBuffer: NodeObject {
3023

3124
override class func isObjectType(for value: NodeValueBase) throws -> Bool {
@@ -55,7 +48,12 @@ public final class NodeArrayBuffer: NodeObject {
5548
let env = ctx.environment
5649
var result: napi_value!
5750
let hint = Unmanaged.passRetained(Hint((deallocator, bytes))).toOpaque()
58-
try env.check(napi_create_external_arraybuffer(env.raw, bytes.baseAddress, bytes.count, cBufFinalizer, hint, &result))
51+
try env.check(napi_create_external_arraybuffer(env.raw, bytes.baseAddress, bytes.count, { rawEnv, _, hint in
52+
NodeContext.withUnsafeEntrypoint(rawEnv!) { _ in
53+
let (deallocator, bytes) = Unmanaged<Hint>.fromOpaque(hint!).takeRetainedValue().value
54+
deallocator.action(bytes)
55+
}
56+
}, hint, &result))
5957
super.init(NodeValueBase(raw: result, in: ctx))
6058
}
6159

Sources/NodeAPI/NodeAsyncQueue.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@ extension NodeEnvironment {
1414
private class Token {}
1515
private typealias CallbackBox = Box<(NodeEnvironment) -> Void>
1616

17-
private func cFinalizer(
18-
rawEnv: napi_env!,
19-
data: UnsafeMutableRawPointer!, hint: UnsafeMutableRawPointer?
20-
) {
21-
Unmanaged<Token>.fromOpaque(data).release()
22-
}
23-
2417
private func cCallback(
2518
env: napi_env?, cb: napi_value?,
2619
context: UnsafeMutableRawPointer!, data: UnsafeMutableRawPointer!
@@ -33,6 +26,10 @@ private func cCallback(
3326
callback.value(NodeEnvironment(env))
3427
}
3528

29+
private let cCallbackC: napi_threadsafe_function_call_js = {
30+
cCallback(env: $0, cb: $1, context: $2, data: $3)
31+
}
32+
3633
// this is the only async API we implement because it's more or less isomorphic
3734
// to napi_async_init+napi_[open|close]_callback_scope (which are in turn
3835
// supersets of the other async APIs) and unlike the callback APIs, where you
@@ -72,8 +69,10 @@ public final class NodeAsyncQueue: @unchecked Sendable {
7269
environment.raw, nil,
7370
asyncResource?.rawValue(), label.rawValue(),
7471
maxQueueSize ?? 0, 1,
75-
box.toOpaque(), cFinalizer,
76-
nil, { cCallback(env: $0, cb: $1, context: $2, data: $3) },
72+
box.toOpaque(), { rawEnv, data, hint in
73+
Unmanaged<Token>.fromOpaque(data!).release()
74+
},
75+
nil, cCallbackC,
7776
&result
7877
))
7978
} catch {

Sources/NodeAPI/NodeBuffer.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ public final class NodeBuffer: NodeTypedArray<UInt8> {
3030
var result: napi_value!
3131
let hint = Unmanaged.passRetained(Hint((deallocator, bytes))).toOpaque()
3232
try env.check(
33-
napi_create_external_buffer(env.raw, bytes.count, bytes.baseAddress, cBufFinalizer, hint, &result)
33+
napi_create_external_buffer(env.raw, bytes.count, bytes.baseAddress, { rawEnv, _, hint in
34+
NodeContext.withUnsafeEntrypoint(rawEnv!) { _ in
35+
let (deallocator, bytes) = Unmanaged<Hint>.fromOpaque(hint!).takeRetainedValue().value
36+
deallocator.action(bytes)
37+
}
38+
}, hint, &result)
3439
)
3540
super.init(NodeValueBase(raw: result, in: ctx))
3641
}

Sources/NodeAPI/NodeClass.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
private typealias ConstructorWrapper = Box<NodeFunction.Callback>
44

55
private func cConstructor(rawEnv: napi_env!, info: napi_callback_info!) -> napi_value? {
6-
NodeContext.withUnsafeEntrypoint(rawEnv) { ctx -> napi_value in
7-
let arguments = try NodeArguments(raw: info, in: ctx)
6+
let info = UncheckedSendable(info)
7+
return NodeContext.withUnsafeEntrypoint(rawEnv) { ctx -> napi_value in
8+
let arguments = try NodeArguments(raw: info.value!, in: ctx)
89
let data = arguments.data
910
let callbacks = Unmanaged<ConstructorWrapper>.fromOpaque(data).takeUnretainedValue()
1011
return try callbacks.value(arguments).rawValue()

Sources/NodeAPI/NodeContext.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@ final class NodeContext {
118118
}
119119
}
120120

121-
static func withUnsafeEntrypoint<T>(_ raw: napi_env, action: @NodeActor (NodeContext) throws -> T) -> T? {
121+
static func withUnsafeEntrypoint<T>(_ raw: napi_env, action: @NodeActor @Sendable (NodeContext) throws -> T) -> T? {
122122
withUnsafeEntrypoint(NodeEnvironment(raw), action: action)
123123
}
124124

125-
static func withUnsafeEntrypoint<T>(_ environment: NodeEnvironment, action: @NodeActor (NodeContext) throws -> T) -> T? {
125+
static func withUnsafeEntrypoint<T>(_ environment: NodeEnvironment, action: @NodeActor @Sendable (NodeContext) throws -> T) -> T? {
126126
NodeActor.unsafeAssumeIsolated {
127127
try? withContext(environment: environment, isTopLevel: true, do: action)
128128
}

Sources/NodeAPI/NodeEnvironment.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
@dynamicMemberLookup
44
@NodeActor public final class NodeEnvironment {
5-
nonisolated let raw: napi_env
5+
nonisolated(unsafe) let _raw: UncheckedSendable<napi_env>
6+
nonisolated var raw: napi_env { _raw.value }
67

78
nonisolated init(_ raw: napi_env) {
8-
self.raw = raw
9+
self._raw = .init(raw)
910
}
1011

1112
public static var current: NodeEnvironment {
1213
NodeContext.current.environment
1314
}
1415

15-
public nonisolated static func performUnsafe(_ raw: OpaquePointer, perform: @NodeActor () throws -> Void) {
16-
NodeActor.unsafeAssumeIsolated {
17-
NodeContext.withContext(environment: Self(raw)) { _ in
16+
public nonisolated static func performUnsafe<T>(_ raw: OpaquePointer, perform: @NodeActor @Sendable () throws -> T) -> T? {
17+
NodeActor.unsafeAssumeIsolated { [env = NodeEnvironment(raw)] in
18+
NodeContext.withContext(environment: env) { _ in
1819
try perform()
1920
}
2021
}

Sources/NodeAPI/NodeExternal.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
@_implementationOnly import CNodeAPI
22

3-
private func cFinalizer(rawEnv: napi_env!, data: UnsafeMutableRawPointer!, hint: UnsafeMutableRawPointer!) {
4-
Unmanaged<AnyObject>.fromOpaque(data).release()
5-
}
6-
73
public final class NodeExternal: NodeValue {
84

95
@_spi(NodeAPI) public let base: NodeValueBase
@@ -17,7 +13,9 @@ public final class NodeExternal: NodeValue {
1713
let unmanaged = Unmanaged.passRetained(value as AnyObject)
1814
let opaque = unmanaged.toOpaque()
1915
var result: napi_value!
20-
try env.check(napi_create_external(env.raw, opaque, cFinalizer, nil, &result))
16+
try env.check(napi_create_external(env.raw, opaque, { rawEnv, data, hint in
17+
Unmanaged<AnyObject>.fromOpaque(data!).release()
18+
}, nil, &result))
2119
self.base = NodeValueBase(raw: result, in: ctx)
2220
}
2321

0 commit comments

Comments
 (0)