Giter Site home page Giter Site logo

go-plugin's Introduction

Go Plugin System over WebAssembly

go-plugin is a Go (golang) plugin system over WebAssembly (abbreviated Wasm). As a plugin is compiled to Wasm, it can be size-efficient, memory-safe, sandboxed and portable. The plugin system auto-generates Go SDK for plugins from Protocol Buffers files. While it is powered by Wasm, plugin authors/users don't have to be aware of the Wasm specification since the raw Wasm APIs are capsulated by the SDK.

It uses the same definition as gRPC, but go-plugin communicates with plugins in memory, not over RPC.

It is inspired by hashicorp/go-plugin.

Features

The Go plugin system supports a number of features:

Auto-generated Go interfaces: The plugin system generates Go code for hosts and plugins from Protocol Buffers files like gRPC. It is easy to learn how to use go-plugin for protobuf/gRPC users.

Plugins are Go interface implementations: Raw Wasm APIs are hidden so that user can write and consume plugins naturally. To a plugin author: you just implement an interface as if it were going to run in the same process. For a plugin user: you just use and call functions on an interface as if it were in the same process. This plugin system handles the communication in between.

Safe: Wasm describes a memory-safe, sandboxed execution environment. Plugins cannot access filesystem and network unless hosts allow those operations. Even 3rd-party plugins can be executed safely. Plugins can't crash the host process as it is sandboxed.

Portable: Wasm is designed as a portable compilation target for programming languages. Plugins compiled to Wasm can be used anywhere. A plugin author doesn't have to distribute multi-arch binaries.

Efficient: The Wasm stack machine is designed to be encoded in a size- and load-time-efficient binary format.

Bidirectional communication: Wasm allows embedding host functions. As Wasm restricts some capabilities such as network access for security, plugins can call host functions that explicitly embedded by a host to extend functionalities.

Stdout/Stderr Syncing: Plugins can use stdout/stderr as usual and the output will get mirrored back to the host process. The host process can control what io.Writer is attached to stdout/stderr of plugins.

Protocol Versioning: A very basic "protocol version" is supported that can be incremented to invalidate any previous plugins. This is useful when interface signatures are changing, protocol level changes are necessary, etc. When a protocol version is incompatible, a human friendly error message is shown to the end user.

Architecture

go-plugin generates Go SDK for a host and TinyGo SDK for plugins. As the Wasm support in Go is not mature, plugins need to be compiled to Wasm by TinyGo, which is an alternative compile for Go source code, at the moment. The plugin system works by loading the Wasm file and communicating over exporting/exported methods.

This architecture has a number of benefits:

  • Plugins can't crash your host process: a panic in a plugin is handled by the Wasm runtime and doesn't panic the plugin user.
  • Plugins are very easy to write: just write a Go application and tinygo build.
  • Plugins are very easy to distribute: just compile the TinyGo source code to the Wasm binary once and distribute it.
  • Plugins are very easy to install: just put the Wasm binary in a location where the host will find it.
  • Plugins can be secure: the plugin is executed in a sandbox and doesn't have access to the local filesystem and network by default.

Installation

Download a binary here and put it in $PATH.

Usage

To use the plugin system, you must take the following steps. These are high-level steps that must be done. Examples are available in the examples/ directory.

  1. Choose the interface(s) you want to expose for plugins.
  2. Generate SDK for a host and plugin by go-plugin.
  3. Implement the Go interface defined in the plugin SDK.
  4. Compile your plugin to Wasm.
  5. Load the plugin and call the defined methods.

The development flow is as below.

Tutorial

Let's create a hello-world plugin.

Prerequisite

Install the following tools:

Choose the interface you want to expose for plugins

Create greeting.proto.

syntax = "proto3";
package greeting;

option go_package = "github.com/knqyf263/go-plugin/examples/helloworld/greeting";

// The greeting service definition.
// go:plugin type=plugin version=1
service Greeter {
  // Sends a greeting
  rpc SayHello(GreetRequest) returns (GreetReply) {}
}

// The request message containing the user's name.
message GreetRequest {
  string name = 1;
}

// The reply message containing the greetings
message GreetReply {
  string message = 1;
}

Most of the definitions are simply as per the Protocol Buffers specification. The only difference is the line starting with // go:plugin. It defines parameters for go-plugin. type=plugin means the service defines the plugin interface.

Generate SDK

