diff --git a/MODULE.bazel b/MODULE.bazel index 26de1fab9..960188a2d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -111,6 +111,7 @@ register_toolchains( ) include("//examples/cross_compilation/android_app:android.MODULE.bazel") +include("//examples/cross_compilation/wasm:wasm.MODULE.bazel") # Dev dependencies bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.5.0", dev_dependency = True) diff --git a/doc/rules.md b/doc/rules.md index 12c894e87..2b687046b 100755 --- a/doc/rules.md +++ b/doc/rules.md @@ -58,8 +58,8 @@ please use one of the platform-specific application rules in [rules_apple](https://github.com/bazelbuild/rules_apple) instead of `swift_binary`. -Setting `linkshared = True` links a shared library instead of an executable; -see the `linkshared` attribute. +Setting `linkshared = True` links a shared library or (on WebAssembly) a +reactor module instead of an executable; see the `linkshared` attribute. **ATTRIBUTES** @@ -75,7 +75,7 @@ see the `linkshared` attribute. | defines | A list of defines to add to the compilation command line.

Note that unlike C-family languages, Swift defines do not have values; they are simply identifiers that are either defined or undefined. So strings in this list should be simple identifiers, **not** `name=value` pairs.

Each string is prepended with `-D` and added to the command line. Unlike `copts`, these flags are added for the target and every target that depends on it, so use this attribute with caution. It is preferred that you add defines directly to `copts`, only using this feature in the rare case that a library needs to propagate a symbol up to those that depend on it. | List of strings | optional | `[]` | | env | Specifies additional environment variables to set when the test is executed by `bazel run` or `bazel test`.

The values of these environment variables are subject to `$(location)` and "Make variable" substitution.

NOTE: The environment variables are not set when you run the target outside of Bazel (for example, by manually executing the binary in `bazel-bin/`). | Dictionary: String -> String | optional | `{}` | | linkopts | Additional linker options that should be passed to `clang`. These strings are subject to `$(location ...)` expansion. | List of strings | optional | `[]` | -| linkshared | If `True`, link the target as a shared library / loadable module instead of an executable, similar to `cc_binary`'s `linkshared`. The binary has no `main` entry point and the renamed-entry-point machinery is disabled.

This produces a dynamic library named `lib.so` (`.dylib` on Apple platforms) suitable for loading with `dlopen` / `System.loadLibrary` (e.g. an Android JNI library; export functions with `@_cdecl`). | Boolean | optional | `False` | +| linkshared | If `True`, link the target as a shared library / loadable module instead of an executable, similar to `cc_binary`'s `linkshared`. The binary has no `main` entry point and the renamed-entry-point machinery is disabled.

On most platforms this produces a dynamic library named `lib.so` (`.dylib` on Apple platforms) suitable for loading with `dlopen` / `System.loadLibrary` (e.g. an Android JNI library; export functions with `@_cdecl`).

When targeting WebAssembly it instead produces a "reactor" module (`.wasm`, linked with `-mexec-model=reactor`): the module has no `_start`, runs its initializers via the exported `_initialize`, and exposes the functions a host instantiates and calls. Force-export those functions by passing `-Xlinker --export=` (or `-Wl,--export=`) flags in `linkopts`. | Boolean | optional | `False` | | malloc | Override the default dependency on `malloc`.

By default, Swift binaries are linked against `@bazel_tools//tools/cpp:malloc"`, which is an empty library and the resulting binary will use libc's `malloc`. This label must refer to a `cc_library` rule. | Label | optional | `"@bazel_tools//tools/cpp:malloc"` | | module_name | The name of the Swift module being built.

