go-plugin icon indicating copy to clipboard operation
go-plugin copied to clipboard

Add loading plugins from binary

Open brennanjl opened this issue 2 years ago • 3 comments

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.

brennanjl avatar Jun 10 '23 21:06 brennanjl

sounds good to me. @dmvolod any objections?

codefromthecrypt avatar Jun 11 '23 03:06 codefromthecrypt

IMO, it is better to take io.Reader as input.

knqyf263 avatar Jun 11 '23 05:06 knqyf263

This feature is very much demanded in my project. Any chance to get it anytime soon?

inliquid avatar Sep 28 '23 18:09 inliquid