-
Notifications
You must be signed in to change notification settings - Fork 440
Add support for WebAssembly Macros #2623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good from my side but I would like to get a review from @rintaro as well.
#if compiler(>=6.0) | ||
|
||
@_expose(wasm, "swift_wasm_macro_pump") | ||
@_cdecl("swift_wasm_macro_pump") | ||
func wasmPump() { | ||
readabilityHandler() | ||
} | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this code require Swift 6 to build for wasm? What happens if you build using a Swift 5 compiler? If it doesn’t build with an older compiler, I think we should put a #error
in the #else
branch telling you that you need a Swift 6 compiler to build the plugin server for WASM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out this was causing pre-6.0 builds (even those not targeting wasm) to fail — somehow Swift ignores the #if compiler
check and complains that it doesn't recognize @_expose(wasm)
. I moved the export over to C, so this should now work fine with older compilers. That said I haven't tested a pre-6.0 Wasm-compatible compiler yet, since afaik Swift didn't officially support Wasm until 6.0 — but I can give it a spin with a SwiftWasm toolchain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, if you're having trouble with an #if compiler
check, use something like #if compiler(>=5.3) && hasAttribute(_expose)
, and older compilers won't look into the #if
at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this didn't quite work but it got me poking around the Parser::parseIfConfig
code and I discovered a bug:
The following fails to compile
#if os(WASI)
#if compiler(>=100)
foo
#endif
#endif
whereas it compiles fine if we swap the conditions
#if compiler(>=100)
#if os(WASI)
foo
#endif
#endif
Seems like if we're already in an inactive #if
clause, the parser doesn't respect the "disable parsing entirely" functionality of nested #if compiler
decls. I'll see if I can make a followup in apple/swift to fix this but in the meantime the fix is thankfully as easy as swapping the statements around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the above PR won't be useful for this PR since we need the check to work in pre-6.X compilers (by definition), but hopefully it'll be useful to someone who runs across this issue in the future.
A couple of things worth mentioning:
|
I'm not sure we have a way to run smoke test with Wasm target in CI right now. But we are trying to build Wasm SDK in CI here swiftlang/swift#72728 and after the integration, we will be able to use Wasm target in CI. |
9305330
to
158ddd5
Compare
// Wasm doesn't support dup{,2} so we use the original file descriptors. | ||
let inputFD = fileno(_stdin) | ||
let outputFD = fileno(_stdout) | ||
#else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of #if
in the body, could you just wrap the whole decl? The body for wasm would be just
#if os(WASI)
public init() throws {
self.init(
inputFileDescriptor: fileno(_stdin),
outputFileDescriptor: fileno(_stdout)
)
}
#else
...
This way we can find #endif
much easier.
// Wasm Custom Section "foo". this must be a metadata section rather | ||
// than a data section so we can't use __attribute__((section)) for it. | ||
// See: https://reviews.llvm.org/D43097 | ||
__asm__("\t.section .custom_section.swift_wasm_macro_abi,\"\",@\n\t.4byte " STR(SWIFT_WASM_MACRO_ABI) "\n"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you teach me what this does exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds a custom swift_wasm_macro_abi
section to the binary with a little-endian uint32 value. We check the ABI version in the wasm executor in order to future-proof. See:
Will add a brief explanation to the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm actually @kateinoigakukun what do you think about using the export name to indicate the ABI instead? E.g. we could export the pump function with the name swift_wasm_macro_pump_v1
and the runtime could check for the presence of this to indicate the v1 ABI. The custom section support feels shaky to me — I wouldn't be surprised if there are tools that don't know how to handle custom wasm sections (or, alternatively, strip them out.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed this as a tentative change, happy to revert if the Custom Sections approach is preferable but I do like this atm because it's a bit more lightweight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For posterity: discussed this with @kateinoigakukun here; we aligned on using the exported function name rather than a custom section, since some Wasm tooling (ahem wasm-strip) can be too trigger-happy about stripping custom sections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Swift tries to be conservative with custom sections because it's a terribly non-portable solution. Although portability isn't at issue here, it might maybe make sense to use an exported function name. That'd be the typical solution here if we were talking about a C function on another platform, I think.
// fatalError writes to stdout, which is the message | ||
// output stream under WASI | ||
public func internalError(_ message: String) -> Never { | ||
fputs("Internal Error: \(message)\n", _stderr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fatalError writes to stdout
As far as I can see it's stderr
. Am I missing anything? https://github.com/apple/swift/blob/77f53a5e50f0cf964ae9dcc42f78ec43227a3db2/stdlib/public/runtime/Errors.cpp#L323-L332
But anyway, I think I'm going to use fputs("Internal Error: \(message)\n", _stderr)
in all platforms, (we don't want #file
etc, and other magic in fatalError
). So I will bring internalError()
back to CompilerPlugin.swift
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, looks like you're right. Not sure why I thought it wrote to stdout — might've forgotten to attach stderr to the WASI bridge at some point.
#if os(WASI) | ||
// Rather than blocking on read(), let the host tell us when there's data. | ||
readabilityHandler = { | ||
do { | ||
_ = try impl.handleNextMessage() | ||
} catch { | ||
internalError("\(error)") | ||
} | ||
} | ||
#else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this logic should be sinked to impl.main()
, so handleNextMessage()
can be internal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I'm worried that moving readabilityHandler
into SwiftCompilerPluginMessageHandling
would be leaky, since the swift_wasm_macro_pump
export (which invokes readabilityHandler
) is an impl detail of the SwiftCompilerPlugin
target. Also worth mentioning that CompilerPluginMessageListener
is @_spi(PluginMessage)
so I don't think we're creating any new API contracts even if handleNextMessage
is technically public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I just realized that _SwiftSyntaxCShims
is a dependency of SwiftCompilerPluginMessageHandling
so the latter is already strongly coupled to our wasm ABI. Moved readabilityHandler
into that module.
while let message = try connection.waitForNextMessage(HostToPluginMessage.self) { | ||
let result = handler.handleMessage(message) | ||
try connection.sendMessage(result) | ||
while try handleNextMessage() {} | ||
} | ||
|
||
/// Receives and handles a single message from the plugin host. | ||
/// | ||
/// - Returns: `true` if there was a message to read, `false` | ||
/// if the end-of-file was reached. | ||
public func handleNextMessage() throws -> Bool { | ||
guard let message = try connection.waitForNextMessage(HostToPluginMessage.self) else { | ||
return false | ||
} | ||
let result = handler.handleMessage(message) | ||
try connection.sendMessage(result) | ||
return true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened a PR #2631
My intention is, in this PR, you'd make it like:
public func main() {
#if os(WASI)
readabilityHandler = { _ = self.handleNextMessage() }
#else
while handleNextMessage() {}
#endif
}
func handleNextMessage() -> Bool {
do {
guard let message = try connection.waitForNextMessage(HostToPluginMessage.self) else {
return false
}
let result = handler.handleMessage(message)
try connection.sendMessage(result)
return true
} catch {
fputs("Internal Error: \(message)\n", _stderr)
exit(1)
}
}
I think this should minimize #if os(WASI)
branch code and improves the readability.
# Conflicts: # Sources/SwiftCompilerPlugin/CompilerPlugin.swift # Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift
Hey @rintaro, could you please take another look at this when you have a minute? Thanks! |
@swift-ci Please test |
@rintaro I updated spi.yml which should fix the test failure — could you please request a re-run? |
@swift-ci Please test |
would appreciate another run, third time's the charm hopefully 🤞 |
@swift-ci test |
@swift-ci please test Windows |
Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift
Outdated
Show resolved
Hide resolved
Hi folks, seems like the one remaining request is to adjust the indentation here once the corresponding fix lands in swift-format: #2623 (comment) Seeing as the swift-format PR is in turn blocked by some CI issues, would it make sense to merge this PR without that one whitespace change and follow up once the swift-format PR lands? |
@swift-ci test |
Linux and macOS are green! Can we please run the Windows suite too? cc @kateinoigakukun |
@swift-ci test Windows |
👋 seeing as all tests have passed are we good to merge this? |
Unfortunately, it looks like all CI jobs have failed, and this PR has not been approved by any code owners. So it's not ready to merge yet. |
looks like CI was re-run today and a few things are out of sync now. will bring it back into working condition. |
Thanks! |
🙌 could we please run CI again? |
@swift-ci test |
@swift-ci Please test Windows |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for patiently dealing with me! LGTM
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 ```
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 aread
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
And then, with the changes from the swift and swift-driver PRs, a client can be compiled with