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

Async host functions in ForegroundPlugin #82

Closed
Celend opened this issue Aug 3, 2024 · 4 comments
Closed

Async host functions in ForegroundPlugin #82

Celend opened this issue Aug 3, 2024 · 4 comments

Comments

@Celend
Copy link

Celend commented Aug 3, 2024

As the discord thread, I'm not sure this is suitable for other languages, so I create the issue first on js-sdk.

Use BackgroundPlugin to support async host functions had more overhead and limitations, and async result not just used for HTTP requests, maybe a simple db query and IO operations which cannot be synced Node.js.

Flowgram:

sequenceDiagram
  Wasm ->> Host: call host asyncInvoke, id 1
  Host -->> Wasm: return immediately
  Host ->> Wasm: call wasm asyncResult, id 1
Loading

The call id maybe unnecessery if we don't planning to support concurrency.

So now we can use async host fucntion in ForegroundPlugin, with no overhead and new thread, easy to control and maintain.

@G4Vi
Copy link

G4Vi commented Aug 5, 2024

I brought this up with the team and we probably aren't interested in adding this to Extism. We are waiting on and following Wasm async standardization and community efforts and we'll likely chose one of them for Extism 2.0, but do not want to lock Extism to a specific async mechanism for the time being.

https://wasmfx.dev/

https://v8.dev/blog/jspi https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md

However, if you implement async system like this just for your own usage, please share it with us!

@G4Vi G4Vi closed this as completed Aug 5, 2024
@Celend
Copy link
Author

Celend commented Aug 6, 2024

Thanks for reply! the commuity proposal is absolutely the best approach, but I can't wait for it, it's looks like unsuable whthin this year :(

I understood the team considerations, so I'm implementing in my application, basically is a chain of callbacks, then notify result in async. very similar to async.series. If not support concurrency, is not that hard to implement.

@Celend
Copy link
Author

Celend commented Aug 6, 2024

Here is the basic example:

main.go

package main

import "github.com/extism/go-pdk"

func main() {
}

//go:wasmimport async invoke
func asyncInvoke(uint64) uint64

//export run
func run() int32 {
	println("go run")

	makeAsyncCall(&AsyncCall{
		Method: "Example",
		Data:   "From GO",
		Cb: func(err error, data string) {
			println("go callback", data)

			makeAsyncCall(&AsyncCall{
				Method: "M2",
				Data:   "From GO 2",
				Cb: func(err error, data string) {
					println("go callback", data)
				},
			})

		},
	})

	return 0
}

type AsyncCall struct {
	Method string
	Data   string
	Id     uint64
	Cb     func(err error, data string)
}

var callIdCounter uint64 = 0
var currentCalls = make(map[uint64]*AsyncCall)

func makeAsyncCall(call *AsyncCall) {
	callIdCounter++
	call.Id = callIdCounter

	req, err := pdk.AllocateJSON([]any{
		call.Method,
		call.Id,
		call.Data,
	})
	if err != nil {
		panic(err)
	}
	currentCalls[call.Id] = call
	asyncInvoke(req.Offset())
}

//export asyncResult
func asyncResult() uint64 {
	input := [2]any{}
	err := pdk.InputJSON(&input)
	if err != nil {
		return 0
	}

	var id uint64 = uint64(input[0].(float64))

	println("go asyncResult", id, input[1])
	call := currentCalls[id]
	call.Cb(nil, input[1].(string))

	delete(currentCalls, call.Id)
	return 0
}

test.ts

import createPlugin, {CurrentPlugin} from "@extism/extism";

(async() => {
    const p = await createPlugin({
        wasm: [
            {path: './callback.wasm', name: 'main'}
        ],
    }, {
        useWasi: true,
        enableWasiOutput: true,
        functions: {
            async: {
                invoke: (cp: CurrentPlugin, offset: bigint) => {
                    const [method, id, param] = cp.read(offset)!.json()
                    console.log(`js called method: ${method}, callId: ${id}, param: ${param}`)
                    setTimeout(async() => {
                        await p.call('asyncResult', JSON.stringify([id, `js result, go req: ${param}`]))
                    }, 100)
                    return 0n
                }
            }
        }
    })
    const instant = await p.getInstance()
    await p.call("run")
})()

test output:

go run
js called method: Example, callId: 1, param: From GO
go asyncResult 1 js result, go req: From GO
go callback js result, go req: From GO
js called method: M2, callId: 2, param: From GO 2
go asyncResult 2 js result, go req: From GO 2
go callback js result, go req: From GO 2

todo:

  • prevent nested callback
  • wasm async export function
  • error handling

@Celend
Copy link
Author

Celend commented Aug 6, 2024

My app had ready wrap all operations into single one protobuf message, so it's very easy to port.

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

No branches or pull requests

2 participants