200 lines
7.2 KiB
Go
200 lines
7.2 KiB
Go
// Package gojs allows you to run wasm binaries compiled by Go when
|
|
// `GOOS=js GOARCH=wasm`. See https://wazero.io/languages/go/ for more.
|
|
//
|
|
// # Experimental
|
|
//
|
|
// Go defines js "EXPERIMENTAL... exempt from the Go compatibility promise."
|
|
// Accordingly, wazero cannot guarantee this will work from release to release,
|
|
// or that usage will be relatively free of bugs. Moreover, `GOOS=wasi` will
|
|
// happen, and once that's available in two releases wazero will remove this
|
|
// package.
|
|
//
|
|
// Due to these concerns and the relatively high implementation overhead, most
|
|
// will choose TinyGo instead of gojs.
|
|
package gojs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/tetratelabs/wazero"
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/gojs"
|
|
internalconfig "github.com/tetratelabs/wazero/internal/gojs/config"
|
|
"github.com/tetratelabs/wazero/internal/gojs/run"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
)
|
|
|
|
// MustInstantiate calls Instantiate or panics on error.
|
|
//
|
|
// This is a simpler function for those who know host functions are not already
|
|
// instantiated, and don't need to unload them separate from the runtime.
|
|
func MustInstantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) {
|
|
if _, err := Instantiate(ctx, r, guest); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Instantiate detects and instantiates host functions for wasm compiled with
|
|
// `GOOS=js GOARCH=wasm`. `guest` must be a result of `r.CompileModule`.
|
|
//
|
|
// # Notes
|
|
//
|
|
// - Failure cases are documented on wazero.Runtime InstantiateModule.
|
|
// - Closing the wazero.Runtime has the same effect as closing the result.
|
|
// - To add more functions to `goModule`, use FunctionExporter.
|
|
func Instantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) (api.Closer, error) {
|
|
goModule, err := detectGoModule(guest.ImportedFunctions())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
builder := r.NewHostModuleBuilder(goModule)
|
|
NewFunctionExporter().ExportFunctions(builder)
|
|
return builder.Instantiate(ctx)
|
|
}
|
|
|
|
// detectGoModule is needed because the module name defining host functions for
|
|
// `GOOS=js GOARCH=wasm` was renamed from "go" to "gojs" in Go 1.21. We can't
|
|
// use the version that compiles wazero because it could be different from what
|
|
// compiled the guest.
|
|
//
|
|
// See https://github.com/golang/go/commit/02411bcd7c8eda9c694a5755aff0a516d4983952
|
|
func detectGoModule(imports []api.FunctionDefinition) (string, error) {
|
|
for _, f := range imports {
|
|
moduleName, _, _ := f.Import()
|
|
switch moduleName {
|
|
case "go", "gojs":
|
|
return moduleName, nil
|
|
}
|
|
}
|
|
return "", errors.New("guest wasn't compiled with GOOS=js GOARCH=wasm")
|
|
}
|
|
|
|
// FunctionExporter builds host functions for wasm compiled with
|
|
// `GOOS=js GOARCH=wasm`.
|
|
type FunctionExporter interface {
|
|
// ExportFunctions builds functions to an existing host module builder.
|
|
//
|
|
// This should be named "go" or "gojs", depending on the version of Go the
|
|
// guest was compiled with. The module name changed from "go" to "gojs" in
|
|
// Go 1.21.
|
|
ExportFunctions(wazero.HostModuleBuilder)
|
|
}
|
|
|
|
// NewFunctionExporter returns a FunctionExporter object.
|
|
func NewFunctionExporter() FunctionExporter {
|
|
return &functionExporter{}
|
|
}
|
|
|
|
type functionExporter struct{}
|
|
|
|
// ExportFunctions implements FunctionExporter.ExportFunctions
|
|
func (e *functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
|
|
hfExporter := builder.(wasm.HostFuncExporter)
|
|
|
|
hfExporter.ExportHostFunc(gojs.GetRandomData)
|
|
hfExporter.ExportHostFunc(gojs.Nanotime1)
|
|
hfExporter.ExportHostFunc(gojs.WasmExit)
|
|
hfExporter.ExportHostFunc(gojs.CopyBytesToJS)
|
|
hfExporter.ExportHostFunc(gojs.ValueCall)
|
|
hfExporter.ExportHostFunc(gojs.ValueGet)
|
|
hfExporter.ExportHostFunc(gojs.ValueIndex)
|
|
hfExporter.ExportHostFunc(gojs.ValueLength)
|
|
hfExporter.ExportHostFunc(gojs.ValueNew)
|
|
hfExporter.ExportHostFunc(gojs.ValueSet)
|
|
hfExporter.ExportHostFunc(gojs.WasmWrite)
|
|
hfExporter.ExportHostFunc(gojs.ResetMemoryDataView)
|
|
hfExporter.ExportHostFunc(gojs.Walltime)
|
|
hfExporter.ExportHostFunc(gojs.ScheduleTimeoutEvent)
|
|
hfExporter.ExportHostFunc(gojs.ClearTimeoutEvent)
|
|
hfExporter.ExportHostFunc(gojs.FinalizeRef)
|
|
hfExporter.ExportHostFunc(gojs.StringVal)
|
|
hfExporter.ExportHostFunc(gojs.ValueDelete)
|
|
hfExporter.ExportHostFunc(gojs.ValueSetIndex)
|
|
hfExporter.ExportHostFunc(gojs.ValueInvoke)
|
|
hfExporter.ExportHostFunc(gojs.ValuePrepareString)
|
|
hfExporter.ExportHostFunc(gojs.ValueInstanceOf)
|
|
hfExporter.ExportHostFunc(gojs.ValueLoadString)
|
|
hfExporter.ExportHostFunc(gojs.CopyBytesToGo)
|
|
hfExporter.ExportHostFunc(gojs.Debug)
|
|
}
|
|
|
|
// Config extends wazero.ModuleConfig with GOOS=js specific extensions.
|
|
// Use NewConfig to create an instance.
|
|
type Config interface {
|
|
// WithOSWorkdir sets the initial working directory used to Run Wasm to
|
|
// the value of os.Getwd instead of the default of root "/".
|
|
//
|
|
// Here's an example that overrides this to the current directory:
|
|
//
|
|
// err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig).
|
|
// WithOSWorkdir())
|
|
//
|
|
// Note: To use this feature requires mounting the real root directory via
|
|
// wazero.FSConfig `WithDirMount`. On windows, this root must be the same drive
|
|
// as the value of os.Getwd. For example, it would be an error to mount `C:\`
|
|
// as the guest path "", while the current directory is inside `D:\`.
|
|
WithOSWorkdir() Config
|
|
}
|
|
|
|
// NewConfig returns a Config that can be used for configuring module instantiation.
|
|
func NewConfig(moduleConfig wazero.ModuleConfig) Config {
|
|
return &cfg{moduleConfig: moduleConfig, internal: internalconfig.NewConfig()}
|
|
}
|
|
|
|
type cfg struct {
|
|
moduleConfig wazero.ModuleConfig
|
|
internal *internalconfig.Config
|
|
}
|
|
|
|
func (c *cfg) clone() *cfg {
|
|
return &cfg{moduleConfig: c.moduleConfig, internal: c.internal.Clone()}
|
|
}
|
|
|
|
// WithOSWorkdir implements Config.WithOSWorkdir
|
|
func (c *cfg) WithOSWorkdir() Config {
|
|
ret := c.clone()
|
|
ret.internal.OsWorkdir = true
|
|
return ret
|
|
}
|
|
|
|
// Run instantiates a new module and calls "run" with the given config.
|
|
//
|
|
// # Parameters
|
|
//
|
|
// - ctx: context to use when instantiating the module and calling "run".
|
|
// - r: runtime to instantiate both the host and guest (compiled) module in.
|
|
// - compiled: guest binary compiled with `GOOS=js GOARCH=wasm`
|
|
// - config: the Config to use including wazero.ModuleConfig or extensions of
|
|
// it.
|
|
//
|
|
// # Example
|
|
//
|
|
// After compiling your Wasm binary with wazero.Runtime's `CompileModule`, run
|
|
// it like below:
|
|
//
|
|
// // Instantiate host functions needed by gojs
|
|
// gojs.MustInstantiate(ctx, r)
|
|
//
|
|
// // Assign any configuration relevant for your compiled wasm.
|
|
// config := gojs.NewConfig(wazero.NewConfig())
|
|
//
|
|
// // Run your wasm, notably handing any ExitError
|
|
// err = gojs.Run(ctx, r, compiled, config)
|
|
// if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
|
|
// log.Panicln(err)
|
|
// } else if !ok {
|
|
// log.Panicln(err)
|
|
// }
|
|
//
|
|
// # Notes
|
|
//
|
|
// - Wasm generated by `GOOS=js GOARCH=wasm` is very slow to compile: Use
|
|
// wazero.RuntimeConfig with wazero.CompilationCache when re-running the
|
|
// same binary.
|
|
// - The guest module is closed after being run.
|
|
func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, moduleConfig Config) error {
|
|
c := moduleConfig.(*cfg)
|
|
return run.Run(ctx, r, compiled, c.moduleConfig, c.internal)
|
|
}
|