Skip to content

Commit 6e35812

Browse files
authored
Add support for WebAssembly Macros (swiftlang#2623)
This PR allows `CompilerPlugin`s to be built with a wasm32-unknown-wasi target, enabling them to be invoked by the Wasm Plugin runtime in swiftlang/swift#73031. I've chosen to create a `swift_wasm_macro_pump` export to allow the caller to "drive the event loop" since issuing a `read` would by default be blocking. We need nonblocking IO because some runtimes (eg JavaScriptCore) run Wasm on the same thread as the rest of the interpreted code (JavaScript). To test this, one can build an example macro with ```bash Examples$ swift build \ --experimental-swift-sdk wasm32-unknown-wasi \ --product MacroExamplesImplementation \ -c release ``` And then, with the changes from the swift and swift-driver PRs, a client can be compiled with ```bash Examples$ swiftc Client.swift \ -load-plugin-executable .build/release/MacroExamplesImplementation.wasm#MacroExamplesImplementation ```
1 parent 9d00296 commit 6e35812

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
lines changed

Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,21 @@ struct HostCapability {
7474
///
7575
/// The low level connection and the provider is injected by the client.
7676
@_spi(PluginMessage)
77-
public class CompilerPluginMessageListener<Connection: MessageConnection, Provider: PluginProvider> {
77+
public class CompilerPluginMessageListener<Connection: MessageConnection, Handler: PluginMessageHandler> {
7878
/// Message channel for bidirectional communication with the plugin host.
7979
let connection: Connection
8080

81-
let handler: CompilerPluginMessageHandler<Provider>
81+
let handler: Handler
8282

83-
public init(connection: Connection, provider: Provider) {
83+
public init(connection: Connection, messageHandler: Handler) {
8484
self.connection = connection
85-
self.handler = CompilerPluginMessageHandler(provider: provider)
85+
self.handler = messageHandler
86+
}
87+
88+
public init<Provider: PluginProvider>(connection: Connection, provider: Provider)
89+
where Handler == PluginProviderMessageHandler<Provider> {
90+
self.connection = connection
91+
self.handler = PluginProviderMessageHandler(provider: provider)
8692
}
8793

8894
/// Run the main message listener loop.
@@ -91,11 +97,26 @@ public class CompilerPluginMessageListener<Connection: MessageConnection, Provid
9197
/// On internal errors, such as I/O errors or JSON serialization errors, print
9298
/// an error message and `exit(1)`
9399
public func main() {
100+
#if os(WASI)
101+
// Rather than blocking on read(), let the host tell us when there's data.
102+
readabilityHandler = { _ = self.handleNextMessage() }
103+
#else
104+
while handleNextMessage() {}
105+
#endif
106+
}
107+
108+
/// Receives and handles a single message from the plugin host.
109+
///
110+
/// - Returns: `true` if there was a message to read, `false`
111+
/// if the end-of-file was reached.
112+
private func handleNextMessage() -> Bool {
94113
do {
95-
while let message = try connection.waitForNextMessage(HostToPluginMessage.self) {
96-
let result = handler.handleMessage(message)
97-
try connection.sendMessage(result)
114+
guard let message = try connection.waitForNextMessage(HostToPluginMessage.self) else {
115+
return false
98116
}
117+
let result = handler.handleMessage(message)
118+
try connection.sendMessage(result)
119+
return true
99120
} catch {
100121
// Emit a diagnostic and indicate failure to the plugin host,
101122
// and exit with an error code.
@@ -105,10 +126,18 @@ public class CompilerPluginMessageListener<Connection: MessageConnection, Provid
105126
}
106127
}
107128

108-
/// 'CompilerPluginMessageHandler' is a type that handle a message and do the
109-
/// corresponding operation.
129+
/// A type that handles a plugin message and returns a response.
130+
///
131+
/// - SeeAlso: ``PluginProviderMessageHandler``
132+
@_spi(PluginMessage)
133+
public protocol PluginMessageHandler {
134+
/// Handles a single message received from the plugin host.
135+
func handleMessage(_ message: HostToPluginMessage) -> PluginToHostMessage
136+
}
137+
138+
/// A `PluginMessageHandler` that uses a `PluginProvider`.
110139
@_spi(PluginMessage)
111-
public class CompilerPluginMessageHandler<Provider: PluginProvider> {
140+
public class PluginProviderMessageHandler<Provider: PluginProvider>: PluginMessageHandler {
112141
/// Object to provide actual plugin functions.
113142
let provider: Provider
114143

@@ -199,6 +228,10 @@ public class CompilerPluginMessageHandler<Provider: PluginProvider> {
199228
}
200229
}
201230

231+
@_spi(PluginMessage)
232+
@available(*, deprecated, renamed: "PluginProviderMessageHandler")
233+
public typealias CompilerPluginMessageHandler<Provider: PluginProvider> = PluginProviderMessageHandler<Provider>
234+
202235
struct UnimplementedError: Error, CustomStringConvertible {
203236
var description: String { "unimplemented" }
204237
}
@@ -216,3 +249,31 @@ extension PluginProvider {
216249
throw UnimplementedError()
217250
}
218251
}
252+
253+
#if compiler(>=6) && os(WASI)
254+
255+
/// A callback invoked by the Wasm Host when new data is available on `stdin`.
256+
///
257+
/// This is safe to access without serialization as Wasm plugins are single-threaded.
258+
nonisolated(unsafe) private var readabilityHandler: () -> Void = {
259+
fatalError(
260+
"""
261+
CompilerPlugin.main wasn't called. Did you annotate your plugin with '@main'?
262+
"""
263+
)
264+
}
265+
266+
@_expose(wasm, "swift_wasm_macro_v1_pump")
267+
@_cdecl("swift_wasm_macro_v1_pump")
268+
func wasmPump() {
269+
readabilityHandler()
270+
}
271+
272+
// we can't nest the whole #if-#else in '#if os(WASI)' due to a bug where
273+
// '#if compiler' directives have to be the top-level #if, otherwise
274+
// the compiler doesn't skip unknown syntax.
275+
#elseif os(WASI)
276+
277+
#error("Building swift-syntax for WebAssembly requires compiler version 6.0 or higher.")
278+
279+
#endif

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import SwiftSyntax
2626
@_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
2727
#endif
2828

29-
extension CompilerPluginMessageHandler {
29+
extension PluginProviderMessageHandler {
3030
/// Get concrete macro type from a pair of module name and type name.
3131
private func resolveMacro(_ ref: PluginMessage.MacroReference) throws -> Macro.Type {
3232
try provider.resolveMacro(moduleName: ref.moduleName, typeName: ref.typeName)

Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public enum PluginMessage {
229229
public var notes: [Note]
230230
public var fixIts: [FixIt]
231231

232-
internal init(
232+
public init(
233233
message: String,
234234
severity: Severity,
235235
position: Position,

Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ public struct StandardIOMessageConnection: MessageConnection {
4040
self.outputFileDescriptor = outputFileDescriptor
4141
}
4242

43+
#if os(WASI)
44+
/// Convenience initializer for Wasm executable plugins. Connects
45+
/// directly to `stdin` and `stdout` as WASI doesn't support
46+
/// `dup{,2}`.
47+
public init() throws {
48+
let inputFD = fileno(_stdin)
49+
let outputFD = fileno(_stdout)
50+
self.init(inputFileDescriptor: inputFD, outputFileDescriptor: outputFD)
51+
}
52+
#else
4353
/// Convenience initializer for normal executable plugins. Upon creation:
4454
/// - Redirect `stdout` to `stderr` so that print statements from the plugin
4555
/// are treated as plain-text output
@@ -83,6 +93,7 @@ public struct StandardIOMessageConnection: MessageConnection {
8393

8494
self.init(inputFileDescriptor: inputFD, outputFileDescriptor: outputFD)
8595
}
96+
#endif
8697

8798
/// Write the buffer to the file descriptor. Throws an error on failure.
8899
private func _write(contentsOf buffer: UnsafeRawBufferPointer) throws {

0 commit comments

Comments
 (0)