If left unspecified, the module name will be computed based on the target's build label, by stripping the leading `//` and replacing `/`, `:`, and other non-identifier characters with underscores. | String | optional | `""` | | package_name | The semantic package of the Swift target being built. Targets with the same package_name can access APIs using the 'package' access control modifier in Swift 5.9+. | String | optional | `""` | diff --git a/doc/standalone_toolchain.md b/doc/standalone_toolchain.md index 320c7fb11..ee7e5d256 100644 --- a/doc/standalone_toolchain.md +++ b/doc/standalone_toolchain.md @@ -203,6 +203,41 @@ public func greetingFromSwift(_ env: UnsafeMutablePointer, _ clazz: jcl See `examples/cross_compilation/android_app` for an end to end example. +## Building for WebAssembly + +The `swift` module extension can also download the Swift WebAssembly SDK, +which defines matching Swift and C/C++ toolchains targeting +`wasm32-unknown-wasip1`. + +Add the `wasm_sdk` tag, referencing the `toolchain` tag by name (the Swift +module format is not stable across compiler versions, so the SDK is always +downloaded for exactly the toolchain's version): + +```bzl +swift.wasm_sdk(toolchain_name = "swift_toolchain") + +register_toolchains( + "@swift_toolchain//:swift_toolchain_wasm32_xcode", + "@swift_toolchain//:cc_toolchain_wasm32_xcode", +) +``` + +and build with a platform that has the `@platforms//os:wasi` and +`@platforms//cpu:wasm32` constraints. + +### WebAssembly reactors + +A plain `swift_binary` links a WASI *command* module (with a `main`). To produce +a **reactor** instead, set `linkshared = True`: this links with +`-mexec-model=reactor`, so the `.wasm` module has no `main`, runs its +initializers via the exported `_initialize`, and exposes the functions a JS host +calls. Keep each exported function with +`linkopts = ["-Xlinker", "--export="]`. The Swift standard library is +linked statically from the SDK, so the reactor is a self-contained +`wasm32-wasip1` module (runnable with `wasmtime` et al.). + +See `examples/cross_compilation/wasm` for an end to end example. + ## Using the extension from a non-root module The extension is intended for the root module — it fails if a non-root diff --git a/examples/cross_compilation/wasm/BUILD.bazel b/examples/cross_compilation/wasm/BUILD.bazel new file mode 100644 index 000000000..e26a9d3b0 --- /dev/null +++ b/examples/cross_compilation/wasm/BUILD.bazel @@ -0,0 +1,88 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//examples/embedded:transition.bzl", "transition_binary") +load("//swift:swift.bzl", "swift_binary", "swift_library") + +platform( + name = "wasm32-wasip1", + constraint_values = [ + "@platforms//cpu:wasm32", + "@platforms//os:wasi", + ], +) + +swift_library( + name = "Greeter", + srcs = ["Sources/Greeter/Greeter.swift"], + module_name = "Greeter", +) + +# A reactor module (no `main`; exports functions for a host). `@_cdecl` names +# the exports; wasm-ld still needs an explicit `--export=` to retain each one. +# This only builds for wasm (the transition below configures it), so the +# `//examples/...` wildcard skips it on a host platform. +swift_binary( + name = "Reactor", + srcs = ["Sources/Reactor/Reactor.swift"], + linkopts = [ + "-Xlinker", + "--export=greeting_into", + "-Xlinker", + "--export=greeting_length", + ], + linkshared = True, + target_compatible_with = ["@platforms//os:wasi"], + deps = [":Greeter"], +) + +# Build `:Reactor` for the wasm platform. Consumers can instead set +# `--platforms=//examples/cross_compilation/wasm:wasm32-wasip1` on the command +# line. +transition_binary( + name = "Reactor.wasm", + binary = ":Reactor", + platform = ":wasm32-wasip1", +) + +# Executes the reactor under a hermetic wasmtime (downloaded in +# wasm.MODULE.bazel) and asserts on the value Swift computes. +sh_test( + name = "reactor_test", + srcs = ["reactor_test.sh"], + args = [ + "$(rlocationpath :wasmtime)", + "$(rlocationpath :Reactor.wasm)", + ], + data = [ + ":Reactor.wasm", + ":wasmtime", + ], +) + +alias( + name = "wasmtime", + actual = select({ + "@bazel_tools//src/conditions:darwin_arm64": "@wasmtime_darwin_aarch64//:wasmtime", + "@bazel_tools//src/conditions:darwin_x86_64": "@wasmtime_darwin_x86_64//:wasmtime", + "@bazel_tools//src/conditions:linux_aarch64": "@wasmtime_linux_aarch64//:wasmtime", + "@bazel_tools//src/conditions:linux_x86_64": "@wasmtime_linux_x86_64//:wasmtime", + }), +) + +# A static web app embedding the reactor: `index.html` + `Reactor.wasm` in one +# directory. Serve it (e.g. `python3 -m http.server -d +# bazel-bin/examples/cross_compilation/wasm/web_app`) and open it — the page +# calls the reactor's exports from JavaScript and shows the greeting Swift +# produced. `web/verify.mjs` does the same headlessly under Node. +genrule( + name = "web_app", + srcs = [ + "web/index.html", + ":Reactor.wasm", + ], + outs = [ + "web_app/index.html", + "web_app/Reactor.wasm", + ], + cmd = "cp $(location web/index.html) $(RULEDIR)/web_app/index.html && " + + "cp $(location :Reactor.wasm) $(RULEDIR)/web_app/Reactor.wasm", +) diff --git a/examples/cross_compilation/wasm/Sources/Greeter/Greeter.swift b/examples/cross_compilation/wasm/Sources/Greeter/Greeter.swift new file mode 100644 index 000000000..cf333fd7b --- /dev/null +++ b/examples/cross_compilation/wasm/Sources/Greeter/Greeter.swift @@ -0,0 +1,14 @@ +/// A plain `swift_library` used as a normal dependency of the WebAssembly +/// reactor entry point. Nothing in here is platform-specific; it is compiled +/// for whichever platform the depending target is built for. +public struct Greeter { + private let subject: String + + public init(subject: String) { + self.subject = subject + } + + public func greeting() -> String { + return "Hello from Swift, \(subject)!" + } +} diff --git a/examples/cross_compilation/wasm/Sources/Reactor/Reactor.swift b/examples/cross_compilation/wasm/Sources/Reactor/Reactor.swift new file mode 100644 index 000000000..c9f0bbba5 --- /dev/null +++ b/examples/cross_compilation/wasm/Sources/Reactor/Reactor.swift @@ -0,0 +1,27 @@ +import Greeter + +// A WebAssembly "reactor" module: it has no `main`/entry point. Instead it +// exports functions that a host (e.g. JavaScript via `WebAssembly.instantiate`) +// calls after instantiation. The `@_cdecl` attribute gives each function a +// plain C name; the linker still needs `--export=` (passed via `linkopts` in +// the BUILD file) to keep them in the final module. + +/// Writes the greeting into `buffer` (NUL-terminated, truncated to `capacity`) +/// and returns the number of bytes written, excluding the terminator. +@_cdecl("greeting_into") +public func greeting_into(_ buffer: UnsafeMutablePointer, _ capacity: Int32) -> Int32 { + let message = Greeter(subject: "WebAssembly").greeting() + let bytes = Array(message.utf8) + let limit = min(bytes.count, Int(capacity) - 1) + for index in 0 ..< limit { + buffer[index] = CChar(bitPattern: bytes[index]) + } + buffer[limit] = 0 + return Int32(limit) +} + +/// Returns the length the greeting would occupy (so the host can size a buffer). +@_cdecl("greeting_length") +public func greeting_length() -> Int32 { + return Int32(Greeter(subject: "WebAssembly").greeting().utf8.count) +} diff --git a/examples/cross_compilation/wasm/reactor_test.sh b/examples/cross_compilation/wasm/reactor_test.sh new file mode 100755 index 000000000..d02133998 --- /dev/null +++ b/examples/cross_compilation/wasm/reactor_test.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Executes the Swift reactor under wasmtime and asserts on the value Swift +# computes: `greeting_length` returns the UTF-8 length of +# "Hello from Swift, WebAssembly!" (30). + +set -euo pipefail + +wasmtime="$TEST_SRCDIR/$1" +reactor="$TEST_SRCDIR/$2" + +# wasmtime writes a compilation cache under XDG_CACHE_HOME (falling back to +# $HOME), which can be read-only inside the test sandbox (e.g. when CI passes +# --test_env=HOME); point it at the writable test tmpdir. +export XDG_CACHE_HOME="${TEST_TMPDIR:-${TMPDIR:-/tmp}}" + +actual="$("$wasmtime" run --invoke greeting_length "$reactor")" +if [[ "$actual" != "30" ]]; then + echo "error: expected greeting_length to return 30, got: $actual" >&2 + exit 1 +fi diff --git a/examples/cross_compilation/wasm/wasm.MODULE.bazel b/examples/cross_compilation/wasm/wasm.MODULE.bazel new file mode 100644 index 000000000..b705a092f --- /dev/null +++ b/examples/cross_compilation/wasm/wasm.MODULE.bazel @@ -0,0 +1,60 @@ +swift = use_extension("//swift:extensions.bzl", "swift", dev_dependency = True) +swift.wasm_sdk(toolchain_name = "swift_toolchain") + +register_toolchains( + # We register only the host platforms used by CI rather than + # `@swift_toolchain//:all`; see the note in the root MODULE.bazel. + "@swift_toolchain//:cc_toolchain_wasm32_ubuntu22.04", + "@swift_toolchain//:cc_toolchain_wasm32_ubuntu22.04-aarch64", + "@swift_toolchain//:cc_toolchain_wasm32_xcode", + "@swift_toolchain//:swift_toolchain_wasm32_ubuntu22.04", + "@swift_toolchain//:swift_toolchain_wasm32_ubuntu22.04-aarch64", + "@swift_toolchain//:swift_toolchain_wasm32_xcode", + dev_dependency = True, +) + +# A hermetic wasmtime so `:reactor_test` can execute the reactor. Only the +# repository selected for the host is ever downloaded. +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +_WASMTIME_BUILD = """\ +package(default_visibility = ["//visibility:public"]) + +exports_files(["wasmtime"]) +""" + +http_archive( + name = "wasmtime_darwin_aarch64", + build_file_content = _WASMTIME_BUILD, + dev_dependency = True, + sha256 = "acee50be70dbe90b0ab2ac7db1321fc44715153a1b1cc58291c97b6d7cffc558", + strip_prefix = "wasmtime-v46.0.1-aarch64-macos", + url = "https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.1/wasmtime-v46.0.1-aarch64-macos.tar.xz", +) + +http_archive( + name = "wasmtime_darwin_x86_64", + build_file_content = _WASMTIME_BUILD, + dev_dependency = True, + sha256 = "0513db67e7089c7e5f743a01427782bc4def83854222f4bc9b1d75f0b925240b", + strip_prefix = "wasmtime-v46.0.1-x86_64-macos", + url = "https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.1/wasmtime-v46.0.1-x86_64-macos.tar.xz", +) + +http_archive( + name = "wasmtime_linux_aarch64", + build_file_content = _WASMTIME_BUILD, + dev_dependency = True, + sha256 = "071c4def2a08f0ebc95c52dfd4f2886eb697ba495804217cf76e13b09d70a1be", + strip_prefix = "wasmtime-v46.0.1-aarch64-linux", + url = "https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.1/wasmtime-v46.0.1-aarch64-linux.tar.xz", +) + +http_archive( + name = "wasmtime_linux_x86_64", + build_file_content = _WASMTIME_BUILD, + dev_dependency = True, + sha256 = "9ae0b17ea298bcc52277a8208d6ab7fae8e1a89579672f9d82f9d86c116edb62", + strip_prefix = "wasmtime-v46.0.1-x86_64-linux", + url = "https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.1/wasmtime-v46.0.1-x86_64-linux.tar.xz", +) diff --git a/examples/cross_compilation/wasm/web/index.html b/examples/cross_compilation/wasm/web/index.html new file mode 100644 index 000000000..cc7b1b1c7 --- /dev/null +++ b/examples/cross_compilation/wasm/web/index.html @@ -0,0 +1,88 @@ + + + + + + Swift → WebAssembly (rules_swift) + + + +
+