Run the following command.

$ protoc --go-plugin_out=. --go-plugin_opt=paths=source_relative greeting.proto

Then, you will find 4 files generated in the same directory, greet.pb.go, greet_host.pb.go, greet_plugin.pb.go and greet_vtproto.pb.go.

Implement a plugin

The Greeter interface is generated as below in the previous step.

type Greeter interface {
	SayHello(context.Context, GreetRequest) (GreetReply, error)
}

A plugin author needs to implement Greeter and registers the struct via RegisterGreeter. In this tutorial, we use plugin.go as a file name, but it doesn't matter.

//go:build tinygo.wasm

package main

import (
	"context"

	"github.com/path/to/your/greeting"
)

// main is required for TinyGo to compile to Wasm.
func main() {
	greeting.RegisterGreeter(MyPlugin{})
}

type MyPlugin struct{}

func (m MyPlugin) SayHello(ctx context.Context, request greeting.GreetRequest) (greeting.GreetReply, error) {
	return greeting.GreetReply{
		Message: "Hello, " + request.GetName(),
	}, nil
}

Then, compile it to Wasm by TinyGo.

$ tinygo build -o plugin.wasm -scheduler=none -target=wasi --no-debug plugin.go

Implement a host

Load the plugin binary and call SayHello.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/path/to/your/greeting"
)

func main() {
	ctx := context.Background()
	
	// Initialize a plugin loader
	p, err := greeting.NewGreeterPlugin(ctx)
	if err != nil {...}
	defer p.Close(ctx)

	// Load a plugin
	plugin, err := p.Load(ctx, "path/to/plugin.wasm")
	if err != nil {...}

	// Call SayHello
	reply, err := plugin.SayHello(ctx, greeting.GreetRequest{Name: "go-plugin"})
	if err != nil {...}
	
	// Display the reply
	fmt.Println(reply.GetMessage())
}

Run

$ go run main.go
Hello, go-plugin

That's it! It is easy and intuitive. You can see the hello-world example here.

References

Host functions

Wasm has limited capability as it is secure by design, but those can't be achieved with Wasm itself. To expand the capability, many compilers implement system calls using WebAssembly System Interface (WASI). But it is still draft (wasi_snapshot_preview1) and some functions are not implemented yet in wazero that go-plugin uses for Wasm runtime. For example, sock_recv and sock_send are not supported for now. It means plugins don't have network access.

Host functions can be used for this purpose. A host function is a function expressed outside WebAssembly but passed to a plugin as an import. You can define functions in your host and pass them to plugins so that plugins can call the functions. Even though Wasm itself doesn't have network access, you can embed such function to plugins.

You can define a service for host functions in a proto file. Note that // go:plugin type=host is necessary so that go-plugin recognizes the service is for host functions. The service name is HostFunctions in this example, but it doesn't matter.

// go:plugin type=host
service HostFunctions {
  // Sends a HTTP GET request
  rpc HttpGet(HttpGetRequest) returns (HttpGetResponse) {}
}

NOTE: the service for host functions must be defined in the same file where other plugin services are defined.

Let's say Greeter is defined in the same file as HostFunctions. Then, Load() will be able to take HostFunctions as an argument as mentioned later.

// go:plugin type=plugin version=1
service Greeter {
  rpc SayHello(GreetRequest) returns (GreetReply) {}
}

go-plugin generates the corresponding Go interface as below.

// go:plugin type=host
type HostFunctions interface {
    HttpGet(context.Context, HttpGetRequest) (HttpGetResponse, error)
}

Implement the interface.

// myHostFunctions implements HostFunctions
type myHostFunctions struct{}

// HttpGet is embedded into the plugin and can be called by the plugin.
func (myHostFunctions) HttpGet(ctx context.Context, request greeting.HttpGetRequest) (greeting.HttpGetResponse, error) {
    ...
}

And pass it when loading a plugin. As described above, Load() takes the HostFunctions interface.

greetingPlugin, err := p.Load(ctx, "plugin/plugin.wasm", myHostFunctions{})

Now, plugins can call HttpGet(). You can see an example here.

Define an interface version

You can define an interface version in the // go:plugin line.

// go:plugin type=plugin version=2
service Greeter {
  // Sends a greeting
  rpc Greet(GreetRequest) returns (GreetReply) {}
}

This is useful when interface signatures are changing. When an interface version is incompatible, a human friendly error message is shown to the end user like the following.

