#3491·napi-rs

wasm32-wasip1-threads: wasi-sdk 34 is incompatible with Rust < 1.100 (unresolvable env.__wasi_init_tp import)

Author: BrooooooklynCreated Sep 9, 2026Updated Sep 9, 2026

Summary

Building a napi-rs addon for wasm32-wasip1-threads with WASI_SDK_PATH pointing at wasi-sdk 34 produces a wasm module that cannot instantiate. The module carries an unresolvable import:

LinkError: WebAssembly.Instance(): Import #142 "env" "__wasi_init_tp": function import requires a callable

This is a version skew between the Rust toolchain and wasi-sdk, not a defect in napi-rs or emnapi. Nothing in @napi-rs/wasm-runtime, @emnapi/core, @emnapi/runtime, @emnapi/wasi-threads or @tybys/wasm-util can supply the symbol, because upstream deleted it rather than handing it to the embedder.

Filing it here so the constraint is written down, since the failure looks like a napi-rs loader bug.

Root cause

wasi-libc #820"Adjust how TLS initialization works" (7d831af67cc41327a826b446e90eff8fbe4fb601, merged 2026-07-15) removed __wasi_init_tp. TLS now initializes lazily inside pthread_self():

c
static inline pthread_t __pthread_self() {
  pthread_t ret = (pthread_t) __get_tp();
  if (ret->self != ret)   // not yet initialized
    __init_tp(ret);
  return ret;
}

__init_tp is hidden, so it is archive-internal. The symbol was dropped from expected/wasm32-wasip1-threads/defined-symbols.txt and never added to undefined-symbols.txt. There is no new embedder contract to implement.

wasi-sdk 34 (released 2026-08-25) is the first release carrying it.

$ llvm-nm wasi-sdk-33/share/wasi-sysroot/lib/wasm32-wasip1-threads/libc.a | grep __wasi_init_tp
00000001 T __wasi_init_tp          # libc.a(__init_tls.c.obj) — DEFINED

$ llvm-nm wasi-sdk-34/share/wasi-sysroot/lib/wasm32-wasip1-threads/libc.a | grep __wasi_init_tp
                                   # absent from the entire SDK tree

Rust still ships its own pre-34 startup objects. On stable 1.98.0:

$ llvm-nm $(rustc --print sysroot)/lib/rustlib/wasm32-wasip1-threads/lib/self-contained/crt1-reactor.o
         U __wasi_init_tp          # still calls it

$ llvm-nm $(rustc --print sysroot)/lib/rustlib/wasm32-wasip1-threads/lib/self-contained/libc.a
00000001 T __wasi_init_tp          # Rust's bundled wasi-libc still defines it

Setting WASI_SDK_PATH makes the CLI link with the wasi-sdk sysroot while cargo still contributes Rust's crt1, so the call has no definition:

Rust 1.98 crt1-reactor.o    calls __wasi_init_tp
                                 |
                                 +--> wasm-ld --> env.__wasi_init_tp   (unsatisfiable)
                                 |
wasi-sdk 34 libc.a          no longer defines it

Disassembling _initialize in the built artifact confirms the stale startup path:

sdk 33 era crt1   _initialize:  call env.__wasi_init_tp ; call __wasm_call_ctors
sdk 34     crt1   _initialize:                            call __wasm_call_ctors

Affected versions

Component Status
Rust stable 1.98.x, beta affected — crt1 built with wasi-sdk 33
Rust nightly >= 2026-08-27 fixed — rust-lang/rust#161773 "Update WASI targets to wasi-sdk-34", merged 2026-08-26
Rust 1.100 first stable with the fix (src/version on main is 1.100.0)
wasi-sdk <= 33 not affected
wasi-sdk >= 34 affected with any pre-1.100 Rust

wasi-sdk 34's release notes do not mention the change. It is folded into "Update to the latest wasi-libc".

Reproduction

Note the ordering: with @napi-rs/cli 3.9.0 you hit a different wasi-sdk 34 problem first (see below). Fix or bypass that one, and this is what remains.

bash
rustup default 1.98.0
rustup target add wasm32-wasip1-threads

wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-34/wasi-sdk-34.0-x86_64-linux.tar.gz
tar -xf wasi-sdk-34.0-x86_64-linux.tar.gz
export WASI_SDK_PATH="$(pwd)/wasi-sdk-34.0-x86_64-linux"

napi build --platform --release --target wasm32-wasip1-threads   # links fine
node -e "require('./<name>.wasi.cjs')"                            # LinkError

Confirm the import is present:

javascript
const m = new WebAssembly.Module(require('fs').readFileSync('./<name>.wasm32-wasi.wasm'))
WebAssembly.Module.imports(m).filter((i) => i.name === '__wasi_init_tp')
// [ { module: 'env', name: '__wasi_init_tp', kind: 'function' } ]

Caution when verifying a fix: NAPI_RS_FORCE_WASI=true silently falls back to the native binding when the WASI binding fails to load. Use NAPI_RS_FORCE_WASI=error, which does not fall back. I lost time to exactly this.

Workarounds

  1. Keep wasi-sdk <= 33 until Rust 1.100 is stable. Least surprising.
  2. Use Rust nightly >= 2026-08-27 with wasi-sdk 34. Matches upstream.
  3. Stub the import in the loader's overwriteImports: importObject.env.__wasi_init_tp = () => {}. This happens to be safe on wasi-sdk 34 because TLS self-initializes on the first pthread_self(), but it papers over a toolchain skew and would silently do the wrong thing on any other combination. I would not recommend shipping it. For comparison, bytecodealliance/wasm-tools#2616 hit the same shape and rebuilt the artifact rather than stubbing.

Suggestion for napi-rs

No code fix is possible here, but the failure is very hard to read. Two options worth considering:

  • When WASI_SDK_PATH reports major >= 34 and the active rustc predates the wasi-sdk 34 update, emit a warning that names this skew. The rustc side is detectable from $(rustc --print sysroot)/lib/rustlib/wasm32-wasip1-threads/lib/self-contained/crt1-reactor.o still referencing __wasi_init_tp.
  • Or document the wasi-sdk <= 33 constraint next to the WASI_SDK_PATH docs until 1.100 ships.

Happy to send a PR for whichever you prefer.

Related but separate

wasi-sdk 34 also broke the emnapi archive selection, through a different wasi-libc change (#846, which dropped the unused int op from the futex helpers):

wasm-ld: function signature mismatch: __wasilibc_futex_wait_atomic_wait
  >>> defined as (i32, i32, i32, i64) -> i32 in emnapi/lib/wasm32-wasip1-threads/libemnapi-napi-rs-mt.a(wasi_wait.c.obj)
  >>> defined as (i32, i32, i64) -> i32       in wasi-sdk-34/.../libc.a(futex.c.obj)

emnapi already ships a second archive set at emnapi/lib/wasm32-wasip1-threads-wasi-sdk-34/, but setWasiEnv() in cli/src/api/build.ts derives the directory from the target triple alone and never picks it. That one is fixable in napi-rs and I have a change ready; I will open it separately so the two issues stay untangled.

Environment

@napi-rs/cli   3.9.0
emnapi         2.0.0-alpha.5
@emnapi/core   2.0.0-alpha.5
rustc          1.98.0 (88d9e12ae 2026-08-18)
wasi-sdk       34.0  (also verified against 27, 30, 32, 33)