Swift, compiled to WebAssembly

+

A swift_binary(linkshared = True) reactor module + built with rules_swift — instantiated and driven from JavaScript:

+
loading…
+

The text above was produced by the Swift Greeter + library running in WebAssembly, read out of the module's linear memory.

+
+ + + diff --git a/examples/cross_compilation/wasm/web/verify.mjs b/examples/cross_compilation/wasm/web/verify.mjs new file mode 100644 index 000000000..c2551888e --- /dev/null +++ b/examples/cross_compilation/wasm/web/verify.mjs @@ -0,0 +1,58 @@ +// Headless end-to-end check of the WebAssembly reactor, mirroring index.html: +// instantiate with a minimal WASI shim, run `_initialize`, then read the +// greeting that Swift writes into linear memory. Exits non-zero on mismatch. +// +// node examples/cross_compilation/wasm/web/verify.mjs \ +// bazel-bin/examples/cross_compilation/wasm/Reactor.wasm +// +// (index.html does exactly this in a browser.) + +import { readFileSync } from "node:fs"; +import { webcrypto as crypto } from "node:crypto"; + +const wasmPath = process.argv[2] ?? "bazel-bin/examples/cross_compilation/wasm/Reactor.wasm"; +const expected = "Hello from Swift, WebAssembly!"; + +let instance; +const dv = () => new DataView(instance.exports.memory.buffer); +const u8 = () => new Uint8Array(instance.exports.memory.buffer); +const SUCCESS = 0, BADF = 8; +const wasi = { + args_sizes_get: (a, b) => { dv().setUint32(a, 0, true); dv().setUint32(b, 0, true); return SUCCESS; }, + args_get: () => SUCCESS, + environ_sizes_get: (a, b) => { dv().setUint32(a, 0, true); dv().setUint32(b, 0, true); return SUCCESS; }, + environ_get: () => SUCCESS, + fd_fdstat_get: (fd, ptr) => { for (let i = 0; i < 24; i++) dv().setUint8(ptr + i, 0); return SUCCESS; }, + fd_prestat_get: () => BADF, + fd_prestat_dir_name: () => BADF, + fd_close: () => SUCCESS, + fd_read: (fd, iovs, n, nread) => { dv().setUint32(nread, 0, true); return SUCCESS; }, + fd_seek: (fd, off, whence, newOff) => { dv().setUint32(newOff, 0, true); return SUCCESS; }, + fd_write: (fd, iovs, n, nwritten) => { + let written = 0; + for (let i = 0; i < n; i++) written += dv().getUint32(iovs + i * 8 + 4, true); + dv().setUint32(nwritten, written, true); + return SUCCESS; + }, + path_open: () => BADF, + proc_exit: (code) => { throw new Error("proc_exit(" + code + ")"); }, + random_get: (ptr, len) => { crypto.getRandomValues(u8().subarray(ptr, ptr + len)); return SUCCESS; }, +}; + +const bytes = readFileSync(wasmPath); +instance = (await WebAssembly.instantiate(bytes, { wasi_snapshot_preview1: wasi })).instance; +instance.exports._initialize(); + +const length = instance.exports.greeting_length(); +const memory = instance.exports.memory; +const ptr = memory.buffer.byteLength; +memory.grow(Math.ceil((length + 1) / 65536)); +const written = instance.exports.greeting_into(ptr, length + 1); +const greeting = new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, written)); + +console.log("greeting:", JSON.stringify(greeting)); +if (greeting !== expected) { + console.error(`FAIL: expected ${JSON.stringify(expected)}`); + process.exit(1); +} +console.log("OK: Swift → WebAssembly greeting verified end-to-end"); diff --git a/swift/extensions.bzl b/swift/extensions.bzl index 409cf7b72..ba2e227d4 100644 --- a/swift/extensions.bzl +++ b/swift/extensions.bzl @@ -27,12 +27,14 @@ load( "//swift/internal/extensions:swift_sdks.bzl", "ANDROID_ARCHS", "swift_android_sdk_repository", + "swift_wasm_sdk_repository", ) load( "//swift/internal/extensions:toolchains.bzl", _android_sdk_toolchains_for_platform = "android_sdk_toolchains_for_platform", _toolchains_for_platform = "toolchains_for_platform", _toolchains_repository = "toolchains_repository", + _wasm_sdk_toolchains_for_platform = "wasm_sdk_toolchains_for_platform", ) load("//tools/explicit_modules:extensions.bzl", _system_sdk = "system_sdk") @@ -91,6 +93,44 @@ def _setup_android_sdk(*, tag, toolchain_name, swift_version, platforms): ) return build_file_content +def _setup_wasm_sdk(*, tag, toolchain_name, swift_version, platforms): + """Creates the repositories for a `swift.wasm_sdk` tag. + + Args: + tag: The `wasm_sdk` tag. + toolchain_name: The name of the `swift.toolchain` tag the SDK extends. + swift_version: The Swift release version of that toolchain. + platforms: The host platforms the toolchain was created for. + + Returns: + BUILD file content with the `toolchain` declarations to add to the + toolchains hub repository. + """ + sha256 = tag.sha256 + if not sha256: + if swift_version not in SWIFT_SDK_RELEASES: + fail("No known WebAssembly Swift SDK for version `{}`. Please choose one of {}, or provide the SDK's sha256.".format( + swift_version, + SWIFT_SDK_RELEASES.keys(), + )) + sha256 = SWIFT_SDK_RELEASES[swift_version]["wasm"] + + build_file_content = "" + for platform in platforms: + repository_name = "{}_wasm_sdk_{}".format(toolchain_name, platform) + swift_wasm_sdk_repository( + name = repository_name, + sha256 = sha256, + swift_version = swift_version, + toolchain_repo = "{}_{}".format(toolchain_name, platform), + url = swift_sdk_download_url(swift_version, "wasm"), + ) + build_file_content += _wasm_sdk_toolchains_for_platform( + platform = platform, + sdk_repository = repository_name, + ) + return build_file_content + def _sdk_tags_by_toolchain_name(tags, kind): """Groups SDK tags by the toolchain they extend, rejecting duplicates.""" tags_by_name = {} @@ -117,6 +157,10 @@ def _standalone_toolchain_impl(module_ctx): root_module.tags.android_sdk, "android_sdk", ) + wasm_sdk_tags = _sdk_tags_by_toolchain_name( + root_module.tags.wasm_sdk, + "wasm_sdk", + ) toolchain_names = [ toolchain.name @@ -128,6 +172,12 @@ def _standalone_toolchain_impl(module_ctx): toolchain_name, toolchain_names, )) + for toolchain_name in wasm_sdk_tags: + if toolchain_name not in toolchain_names: + fail("The `wasm_sdk` tag references unknown toolchain `{}`. Please use the name of a `toolchain` tag: {}".format( + toolchain_name, + toolchain_names, + )) toolchains_build_file_content = "" for toolchain in root_module.tags.toolchain: @@ -169,6 +219,13 @@ def _standalone_toolchain_impl(module_ctx): swift_version = swift_version, platforms = platforms, ) + if toolchain.name in wasm_sdk_tags: + toolchains_build_file_content += _setup_wasm_sdk( + tag = wasm_sdk_tags[toolchain.name], + toolchain_name = toolchain.name, + swift_version = swift_version, + platforms = platforms, + ) _toolchains_repository( name = toolchain.name, @@ -202,6 +259,25 @@ defines Swift toolchains targeting Android. """, ) +_wasm_sdk = tag_class( + attrs = { + "sha256": attr.string( + doc = """\ +The expected SHA-256 of the SDK artifact bundle. May be omitted for Swift +versions known to this version of rules_swift. +""", + ), + "toolchain_name": attr.string( + doc = "The name of the `toolchain` tag to add this Swift SDK to.", + mandatory = True, + ), + }, + doc = """\ +Downloads the WebAssembly Swift SDK matching a `toolchain` tag's Swift version +and defines Swift and C++ toolchains targeting `wasm32-unknown-wasip1`. +""", +) + _toolchain = tag_class(attrs = { "name": attr.string( doc = "Repository name of the generated toolchain", @@ -224,5 +300,6 @@ swift = module_extension( tag_classes = { "android_sdk": _android_sdk, "toolchain": _toolchain, + "wasm_sdk": _wasm_sdk, }, ) diff --git a/swift/internal/extensions/BUILD.bazel.tpl b/swift/internal/extensions/BUILD.bazel.tpl index 9520c53a8..722214771 100644 --- a/swift/internal/extensions/BUILD.bazel.tpl +++ b/swift/internal/extensions/BUILD.bazel.tpl @@ -16,6 +16,8 @@ exports_files([ ### Tools referenced by Swift SDK cross-compilation repositories. ### exports_files([ + "usr/bin/clang", + "usr/bin/llvm-ar", "usr/bin/swiftc", "usr/bin/swift-autolink-extract", "usr/bin/swift-symbolgraph-extract", @@ -37,6 +39,24 @@ filegroup( ), ) +# The subset of the toolchain that link actions driven by this toolchain's +# clang need (used by the WebAssembly SDK's cc toolchain). +filegroup( + name = "swift_sdk_linker_inputs", + srcs = glob( + [ + "usr/bin/clang*", + "usr/bin/ld.lld", + "usr/bin/ld64.lld", + "usr/bin/lld", + "usr/bin/llvm-ar", + "usr/bin/wasm-ld", + "usr/lib/clang/**", + ], + allow_empty = True, + ), +) + filegroup( name = "files", srcs = glob( diff --git a/swift/internal/extensions/swift_sdk_releases.bzl b/swift/internal/extensions/swift_sdk_releases.bzl index 8cfea21f6..ae0a28a50 100644 --- a/swift/internal/extensions/swift_sdk_releases.bzl +++ b/swift/internal/extensions/swift_sdk_releases.bzl @@ -14,6 +14,7 @@ https://www.swift.org/api/v1/install/releases.json. SWIFT_SDK_RELEASES = { "6.3.2": { "android": "939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68", + "wasm": "a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c", }, } diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl index e5a7b383e..084b95137 100644 --- a/swift/internal/extensions/swift_sdks.bzl +++ b/swift/internal/extensions/swift_sdks.bzl @@ -2,12 +2,13 @@ A "Swift SDK" is an artifact bundle published by swift.org for cross-compiling Swift to a platform the host toolchain cannot target by itself (the bundles that -`swift sdk install` consumes). The `android_sdk` extension defines a repository -rule that downloads such a bundle and generates a `swift_toolchain` targeting -`{aarch64,x86_64}-unknown-linux-android`; this module holds the helpers and -BUILD-file template it uses. C/C++ compilation and linking go through a -separately registered Android cc toolchain (e.g. `@androidndk//:all`), which also -provides the sysroot the Swift toolchain reads. +`swift sdk install` consumes). The per-platform extensions (`android_sdk`, +`wasm_sdk`) define repository rules that download such a bundle and generate a +`swift_toolchain` for the target; this module holds the helpers and BUILD-file +templates they share. Android's C/C++ compilation and linking go through a +separately registered Android cc toolchain (e.g. `@androidndk//:all`), while the +WebAssembly repository also generates a rules_cc `cc_toolchain` that drives the +paired toolchain's clang. Because the Swift module format is not stable across compiler versions, a Swift SDK must come from exactly the same release as the host toolchain it is paired @@ -51,6 +52,106 @@ swift_tools( ) """ +# The WebAssembly repository also generates a rules_cc cc_toolchain, so its +# BUILD header needs the rules_cc toolchain loads. +_WASM_BUILD_HEADER_TEMPLATE = """\ +load("@rules_cc//cc/toolchains:args.bzl", "cc_args") +load("@rules_cc//cc/toolchains:make_variable.bzl", "cc_make_variable") +load("@rules_cc//cc/toolchains:tool.bzl", "cc_tool") +load("@rules_cc//cc/toolchains:tool_map.bzl", "cc_tool_map") +load("@rules_cc//cc/toolchains:toolchain.bzl", "cc_toolchain") +load("@rules_swift//swift/toolchains:swift_toolchain.bzl", "swift_toolchain") +load("@rules_swift//swift/toolchains:swift_tools.bzl", "swift_tools") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "sdk_files", + srcs = glob(["{bundle_dir}/**"]), +) + +swift_tools( + name = "tools", + swift_driver = "@{toolchain_repo}//:usr/bin/swiftc", + swift_autolink_extract = "@{toolchain_repo}//:usr/bin/swift-autolink-extract", + swift_symbolgraph_extract = "@{toolchain_repo}//:usr/bin/swift-symbolgraph-extract", + additional_inputs = {compiler_inputs}, +) +""" + +_CC_TOOLCHAIN_TEMPLATE = """ +cc_tool( + name = "clang", + src = "{clang}", + data = {clang_data}, + tags = ["manual"], +) + +cc_tool( + name = "ar", + src = "{ar}", + tags = ["manual"], +) + +cc_tool_map( + name = "cc_tools", + tags = ["manual"], + tools = {{ + "@rules_cc//cc/toolchains/actions:ar_actions": ":ar", + "@rules_cc//cc/toolchains/actions:assembly_actions": ":clang", + "@rules_cc//cc/toolchains/actions:c_compile": ":clang", + "@rules_cc//cc/toolchains/actions:cpp_compile_actions": ":clang", + "@rules_cc//cc/toolchains/actions:link_actions": ":clang", + }}, +) +""" + +_CC_TOOLCHAIN_FOR_TARGET_TEMPLATE = """ +cc_args( + name = "cc_args_{suffix}", + actions = [ + "@rules_cc//cc/toolchains/actions:compile_actions", + "@rules_cc//cc/toolchains/actions:link_actions", + ], + args = {args}, +) + +cc_args( + name = "cc_link_args_{suffix}", + actions = [ + "@rules_cc//cc/toolchains/actions:link_actions", + ], + args = {link_args}, +) + +cc_make_variable( + name = "cc_target_triple_{suffix}", + value = "{triple}", + variable_name = "CC_TARGET_TRIPLE", +) + +cc_toolchain( + name = "cc_toolchain_{suffix}", + args = [ + ":cc_args_{suffix}", + ":cc_link_args_{suffix}", + ], + compiler = "clang", + enabled_features = [ + "@rules_cc//cc/toolchains/args/archiver_flags:feature", + "@rules_cc//cc/toolchains/args/libraries_to_link:feature", + "@rules_cc//cc/toolchains/args/link_flags:feature", + # Needed so `swift_binary(linkshared = True)` links a shared library + # (passes `-shared` for the dynamic_library link action). + "@rules_cc//cc/toolchains/args/shared_flag:feature", + ], + make_variables = [ + ":cc_target_triple_{suffix}", + ], + tool_map = ":cc_tools", +) +""" + def _execroot_relative_path(path): """Returns the execution-root-relative path for an external repository path. @@ -221,3 +322,107 @@ target Android. """, implementation = _swift_android_sdk_impl, ) + +def _swift_wasm_sdk_impl(repository_ctx): + bundle_dir = _download_sdk_bundle(repository_ctx) + toolchain_repo = repository_ctx.attr.toolchain_repo + + repo_root = "external/" + repository_ctx.name + sdk_dir = "{}/{}/{}".format( + repo_root, + bundle_dir, + "{0}/wasm32-unknown-wasip1".format(bundle_dir.removesuffix(".artifactbundle")), + ) + if not repository_ctx.path(sdk_dir.removeprefix(repo_root + "/")).exists: + fail("The WebAssembly Swift SDK bundle has an unexpected layout; " + + "missing " + sdk_dir) + wasi_sdk = sdk_dir + "/WASI.sdk" + resource_dir = sdk_dir + "/swift.xctoolchain/usr/lib/swift_static" + + build_content = _WASM_BUILD_HEADER_TEMPLATE.format( + bundle_dir = bundle_dir, + compiler_inputs = _build_list([ + ":sdk_files", + "@{}//:swift_sdk_compiler_inputs".format(toolchain_repo), + ]), + toolchain_repo = toolchain_repo, + ) + + build_content += _SWIFT_TOOLCHAIN_TEMPLATE.format( + arch = "wasm32", + copts = _build_list([ + "-resource-dir", + resource_dir, + ]), + features = _build_list([ + "swift.module_map_no_private_headers", + "swift.no_embed_debug_module", + # wasm-ld cannot alias a renamed entry point back to the symbol + # that wasi-libc's startup code expects. + "swift.no_entry_point_rename", + "swift.use_autolink_extract", + ]), + linker_inputs = _build_list([":sdk_files"]), + # The runtime objects and libraries that `swiftc` would add when + # linking a static executable for WASI; see + # `swift_static/wasi/static-executable-args.lnk` in the SDK. + linkopts = _build_list([ + "{}/wasi/wasm32/swiftrt.o".format(resource_dir), + "-L{}/wasi".format(resource_dir), + "-lc++", + "-lc++abi", + "-lswiftSwiftOnoneSupport", + "-ldl", + "-lm", + "-lwasi-emulated-mman", + "-lwasi-emulated-signal", + "-lwasi-emulated-process-clocks", + # The Swift driver always passes these bases to wasm-ld. + # `--table-base=4096` in particular is required: without it, + # optimized (`-O`) generic-metadata instantiation reads out of + # bounds at runtime (`-Onone` happens to tolerate the default). + "-Wl,--global-base=4096", + "-Wl,--table-base=4096", + ]), + os = "wasi", + sdkroot = wasi_sdk, + suffix = "wasm32", + swift_version = repository_ctx.attr.swift_version, + ) + + build_content += _CC_TOOLCHAIN_TEMPLATE.format( + ar = "@{}//:usr/bin/llvm-ar".format(toolchain_repo), + clang = "@{}//:usr/bin/clang".format(toolchain_repo), + clang_data = _build_list([ + ":sdk_files", + "@{}//:swift_sdk_linker_inputs".format(toolchain_repo), + ]), + ) + + build_content += _CC_TOOLCHAIN_FOR_TARGET_TEMPLATE.format( + args = _build_list([ + "--target=wasm32-unknown-wasip1", + "--sysroot=" + wasi_sdk, + ]), + # The Swift SDK's clang resource directory provides the compiler + # builtins (libclang_rt) for wasm32, which the host toolchain's own + # resource directory does not include. + link_args = _build_list([ + "-resource-dir", + resource_dir + "/clang", + ]), + suffix = "wasm32", + triple = "wasm32-unknown-wasip1", + ) + + repository_ctx.file("BUILD.bazel", build_content) + +swift_wasm_sdk_repository = repository_rule( + attrs = _common_attrs(), + doc = """\ +Downloads the WebAssembly Swift SDK artifact bundle and defines Swift and C++ +toolchains that target `wasm32-unknown-wasip1` using a standalone host +toolchain's compiler. +""", + implementation = _swift_wasm_sdk_impl, +) diff --git a/swift/internal/extensions/toolchains.bzl b/swift/internal/extensions/toolchains.bzl index c520f1942..cb6916ba7 100644 --- a/swift/internal/extensions/toolchains.bzl +++ b/swift/internal/extensions/toolchains.bzl @@ -55,6 +55,33 @@ toolchain( ) """ +_WASM_SDK_TOOLCHAIN_PLATFORM = """ +# Swift SDK toolchains from repository: `{sdk_repository}` +toolchain( + name = "swift_toolchain_{target}_{platform}", + exec_compatible_with = {exec_compatible_with}, + target_compatible_with = {target_compatible_with}, + toolchain = "@{sdk_repository}//:swift_toolchain_{target_suffix}", + toolchain_type = "@rules_swift//toolchains:toolchain_type", + visibility = ["//visibility:public"], +) + +# The paired rules_cc toolchain drives the SDK bundle's own clang for the C +# side of the build (unlike Android, where the cc toolchain comes from the +# separately registered NDK). It only resolves for this wasm target platform, +# and because root-module registrations take precedence in toolchain +# resolution, a consumer who registers their own wasm cc toolchain wins over +# this one automatically. +toolchain( + name = "cc_toolchain_{target}_{platform}", + exec_compatible_with = {exec_compatible_with}, + target_compatible_with = {target_compatible_with}, + toolchain = "@{sdk_repository}//:cc_toolchain_{target_suffix}", + toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", + visibility = ["//visibility:public"], +) +""" + def _exec_compatible_with_for_platform(platform): # This assumption is baked into the API so we have to go along with it if platform == "xcode": @@ -102,6 +129,30 @@ def android_sdk_toolchains_for_platform(platform, sdk_repository, archs): ) return content +def wasm_sdk_toolchains_for_platform(platform, sdk_repository): + """Returns `toolchain` declarations for a WebAssembly Swift SDK. + + Args: + platform: The platform name (e.g. "xcode" or "ubuntu22.04") whose + standalone toolchain the SDK is paired with. + sdk_repository: The name of the repository created by + `swift_wasm_sdk_repository`. + + Returns: + BUILD file content declaring the Swift and C++ toolchains. + """ + return _WASM_SDK_TOOLCHAIN_PLATFORM.format( + exec_compatible_with = _exec_compatible_with_for_platform(platform), + platform = platform, + sdk_repository = sdk_repository, + target = "wasm32", + target_compatible_with = [ + "@platforms//os:wasi", + "@platforms//cpu:wasm32", + ], + target_suffix = "wasm32", + ) + def _toolchains_impl(repository_ctx): repository_ctx.file("BUILD.bazel", repository_ctx.attr.build_file_content) diff --git a/swift/swift_binary.bzl b/swift/swift_binary.bzl index 2186bc9e4..a0d28b078 100644 --- a/swift/swift_binary.bzl +++ b/swift/swift_binary.bzl @@ -78,6 +78,12 @@ def _maybe_parse_as_library_copts(srcs): srcs[0].basename != "main.swift" return ["-parse-as-library"] if use_parse_as_library else [] +def _is_wasm(ctx): + """Returns True if the target platform is WebAssembly.""" + return ctx.target_platform_has_constraint( + ctx.attr._wasi_os_constraint[platform_common.ConstraintValueInfo], + ) + def _swift_binary_impl(ctx): toolchains = find_all_toolchains(ctx) feature_configuration = configure_features_for_binary( @@ -87,10 +93,12 @@ def _swift_binary_impl(ctx): unsupported_features = ctx.disabled_features, ) - # A binary linked as a shared object (`linkshared`) has no `main`, so the - # entry-point rename (and the matching `--defsym main=...` at link time) - # must be skipped, just as it is when the toolchain requests it via - # `swift.no_entry_point_rename`. + is_wasm = _is_wasm(ctx) + + # A binary linked as a shared object (`linkshared`) or a WebAssembly + # reactor has no `main`, so the entry-point rename (and the matching + # `--defsym main=...` at link time) must be skipped, just as it is when the + # toolchain requests it via `swift.no_entry_point_rename`. skip_entry_point = ctx.attr.linkshared or is_feature_enabled( feature_configuration = feature_configuration, feature_name = SWIFT_FEATURE_NO_ENTRY_POINT_RENAME, @@ -195,12 +203,21 @@ def _swift_binary_impl(ctx): else: name = ctx.label.name - # `linkshared` produces a real dynamic library (`lib.so` / `.dylib`), - # matching `cc_binary`'s `linkshared`; otherwise an executable. - if ctx.attr.linkshared: + # On WebAssembly a `linkshared` binary is a "reactor": an executable-shaped + # wasm module linked with `-mexec-model=reactor` (no `main`; it exports + # functions for a host to call). Everywhere else `linkshared` produces a + # real dynamic library, matching `cc_binary`. + shared_link_flags = [] + if ctx.attr.linkshared and not is_wasm: output_type = "dynamic_library" else: output_type = "executable" + if ctx.attr.linkshared and is_wasm: + shared_link_flags = ["-mexec-model=reactor"] + + # Give WebAssembly outputs the conventional `.wasm` extension. + if is_wasm: + name = name + ".wasm" linking_outputs = register_link_binary_action( actions = ctx.actions, @@ -216,7 +233,9 @@ def _swift_binary_impl(ctx): output_type = output_type, stamp = ctx.attr.stamp, toolchains = toolchains, - user_link_flags = binary_link_flags + entry_point_linkopts, + user_link_flags = ( + binary_link_flags + entry_point_linkopts + shared_link_flags + ), variables_extension = variables_extension, ) @@ -324,11 +343,22 @@ If `True`, link the target as a shared library / loadable module instead of an executable, similar to `cc_binary`'s `linkshared`. The binary has no `main` entry point and the renamed-entry-point machinery is disabled. -This produces a dynamic library named `lib.so` (`.dylib` on Apple -platforms) suitable for loading with `dlopen` / `System.loadLibrary` (e.g. an -Android JNI library; export functions with `@_cdecl`). +On most platforms this produces a dynamic library named `lib.so` +(`.dylib` on Apple platforms) suitable for loading with `dlopen` / +`System.loadLibrary` (e.g. an Android JNI library; export functions with +`@_cdecl`). + +When targeting WebAssembly it instead produces a "reactor" module +(`.wasm`, linked with `-mexec-model=reactor`): the module has no +`_start`, runs its initializers via the exported `_initialize`, and exposes +the functions a host instantiates and calls. Force-export those functions by +passing `-Xlinker --export=` (or `-Wl,--export=`) flags in +`linkopts`. """, ), + "_wasi_os_constraint": attr.label( + default = Label("@platforms//os:wasi"), + ), }, ), doc = """\ @@ -346,8 +376,8 @@ please use one of the platform-specific application rules in [rules_apple](https://github.com/bazelbuild/rules_apple) instead of `swift_binary`. -Setting `linkshared = True` links a shared library instead of an executable; -see the `linkshared` attribute. +Setting `linkshared = True` links a shared library or (on WebAssembly) a +reactor module instead of an executable; see the `linkshared` attribute. """, exec_groups = { # The `plugins` attribute associates its `exec` transition with this