Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions doc/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -75,7 +75,7 @@ see the `linkshared` attribute.
| <a id="swift_binary-defines"></a>defines | A list of defines to add to the compilation command line.<br><br>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.<br><br>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 | `[]` |
| <a id="swift_binary-env"></a>env | Specifies additional environment variables to set when the test is executed by `bazel run` or `bazel test`.<br><br>The values of these environment variables are subject to `$(location)` and "Make variable" substitution.<br><br>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/`). | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | `{}` |
| <a id="swift_binary-linkopts"></a>linkopts | Additional linker options that should be passed to `clang`. These strings are subject to `$(location ...)` expansion. | List of strings | optional | `[]` |
| <a id="swift_binary-linkshared"></a>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.<br><br>This produces a dynamic library named `lib<name>.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` |
| <a id="swift_binary-linkshared"></a>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.<br><br>On most platforms this produces a dynamic library named `lib<name>.so` (`.dylib` on Apple platforms) suitable for loading with `dlopen` / `System.loadLibrary` (e.g. an Android JNI library; export functions with `@_cdecl`).<br><br>When targeting WebAssembly it instead produces a "reactor" module (`<name>.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=<symbol>` (or `-Wl,--export=<symbol>`) flags in `linkopts`. | Boolean | optional | `False` |
| <a id="swift_binary-malloc"></a>malloc | Override the default dependency on `malloc`.<br><br>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. | <a href="https://bazel.build/concepts/labels">Label</a> | optional | `"@bazel_tools//tools/cpp:malloc"` |
| <a id="swift_binary-module_name"></a>module_name | The name of the Swift module being built.<br><br>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 | `""` |
| <a id="swift_binary-package_name"></a>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 | `""` |
Expand Down
35 changes: 35 additions & 0 deletions doc/standalone_toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,41 @@ public func greetingFromSwift(_ env: UnsafeMutablePointer<JNIEnv?>, _ 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 `<name>.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=<symbol>"]`. 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
Expand Down
88 changes: 88 additions & 0 deletions examples/cross_compilation/wasm/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
)
14 changes: 14 additions & 0 deletions examples/cross_compilation/wasm/Sources/Greeter/Greeter.swift
Original file line number Diff line number Diff line change
@@ -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)!"
}
}
27 changes: 27 additions & 0 deletions examples/cross_compilation/wasm/Sources/Reactor/Reactor.swift
Original file line number Diff line number Diff line change
@@ -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<CChar>, _ 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)
}
21 changes: 21 additions & 0 deletions examples/cross_compilation/wasm/reactor_test.sh
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions examples/cross_compilation/wasm/wasm.MODULE.bazel
Original file line number Diff line number Diff line change
@@ -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",
)
88 changes: 88 additions & 0 deletions examples/cross_compilation/wasm/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Swift → WebAssembly (rules_swift)</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
margin: 0; min-height: 100vh; display: grid; place-items: center;
background: #0b1021; color: #e7ecff; }
main { text-align: center; padding: 2rem; max-width: 42rem; }
h1 { font-weight: 600; margin-bottom: .25rem; }
.bubble { font-size: 2rem; padding: 1.25rem 1.75rem; margin: 1.75rem 0;
background: #1b2547; border-radius: 1rem; min-height: 1.5em; }
code { background: #1b2547; padding: .15em .4em; border-radius: .35em; }
.muted { color: #9fb0e0; line-height: 1.5; }
</style>
</head>
<body>
<main>
<h1>Swift, compiled to WebAssembly</h1>
<p class="muted">A <code>swift_binary(linkshared = True)</code> reactor module
built with <code>rules_swift</code> — instantiated and driven from JavaScript:</p>
<div class="bubble" id="out">loading…</div>
<p class="muted">The text above was produced by the Swift <code>Greeter</code>
library running in WebAssembly, read out of the module's linear memory.</p>
</main>
<script type="module">
const out = document.getElementById("out");

// The reactor imports `wasi_snapshot_preview1` for its runtime startup.
// This greeting does no real I/O, so most calls are success stubs;
// `random_get` is wired to Web Crypto and `fd_write` to the console.
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: (argc, bufSize) => { dv().setUint32(argc, 0, true); dv().setUint32(bufSize, 0, true); return SUCCESS; },
args_get: () => SUCCESS,
environ_sizes_get: (count, size) => { dv().setUint32(count, 0, true); dv().setUint32(size, 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, text = "";
for (let i = 0; i < n; i++) {
const ptr = dv().getUint32(iovs + i * 8, true);
const len = dv().getUint32(iovs + i * 8 + 4, true);
text += new TextDecoder().decode(u8().subarray(ptr, ptr + len));
written += len;
}
if (text) console.log(text);
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; },
};

try {
const bytes = await (await fetch("Reactor.wasm")).arrayBuffer();
instance = (await WebAssembly.instantiate(bytes, { wasi_snapshot_preview1: wasi })).instance;

// WASI reactor: run global/runtime initializers once before any export.
instance.exports._initialize();

const length = instance.exports.greeting_length();
// Put the output buffer in a freshly grown page (guaranteed unused).
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));

out.textContent = "“" + greeting + "”";
} catch (err) {
out.textContent = "error: " + err;
console.error(err);
}
</script>
</body>
</html>
Loading
Loading