API version mismatch, host: 2, plugin: 1

Tips

File access

Refer to this example.

JSON parsing

TinyGo currently doesn't support encoding/json. https://tinygo.org/docs/reference/lang-support/stdlib/

You have to use third-party JSON libraries such as gjson and easyjson.

Also, you can export a host function. The example is available here.

Logging

fmt.Printf can be used in plugins if you attach os.Stdout as below. See the example for more details.

mc := wazero.NewModuleConfig().
    WithStdout(os.Stdout). // Attach stdout so that the plugin can write outputs to stdout
    WithStderr(os.Stderr). // Attach stderr so that the plugin can write errors to stderr
    WithFS(f)              // Loaded plugins can access only files that the host allows.
p, err := cat.NewFileCatPlugin(ctx, cat.WazeroModuleConfig(mc))

If you need structured and leveled logging, you can define host functions so that plugins can call those logging functions.

// The host functions embedded into the plugin
// go:plugin type=host
service LoggingFunctions {
  // Debug log
  rpc Debug(LogMessage) returns (google.protobuf.Empty) {}
  // Info log
  rpc Info(LogMessage) returns (google.protobuf.Empty) {}
  // Warn log
  rpc Info(LogMessage) returns (google.protobuf.Empty) {}
  // Error log
  rpc Error(LogMessage) returns (google.protobuf.Empty) {}
}

Plugin distribution

A plugin author can use OCI registries such as GitHub Container registry (GHCR) to distribute plugins.

Push:

$ oras push ghcr.io/knqyf263/my-plugin:latest plugin.wasm:application/vnd.module.wasm.content.layer.v1+wasm

Pull:

$ oras pull ghcr.io/knqyf263/my-plugin:latest

Other TinyGo tips

You can refer to https://wazero.io/languages/tinygo/.

Under the hood

go-plugin uses wazero for Wasm runtime. Also, it customizes protobuf-go and vtprotobuf for generating Go code from proto files.

Q&A

Why not hashicorp/go-plugin?

Launching a plugin as a subprocess is not secure. In addition, plugin authors need to distribute multi-arch binaries.

It is not schema-driven like Protocol Buffers and can easily break signature.

Why not using protobuf-go directly?

TinyGo doesn't support Protocol Buffers natively as of today. go-plugin generates Go code differently from protobuf-go so that TinyGo can compile it.

Why replacing known types with custom ones?

You might be aware that your generated code imports github.com/knqyf263/go-plugin/types/known, not github.com/protocolbuffers/protobuf-go/types/known when you import types from google/protobuf/xxx.proto (a.k.a well-known types) in your proto file. As described above, TinyGo cannot compile github.com/protocolbuffers/protobuf-go/types/known since those types use reflection. go-plugin provides well-known types compatible with TinyGo and use them.

Why using // go:plugin for parameters rather than protobuf extensions?

An extension must be registered in Protobuf Global Extension Registry to issue a unique extension number. Even after that, users needs to download a proto file for the extension. It is inconvenient for users and the use case in go-plugin is simple enough, so I decided to use comments.

Why not supporting Go for plugins?

Go doesn't support WASI. You can see other reasons here. We might be able to add support for Go as an experimental feature.

What about other languages?

go-plugin currently supports TinyGo plugins only, but technically, any language that can be compiled into Wasm can be supported. Welcome your contribution :)

TODO

  • Specification
    • Packages
    • Messages
      • Nested Types
    • Fields
      • Singular Message Fields
        • double
        • float
        • int32
        • int64
        • uint32
        • uint64
        • sint32
        • sint64
        • fixed32
        • fixed64
        • sfixed32
        • sfixed64
        • bool
        • string
        • bytes
      • Repeated Fields
      • Map Fields
      • Oneof Fields (not planned)
    • Enumerations
    • Extensions (not planned)
    • Services
  • Well-known types
    • Any (Some functions/methods are not yet implemented)
    • Api
    • BoolValue
    • BytesValue
    • DoubleValue
    • Duration
    • Empty
    • Enum
    • EnumValue
    • Field
    • Field_Cardinality
    • Field_Kind
    • FieldMask
    • FloatValue
    • Int32Value
    • Int64Value
    • ListValue
    • Method
    • Mixin
    • NullValue
    • Option
    • SourceContext
    • StringValue
    • Struct
    • Syntax
    • Timestamp
    • Type
    • UInt32Value
    • UInt64Value
    • Value
  • Generate codes
    • Structs without reflection
    • Marshaling/Unmarshaling
    • Host code calling plugins
    • Plugin code called by host
    • Interface version
    • Host functions

