tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
14.81k stars 872 forks source link

Is there a way to unite exported functions under the same object path? #4204

Closed tkhametov closed 4 months ago

tkhametov commented 4 months ago

Hello!

Sorry, I am quite a newbie here. I am trying to gather some hands-on experience in WASI/WASM with Golang. As a test task, I would like to execute multiple methods exported from different WASI modules written on Go in my JavaScript code. JS will be simply used as a platform to run all my WASM/WASI code in the browser, hence it shouldn't hard code names of exported functions.

I have written two simple projects: guest/main.go exports a function that is supposed to be executed by the host code.

package main

//go:wasm-module env
//export env.addNumbers
func addNumbers(x, y uint32) uint32 {
    return x + y
}

// main is required for the `wasi` target, even if it isn't used.
func main() {}

host/main.go is meant to import a function from guest/main.go and execute the code.

package main

import "fmt"

//go:wasmimport env addNumbers
func addNumbers(x, y uint32) uint32

func main() {
    fmt.Printf("The sum of 5 and 7 is %d\n", addNumbers(5, 7))
    c := make(chan struct{})
    <-c
}

And here's my JS code that joins these two projects together.

(async function () {
   try {
      const go = new Go();
      const guestMod = await WebAssembly.instantiateStreaming(fetch("bin/guest.wasm"), go.importObject)
      go.importObject.env.addNumbers = guestMod.instance.exports["env.addNumbers"]

      const hostMod = await WebAssembly.instantiateStreaming(fetch("bin/main.wasm"), go.importObject)
      go.run(hostMod.instance)

   } catch (e) {
      console.error(e)
   }
})();

Everything works as expected, except for the following line:

go.importObject.env.addNumbers = guestMod.instance.exports["env.addNumbers"]

As I mentioned, the JS-clue shouldn't know about exported methods and it should just copy all methods from a particular export section, e.g.:

// Copy all exported methods in "env" object 
go.importObject.env = { ...guestMod.instance.exports.env }

I know that it's exported as "env.addNumbers" because I did specify it like that: //export env.addNumbers

(func $env.addNumbers (;30;) (export "env.addNumbers") (param $var0 i32) (param $var1 i32) (result i32)
   local.get $var0
   local.get $var1
   i32.add
)

I have briefly gone through this issue also https://github.com/tinygo-org/tinygo/issues/3839 and understand that //go:wasm-module in the scope of export functions means nothing. But is there another solution to export my function "addNumbers" under the specified path? Let's say if I specify

//go:wasm-module math
//export addNumbers

and

//go:wasm-module math
//export multiplyNumbers

they would go under instance.exports.math section?

P.S. One of the possible solutions would be to clone everything from export, but since tynigo exports some internal methods, I cannot just blindly copy them https://wazero.io/languages/tinygo/#why-does-my-wasm-import-wasi-functions-even-when-i-dont-use-it

aykevl commented 4 months ago

I think you're misunderstanding how WebAssembly modules work. You can import functions given a module + function name, such as (env, addNumbers). You can export a function with a particular name, such as addNumbers. You can also export it with extra characters in there, so that the function name becomes env_addNumbers, env::addNumbers, env.addNumbers, or anything you like. There's nothing special about that dot, it's just a function name.

Specifically here:

As I mentioned, the JS-clue shouldn't know about exported methods and it should just copy all methods from a particular export section, e.g.:

// Copy all exported methods in "env" object 
go.importObject.env = { ...guestMod.instance.exports.env }

It doesn't work like that. There are no named export sections, just one (unnamed) section with exported functions. You can export addNumbers (or env.addNumbers or whatever), but it's a plain old function name. Perhaps you can try using Developer Tools and inspect the Module object manually? That may give a better idea of how it works.

tkhametov commented 4 months ago

Thanks for the explanation, @aykevl! I appreciate your help! Now I understand your point. I realized that there's a new target wasm-unknown in new releases that drastically reduces the emitted functions and generates a pure module without any dependencies. It looks like I can leverage that to implement what I need. Thanks