Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM: strange behavior when multi-value is used #2512

Closed
jzabinski-dolios opened this issue Jan 11, 2022 · 6 comments
Closed

WASM: strange behavior when multi-value is used #2512

jzabinski-dolios opened this issue Jan 11, 2022 · 6 comments
Labels
wasm WebAssembly

Comments

@jzabinski-dolios
Copy link

jzabinski-dolios commented Jan 11, 2022

dev branch

Go supports the use of multiple results. So does WebAssembly.

If I write a simple Go function that returns multiple values:

//export multiValue
func multiValue() (int, int) {
	return 1, 2
}

Ideally that would result in the WASM equivalent of this WAT:

(func $[number] (result i32 i32)
  i32.const 1
  i32.const 2
)

Instead the following is generated:

(func $34 (param $0 i32)
  (call $18
   (local.get $0)
  )
  (call $21)
 )

(func $18 (param $0 i32)
  (i64.store
   (local.get $0)
   (i64.const 8589934593)
  )
 )

(func $20
 )

 (func $21
  (call $20)
  (call $20)
 )

Basically it seems to:

  1. Create a parameter that expects an i32
  2. Stores the value from 1 somewhere in linear memory
  3. Calls a function twice that does nothing

My guess is that TinyGo does not currently support multiple results, but the documentation implies to me that it is supported. (It says that 'all basic types and all regular control flow' is supported. I think of multiple function results as a basic type.)

I turned on debug information, and the following resulted:

(func $multiValue.command_export (param $0 i32)
  (call $multiValue
   (local.get $0)
  )
  (call $__wasm_call_dtors)
 )

(func $multiValue (param $0 i32)
  (i64.store
   (local.get $0)
   (i64.const 8589934593)
  )
 )

(func $dummy
  (nop)
 )
 (func $__wasm_call_dtors
  (call $dummy)
  (call $dummy)
 )
@dgryski dgryski added the wasm WebAssembly label Jan 11, 2022
@niaow
Copy link
Member

niaow commented Jan 11, 2022

While multi-value returns are supported by WASM, they are not supported by the WASM C ABI. They are represented as a struct. As for the parameter, what is happening there is that the WASM C ABI passes the return value indirectly.

In other words, the underlying function type is effectively func() struct{ x, y int }, which gets implemented by the WASM C ABI as func(dst *struct{ x, y int }).

As for the .command_export I am not entirely sure, but it seems to be from https://reviews.llvm.org/D81689.

References:

@jzabinski-dolios
Copy link
Author

Thank you, that supplies some important pieces to this puzzle.

Since this function is being exported to the browser, it needs to be callable by Javascript. It sounds like an important next step in research would be to understand what exactly is expected to be passed by Javascript to the function. (It would be the equivalent of 'a pointer to a struct containing places for two integers', but I'm not sure off-hand how to represent that in Javascript. It might be as simple as an Int32Array.)

That being said, the function doesn't look like it actually does the intended work of returning two integers. How is that getting lost?

@niaow
Copy link
Member

niaow commented Jan 12, 2022

It does return 2 values through the pointer, but it does so with a single store. If you take the i32 values and concatenate them ((2*(2^32))+1) you get the argument to the 64-bit store (8589934593).

@jzabinski-dolios
Copy link
Author

jzabinski-dolios commented Jan 12, 2022

Oh thank you, I missed that!

Now that we have a little more information, I was able to get this working fine. For posterity:

If you have a Go function like this:

//export multiValue
func multiValue() (int, int) {
	return 1, 2
}

And we use these settings for tinygo build:

-no-debug
-target=wasm
-gc=none
-scheduler=none
-panic=trap

TinyGo will compile that to the equivalent WASM of the following WAT function:

(func $[some number] (param $0 i32)
  (i64.store
   (local.get $0)
   (i64.const 8589934593)
  )
 )

Basically the function is looking for an address in its linear memory in which to put the function result. It is expecting the embedder to know three things:

  1. The size of the bytes to be stored by the function called
  2. The nature of the stored bytes (int, float64, etc)
  3. A safe place within linear memory to put its bytes

And if you want to use those values from the embedding Javascript, all you need to do is access the embedded linear memory. (Made accessible elsewhere in the WAT/WASM via (export "memory" (memory $0)).)

The way to call multiValue() within Go would be a,b := multiValue().

The way to call multiValue() within (NodeJS) Javascript would be:

const fs = require('fs');

const memStorageBytes = fs.readFileSync('./[some file].wasm');

WebAssembly.instantiate(new Uint8Array(memStorageBytes))
    .then(obj => obj.instance.exports)
    .then(exported => {
        // Tell multiValue to do its thing, storing results beginning at byte offset 0.
        // The safety of storing the result at this offset must be known at compile time.
        exported.multiValue(0);
        // The nature of the bytes stored by multiValue() must be known at compile time.
        // There is no way to derive this information at runtime: you have to just know.
        const results = new Uint32Array(exported.memory.buffer);
        // Since byte offset 0 was used above, we know that the first two elements of 'results' are interesting.
        const [a,b] = results;
        console.log(a, b);
    });

Result from the console should be 1 2.

Update 1/13/21: Added a few comments to clarify code

@dgryski
Copy link
Member

dgryski commented Jan 13, 2022

This seems like the sort of thing that ought to be documented somewhere obvious for future reference.

@aykevl
Copy link
Member

aykevl commented Jan 21, 2022

The way that Go code looks and the way it is represented in WebAssembly are two very different things. They happen to match in many cases, but in many other cases they don't. For example, Go (and TinyGo) support int64, but older browsers do not support i64 in the WebAssembly<->JS boundary. Therefore it is emitted as a pointer to an object. Another example: Go has int8, but WebAssembly hasn't. So it is extended to i32.

We could document this. If we do that, this would be the most appropriate place: https://tinygo.org/docs/concepts/compiler-internals/calling-convention/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wasm WebAssembly
Projects
None yet
Development

No branches or pull requests

4 participants