tetratelabs / wazero

wazero: the zero dependency WebAssembly runtime for Go developers
https://wazero.io
Apache License 2.0
4.83k stars 252 forks source link

[Question] How to handle importing memory #2156

Closed dimitropoulos closed 6 months ago

dimitropoulos commented 6 months ago

Is your feature request related to a problem? Please describe. I have a wasm module that contains the field

(memory $env.memory (import "env" "memory") 1024)

And I can't figure out how I would use wazero to provide it.

In JavaScript you can just do something like const memory = new WebAssembly.Memory({ initial: 1024 }) and provide it in the imports object.

Describe the solution you'd like An way to provide the memory to the module.

Describe alternatives you've considered I've tried every combination I can think of using NewFunctionBuilder and WithFunc returning a memory, but it doesn't seem to work.

Additional context One real-world example of a wasm file that does this can be found on https://diekmann.github.io/wasm-fizzbuzz/doom. In my case I have others (compiled with emcc) that do the same thing. I believe this mechanism is used to pre-load the memory object before the wasm boots.

dimitropoulos commented 6 months ago

I wanted to also add that as far as I can tell this is not a duplicate of https://github.com/tetratelabs/wazero/issues/1059 since that issue is about exporting memory. Or, at least, I didn't see a way to do this in any file in the linked repo.

ncruces commented 6 months ago

I do think the solution you need is the one outlined in https://github.com/tetratelabs/wazero/issues/1059#issuecomment-1502533911.

For an "env" module that only creates and exports a memory that your module can import, you can have a look at: https://github.com/wasilibs/go-re2/blob/f5c90396b5b6b752f7d1fb22563f44aaa052f292/internal/re2_wazero.go#L159-L161

This uses this module built with wat2wasm and embed with go:embed: https://github.com/wasilibs/go-re2/blob/f5c90396b5b6b752f7d1fb22563f44aaa052f292/internal/re2_wazero.go#L31-L34

dimitropoulos commented 6 months ago

ohhh ok wow. thanks! it took me a while to understand that there are (intentionally) two different modules.

For anyone in the future, here's what I used:

(the doom-fizzbuzz.wasm can be downloaded from the above link) (the memory.wasm is just a memory.wat file with (module (memory (export "memory") 102 65536 shared)) compiled with the command in the comments before the embed)

package main

import (
    "context"
    _ "embed"

    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/api"
    "github.com/tetratelabs/wazero/experimental"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func js_milliseconds_since_start() uint32 {
    return uint32(0)
}

func js_console_log(arg0 uint32, arg1 uint32) {
}

func js_draw_screen(arg0 uint32) {
}

func js_stdout(arg0 uint32, arg1 uint32) {
}

func js_stderr(arg0 uint32, arg1 uint32) {
}

//go:embed doom-fizzbuzz.wasm
var wasmBytes []byte

// wat2wasm memory.wat --output=memory.wasm --enable-threads
//
//go:embed memory.wasm
var memoryWasm []byte

func main() {
    ctx := context.Background()

    r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(api.CoreFeaturesV2|experimental.CoreFeaturesThreads))
    defer r.Close(ctx)

    wasi_snapshot_preview1.MustInstantiate(ctx, r)

    _, err := r.NewHostModuleBuilder("js").
        NewFunctionBuilder().WithFunc(js_milliseconds_since_start).Export("js_milliseconds_since_start").
        NewFunctionBuilder().WithFunc(js_console_log).Export("js_console_log").
        NewFunctionBuilder().WithFunc(js_draw_screen).Export("js_draw_screen").
        NewFunctionBuilder().WithFunc(js_stdout).Export("js_stdout").
        NewFunctionBuilder().WithFunc(js_stderr).Export("js_stderr").
        Instantiate(ctx)
    if err != nil {
        panic(err)
    }

    _, err = r.InstantiateWithConfig(ctx, memoryWasm, wazero.NewModuleConfig().WithName("env"))
    if err != nil {
        panic(err)
    }

    mod, err := r.Instantiate(ctx, wasmBytes)
    if err != nil {
        panic(err)
    }

    mainFunc := mod.ExportedFunction("main")
    arg0 := uint64(1) // placeholder
    arg1 := uint64(1) // placeholder
    _, err = mainFunc.Call(ctx, arg0, arg1)
    if err != nil {
        panic(err)
    }
}

thanks so much @ncruces!

anuraaga commented 6 months ago

Hi @dimitropoulos - for reference, I have had to switch from the static memory module approach to creating the module dynamically (being very simple, it's not too bad).

https://github.com/wasilibs/go-re2/commit/9f8aa894048abebc4f9d0fd48ce77415679ae5da

While I had initially thought virtual memory allocation has no limits, actually it can still be capped on systems with lower memory so had to make max memory dynamic.