denoland / deno

A modern runtime for JavaScript and TypeScript.
https://deno.com
MIT License
98.07k stars 5.4k forks source link

WASM u32 return type misinterpreted as i32 in JavaScript #26998

Closed swwind closed 3 days ago

swwind commented 3 days ago

Version: Deno 2.1.1

I'm exploring Deno's capability to import WebAssembly (WASM) directly into JavaScript. However, I encountered an issue where a function returning a u32 in Rust is always interpreted as i32 in JavaScript.

For example, consider the following Rust code:

#[no_mangle]
pub fn add(left: u32, right: u32) -> u32 {
    left.wrapping_add(right)
}

This compiles to the following WebAssembly (shown in WAT):

(module $wasm.wasm
  (type (;0;) (func (param i32 i32) (result i32)))
  (func $add (type 0) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add)
  (table (;0;) 1 1 funcref)
  (memory (;0;) 16)
  (global $__stack_pointer (mut i32) (i32.const 1048576))
  (global (;1;) i32 (i32.const 1048576))
  (global (;2;) i32 (i32.const 1048576))
  (export "memory" (memory 0))
  (export "add" (func $add))
  (export "__data_end" (global 1))
  (export "__heap_base" (global 2)))

Although this function is meant to return a u32, it is represented as i32 in the WASM type system.

When I import and call this function in Deno, the result appears incorrect:

add(2147483647, 1); // Produces -2147483648

Some informations I found

  1. WebAssembly supports only a limited set of number types: i32, i64, f32, and f64. The distinction between signed and unsigned integers is made through specific instructions like i32.gt (signed) and i32.gt_u (unsigned). Consequently, a function returning i32 in WASM may correspond to either i32 or u32 in the original Rust source.

  2. Tools like wasm-bindgen handle unsigned integer transformations during post-processing. They wrap the result using number >>> 0 in JavaScript to correctly interpret u32 values. For example, a function returning u32 in Rust would be transformed into a JS-compatible form like this:

    function add(left, right) {
       return wasm_add(left, right) >>> 0;
    }

So, how can Deno handle this situation effectively? Since there’s nothing in the compiled WASM output explicitly indicating whether a return type is i32 or u32, how can deno ensure that the result is interpreted correctly as u32?

littledivy commented 3 days ago

u32 is not supported in Wasm and Deno cannot handle it. Only build tools like wasm-bindgen know the type from the source and can coerce it to an unsigned value.

There are two ways for a user:

add(2147483647, 1); // Produces -2147483648

Due to the reasons mentioned this is correct; it is interpreted as i32.

marvinhagemeister commented 3 days ago

For reference https://webassembly.github.io/spec/core/syntax/types.html#syntax-numtype

swwind commented 3 days ago

Thanks for the reminder. I was hoping Deno's direct import of WASM would make things as straightforward as importing another JS file. However, it seems we still can’t avoid relying on wasm-bindgen and other tools to make the types correct for JS.