knqyf263 / go-plugin

Go Plugin System over WebAssembly
MIT License
586 stars 30 forks source link

Add loading plugins from binary #52

Open brennanjl opened 1 year ago

brennanjl commented 1 year ago

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.

codefromthecrypt commented 1 year ago

sounds good to me. @dmvolod any objections?

knqyf263 commented 1 year ago

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

inliquid commented 1 year ago

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