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

Preview feedback issue #5

Closed
nilslice opened this issue Jul 30, 2023 · 29 comments · Fixed by #7
Closed

Preview feedback issue #5

nilslice opened this issue Jul 30, 2023 · 29 comments · Fixed by #7

Comments

@nilslice
Copy link
Member

Adding a new issue here to take feedback on the SDK while it's being actively developed.

@nilslice
Copy link
Member Author

@k33g - you should be able to access this repo now, but keep in mind all work is being done on a branch: feat/go-sdk, in the PR #1. So checkout that branch if you want to use the latest!

For simplifying any experimentation, if it is difficult to use the private repo with go.mod or the Go toolchain, feel free to clone this and host it in another repo in your own account.

@nilslice
Copy link
Member Author

Thank you for any & all feedback!

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

Plugins tests

👋 Hello, I did some tests with these plugins:

I can call all these plugins with the Extism CLI:

extism call ./plugin.wasm \
  handle --input "Bob Morane" \
  --wasi

I ran some Go tests like this one:

func TestK33gRustHandler(t *testing.T) {
	manifest := manifest("k33g_rust_handler_plugin.wasm")
	if plugin, ok := plugin(t, manifest); ok {
		defer plugin.Close()

		exit, output, err := plugin.Call("handle", []byte("Bob Morane"))

		if err != nil {
			fmt.Println("😡", err.Error())
		}

		if assertCall(t, err, exit) {
			actual := string(output)
			fmt.Println("🙂", actual)
			expected := `{"success":"🦀 Hello Bob Morane","failure":"no error"}`
			assert.Equal(t, expected, actual)
		}
	}
}

Everything is ok for the Rust and JavaScript plugin. But I get a Call failed with the GoLang plugin (I will try with a simpler one)

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

🎉 ok, I found the problem (it's me again), my handle function didn't return anything:

//export handle
func handle() {

	receiver.SetHandler(func(param []byte) ([]byte, error) {
		res := `{"message":"👋 Hello `+ string(param) + `", "number":42}`

		return []byte(res), nil
	})
}

It works with this:

//export handle
func handle() int32 {

	receiver.SetHandler(func(param []byte) ([]byte, error) {
		res := `{"message":"👋 Hello `+ string(param) + `", "number":42}`

		return []byte(res), nil
	})
	return 0
}

So the conclusion is that everything works like a charm 🥰

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

Host application tests (HTTP server)

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"sync"

	"github.com/extism/extism"
	"github.com/gofiber/fiber/v2"
	"github.com/tetratelabs/wazero"
)

// store all your plugins in a normal Go hash map, protected by a Mutex
var m sync.Mutex
var plugins = make(map[string]extism.Plugin)

func StorePlugin(plugin extism.Plugin) {
	// store all your plugins in a normal Go hash map, protected by a Mutex
	plugins["code"] = plugin
}

func GetPlugin() (extism.Plugin, error) {
	if plugin, ok := plugins["code"]; ok {
		return plugin, nil
	} else {
		return extism.Plugin{}, errors.New("🔴 no plugin")
	}
}

func main() {
	wasmFilePath := os.Args[1:][0]
	wasmFunctionName := os.Args[1:][1]
	httpPort := os.Args[1:][2]

	//ctx := extism.NewContext()
	ctx := context.Background()

	config := extism.PluginConfig{
		ModuleConfig: wazero.NewModuleConfig().WithSysWalltime(),
		EnableWasi:   true,
	}
	
	//defer ctx.Free() // this will free the context and all associated plugins

	manifest := extism.Manifest{
		Wasm: []extism.Wasm{
			extism.WasmFile{
				Path: wasmFilePath},
		},
	}


	//plugin, err := ctx.PluginFromManifest(manifest, []extism.Function{}, true)
	plugin, err := extism.NewPlugin(ctx, manifest, config, nil)
	if err != nil {
		log.Println("🔴 !!! Error when loading the plugin", err)
		os.Exit(1)
	}

	StorePlugin(plugin)
	
	app := fiber.New(fiber.Config{DisableStartupMessage: true})

	app.Post("/", func(c *fiber.Ctx) error {

		params := c.Body()

		m.Lock()
		defer m.Unlock()
		
		plugin, err := GetPlugin()

		if err != nil {
			log.Println("🔴 !!! Error when getting the plugin", err)
			c.Status(http.StatusInternalServerError)
			return c.SendString(err.Error())
		}
		

		_, out, err := plugin.Call(wasmFunctionName, params)

		if err != nil {
			fmt.Println(err)
			c.Status(http.StatusConflict)
			return c.SendString(err.Error())
		} else {
			c.Status(http.StatusOK)
			return c.SendString(string(out))
		}

	})

	fmt.Println("🌍 http server is listening on:", httpPort)
	app.Listen(":" + httpPort)
}