go-plugin's People

Contributors

codefromthecrypt avatar dmvolod avatar evacchi avatar knqyf263 avatar lburgazzoli avatar mathetake avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-plugin's Issues

wasm error running example

Running the host-functions example I get the following error:

2022/11/12 14:42:05 Sending a HTTP request...
2022/11/12 14:42:05 wasm error: unreachable
wasm stack trace:
	.runtime.runtimePanic(i32,i32)
	.runtime.lookupPanic()
	.malloc(i32) i32 (recovered by wazero)
wasm stack trace:
	env.log(i32,i32) i64
	.greeter_greet(i32,i32) i64
  • go version go1.18.7 darwin/arm64
  • macOS 12.6.1
  • Chip M1 Max

Plugin is not compiling with tinygo v0.29.0

I posted an issue in tinygo repo, as I believe it's related to compiler, at least when it comes to Windows. However I'm not sure about Linux.

So I just cross-post original issue here as you guys may know better the reason which causing it:

I have some code using knqyf263/go-plugin which compiles with no problem on both Windows and Linux platforms using tinygo v0.28.1. However it stopped working after upgrade to v0.29.0:

On Windows:

task: [plugin] tinygo build -o wasm/plugin.wasm -scheduler=none -target=wasi --no-debug ./wasm
# os
c:\tinygo\src\os\stat_linuxlike.go:18:6: fillFileStatFromSys redeclared in this block
c:\tinygo\src\os\stat_linux.go:14:6:    other declaration of fillFileStatFromSys
c:\tinygo\src\os\stat_linuxlike.go:50:6: timespecToTime redeclared in this block
c:\tinygo\src\os\stat_linux.go:46:6:    other declaration of timespecToTime
c:\tinygo\src\os\stat_linuxlike.go:55:6: atime redeclared in this block
c:\tinygo\src\os\stat_linux.go:51:6:    other declaration of atime
exit status 1~~

On Linux:

task: [plugin] tinygo build -o wasm/plugin.wasm -scheduler=none -target=wasi --no-debug ./wasm
tinygo:wasm-ld: warning: function signature mismatch: log
>>> defined as (i32, i32) -> i64 in lto.tmp
>>> defined as (f64) -> f64 in /usr/local/lib/tinygo/lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a(log.o)
task: [run] go build && ./test-wasm-plugin
Caught panic as *fmt.wrapError: wasm error: unreachable
wasm stack trace:
        .interface:{HttpGet:func:{named:context.Context,pointer:named:gitlab.com/***/test-wasm-plugin/api.HttpGetReq}{pointer:named:gitlab.com/***/test-wasm-plugin/api.HttpGetResp,named:error},Log:func:{named:context.Context,pointer:named:gitlab.com/***/test-wasm-plugin/api.LogReq}{pointer:named:github.com/knqyf263/go-plugin/types/known/emptypb.Empty,named:error}}.Log$invoke(i32,i32)
        .(main.MyPlugin).SendRequest(i32,i32)
        .greeter_send_request(i32,i32) i64

Upd.: problem on Windows was due to broken installation please disregard this part.

Is using values instead of pointers is by design?

I noticed that gen-go-pugin generates code which uses values, so the code looks like:

// The greeting service definition.
// go:plugin type=plugin version=1
type Greeter interface {
	SayHello(context.Context, GreetReq) (GreetResp, error)
}

While gRPC plugin generates this code from the same *.proto definition:

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type GreeterClient interface {
	SayHello(ctx context.Context, in *GreetReq, opts ...grpc.CallOption) (*GreetResp, error)
}

This also makes gopls complain about copying of sync.Mutex:
изображение

If it's by design, I would like to learn more about it.

Plugin seems to be closing. Saving plugins in a map and calling on functionality elsewhere

Hey, I might have missed something. I am loading plugins like so

        pluginMap := make(map[string]*MyPlugin)
	filepath.Walk(root, func(path string, info os.FileInfo, err error) error  {

		if !info.IsDir() && filepath.Ext(path) == EXT {
			if this, err := p.Load(ctx, path, PluginFunctions{}); err != nil {
				return err
			} else {
				pluginMap[uuid.New().String()] = &this
			}
		}
		return nil
	})
	m.plugins = pluginMap

