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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ and Node.js (for the wasm/e2e tests).
CI pins cargo-expand to the version in `.github/actions/setup-test-env`. Its
output shape feeds the snapshots, so regenerate them with that version.

`src/lib.rs` pulls in `README.md` with `#![doc = include_str!(...)]`, so every
` ```rust ` block in the README is compiled as a doctest. Mark illustrative
snippets `ignore`, or write them so they compile on their own.

## Expansion snapshots (`tests/expand/*.expanded.rs`)

The snapshot files record the full macro expansion of `#[derive(Tsify)]`,
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ This is the behavior due to [`typescript_custom_section`](https://rustwasm.githu

Tsify container attributes

- `into_wasm_abi` (deprecated) implements `IntoWasmAbi` and `OptionIntoWasmAbi`. This can be converted directly from Rust to JS via `serde_json` or `serde-wasm-bindgen`. Deprecated in favour of using `Ts<T>` as on function parameters and return type.
- `from_wasm_abi` (deprecated) implements `FromWasmAbi` and `OptionFromWasmAbi`. This is the opposite operation of the above. Deprecated in favour of using `Ts<T>` as on function parameters and return type.
- `into_wasm_abi` (deprecated) implements `IntoWasmAbi` and `OptionIntoWasmAbi`. This can be converted directly from Rust to JS via `serde_json` or `serde-wasm-bindgen`. Deprecated in favour of using `Ts<T>` as on function parameters and return type ([why](#why-are-the-wasm_abi-attributes-deprecated)).
- `from_wasm_abi` (deprecated) implements `FromWasmAbi` and `OptionFromWasmAbi`. This is the opposite operation of the above. Deprecated in favour of using `Ts<T>` as on function parameters and return type ([why](#why-are-the-wasm_abi-attributes-deprecated)).
- `namespace` generates a namespace for the enum variants.
- `type` overrides at the container level.
- `type_params` overrides params at the container level.
Expand Down Expand Up @@ -110,6 +110,20 @@ Serde attributes
- `default`
- `transparent`

### Why are the `wasm_abi` attributes deprecated?

`#[tsify(into_wasm_abi, from_wasm_abi)]` moves (de)serialization *into the wasm-bindgen ABI boundary*, and that boundary cannot report failure.

`wasm_bindgen::convert::FromWasmAbi::from_abi` returns `Self`, not `Result<Self, _>`, and there is no fallible variant of it or of `RefFromWasmAbi` / `LongRefFromWasmAbi` / `VectorFromWasmAbi`. So when serde fails to deserialize what JavaScript passed in, the generated impl has only one way out: `wasm_bindgen::throw_str`, which raises a JavaScript exception that unwinds straight past the wasm frames. As wasm-bindgen's own documentation warns:

> Note that it is very easy to leak memory with this function because this function, unlike `panic!` on other platforms, **will not run destructors**.

Everything alive at that moment leaks: the serde error, the partially deserialized value, and — because arguments are converted one after another — every argument already converted before the failing one. From JavaScript this looks like an ordinary, catchable exception, so an application can appear to handle bad input correctly while its wasm heap grows on every failure, until the instance dies with `RuntimeError: memory access out of bounds` (see [#65](https://github.com/madonoharu/tsify/issues/65) and [#86](https://github.com/madonoharu/tsify/issues/86)).

`Ts<T>` keeps the boundary infallible: it is a `#[repr(transparent)]` wrapper whose `FromWasmAbi` impl only forwards the underlying `JsValue`. Deserialization then happens inside your function, where it is an ordinary `Result` — the `from_js` example at the top of this page shows the shape. Because the function returns normally, destructors run and nothing leaks. The generated TypeScript is unchanged, so `.d.ts` consumers are unaffected.

`Ts<T>` needs only `#[derive(Tsify)]` — do not add `#[tsify(from_wasm_abi)]` alongside it. Note also that `Ts<Vec<T>>` is not supported, only `Vec<Ts<T>>`; to convert a whole vector, use `items.into_iter().map(|x| x.to_rust()).collect::<Result<Vec<_>, _>>()?`.

## Type Override

```rust
Expand Down
Loading