---
title: 'The WebAssembly Target'
source: 'https://academia.sh/en/courses/rust-advanced/webassembly-target'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by running code on the local target how conditional compilation separates a target-dependent decision in the source'
    - 'Proves by comparison that a body relying on the file system and one that does not produce the same result'
    - 'Explains that source looking portable does not mean it is portable, and that target dependency must be read from the source'
---

# The WebAssembly Target

The same source needing a different body depending on the target it runs on: a body that relies on a part of the standard library (the file system) and one that does not are separated with conditional compilation, both are run on the local target, and both give the same result.

The previous lesson measured the case where the boundary sat between two languages: as
long as the declaration was correct, it did not matter which processor or operating system
it ran on. This lesson reverses that same independence. Here the language stays the same —
the entire source is Rust — but the **target** it runs on changes, and some bodies cannot
stay indifferent to that change. A target running in a browser does not always have access
to a file system, an operating system thread, or a clock; this lesson measures how that
access is made **visible** in the source.

The boundary in the previous lesson rested on a declaration's correctness; the boundary
here rests on the environment the target itself provides — both rest on an assumption the
compiler cannot test directly. This constraint does not belong to Rust; it comes from the
browser's own security model. A browser tab keeps the code it runs in a space isolated
from disk, network, and operating system; a page opening a file on its own, creating a
thread, or establishing a socket runs against the purpose of that isolation. A program
compiled into this space is limited to the capabilities that space offers — and these
capabilities are narrower than what the same program's version running on a server or a
desktop has. Rust's standard library does not hide this difference: part of modules such
as the file system, threads, and time either do not exist at all in this isolated target,
or behave differently.

In this lesson, only the local target is compiled. No real compilation is done for another
target; the target-dependent decision is shown on the **local** target with conditional
compilation — which of the two bodies in the source is selected on this machine is seen by
running code, while the other body sits in the source without ever being compiled. This
selection is not made at run time: the decision managed by `cfg` is made during
compilation, before the program starts running. It is not possible to pick one of the two
bodies inside the same binary and switch to the other at run time — whichever target it
was compiled for, that target's body enters the binary, the other never does.

## A Body Split by Target

`#[cfg(...)]` is not a run-time branch; it should not be confused with an `if` expression.
`if` tests a condition at run time, in a structure where both branches sit in the binary
and only one is executed. `cfg`, by contrast, decides at compile time: the item that does
not satisfy the condition never enters the rest of compilation at all, as if it had been
cut from the source tree entirely. The result of this difference is seen in the run below.

The run below defines two bodies that read a sensor record. One reads from the file
system; the other stands in for a body that would be compiled on the browser target, and
it is never compiled on this machine at all. Which body gets compiled is decided by the
`#[cfg(...)]` attribute.

**LL11.** The sensor record is a four-line text: `12`, `7`, `5`, `21` — summing to `45`.

```rust
// wasm1.rs — a body split by target, the branch selected on the local target
use std::fs;

#[cfg(not(target_family = "wasm"))]
fn read_records() -> String {
    // Body relying on the file system: compiled only on targets that have one.
    fs::read_to_string("sensor.txt").expect("could not read file")
}

#[cfg(target_family = "wasm")]
fn read_records() -> String {
    // This body is compiled on the browser target; it is never compiled on the
    // local target, so an undefined call carried here breaks nothing here.
    body_reads_from_another_source_on_the_wasm_target()
}

fn total(text: &str) -> i32 {
    text.lines().filter_map(|s| s.trim().parse::<i32>().ok()).sum()
}

fn main() {
    fs::write("sensor.txt", "12\n7\n5\n21\n").expect("could not write file");
    let content = read_records();
    println!("file-system-backed body total: {}", total(&content));
}
```

```
file-system-backed body total: 45
```

There are two `read_records` definitions in the source, but the compiled program contains
only one. The body marked `#[cfg(not(target_family = "wasm"))]` entered compilation
because this machine is not part of the wasm family; the body marked
`#[cfg(target_family = "wasm")]` was never compiled at all — even though the call inside
it is defined nowhere, the compiler never noticed, because it never saw that body.
**LL12.** This shows that a body left out by `cfg` passes through no semantic checking at
all, as long as its syntax is valid; "not compiled" and "wrong" are not the same thing —
the compiler passes no judgment at all about a body that is not compiled.