then from "somewhere else" I am calling

func (p PluginManager) CallContentOnPlugins(ctx context.Context) error {
	for _, v := range p.plugins {
		content, err := (*v).Content(ctx, emptypb.Empty{})
		if err != nil {
			return err
		}
		fmt.Println("render content ", content.GetContent())
	}
	return nil
}

If I were to do this directly, in the first code snippet above where I am adding the plugin to the map, it works fine. However when I try to do this from this function, I get the error

error module "" closed with exit_code(0)

My guess is that I would have to keep calling .Load on the plugin however I would have thought all would be OK as I am storing the plugin in the map as a reference, but for some reason that doesnt' work? Any tips as to why?

(on another note, is there ways to expose functions in the plugin to the 'host' at runtime - i.e functions that are not known about in the proto file. Perhaps with reflection, or perhaps with a map of functions in the plugin that are exposed in the proto file?)

Thanks for your help!

How can I get the plugin id before plugin loaded

Is it possible to get plugin id before a plugin is loaded?
I have to ensure the plugin that have the same id (but maybe different version) only exists one instance in the memory.

There are two alternative solutions:

  • I can define a method, and call the method to get plugin's id, but it's not a good design.
  • I can packed the metadata as json with the wasm file into a zip, and read the metadata file before load the plugin. However, go-plugin not support load plugin from memory, so I must extract the wasm file to somewhere before I can load it

too many arguments in call to p.module.Memory().Size

Hi,
I am trying to get the helloworld example working with the evening greeter. My app structure is currently

.
├── build
├── main.go
└── plugins
    ├── greeter
    │   └── protos
    └── plugin-evening

My final app ends up in build, main.go is loading the plugin, pluginsholds all my plugins and the greeter interface, the proto file and the generated pb.go files.plugin-evening` is the example plugin.

I set option go_package = "plugins/greeter/protos"; in the protos file so that the path is the same as where the protos are and the generated go files. I have updated this path in the plugin-evening.go file - however (limited knowledge here) my IDE recommended I set the import to greeting "changeme/plugins/greeter/protos" - I'm not sure why the changeme

I have set the same import in my main.go greeting "changeme/plugins/greeter/protos"

I'm then calling

	ctx := context.Background()
	p, err := greeting.NewGreeterPlugin(ctx, greeting.GreeterPluginOption{})
	if err != nil {
		return err
	}
	defer p.Close(ctx)
	eveningPlugin, err := p.Load(ctx, "plugins/plugin-evening/evening.wasm")
	if err != nil {
		return err
	}
	reply, err := eveningPlugin.Greet(ctx, greeting.GreetRequest{
		Name: "go-plugin",
	})
	if err != nil {
		return err
	}

	fmt.Println(reply.GetMessage())

I'm getting an error:

  ERROR
          # changeme/plugins/greeter/protos
          plugins/greeter/protos/greet_host.pb.go:154:53: too many arguments in call to p.module.Memory().Write
          	have (context.Context, uint32, []byte)
          	want (uint32, []byte)
          plugins/greeter/protos/greet_host.pb.go:155:129: too many arguments in call to p.module.Memory().Size
          	have (context.Context)
          	want ()
          plugins/greeter/protos/greet_host.pb.go:169:51: too many arguments in call to p.module.Memory().Read
          	have (context.Context, uint32, uint32)
          	want (uint32, uint32)
          plugins/greeter/protos/greet_host.pb.go:172:44: too many arguments in call to p.module.Memory().Size
          	have (context.Context)
          	want ()

          exit status 2

any idea as to why that would happen?

Errors returned from plugin are never surfaced to host

When a plugin function returns an error, it seems like that error never makes it's way back to the host.

Version: f7d9444
Reproducible Test: https://github.com/cchamplin/plugin-bug

Host

	_, err = myPlugin.PassDataError(context.Background(), plugin.PassDataRequest{})
	if err == nil {
		t.Error("expected error")
		return
	}

Plugin

func (p TestPlugin) PassDataError(ctx context.Context, request host.PassDataRequest) (result host.PassDataReply, err error) {
	return host.PassDataReply{}, fmt.Errorf("an error from the plugin")
}

Expected: Test to pass
Actual:

=== RUN   TestPlugin_Error
    plugin_test.go:184: expected error
--- FAIL: TestPlugin_Error (0.10s)

FAIL

Error when loading plugin

Hey, I got the following errors when loading my plugin.

wasm error: unreachable
wasm stack trace:
	.runtime.runtimePanic(i32,i32)
	.runtime.lookupPanic()
	.malloc(i32) i32
  • MacOS: 13.0.1
  • Chip: Apple M1 Max
  • Go: go1.19.2 darwin/arm64
  • go-plugin: 0.2.0

Can I use go1.21 to compile plugins?

I just have tried to compile with go tool, since support for wasip1 added in go1.21, and got this error:

$ GOOS=wasip1 GOARCH=wasm go build -tags=tinygo.wasm -o wasm/plugin.wasm ./wasm
package gitlab.com/***/test-wasm-plugin/wasm
        imports gitlab.com/***/test-wasm-plugin/api
        imports github.com/knqyf263/go-plugin/wasm: build constraints exclude all Go files in C:\Users\***\go\pkg\mod\github.com\knqyf263\[email protected]\wasm

Note that I'm passing -tags=tinygo.wasm, because my plugin code is also protected with this build constraint. However, error message sounds like if go-plugin is not intended for use with official go compiler at all?

Host Cannot Call Plugin

I have five methods defined in the .proto file, and only one of them works when calling them in the host. I tried to find out what happened, but it seems like after calling from the host, the default result struct is returned without value.

here are the proto files and generated codes: https://github.com/wshops/wshop-plugin-common

host code

func TestPluginFunction(t *testing.T) {
	ctx := context.Background()
	p, err := pcaptcha.NewCaptchaPlugin(ctx, pcaptcha.CaptchaPluginOption{
		Stdout: os.Stdout,
		Stderr: os.Stderr,
	})
	if err != nil {
		t.Error(err)
	}

	pluginInstance, err := p.Load(ctx, "./plugins/captcha/hcaptcha.wasm")
	if err != nil {
		t.Error(err)
	}

	pi, err := pluginInstance.GetPluginInfo(ctx, emptypb.Empty{})
	if err != nil {
		t.Error(err)
	}
	fmt.Println("plugin version: ", pi.GetVersion())

	res, err := pluginInstance.VerifyCaptcha(ctx, pcaptcha.VerifyCaptchaRequest{Captcha: "123"})
	if err != nil {
		t.Error(err)
	}
	fmt.Println("verify result: ", res.GetIsValid())

	htmlInput, err := pluginInstance.GetCustomHtmlInputField(ctx, emptypb.Empty{})
	if err != nil {
		t.Error(err)
	}
	fmt.Println("input html: ", htmlInput.GetHtml())

	htmlHead, err := pluginInstance.GetCustomHtmlHead(ctx, emptypb.Empty{})
	if err != nil {
		t.Error(err)
	}
	fmt.Println("head html: ", htmlHead.GetHtml())

	htmlBody, err := pluginInstance.GetCustomHtmlBodyEnd(ctx, emptypb.Empty{})
	if err != nil {
		t.Error(err)
	}
	fmt.Println("body html: ", htmlBody.GetHtml())

	t.Cleanup(func() {
		p.Close(ctx)
	})
}

result:

=== RUN   TestPluginFunction
plugin version:  
validating captcha...
verify result:  true
input html:  
head html:  
body html:  

version
go: go1.19.2
go-plugin: v0.3.0
sys: MacOS 13.0.1
chip: Apple M1 Max

Missing proto `fieldmaskpb` package (for known types)

When some message contains field of google.protobuf.FieldMask type, protoc-gen-go-plugin generates code with this import:

fieldmaskpb "github.com/knqyf263/go-plugin/types/known/fieldmaskpb"

However this package is not present in go-plugin repo.

This makes it not possible to use google.protobuf.FieldMask.

protoc-gen-go-plugin always generate it's hardcoded version

A new latest release (v0.6.0) was downloading, but after generating files from scratch, the first hard-coded protoc-gen-go-plugin v0.1.0 version was added.

// Code generated by protoc-gen-go-plugin. DO NOT EDIT.
// versions:
// 	protoc-gen-go-plugin v0.1.0
// 	protoc               v3.21.12

Proposal: Utilize golang text template engine for generating plugin files

This is a non-featured proposal, without adding new features or fixing bugs.

The ideas is utilizing golang text template engine and have a single file with code template per functional area (plugin, host, etc.) instead of using tons of the g.P() outputs with fmt.Sprintf which is not so easy to read and maintain.

Calls to plugin hang when passing large amounts of data back and forth

When calling into plugins the behavior is not consistent depending on how much data is being passed. For small amounts of data everything seems fine. For larger amounts of data each successive call gets slower. For even larger amounts of data only one or two calls is possible before the callee hangs indefinitely. When the hang occurs it seems like we never make it into the plugin function, so it appears to be an issue with memory allocation or garbage collection within in the wasm boundary.

Version: f7d9444
Reproducible Test: https://github.com/cchamplin/plugin-bug

=== RUN   TestPlugin_MemorySmall
2022/12/09 15:14:13 Size of data being passed 1000 (139890 bytes)
2022/12/09 15:14:13 calling plugin 0
2022/12/09 15:14:13 call execution time: 61.440708ms
2022/12/09 15:14:13 calling plugin 1
2022/12/09 15:14:13 call execution time: 61.888625ms
...
2022/12/09 15:14:13 calling plugin 18
2022/12/09 15:14:14 call execution time: 252.296958ms
2022/12/09 15:14:14 calling plugin 19
2022/12/09 15:14:14 call execution time: 18.755334ms
--- PASS: TestPlugin_MemorySmall (1.17s)

=== RUN   TestPlugin_MemoryMedium
2022/12/09 15:14:14 Size of data being passed 5000 (703890 bytes)
2022/12/09 15:14:14 calling plugin 0
2022/12/09 15:14:16 call execution time: 2.158440416s
2022/12/09 15:14:16 calling plugin 1
2022/12/09 15:14:18 call execution time: 1.677055583s
2022/12/09 15:14:18 calling plugin 2
2022/12/09 15:14:20 call execution time: 2.145829459s
2022/12/09 15:14:20 calling plugin 3
2022/12/09 15:14:20 call execution time: 100.430083ms
...
022/12/09 15:15:10 call execution time: 11.31319075s
2022/12/09 15:15:10 calling plugin 18
2022/12/09 15:15:10 call execution time: 122.91525ms
2022/12/09 15:15:10 calling plugin 19
2022/12/09 15:15:10 call execution time: 132.877958ms
--- PASS: TestPlugin_MemoryMedium (56.58s)
=== RUN   TestPlugin_MemoryBig
2022/12/09 15:15:10 Size of data being passed 10000 (1408890 bytes)
2022/12/09 15:15:10 calling plugin 0
...
Never completes

exposing filesystem from plugin to host

In the example here it is outlined that the host can pass an embedded FS to the plugin and the plugin will only be able to access content within that embedded filesystem.

Is it possible to "pass" a filesystem the other way - i.e from plugin to host?

I have embedded some assets in the plugin with embed.FS and I want the host to be able to access those. Is there a way to expose the FS to the host?

Warning/Error with host functions

Hi,
I have

// The host functions embedded into the plugin
// go:plugin type=host
service HostService {
  rpc Log(LogRequest) returns (google.protobuf.Empty) {}
}
message LogRequest {
  string message = 1;
}

and on compilation with

	protoc -I pkg/plugins/interop/service --go-plugin_out=pkg/plugins/interop --go-plugin_opt=paths=source_relative pkg/plugins/interop/service/interop.proto

and generating my plugin with

	tinygo build -o pkg/plugins/examples/basic.wasm -scheduler=none -target=wasi --no-debug pkg/plugins/examples/basic.go

I then have in my plugin

func main() {
	interop.RegisterPluginService(MyPlugin{})
}

type MyPlugin struct{}

func (m MyPlugin) SendMessage(ctx context.Context, request *interop.DataMessage) (*interop.DataMessage, error) {

	hostFunctions := interop.NewHostService()
	hostFunctions.Log(ctx, &interop.LogRequest{
		Message: "Sending a log...",
	})

where SendMessage is one of the plugin functions that the host calls, I then want to call the logger on the host as you can see.

In my host I have

func (PluginFunctions) Log(ctx context.Context, request *interop.LogRequest) (*emptypb.Empty, error) {
	// Use the host logger
	log.Println("logging ", request.GetMessage())
	return &emptypb.Empty{}, nil
}

where the plugin was registered with

		if this, err := p.Load(ctx, path, PluginFunctions{}); err != nil {

now, If I remove in the plugin the `hostFunctions.Log` call everything works as expected. However when I add this back and compile the plugin I get

tinygo build -o pkg/plugins/examples/basic.wasm -scheduler=none -target=wasi --no-debug pkg/plugins/examples/basic.go
tinygo:wasm-ld: warning: function signature mismatch: log

defined as (i32, i32) -> i64 in lto.tmp
defined as (f64) -> f64 in /opt/homebrew/Cellar/tinygo/0.30.0/lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a(log.o)


its a warning, however when i run the host I get

2024/01/11 12:04:49 error starting plugin service wasm error: unreachable
wasm stack trace:
.(main.MyPlugin).SendMessage(i32)
.plugin_service_send_message(i32,i32) i64


- this is hard to debug. Could it be the version of wasm is wasm32 and for some reason the host function needs 64 or something? 
thanks

After update: panic: free: invalid pointer

I just updated my code snippets to latest go-plugin and wazero, and I start seeing this message in host stdout:

panic: free: invalid pointer

It seems it does not affect the logic, just says that something bad (?) happened?

It's with "Cat" example which outputs the contents of a host file by reading it from guest.

I suspect that's because of this change to autogenerated stubs:

	// We don't need the memory after deserialization: make sure it is freed.
	if resPtr != 0 {
		defer p.free.Call(ctx, uint64(resPtr))
	}

Allow using CompiledModule from previous call instead of loading from disk

I have a list of web assembly modules written in go that provide extensibility to my program.

I don't want to compile the plug-in each time i use it though because the program is a long running multi goroutine application where 15ms+ for plugin loading each time would be very bad.

Ideally I'd like the ability to compile module beforehand and use it later by instantiating new instances per goroutine.

Add loading plugins from binary

A really great feature would be able to load a plugin from binary, instead of from filepath. This would be done by simply adding a method similar to Load on the generated plugin (and probably refactoring Load as well). Here's what it would look like in the example given in the README:

func (p *GreeterPlugin) Load(ctx context.Context, pluginPath string) (greeter, error) {
	b, err := os.ReadFile(pluginPath)
	if err != nil {
		return nil, err
	}

	return p.LoadBinary(ctx, b)
}

func (p *GreeterPlugin) LoadBinary(ctx context.Context, pluginBinary []byte) (greeter, error) {
	// Create a new runtime so that multiple modules will not conflict
	r, err := p.newRuntime(ctx)
	if err != nil {
		return nil, err
	}

	// Compile the WebAssembly module using the default configuration.
	code, err := r.CompileModule(ctx, pluginBinary)
	if err != nil {
		return nil, err
	}

	// InstantiateModule runs the "_start" function, WASI's "main".
	module, err := r.InstantiateModule(ctx, code, p.moduleConfig)
	if err != nil {
		// Note: Most compilers do not exit the module after running "_start",
		// unless there was an Error. This allows you to call exported functions.
		if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
			return nil, fmt.Errorf("unexpected exit_code: %d", exitErr.ExitCode())
		} else if !ok {
			return nil, err
		}
	}

	// Compare API versions with the loading plugin
	apiVersion := module.ExportedFunction("greeter_api_version")
	if apiVersion == nil {
		return nil, errors.New("greeter_api_version is not exported")
	}
	results, err := apiVersion.Call(ctx)
	if err != nil {
		return nil, err
	} else if len(results) != 1 {
		return nil, errors.New("invalid greeter_api_version signature")
	}
	if results[0] != GreeterPluginAPIVersion {
		return nil, fmt.Errorf("API version mismatch, host: %d, plugin: %d", GreeterPluginAPIVersion, results[0])
	}

	sayhello := module.ExportedFunction("greeter_say_hello")
	if sayhello == nil {
		return nil, errors.New("greeter_say_hello is not exported")
	}

	malloc := module.ExportedFunction("malloc")
	if malloc == nil {
		return nil, errors.New("malloc is not exported")
	}

	free := module.ExportedFunction("free")
	if free == nil {
		return nil, errors.New("free is not exported")
	}
	return &greeterPlugin{
		runtime:  r,
		module:   module,
		malloc:   malloc,
		free:     free,
		sayhello: sayhello,
	}, nil
}

I'd be more than happy to submit a PR for this if you all would be ok with it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.