Skip to content
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

Add support for WebAssembly Macros #2623

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

kabiroberai
Copy link

@kabiroberai kabiroberai commented Apr 24, 2024

This PR allows CompilerPlugins to be built with a wasm32-unknown-wasi target, enabling them to be invoked by the Wasm Plugin runtime in apple/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

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

Examples$ swiftc Client.swift \
  -load-plugin-executable .build/release/MacroExamplesImplementation.wasm#MacroExamplesImplementation

@kabiroberai kabiroberai marked this pull request as draft April 24, 2024 20:22
@kabiroberai kabiroberai marked this pull request as ready for review April 24, 2024 21:12
Copy link
Collaborator

@ahoppen ahoppen left a 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.

Comment on lines 316 to 156
#if compiler(>=6.0)

@_expose(wasm, "swift_wasm_macro_pump")
@_cdecl("swift_wasm_macro_pump")
func wasmPump() {
readabilityHandler()
}

#endif
Copy link
Collaborator

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.

Copy link
Author

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.

@ahoppen ahoppen requested a review from rintaro April 24, 2024 21:26
@kabiroberai
Copy link
Author

A couple of things worth mentioning:

  1. I didn't add wasm_support.c to the CMake source set for _SwiftSyntaxCShims since I don't think we ever build SwiftSyntax for WASI with CMake — and I'm not sure how I would even test that. Is this okay or should I try to allow cross-compiling with CMake?
  2. Can we add a smoke test to ensure that SwiftSyntax continues to build for WASI? Unsure of the best way to go about doing this.

@kateinoigakukun
Copy link
Member

@kabiroberai

  1. Can we add a smoke test to ensure that SwiftSyntax continues to build for WASI? Unsure of the best way to go about doing this.

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 apple/swift#72728 and after the integration, we will be able to use Wasm target in CI.

// Wasm doesn't support dup{,2} so we use the original file descriptors.
let inputFD = fileno(_stdin)
let outputFD = fileno(_stdout)
#else
Copy link
Member

@rintaro rintaro Apr 29, 2024

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");
Copy link
Member

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?

Copy link
Author

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:

https://github.com/apple/swift/pull/73031/files#diff-2261ec558c289a4bd568f211772e906caa997f8c26b38e4c6d5d37b7b1554379R47

Will add a brief explanation to the comment.

Copy link
Author

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.)

Copy link
Author

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.

Copy link
Author

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.

Copy link
Contributor

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)
Copy link
Member

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

Copy link
Author

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.

Comment on lines 115 to 124
#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
Copy link
Member

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.

Copy link
Author

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.

Copy link
Author

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.

Comment on lines 89 to 102
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
Copy link
Member

@rintaro rintaro Apr 29, 2024

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
@kabiroberai
Copy link
Author

Hey @rintaro, could you please take another look at this when you have a minute? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants