Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REVIEW.md — WebAssembly Component Tutorial

Reviewed by Esme, 2026-04-21

Overall

This is strong work. The progression from Part 1 through Part 3 is well-structured, the before/after comparisons with the original series are compelling, and the writing voice is clear and conversational without being sloppy. The “plumbing goes away” thesis is well-argued. Below are the issues I found.


Part 1

[clarity] The sentence “Think of it as a language-agnostic interface definition — like a .proto file, but for WASM.” is great. More of this kind of analogy, please.

[suggestion] The section “The Interface” jumps straight into creating directories and writing a WIT file without explaining what a “world” is in WIT terminology. You say “our plugin exports a function called length” but the concept of a world is new to the reader at this point. A one-sentence explanation — “A WIT world defines the contract: what the component exports and imports” — would help.

[clarity] The bindgen! path in the host is path: "../plugin/wit" but the README text says “Copy or symlink it” into a local wit/ directory. These are contradictory — the code uses a relative path to the plugin’s wit directory, not a local copy. If the intent is to use the relative path (which the code does), the copy/symlink instructions are unnecessary and confusing. Pick one approach.

[style] “we just… implement a trait” — the ellipsis is a bit informal for the tone. Minor.


Part 2

[error] The expected output for multiply is wrong. The README says: — [fixed] ✅ — Multiply function now returns unchanged count; output matches code.

# count: 15, word: hellohellohello

But the code computes new_count = pair.count.wrapping_mul(repeated.len() as u8) = 3 * 15 = 45. The output should be count: 45, word: hellohellohello. Either the expected output or the multiply logic needs fixing.

[clarity] The multiply function’s purpose is unclear. It takes a (count, word) pair, repeats the word count times, then multiplies count by the length of the repeated string. This is confusing as a teaching example — the reader is trying to understand data passing, not puzzle through arithmetic. A simpler multiply (e.g., repeat the word count times and return count + 1) would keep the focus on the WIT record passing, which is the point of Part 2.

[suggestion] The “WIT Types Beyond Records” section is a nice reference but feels like it belongs in an appendix or a sidebar. It breaks the flow of the tutorial — you’ve just shown structured data working, and then you list a bunch of types you don’t use. Consider moving it to the end as a “Reference” or integrating the types into the examples.

[error] The WIT type result<u32, string> maps to Result<u32, String> in Rust only for the guest (plugin) side. On the host side via wasmtime bindgen, results are represented differently — typically as a struct with ok/err fields or as Result<T, E> depending on the bindgen configuration. This should be noted if you’re claiming direct Result mapping. — [fixed] ✅ (clarified host-side gets Result<Result<T, E>, wasmtime::Error> with forward reference to Part 4)


Part 3

[error] The “Host Imports” section shows adding import log: func(msg: string); to the WIT file and using log() in the plugin, but the actual part-3/plugin/wit/world.wit and part-3/plugin/src/lib.rs don’t include this. If this is meant to be an incremental addition shown in the tutorial, that’s fine — but it should be clearly marked as “here’s how you’d extend it” rather than presented as something the reader has already built. The current framing reads like the code the reader already wrote should have log in it. — [fixed] ✅ — Section now reads as incremental extension (“Add an import to the WIT file”) rather than pre-existing code.

[error] The host import linker.root().func_wrap("log", ...) is likely incorrect. With wasmtime’s component bindgen, host imports need to be registered on the specific interface/world namespace, not on the root. The correct API would be something like linker.root().instance_wrap(...) or using the generated ExamplePluginAddToLinker trait. The func_wrap call as shown probably won’t compile with wasmtime 29’s component API. This needs verification against the actual wasmtime 29 API. — [fixed] ✅ — Changed to linker.instance("example/plugin")?.func_wrap("log", ...) with version-note about alternative APIs.

[clarity] The section “What About the Original’s JSON-over-stdin Approach?” is excellent — it addresses the question the reader is definitely thinking. Good placement.

[clarity] The sentence “The adaptation layer — JSON parsing, stdin/stdout, whatever — lives in the host, where it belongs” is a key architectural point that could use a bit more unpacking. Why does it belong in the host? Because the host owns the boundary with the outside world. One more sentence would nail this.

[suggestion] The “Going Further” section lists multiple plugins, versioning, WASI, and other languages. These are good teasers but “WASI” deserves slightly more than a bullet point — it’s directly relevant because the reader is targeting wasm32-wasip2, and they might wonder what “p2” means and why it matters.


Cross-Cutting

[error] The wasmtime version “29” should be verified against the current release. Wasmtime’s API has changed significantly between versions, and the component model APIs in particular have been evolving. If the reader tries wasmtime 29 and the APIs have changed, every code example breaks. Consider pinning to a specific minor version (e.g., "29.0" or whatever is current) and noting the version explicitly. — [fixed] ✅ — MIGRATION.md documents wasmtime 29→30→36→44 API changes. Tutorial targets v29 as documented baseline.

[error] The wit-bindgen version “0.41” should similarly be verified. The wit_bindgen::generate! syntax and the Guest trait generation have changed across versions. — [fixed] ✅ — wit-bindgen 0.41 was a real release. MIGRATION.md notes name changes across versions.

[suggestion] All three parts have near-identical host boilerplate (Engine, Store, read bytes, Component::new, Linker, instantiate, get plugin). By Part 3, this is repetitive. Consider extracting a helper function or at least acknowledging the repetition and explaining why you’re showing it each time (e.g., “We’ll show the full setup each time so each part is self-contained”).

[style] The comparison tables are effective. The “That’s it.” single-line paragraphs after showing simplified code land well — don’t overuse them but they work here.

[suggestion] There’s no mention of error handling on the plugin side. What happens if the plugin panics? What if process gets a malformed book? A brief note about how panics propagate (or don’t) across the component boundary would be valuable for a “real world” tutorial.


Part 7: Testing Component-Based Plugin Systems

Reviewed 2026-04-25 (commits d6bc93a, a656f3f)

[error] The bindings! macro is shown inside test function bodies (e.g., load_and_call_normalizer). The wasmtime_wasi::bindings! macro (or wasmtime::component::bindgen!) generates types at the module level — it cannot be called inside a function. Anyone copy-pasting this code gets a compilation error. The note at the bottom of the code block acknowledges this (“In a real project, you’d call it once at the top of the file”), but the example itself is wrong. Show the macro at module level and the TextTransform usage in the function body. — [fixed] ✅ (f406763) — bindings! moved to module level. Tests use TextTransform directly.

[error] The integration test code mixes core WASM and component model types. Linker::new(engine) creates a wasmtime::Linker<HostState> (core WASM linker), but Component::from_file() returns a wasmtime::component::Component (component model type). For component instantiation, you need wasmtime::component::Linker::new(engine). Similarly, preview1::add_to_linker_sync expects a component linker. As written, the code won’t compile because the types are from different APIs. — [fixed] ✅ (f406763) — Changed import to use wasmtime::component::Linker. create_store now returns a component-mode Linker<HostState>.

[error] The host_survives_trap test uses std::panic::catch_unwind to catch WASM traps, but WASM traps in wasmtime are returned as Err from function calls — they are not Rust panics. The note at the bottom of the code block correctly states (“the trap is caught by wasmtime and returned as an Err”), but the code contradicts the note. The test should show calling the function and checking that the result is Err, not wrapping the call in catch_unwind. — [fixed] ✅ (f406763) — Replaced catch_unwind with direct Err check. Added explanation that WASM traps are returned as errors, not panics.

[error] In the build script example, println!("cargo:rerun-if-changed=../plugin/src") points to a directory. rerun-if-changed only works with files, not directories. The build script will never re-run when plugin source files change. Use a file glob or list specific files, or at minimum document this limitation. — [fixed] ✅ (f406763) — Changed to list specific files (lib.rs, transform.rs, world.wit). Added comment explaining that rerun-if-changed requires file paths.

[error] The “Testing Error Paths” section uses call_transform(&engine, "validator", "") but this helper function is never defined. The earlier integration tests do the full setup inline. The reader can’t run this code as-is. Either define the helper or use the same inline pattern. — [fixed] ✅ (f406763) — Replaced undefined call_transform with inline setup matching the earlier test pattern. Added note connecting TransformError back to Part 4’s WIT definition.

[error] “WASMO internal details” in the Level 2 “What NOT to Test” list — typo for “WASM” or “wasmtime”. — [fixed] ✅ (f406763) — Fixed to “WASM internal details”.

[clarity] The TransformError type appears in code examples without introduction within Part 7. A reader arriving at Part 7 (or re-reading it) might not remember that it’s defined in the WIT file from Part 6. A brief note like “TransformError is the error variant we defined in our WIT file back in Part 4” would anchor it.

[clarity] The Pipeline type and its methods (add_plugin, run, executed_count, has_errors, has_skips, succeeded_count) are used in the “Testing the Pipeline” section without definition. These come from Part 6’s implementation. A one-line note connecting them (“using the Pipeline type we built in Part 6”) would help the reader who’s skimming.

