second-state / WasmEdge-go

The GO language SDK and API for WasmEdge
https://www.secondstate.io/articles/extend-golang-app-with-webassembly-rust/
Apache License 2.0
107 stars 16 forks source link

Is aot support host function call? #35

Open Celebraty opened 1 year ago

Celebraty commented 1 year ago

What i want

just like wasmer, i declare some wasi functions in C++, and call them in C++

extern "C" {
    EMSCRIPTEN_KEEPALIVE void *get_request_data(int field);

EMSCRIPTEN_KEEPALIVE char* calculate_score_wasi(int length) {
    double *prices = (double*)get_request_data(1);
    ...

then, i defined them in golang, and register them to wasi module, it can run correctly

importObject, err = wasiEnv.GenerateImportObject(store, module)
...
funcMap["get_request_data"] = wasmer.NewFunction(store, wasmer.NewFunctionType([]*wasmer.ValueType{wasmer.NewValueType(wasmer.I32)},
                []*wasmer.ValueType{wasmer.NewValueType(wasmer.I32)}), [mydefinedfunc])
importObject.Register("env", funcMap)
...

What's the problem

when i try host function in wasmedge, my register code is

wasmedge.SetLogDebugLevel()
wasmPath = compileWasm(wasmPath) // compile my demo.wasm by aot

var (
    conf    = wasmedge.NewConfigure(wasmedge.WASI)
    err     error
    vm      = wasmedge.NewVMWithConfig(conf)
    wasiobj = vm.GetImportModule(wasmedge.WASI)
)

wasiobj.InitWasi(
    os.Args[1:],     // The args
    os.Environ(),    // The envs
    []string{".:."}, // The mapping preopens
)

importOjb := wasmedge.NewModule("env")
for name, function := range allWasiFunctionDefined {
    temp := wasmedge.NewFunction(hostFunctionType, function, nil, 20)
    importOjb.AddFunction(name, temp)
}

err = vm.RegisterModule(importOjb)
if err != nil {
    fmt.Printf("failed to import function module\n")
    return nil
}

err = vm.LoadWasmFile(wasmPath)
if err != nil {
    fmt.Printf("failed to load wasm, err=%v\n", err)
    return nil
}
err = vm.Validate()
if err != nil {
    fmt.Printf("Validate err=%v\n", err)
    return nil
}
err = vm.Instantiate()
if err != nil {
    fmt.Printf("Instantiate err=%v\n", err)
    return nil
}

then i call the calculate_score_wasi function, get the error

[error] execution failed: out of bounds memory access, Code: 0x88
[error]     When executing function name: "calculate_score_wasi"

so, is my fault or the wasmedge hasn't support ? please help, thanks a lot

q82419 commented 1 year ago

AOT mode supports the host function call.

  1. Whats is the hostFunctionType? Is the function type all correct?
  2. It may be the issue in https://github.com/WasmEdge/WasmEdge/issues/2061 and https://github.com/WasmEdge/WasmEdge/issues/2319
Celebraty commented 1 year ago

@q82419

  1. my hostfunctiontype : wasmedge.NewFunctionType([]wasmedge.ValType{wasmedge.ValType_I32}, []wasmedge.ValType{wasmedge.ValType_I32}); and i think it is correct, because i can run it correctly without aot compile
  2. i think it is not same as them because i reproduce the issue only in hostfunction
q82419 commented 1 year ago

Can you help to provide your wasm file and a simple go code to reproduce it? In this situation, it should pass or there are bugs. Thanks.

Celebraty commented 1 year ago

@q82419

cmd/wasm_edge.go

package main

import (
    "encoding/binary"
    "fmt"
    "math"
    "os"

    "github.com/second-state/WasmEdge-go/wasmedge"
)

var hostFunctionType = wasmedge.NewFunctionType([]wasmedge.ValType{wasmedge.ValType_I32}, []wasmedge.ValType{wasmedge.ValType_I32})

var allWasiFunction = map[string]func(
    data interface{}, frame *wasmedge.CallingFrame, params []interface{}) ([]interface{}, wasmedge.Result){
    "get_prerank_request_data": PrerankGetRequestValue,
    "update_prerank_response":  PrerankUpdateResponse,
}

func compileWasm(wasmPath string) string {
    // Create Configure
    conf := wasmedge.NewConfigure(wasmedge.THREADS, wasmedge.EXTENDED_CONST, wasmedge.TAIL_CALL, wasmedge.WASI)

    // Create Compiler
    compiler := wasmedge.NewCompilerWithConfig(conf)

    outputPath := wasmPath + ".so"
    // Compile WASM AOT
    err := compiler.Compile(wasmPath, outputPath)
    if err != nil {
        fmt.Println("Go: Compile WASM to AOT mode Failed!!")
    }

    conf.Release()
    compiler.Release()
    return outputPath
}

func GetVm(wasmPath string) *wasmedge.VM {
    wasmedge.SetLogErrorLevel()
    wasmPath = compileWasm(wasmPath)  // if i delete the line, everything went ok

    var (
        conf    = wasmedge.NewConfigure(wasmedge.WASI)
        err     error
        vm      = wasmedge.NewVMWithConfig(conf)
        wasiobj = vm.GetImportModule(wasmedge.WASI)
    )

    wasiobj.InitWasi(
        os.Args[1:],     // The args
        os.Environ(),    // The envs
        []string{".:."}, // The mapping preopens
    )

    module := wasmedge.NewModule("env")
    for name, function := range allWasiFunction {
        temp := wasmedge.NewFunction(hostFunctionType, function, nil, 20)
        module.AddFunction(name, temp)
    }

    err = vm.LoadWasmFile(wasmPath)
    if err != nil {
        fmt.Printf("failed to load wasm, err=%v\n", err)
        return nil
    }
    vm.RegisterModule(module)
    err = vm.Validate()
    if err != nil {
        fmt.Printf("Validate err=%v\n", err)
        return nil
    }
    err = vm.Instantiate()
    if err != nil {
        fmt.Printf("Instantiate err=%v\n", err)
        return nil
    }
    conf.Release()
    wasiobj.Release()
    return vm
}

var curPrerankRequest *PreRankColumnRequest
var curPrerankResponse *RankResponse
var curPrerankVm *wasmedge.VM

type PreRankColumnRequest struct {
    Country   string    `json:"country"`
    PpCtrs    []float64 `json:"pp_ctrs"`
    BidPrices []int64   `json:"bid_prices"`
}

type RankResponse struct {
    Score []float64 `json:"score"`
}

func PrerankUpdateResponse(data interface{}, callframe *wasmedge.CallingFrame, params []interface{}) ([]interface{}, wasmedge.Result) {
    curPrerankResponse.Score = make([]float64, len(curPrerankRequest.BidPrices))
    offset := params[0].(int32)
    mem := callframe.GetMemoryByIndex(0)
    buf, _ := mem.GetData(uint(offset), uint(8*len(curPrerankRequest.BidPrices)))

    curpos := 0
    for idx := range curPrerankResponse.Score {
        curPrerankResponse.Score[idx] = ReadFloat64(&buf, &curpos)
    }
    return []interface{}{0}, wasmedge.Result_Success
}

func PrerankGetRequestValue(data interface{}, callframe *wasmedge.CallingFrame, params []interface{}) ([]interface{}, wasmedge.Result) {
    if len(params) != 1 {
        return nil, wasmedge.Result_Fail
    }

    dataLen := uint(len(curPrerankRequest.BidPrices) * 8)

    pointerBuf, _ := callframe.GetMemoryByIndex(0).GetData(uint(params[0].(int32)), 4)
    pos := 0
    pointer, _ := curPrerankVm.Execute("allocMem", int32(dataLen))
    requestType := ReadInt32(&pointerBuf, &pos)
    pos = 0

    buf, _ := callframe.GetMemoryByIndex(0).GetData(uint(pointer[0].(int32)), dataLen)

    switch requestType {
    case 0:
        for _, val := range curPrerankRequest.PpCtrs {
            PutFloat64(&buf, val, &pos)
        }
    case 1:
        for _, val := range curPrerankRequest.BidPrices {
            PutInt64(&buf, val, &pos)
        }
    }
    pos = 0
    PutInt32(&pointerBuf, pointer[0].(int32), &pos)

    return []interface{}{0}, wasmedge.Result_Success
}

func ExecutePrerankWasi(vm *wasmedge.VM, input *PreRankColumnRequest, funcName string) *RankResponse {
    // 为 subject 分配内存,并获得其指针
    // 包括一个字节,用于我们在下面添加的 NULL 结束符
    response := &RankResponse{}
    curPrerankRequest = input
    curPrerankVm = vm
    curPrerankResponse = response

    resp, err := vm.Execute(funcName, int32(len(input.BidPrices)))

    if err != nil {
        fmt.Printf("prerank with wasi err=%v, resp=%v\n", err, resp)
        return nil
    }

    return response
}

func PutFloat64(bytes *[]byte, f float64, curPos *int) {
    bits := math.Float64bits(f)
    binary.LittleEndian.PutUint64((*bytes)[*curPos:], bits)
    *curPos += 8
}

func PutInt64(bytes *[]byte, i int64, curPos *int) {
    binary.LittleEndian.PutUint64((*bytes)[*curPos:], uint64(i))
    *curPos += 8
}

func PutInt32(bytes *[]byte, i int32, curPos *int) {
    binary.LittleEndian.PutUint32((*bytes)[*curPos:], uint32(i))
    *curPos += 4
}

func ReadInt32(bytes *[]byte, curPos *int) int32 {
    bits := binary.LittleEndian.Uint32((*bytes)[*curPos : *curPos+4])
    *curPos += 4
    return int32(bits)
}

func ReadFloat64(bytes *[]byte, curPos *int) float64 {
    bits := binary.LittleEndian.Uint64((*bytes)[*curPos : *curPos+8])
    ret := math.Float64frombits(bits)
    *curPos += 8
    return ret
}

func main() {
    vm := GetVm("output/bench.wasm")
    response := ExecutePrerankWasi(vm, &PreRankColumnRequest{}, "prerank_with_wasi")
    fmt.Printf("response=%v\n", response)
}

cpp/wasm_edge.cpp

#include "emscripten/emscripten.h"
#include <stdio.h>
#include <unistd.h>

extern "C" {
    int update_prerank_response(void *request);
    void *get_prerank_request_data(int field);
}

char* prerank_wasi(int length) {
    double *ppctrs = (double *)get_prerank_request_data(0);
    long long *bidPrices = (long long*)get_prerank_request_data(1);

    void *result = malloc(length * 8);
    double *ptr = (double *)result;
    for (int i = 0; i < length; ++i) {
        *ptr = ppctrs[i] * bidPrices[i];
        ++ptr;
    }

    update_prerank_response(result);
    free(ppctrs);
    free(bidPrices);
    free(result);
    return nullptr;
}

build wasm command

mkdir -p output
emcc -g -O3 -o output/bench.html cpp/wasm_edge.cpp -s STANDALONE_WASM -sERROR_ON_UNDEFINED_SYMBOLS=0

reproduce

by the command directly

go run cmd/wasm_edge.go

and then get the err:

[2023-03-15 10:41:10.462] [error] execution failed: out of bounds memory access, Code: 0x88
[2023-03-15 10:41:10.462] [error]     When executing function name: "prerank_with_wasi"
prerank with wasi err=out of bounds memory access, resp=[]

some others

if i delete the wasm_edge.go code:

wasmPath = compileWasm(wasmPath)

then i can get the correct response:

response=&{[]}
q82419 commented 1 year ago

Hi @Celebraty ,

As your code above, this may be the AOT issue I mentioned, not the host functions. Because the error occurred in the WASM, not in host functions.

From your code, I have additional suggestions in the GetVM function:

  1. You should keep your module object which registered into the VM, and call release() after finishing the VM. https://wasmedge.org/book/en/sdk/go/ref.html#host-module-registrations
  2. The module instances get from the vm.GetImportModule() API should not be released (although this doesn't matter, because the object didn't get the ownership of the module instance context).
Celebraty commented 1 year ago

@q82419 thanks for your help, i have tried to add a response for the GetVM function, it can return the module which registered into the VM, and the new error occur :

libc++abi: terminating with uncaught exception of type std::__1::system_error: mutex lock failed: Invalid argument
signal: abort trap

do you have any idea how to resolve the problem

Celebraty commented 1 year ago

@q82419 After more attempts, it was found that the following two errors appeared randomly

first(just as the err before):

[error] execution failed: out of bounds memory access, Code: 0x88
[error]     When executing function name: "prerank_with_wasi"

second:

libc++abi: terminating with uncaught exception of type std::__1::system_error: mutex lock failed: Invalid argument
signal: abort trap
Victor-mqz commented 7 months ago

finally,i found that this issue happens only on mac;there aren't any issue occured on linux