github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/experimental/gojs/gojs.go (about)

     1  // Package gojs allows you to run wasm binaries compiled by Go when
     2  // `GOOS=js GOARCH=wasm`. See https://wazero.io/languages/go/ for more.
     3  //
     4  // # Experimental
     5  //
     6  // Go defines js "EXPERIMENTAL... exempt from the Go compatibility promise."
     7  // Accordingly, wazero cannot guarantee this will work from release to release,
     8  // or that usage will be relatively free of bugs. Moreover, `GOOS=wasi` will
     9  // happen, and once that's available in two releases wazero will remove this
    10  // package.
    11  //
    12  // Due to these concerns and the relatively high implementation overhead, most
    13  // will choose TinyGo instead of gojs.
    14  package gojs
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  
    20  	wazero "github.com/wasilibs/wazerox"
    21  	"github.com/wasilibs/wazerox/api"
    22  	"github.com/wasilibs/wazerox/internal/gojs"
    23  	internalconfig "github.com/wasilibs/wazerox/internal/gojs/config"
    24  	"github.com/wasilibs/wazerox/internal/gojs/run"
    25  	"github.com/wasilibs/wazerox/internal/wasm"
    26  )
    27  
    28  // MustInstantiate calls Instantiate or panics on error.
    29  //
    30  // This is a simpler function for those who know host functions are not already
    31  // instantiated, and don't need to unload them separate from the runtime.
    32  func MustInstantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) {
    33  	if _, err := Instantiate(ctx, r, guest); err != nil {
    34  		panic(err)
    35  	}
    36  }
    37  
    38  // Instantiate detects and instantiates host functions for wasm compiled with
    39  // `GOOS=js GOARCH=wasm`. `guest` must be a result of `r.CompileModule`.
    40  //
    41  // # Notes
    42  //
    43  //   - Failure cases are documented on wazero.Runtime InstantiateModule.
    44  //   - Closing the wazero.Runtime has the same effect as closing the result.
    45  //   - To add more functions to `goModule`, use FunctionExporter.
    46  func Instantiate(ctx context.Context, r wazero.Runtime, guest wazero.CompiledModule) (api.Closer, error) {
    47  	goModule, err := detectGoModule(guest.ImportedFunctions())
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	builder := r.NewHostModuleBuilder(goModule)
    52  	NewFunctionExporter().ExportFunctions(builder)
    53  	return builder.Instantiate(ctx)
    54  }
    55  
    56  // detectGoModule is needed because the module name defining host functions for
    57  // `GOOS=js GOARCH=wasm` was renamed from "go" to "gojs" in Go 1.21. We can't
    58  // use the version that compiles wazero because it could be different from what
    59  // compiled the guest.
    60  //
    61  // See https://github.com/golang/go/commit/02411bcd7c8eda9c694a5755aff0a516d4983952
    62  func detectGoModule(imports []api.FunctionDefinition) (string, error) {
    63  	for _, f := range imports {
    64  		moduleName, _, _ := f.Import()
    65  		switch moduleName {
    66  		case "go", "gojs":
    67  			return moduleName, nil
    68  		}
    69  	}
    70  	return "", errors.New("guest wasn't compiled with GOOS=js GOARCH=wasm")
    71  }
    72  
    73  // FunctionExporter builds host functions for wasm compiled with
    74  // `GOOS=js GOARCH=wasm`.
    75  type FunctionExporter interface {
    76  	// ExportFunctions builds functions to an existing host module builder.
    77  	//
    78  	// This should be named "go" or "gojs", depending on the version of Go the
    79  	// guest was compiled with. The module name changed from "go" to "gojs" in
    80  	// Go 1.21.
    81  	ExportFunctions(wazero.HostModuleBuilder)
    82  }
    83  
    84  // NewFunctionExporter returns a FunctionExporter object.
    85  func NewFunctionExporter() FunctionExporter {
    86  	return &functionExporter{}
    87  }
    88  
    89  type functionExporter struct{}
    90  
    91  // ExportFunctions implements FunctionExporter.ExportFunctions
    92  func (e *functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) {
    93  	hfExporter := builder.(wasm.HostFuncExporter)
    94  
    95  	hfExporter.ExportHostFunc(gojs.GetRandomData)
    96  	hfExporter.ExportHostFunc(gojs.Nanotime1)
    97  	hfExporter.ExportHostFunc(gojs.WasmExit)
    98  	hfExporter.ExportHostFunc(gojs.CopyBytesToJS)
    99  	hfExporter.ExportHostFunc(gojs.ValueCall)
   100  	hfExporter.ExportHostFunc(gojs.ValueGet)
   101  	hfExporter.ExportHostFunc(gojs.ValueIndex)
   102  	hfExporter.ExportHostFunc(gojs.ValueLength)
   103  	hfExporter.ExportHostFunc(gojs.ValueNew)
   104  	hfExporter.ExportHostFunc(gojs.ValueSet)
   105  	hfExporter.ExportHostFunc(gojs.WasmWrite)
   106  	hfExporter.ExportHostFunc(gojs.ResetMemoryDataView)
   107  	hfExporter.ExportHostFunc(gojs.Walltime)
   108  	hfExporter.ExportHostFunc(gojs.ScheduleTimeoutEvent)
   109  	hfExporter.ExportHostFunc(gojs.ClearTimeoutEvent)
   110  	hfExporter.ExportHostFunc(gojs.FinalizeRef)
   111  	hfExporter.ExportHostFunc(gojs.StringVal)
   112  	hfExporter.ExportHostFunc(gojs.ValueDelete)
   113  	hfExporter.ExportHostFunc(gojs.ValueSetIndex)
   114  	hfExporter.ExportHostFunc(gojs.ValueInvoke)
   115  	hfExporter.ExportHostFunc(gojs.ValuePrepareString)
   116  	hfExporter.ExportHostFunc(gojs.ValueInstanceOf)
   117  	hfExporter.ExportHostFunc(gojs.ValueLoadString)
   118  	hfExporter.ExportHostFunc(gojs.CopyBytesToGo)
   119  	hfExporter.ExportHostFunc(gojs.Debug)
   120  }
   121  
   122  // Config extends wazero.ModuleConfig with GOOS=js specific extensions.
   123  // Use NewConfig to create an instance.
   124  type Config interface {
   125  	// WithOSWorkdir sets the initial working directory used to Run Wasm to
   126  	// the value of os.Getwd instead of the default of root "/".
   127  	//
   128  	// Here's an example that overrides this to the current directory:
   129  	//
   130  	//	err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig).
   131  	//			WithOSWorkdir())
   132  	//
   133  	// Note: To use this feature requires mounting the real root directory via
   134  	// wazero.FSConfig `WithDirMount`. On windows, this root must be the same drive
   135  	// as the value of os.Getwd. For example, it would be an error to mount `C:\`
   136  	// as the guest path "", while the current directory is inside `D:\`.
   137  	WithOSWorkdir() Config
   138  }
   139  
   140  // NewConfig returns a Config that can be used for configuring module instantiation.
   141  func NewConfig(moduleConfig wazero.ModuleConfig) Config {
   142  	return &cfg{moduleConfig: moduleConfig, internal: internalconfig.NewConfig()}
   143  }
   144  
   145  type cfg struct {
   146  	moduleConfig wazero.ModuleConfig
   147  	internal     *internalconfig.Config
   148  }
   149  
   150  func (c *cfg) clone() *cfg {
   151  	return &cfg{moduleConfig: c.moduleConfig, internal: c.internal.Clone()}
   152  }
   153  
   154  // WithOSWorkdir implements Config.WithOSWorkdir
   155  func (c *cfg) WithOSWorkdir() Config {
   156  	ret := c.clone()
   157  	ret.internal.OsWorkdir = true
   158  	return ret
   159  }
   160  
   161  // Run instantiates a new module and calls "run" with the given config.
   162  //
   163  // # Parameters
   164  //
   165  //   - ctx: context to use when instantiating the module and calling "run".
   166  //   - r: runtime to instantiate both the host and guest (compiled) module in.
   167  //   - compiled: guest binary compiled with `GOOS=js GOARCH=wasm`
   168  //   - config: the Config to use including wazero.ModuleConfig or extensions of
   169  //     it.
   170  //
   171  // # Example
   172  //
   173  // After compiling your Wasm binary with wazero.Runtime's `CompileModule`, run
   174  // it like below:
   175  //
   176  //	// Instantiate host functions needed by gojs
   177  //	gojs.MustInstantiate(ctx, r)
   178  //
   179  //	// Assign any configuration relevant for your compiled wasm.
   180  //	config := gojs.NewConfig(wazero.NewConfig())
   181  //
   182  //	// Run your wasm, notably handing any ExitError
   183  //	err = gojs.Run(ctx, r, compiled, config)
   184  //	if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
   185  //		log.Panicln(err)
   186  //	} else if !ok {
   187  //		log.Panicln(err)
   188  //	}
   189  //
   190  // # Notes
   191  //
   192  //   - Wasm generated by `GOOS=js GOARCH=wasm` is very slow to compile: Use
   193  //     wazero.RuntimeConfig with wazero.CompilationCache when re-running the
   194  //     same binary.
   195  //   - The guest module is closed after being run.
   196  func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, moduleConfig Config) error {
   197  	c := moduleConfig.(*cfg)
   198  	return run.Run(ctx, r, compiled, c.moduleConfig, c.internal)
   199  }