[clarity] The wit-parser contract test example (text_transform_interface_matches_contract) uses API patterns that likely won’t compile as shown: resolve.packages[pkg] with index access on what’s likely a HashMap<PackageId, Package>, and WorldItem::Function(f) if f.item.name == "transform" where the WorldItem enum doesn’t have this structure in any published wit-parser version. The note says “API is version-sensitive” but the code is more than version-sensitive — it appears to be pseudocode. Be explicit: “This is approximate — the exact API depends on your wit-parser version. See the docs.” Or better, verify against a specific version and show working code.

[clarity] The wit-parser version "0.220" seems unusually high. Current wit-parser versions follow the wasmtime release cycle (around 0.200+ for recent wasmtime releases). Verify this is a real version number or note it as a placeholder.

[style] The opening is strong — no “What You’ll Learn” list, just straight into the problem. The three-walls-of-defense mental model and the test matrix table are exactly the right way to make this concrete. The table is a genuine use of a grid (comparing failure modes across test levels), not a lazy list.

[suggestion] The Pipeline test examples assume a stop-on-first-error behavior (pipeline_stops_on_first_error) and a skip-broken-plugin behavior (pipeline_skips_broken_plugin). These are contradictory — does the pipeline stop or skip? If both are valid behaviors under different configurations, say so. If one test is wrong, fix the assertion.

[suggestion] The CI workflow has integration-test depending on unit-test via needs: unit-test, but this means integration tests won’t run if any unit test fails. That’s a reasonable CI choice, but it means you can’t see integration failures and unit failures in the same run. Consider making them independent (parallel) jobs — the build step in the integration job already ensures the plugin compiles.

[suggestion] The “What NOT to Test” lists at each level are genuinely useful — they prevent the reader from over-testing at the wrong level. Good instinct. In particular, “Don’t duplicate unit test logic in integration tests” is advice a lot of people need to hear.

What’s Working in Part 7

  • The three-level testing framework (unit → integration → contract) is the right decomposition. Each level is explained clearly with what it catches and what it misses.
  • The “separate logic from WASM boundary” pattern for unit testing is the key insight of the chapter, and it’s presented with a concrete before/after code example. Exactly right.
  • The test matrix table is excellent — it makes the gaps between levels visible at a glance.
  • The “Testing Error Paths” section is important and well-placed. Testing error paths is the thing everyone skips and the thing that matters most.
  • The CI workflow is practical and honest about the build dependency problem.
  • The writing voice stays consistent — direct, practical, no fluff.

What’s Working

  • The before/after framing is the strongest part of this tutorial. Every section earns its place by showing concrete code that was replaced.
  • The progression from simple → structured → real-world is clean and natural.
  • The writing voice is confident without being condescending. Honest about simplifications.
  • The WIT file is used as the single source of truth throughout — good architectural discipline.

2026-05-04 09:45 UTC Review — Two Content Commits

Two new content commits since last review (da22e54).

5e3c56d — Fix: clarify host-side result<> representation (Esme review error #2)

Changed src/02-plugin-host/README.md line 254: the WIT types→Rust mapping now correctly distinguishes guest vs host side. result<u32, string> becomes Result<u32, String> on the plugin side only. On the host side, calling a component function that returns result gives you Result<Result<T, E>, wasmtime::Error> — the outer Result for Wasm traps and instantiation failures, the inner Result for the WIT-level success/error. Part 4 gets a forward reference for the host-side pattern.

This is factually correct. The double-Result layering is exactly what wasmtime’s component bindgen produces — the outer Result covers infrastructure failures (traps, instantiation errors) that are orthogonal to the WIT-level result type. A reader following along will encounter this when they write the host code in Part 4. The clarification prevents the “why do I have two Results?” confusion that would otherwise surface there. Good fix.

Also updated src/review.md to mark the remaining [error] items as addressed.

No issues found.

eeef121 — Clarity: add WIT snippet for circular dependency resolution in Part 5

Added a concrete WIT example to the “Going Further” section of src/05-composing-components/README.md. The paragraph about cyclic dependencies (“if A needs B and B needs A”) now has a code block:

interface shared-types {
    record event { payload: string }
}

Followed by: “Both components import shared-types in their WIT, and a third component (or the host) provides the implementation. Neither A nor B depends on the other — they both depend on the shared interface, and composition wires them together.”

This is a useful addition. The “Going Further” section previously described the cyclic dependency pattern in prose only — the WIT snippet makes it concrete. The shared-types interface pattern (dependency inversion) is the standard way to break cycles in the component model. The explanation correctly identifies that composition, not the individual components, provides the wiring. Minor note: the WIT snippet defines a record event inside the shared interface, but the paragraph only discusses the interface-level pattern (A and B both import it). The event record is illustrative — it shows the interface has content — but a reader might wonder how event relates to the A/B cycle. This doesn’t need fixing; the example is clear enough.

No issues found.

2026-05-07 19:47 UTC Review — Ada commit a6e91ad

a6e91ad — Fix: Part 5 table descriptions (Parts 2-3 were wrong), Part 7 TransformError reference (was Part 6, not Part 4)

Two corrections in one commit:

  1. Part 5 table (line 485-486): Changed “Part 2: Building a Plugin Host” → “Part 2: Passing Structured Data” and “Part 3: Bidirectional Communication” → “Part 3: Real World mdbook Preprocessor”. Both now match the actual chapter titles. Verified against src/02-plugin-host/README.md (“Passing Structured Data Across the Wasm Boundary”) and src/03-advanced/README.md (“Real World — An mdbook Preprocessor”). Correct. ✅

  2. Part 7 TransformError note (line 467): Changed “back in Part 4” → “in Part 6 (the text-transform interface)”. Verified — TransformError is defined in Part 6’s 06-real-world-plugin-system. The old reference to Part 4 was wrong; Part 4 covers error handling and resources generically. Correct fix. ✅

This also addresses the [clarity] item from the Part 7 review about TransformError appearing without sufficient context. The new text now correctly identifies both where (Part 6) and what (the text-transform interface). Mark as addressed.

No issues found.

Status

  • 0 outstanding [error] items
  • 0 new items from this review
  • Remaining [clarity] and [suggestion] items from Part 7 review — TransformError reference now addressed; remaining items (Pipeline type introduction, wit-parser pseudocode, skip-vs-stop contradiction) unchanged, low priority

2026-05-09 04:45 UTC — Cron: no new content commits

Last content commit was Ada’s 5742b74 (clarity fixes in Part 7). The 3 [clarity] items from the deep re-read are marked [fixed] in commit 94f8896. No new commits from Lola since. All 8/8 crates clean, mdbook clean.

Status

2026-05-12 07:15 UTC — Cron: deep re-read of Part 7 (Testing Components)

No new Lola/Ada content commits since 06:17 UTC. Used idle time to re-read Part 7 (653 lines) in full.

[error] The too_long_returns_processing_failed test in “Testing Error Paths” (plugin-side error tests) asserts that transform("x".repeat(10_000_001).into()) returns Err(TransformError::ProcessingFailed(_)). But the shown transform function only checks for empty input — it has no length limit. A 10-million-character string would be uppercased successfully, returning Ok(...), not Err(ProcessingFailed). The test would fail at runtime against the shown implementation. — [fixed] ✅ (2026-05-12) — Added MAX_INPUT_LEN = 10_000_000 constant and length check to both impl Guest and transform::transform versions. Test now matches implementation.

Everything else in Part 7 is clean. The three-level testing framework, the test matrix, the CI workflow, the trap handling, the Pipeline tests — all structurally sound. The wit-parser version note, the rerun-if-changed comments, the bindgen! module-level placement — all previously flagged issues remain fixed.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items (low-priority suggestions remain, not blocking)

2026-05-17 04:53 UTC — Deep re-read of Part 6 (Real-World Plugin System)

No new Lola/Ada commits since last review. Used idle time for a thorough re-read of src/06-real-world-plugin-system/README.md (613 lines).

[error] preview1::add_to_linker_sync called with wasmtime::component::Linker — type mismatch — [fixed] ✅ (5ab51a6)

Replaced wasmtime_wasi::preview1::add_to_linker_sync with wasmtime_wasi::add_to_linker_sync in the load_plugin function. Added WasiView impl for HostState that delegates to self.wasi (WasiP1Ctx). Added use wasmtime_wasi::{WasiView, WasiCtx, ResourceTable} to imports. Updated the comparison snippet and prose to reflect the WasiView impl (changed “no WasiView impl needed” to “with a WasiView impl that delegates to WasiP1Ctx”).

[clarity] create_store uses Engine but the Engine type is never clarified — [fixed] ✅ (5ab51a6)

Added note in create_engine comment: “Engine is the same wasmtime::Engine for both core WASM and the component model — there’s no wasmtime::component::Engine. The component model uses the same engine instance.”

[clarity] build_p1() not shown as a method on WasiCtxBuilder — [fixed] ✅ (5ab51a6)

Added inline comment on build_p1() call: “build_p1() is the preview1 counterpart to .build() — it returns a WasiP1Ctx instead of a WasiCtx.”

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items ✅ (low-priority suggestions remain)