Build the application

export TAG="v0.0.0"
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o slingshot-${TAG}-linux-arm64

Run the application

./slingshot-v0.0.0-linux-arm64 \
../go-handler-plugin/k33g_go_handler_plugin.wasm \
handle \
8080

Query the application

curl -X POST \
http://localhost:8080 \
-H 'content-type: text/plain; charset=utf-8' \
-d '😄 Bob Morane'

Result: OK 🟢

Load testing

hey -n 300 -c 100 -m POST \
-d 'John Doe' \
"http://localhost:8080"

Result: OK 🟢

🎉🍾 wow 👏 (it will be perfect for my presentation at GoLab 😀)

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

Dockerize

FROM scratch

ADD slingshot-v0.0.0-linux-arm64 ./
ADD k33g_go_handler_plugin.wasm ./

EXPOSE 8080

CMD ["./slingshot-v0.0.0-linux-arm64", "./k33g_go_handler_plugin.wasm", "handle", "8080"]

Size: 8.46MB 🚀

@mhmd-azeez
Copy link
Collaborator

Wow! thank you very much @k33g! Please let us know if you have any feedback on the API too

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

I will do more tests in the coming days

@mhmd-azeez
Copy link
Collaborator

Everything is ok for the Rust and JavaScript plugin. But I get a Call failed with the GoLang plugin (I will try with a simpler one)

I have fixed the behavior to match the Rust SDK. if the plugin doesn't return anything, we assume it has succeeded as long as there are no errors reported by Wazero

@k33g
Copy link
Collaborator

k33g commented Jul 31, 2023

Everything is ok for the Rust and JavaScript plugin. But I get a Call failed with the GoLang plugin (I will try with a simpler one)

I have fixed the behavior to match the Rust SDK. if the plugin doesn't return anything, we assume it has succeeded as long as there are no errors reported by Wazero

@mhmd-azeez I confirm it works 👍

@nilslice
Copy link
Member Author

Also adding @syke99 to the thread! She has a cool Go project and wants to take the new SDK for a spin.

Quinn, you should be able to access this repo now, but keep in mind all work is being done on a branch: feat/go-sdk, in the PR #1. So checkout that branch if you want to use the latest!

For simplifying any experimentation, if it is difficult to use the private repo with go.mod or the Go toolchain, feel free to clone this and host it in another repo in your own account.

@k33g
Copy link
Collaborator

k33g commented Aug 2, 2023

I'm trying to understand how to write a host function.

Are plugin.GetOutput() and plugin.SetInput() equivalent to plugin.InputBytes() and plugin.ReturnBytes()?
Or do I have to write my own helpers (with the Wazero api) if I want to use bytes or strings?

@mhmd-azeez
Copy link
Collaborator

@k33g this is an example of writing host functions:

func TestHost(t *testing.T) {

the plugin itself is here: https://github.com/extism/go-sdk/blob/feat/go-sdk/plugins/host/main.go

Are plugin.GetOutput() and plugin.SetInput() equivalent to plugin.InputBytes() and plugin.ReturnBytes()

Yes. But you don't need to use them directly. When you call plugin.Call, it takes the input and returns the output for you. Let me know if I can help with anything specific you are trying to do

@k33g
Copy link
Collaborator

k33g commented Aug 2, 2023

@mhmd-azeez 🤔 ok, I will try again (I wanted to exchange strings, but I should miss something)

Before (with the former Extism go SDK), into the host function's body, I could read the memory to get (for example) a string parameter by using keyStr := currentPlugin.InputString(unsafe.Pointer(&inputSlice[0]))

/*
#include <extism.h>
EXTISM_GO_FUNCTION(memory_get);
*/
import "C"

var memoryMap = map[string]string{
	"hello": "👋 Hello World 🌍",
	"message": "I 💜 Extism 😍",
}

//export memory_get
func memory_get(plugin unsafe.Pointer, inputs *C.ExtismVal, nInputs C.ExtismSize, outputs *C.ExtismVal, nOutputs C.ExtismSize, userData uintptr) {

	inputSlice := unsafe.Slice(inputs, nInputs)
	outputSlice := unsafe.Slice(outputs, nOutputs)

	// Get memory pointed to by first element of input slice
	currentPlugin := extism.GetCurrentPlugin(plugin)
	keyStr := currentPlugin.InputString(unsafe.Pointer(&inputSlice[0]))

	returnValue := memoryMap[keyStr]

	currentPlugin.ReturnString(unsafe.Pointer(&outputSlice[0]), returnValue)

	//outputSlice[0] = inputSlice[0]
}

But in the TestHost example, you are using only numbers:

a := api.DecodeI32(stack[0])
b := api.DecodeI32(stack[1])

My question is: what should I use to read a buffer from the shared memory?

With Wazero, I use this:

// printString : print a string to the console
var printString = api.GoModuleFunc(func(ctx context.Context, module api.Module, params []uint64) {

	// Extract the position and size of the returned value
	position := uint32(params[0]) 
	length := uint32(params[1])

	buffer, ok := module.Memory().Read(position, length)
	if !ok {
		log.Panicf("Memory.Read(%d, %d) out of range", position, length)
	}
	fmt.Println(string(buffer))

	params[0] = 0 // return 0
})

func DefineHostFuncPrint(builder wazero.HostModuleBuilder) {
		// hostPrintString
		builder.NewFunctionBuilder().
		WithGoModuleFunction(printString, 
			[]api.ValueType{
				api.ValueTypeI32, // string position
				api.ValueTypeI32, // string length
			}, 
			[]api.ValueType{api.ValueTypeI32}).
		Export("hostPrintString")
}

this is the code of the wasm program:

//export hostPrintString
func hostPrintString(messagePosition, messageLength uint32) uint32

// Print : call host function: hostPrintString
// Print a string
func Print(message string) {
	messagePosition, messageSize := getStringPosSize(message)

	hostPrintString(messagePosition, messageSize)
}

@mhmd-azeez
Copy link
Collaborator

@k33g yeah, there were a few functions missing in this new SDK, I just wrote them and have also added an example of exchanging strings. Will push my changes soon and get back to you

@k33g
Copy link
Collaborator

k33g commented Aug 2, 2023

@k33g yeah, there were a few functions missing in this new SDK, I just wrote them and have also added an example of exchanging strings. Will push my changes soon and get back to you

Sorry, I was in too much of a hurry (and excited) 🙇🏻

@mhmd-azeez
Copy link
Collaborator

@k33g hahaha thank you very much for taking the time to try it out. I have added the necessary functions:

Plugin:

func run_test() int32 {

Host:

func TestHost_memory(t *testing.T) {

Please note that the API might change, sorry for the inconvenience. Let me know if you faced any issues

@k33g
Copy link
Collaborator

k33g commented Aug 2, 2023

@mhmd-azeez 👏 great job!

It works great! 🎉

package main

import (
	receiver "go-handler-plugin/core"
	"github.com/extism/go-pdk"
)

//go:wasm-module env
//export hostMemoryGet
func hostMemoryGet(offset uint64) uint64

func MemoryGet(key string) string {

	// Call the host function
	// 1- copy the key to the shared memory
	memoryKey := pdk.AllocateString(key)
	// call the host function
	// memoryKey.Offset() is the position and the length of memoryKey into the memory (2 values into only one value)
	offset := hostMemoryGet(memoryKey.Offset())
	// read the value into the memory
	// offset is the position and the length of the result (2 values into only one value)
	// get the length and the position of the result in memory
	memoryResult := pdk.FindMemory(offset)
	/*
		memoryResult is a struct instance
		type Memory struct {
			offset uint64
			length uint64
		}
	*/	
	// create a buffer from memoryResult
	// fill the buffer with memoryResult
	buffResult := make([]byte, memoryResult.Length())
	memoryResult.Load(buffResult)

	return string(buffResult)

}

//export handle
func handle() {

	val1 := MemoryGet("hello")
	val2 := MemoryGet("message")
	
	receiver.SetHandler(func(param []byte) ([]byte, error) {
		res := `{"message":"👋 Hello `+ string(param) + `", "number":42, "message":"`+ val1 + " - " + val2 +`"}`
		return []byte(res), nil
	})
}

func main() {

}

I'm working on a presentation for GoLab GIVE SUPER POWERS TO YOUR GOLANG APPLICATION WITH WEBASSEMBLY AND EXTISM, and I think I'm pretty confident to be able to rewrite all my demos with the new Go SDK.

@k33g
Copy link
Collaborator

k33g commented Aug 4, 2023

👋 Hello, I re-wrote all my examples (from the main branch of the sdk) - everything works perfectly

@k33g
Copy link
Collaborator

k33g commented Aug 6, 2023

Question(problem?) about memory (I don't know if it's a "normal" behaviour)

Hello @mhmd-azeez,

I did an Extism TinyGo plugin:

package main

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

//export getName
func getName() int32 {
	name := "GoMonster"
	pdk.OutputMemory(pdk.AllocateString(name))
	return 0
}

//export getAvatar
func getAvatar() int32 {
	avatar := "🥶"
	pdk.OutputMemory(pdk.AllocateString(avatar))
	return 0
}

func main() {}

When I call the function from the host:

pluginMonster, err := extism.NewPlugin(ctx, manifest, config, hostFunctions) // new

_, monsterName, err := pluginMonster.Call("getName", nil)
_, monsterAvatar, err := pluginMonster.Call("getAvatar", nil)

fmt.Println("Monster ->", string(monsterAvatar)+" "+string(monsterName))

I get:

Monster -> 🥶 🥶

instead of: Monster -> 🥶 GoMonster

If I modify the source code like this:

pluginMonster, err := extism.NewPlugin(ctx, manifest, config, hostFunctions) // new

_, monsterName, err := pluginMonster.Call("getName", nil)
name := string(monsterName)

_, monsterAvatar, err := pluginMonster.Call("getAvatar", nil)
avatar := string(monsterAvatar)

fmt.Println("Monster ->", string(avatar)+" "+string(name))

I get:

Monster -> 🥶 GoMonster

So, I know how to avoid my problem, but I don't know if it's "normal"

@k33g
Copy link
Collaborator

k33g commented Aug 6, 2023

Problem with host function:

If I export 2 host functions, every call of a host function triggers the first host function of the slice of HostFunction

Host application
package main

import (
	"context"
	"fmt"

	"github.com/extism/extism"
	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
)

func main() {

	ctx := context.Background() // new

	path := "../100-go-plugin/simple.wasm"

	config := extism.PluginConfig{
		ModuleConfig: wazero.NewModuleConfig().WithSysWalltime(),
		EnableWasi:   true,
	}

	manifest := extism.Manifest{
		Wasm: []extism.Wasm{
			extism.WasmFile{
				Path: path},
		}}

	print_message := extism.HostFunction{
		Name:      "hostPrintMessage",
		Namespace: "env",
		Callback: func(ctx context.Context, plugin *extism.CurrentPlugin, userData interface{}, stack []uint64) {

			offset := stack[0]
			bufferInput, err := plugin.ReadBytes(offset)

			if err != nil {
				fmt.Println("🥵", err.Error())
				panic(err)
			}

			message := string(bufferInput)
			fmt.Println("🟢:", message)

			stack[0] = 0
		},
		Params:  []api.ValueType{api.ValueTypeI64},
		Results: []api.ValueType{api.ValueTypeI64},
	}

	display_message := extism.HostFunction{
		Name:      "hostDisplayMessage",
		Namespace: "env",
		Callback: func(ctx context.Context, plugin *extism.CurrentPlugin, userData interface{}, stack []uint64) {

			offset := stack[0]
			bufferInput, err := plugin.ReadBytes(offset)

			if err != nil {
				fmt.Println("🥵", err.Error())
				panic(err)
			}

			message := string(bufferInput)
			fmt.Println("🟣:", message)

			stack[0] = 0
		},
		Params:  []api.ValueType{api.ValueTypeI64},
		Results: []api.ValueType{api.ValueTypeI64},
	}

	hostFunctions := []extism.HostFunction{
		print_message,
		display_message,
	}
	//fmt.Println("📦", hostFunctions)

	pluginInst, err := extism.NewPlugin(ctx, manifest, config, hostFunctions)

	if err != nil {
		panic(err)
	}

	_, res, err := pluginInst.Call(
		"say_hello",
		[]byte("John Doe"),
	)
	fmt.Println("🙂", string(res))

	_, res, err = pluginInst.Call(
		"say_hey",
		[]byte("Jane Doe"),
	)
	fmt.Println("🙂", string(res)
}
Guest plugin
package main

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

//export hostPrintMessage
func hostPrintMessage(offset uint64) uint64

func printMessage(message string) {
	messageMemory := pdk.AllocateString(message)
	hostPrintMessage(messageMemory.Offset())
}

//export hostDisplayMessage
func hostDisplayMessage(offset uint64) uint64

func displayMessage(message string) {
	messageMemory := pdk.AllocateString(message)
	hostDisplayMessage(messageMemory.Offset())
}

//export say_hello
func say_hello() int32 {
	input := pdk.Input()
	output := "👋 Hello " + string(input)

	printMessage("from say_hello")

	mem := pdk.AllocateString(output)
	pdk.OutputMemory(mem)
	return 0

}

//export say_hey
func say_hey() int32 {
	input := pdk.Input()
	output := "🫱 Hey " + string(input)

	displayMessage("from say_hey")

	mem := pdk.AllocateString(output)
	pdk.OutputMemory(mem)
	return 0

}

func main() {}

When I run the host application, I get the following:

🟣: from say_hello
🙂 👋 Hello John Doe
🟣: from say_hey
🙂 🫱 Hey Jane Doe

instead of:

🟢: from say_hello
🙂 👋 Hello John Doe
🟣: from say_hey
🙂 🫱 Hey Jane Doe

If I reverse the order of the host functions:

hostFunctions := []extism.HostFunction{
	display_message,
	print_message,
}

I get the following:

🟢: from say_hello
🙂 👋 Hello John Doe
🟢: from say_hey
🙂 🫱 Hey Jane Doe

@mhmd-azeez
Copy link
Collaborator

@k33g

So, I know how to avoid my problem, but I don't know if it's "normal"

Thank you for reporting this, it doesn't seem normal to me. I have created #7 to fix it

@mhmd-azeez mhmd-azeez reopened this Aug 6, 2023
@k33g
Copy link
Collaborator

k33g commented Aug 6, 2023

@k33g

So, I know how to avoid my problem, but I don't know if it's "normal"

Thank you for reporting this, it doesn't seem normal to me. I have created #7 to fix it

I did the same tests, the first issue is solved 👍

@mhmd-azeez
Copy link
Collaborator

@k33g the host function bug should be fixed by #8 now

@k33g
Copy link
Collaborator

k33g commented Aug 6, 2023

@k33g the host function bug should be fixed by #8 now

@mhmd-azeez I confirm 🥰🎉

image
I can resume my other tests 😉:
image

@mhmd-azeez
Copy link
Collaborator

Great, looks like a fun game! is that for your talk?

@k33g
Copy link
Collaborator

k33g commented Aug 6, 2023

#7

It was just for fun, but perhaps I will use it for the talk (to explain the host functions)

@syke99
Copy link
Collaborator

syke99 commented Sep 16, 2023

Haven't run it yet, but finally got around to refactoring my application using the new SDK!! Super smooth, and loved it!! Also, I opened this PR to add convenience methods for setting/getting variables to/from a plugin 😎

@bhelx
Copy link
Contributor

bhelx commented Jun 14, 2024

I think we've gotten through this period. If anyone has any more issues let's move the to specific github issues. Thank you to everyone who participated getting this SDK off the ground!

@bhelx bhelx closed this as completed Jun 14, 2024
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

Successfully merging a pull request may close this issue.

5 participants