This may look similar to the foreign function declaration in the previous lesson, but it
carries a different guarantee. An `extern "C"` declaration described a body the compiler
never saw but which genuinely existed and had to be linked; if the linker could not find
that name, compilation broke. A body marked `#[cfg(target_family = "wasm")]`, by contrast,
is never looked for on this machine at all — it does not matter that the name
`body_reads_from_another_source_on_the_wasm_target` inside it is defined nowhere in this
program, because that name never reaches the linker. In the foreign function interface,
what was delegated was a declaration's correctness; here nothing is delegated, because
that body is not even part of this compilation.

The `.expect(...)` call inside `read_records` is also a deliberate choice: the read is
known not to fail, because the file was written one line earlier inside the same `main`
function, but Rust cannot know this at compile time — `read_to_string` always returns a
`Result`, and this value has to be handled. `.expect(...)` unwraps that value, choosing to
stop with a panic on failure; because this lesson never triggers a panic, this path is only
ever followed in the success case.

The run above chose the form `target_family = "wasm"`; one of the attribute names the
language recognizes is also `target_arch = "wasm32"`, and the two do not ask the same
question. `target_family` asks about the broad family a target belongs to (the wasm
family, the unix family); `target_arch` asks about the processor architecture's name
(`wasm32`, `x86_64`). This lesson asked about the family, because the distinction being
measured rested not on architecture but on the class of environment the target provides —
a target without a file system is not limited to a single architecture. Which attribute
name is chosen is a decision readable from the source, and it says which question the body
is split by.

## The Counterpart That Does Not Rely on the File System

A body being split by `cfg` does not mean that body has to keep existing. The same sensor
record can also be obtained without ever touching the file system — for this lesson, the
value is embedded inside the source. The difference between these two options is not an
implementation detail: the first assumes the data comes from a source at run time, the
second assumes the data is already known at compile time. Which assumption is valid
depends on the problem being solved itself — if the sensor record is genuinely changing
data, a file (or another source standing in for it) is needed; if it is a fixed
configuration value, an embedded constant is enough.

```rust
// wasm2.rs — a body that never relies on the file system, independent of target
const EMBEDDED_RECORDS: &str = "12\n7\n5\n21\n";

fn total(text: &str) -> i32 {
    text.lines().filter_map(|s| s.trim().parse::<i32>().ok()).sum()
}

fn main() {
    let total_value = total(EMBEDDED_RECORDS);
    println!("embedded body total: {}", total_value);
}
```

```
embedded body total: 45
```

Both bodies gave the same result (`45`), but by different routes. For the
file-system-backed body to work, the environment had to provide a file system — it does on
this machine, but on a browser target this assumption generally does not hold, because a
browser page cannot access a file system directly. The embedded body, by contrast, depends
on no external source at all; its data is fixed inside the source itself, at compile time.
**LL13.** These two forms, producing the same result, show a target-independent task
(summing four numbers) being fed from a target-dependent source (a file) and a
target-independent source (an embedded constant).

Embedding the data into the source here was done with a `const` declaration; the value
becomes part of the binary during compilation and requires no external source at run time.
This form is not a solution limited to the browser target alone — the same approach is
used for environments where a file system does not exist at all, where only a fixed memory
layout exists. The next lesson will take up that environment directly; the constraint
there is not the browser's isolated space but the absence of the standard library itself.

## A Second Split: Threads

The file system is not the only example. The same pattern also applies to the capability
of opening multiple threads — code a browser page runs does not always have access to the
operating system's ordinary thread-creation call. Opening a thread asks the operating
system to allocate a new unit of execution and schedule it; this is a different service
from opening a file but a request from the same class — neither can be carried out from
inside the source alone, both wait for the environment to provide something. The run below
sets up the same `cfg` pattern once more: the body compiled on the local target opens a
thread and collects its result, while the body written for the wasm family is again never
compiled on this machine at all.

**LL14.** The computed value is `6 * 7`, that is `42`; the value the thread returns is
collected into the main body with a `join` call.

```rust
// wasm3.rs — a body relying on a thread, the branch selected on the local target
use std::thread;

#[cfg(not(target_family = "wasm"))]
fn produce_value() -> i32 {
    // Body that opens a thread: not compiled on targets without multithreading support.
    let worker = thread::spawn(|| 6 * 7);
    worker.join().expect("the thread cannot have crashed")
}

#[cfg(target_family = "wasm")]
fn produce_value() -> i32 {
    computed_by_a_separate_method_on_the_wasm_target()
}

fn main() {
    println!("thread-backed body: {}", produce_value());
}
```

```
thread-backed body: 42
```

In this run too, the difference sits in the same place: the `thread::spawn` call asks the
operating system to open a separate thread, and this request cannot be satisfied on every
target. The `#[cfg(not(target_family = "wasm"))]` label marks the body carrying this
dependency; a body performing the same calculation directly, without opening any thread,
could also have been written (`6 * 7` computed on a single line), and that body would work
identically on every target. The point here is the same as the previous measurement: what
depends on the target is not the calculation itself, but **how** the calculation is
carried out.

There is also a difference between these two examples (file, thread): the absence of the
file system usually shows itself with an error value — the call returns a `Result`, and if
it fails, this is understood through that value. The state of the thread-opening
capability changes by target; on some targets the function does not exist at all, and the
source, written using that function, does not compile; on other targets it may work under
a different restriction (for instance, limited to a single thread). What is common in both
cases is that this difference has to be visible somewhere in the source — either through a
`cfg` label, or through how the returned value is handled.

## The Limiting Measurement

Both runs above gave the same result on this machine — but that does not mean the
file-system-backed body will work on every target. The source itself already says which
part of it is target-dependent: the `#[cfg(not(target_family = "wasm"))]` label declares
that the body under it will be compiled on targets outside the wasm family, and the
`#[cfg(target_family = "wasm")]` label declares that the opposite body will be compiled
only on the wasm target. Reaching this information does not require running a program on
that target; the label itself is a declaration that can be read without looking at any
run.

The reverse also holds: a body carrying no `cfg` label does not automatically mean it will
work on every target. The `total` function carries no `cfg` in this lesson and is shared
by both bodies — because it only parses and sums text, it touches no external source. But
if a function carried a call that opened a thread, read the real clock, or established a
socket while carrying no `cfg`, this dependency would not be visible from the source — the
compiler does not reject such a call, it either gives a compile error when compiling for
that target, or produces an unexpected result at run time. Source looking portable does
not show that it is portable; which assumption is target-dependent can only be read from
the `cfg` labels in the source and the standard library parts used, not from a single run.

The two examples measured in this lesson (file system, thread) are not an exhaustive list,
they are two faces of the same boundary. Reading the real clock falls into a similar
class: a target may not have access to the system clock, or that access may be provided by
a different route. What the three examples share is that all of them ask a service from
the operating system or the hardware — the file system asks for a storage service, the
thread asks for a scheduling service, the clock asks for a hardware counter service. An
isolated target either does not offer some of these services at all, or offers them
through a different interface; the source stays readable to the extent that it marks which
function asks for which service, with `cfg` labels or with the module itself that is used.
All three are not run in this lesson; only the file system and thread examples were run,
the clock example is only mentioned because it belongs to the same class.

This readability is also why real multi-target source (libraries expected to compile
across several operating systems and architectures) tends to look full of `cfg` labels;
the two examples built throughout this lesson are nothing more than a small-scale model of
the patterns in that kind of source. A reliable method before porting source to a target
is to scan the source for `cfg` labels and calls that ask for an external service — that is
exactly what is done in this lesson, only across a single pair of targets (the local
target and the wasm family) and a single body.

## Summary

- A target-dependent decision can be split into two separate bodies in the source with
  `#[cfg(...)]` attributes; only one is compiled on the local target, the other sits in
  the source without ever being compiled — `cfg` does not branch at run time like an `if`,
  it decides at compile time
- No real compilation was done for the browser target; the target-dependent decision was
  **modelled** through the source
- The body relying on the file system and the body that does not gave the same sensor
  total (`45`); the difference is not in the result but in which environment assumption
  they rest on
- A browser target not offering some platform capabilities, such as a file system, shows
  that this target's guarantee comes not from the language itself but from the target —
  the class is **outside the language**
- The file system and opening a thread are two separate instances of the same boundary:
  both ask a service from the operating system, and that service is not offered the same
  way on every target
- Source looking portable does not mean it is portable; target dependency is read from the
  `cfg` labels and the standard library parts used, from the source — not from a run

## Next Step

In this lesson, the boundary sat between two targets of the same language; the difference
was which capabilities the environment offered — both the file system and threads rested
on the assumption that an operating system exists, only whether that assumption was met
changed from target to target. The next lesson carries this boundary to an extreme: an
environment where the standard library does not exist at all, where the operating system
itself is absent. There, even a target-independent form like an embedded constant will run
into the question of whether memory allocation itself exists at all — in this lesson,
"target-dependent" meant only a few functions; there, "target-dependent" is a constraint
that determines how the program's entire body is written.
