github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/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 "github.com/bananabytelabs/wazero" 21 "github.com/bananabytelabs/wazero/api" 22 "github.com/bananabytelabs/wazero/internal/gojs" 23 internalconfig "github.com/bananabytelabs/wazero/internal/gojs/config" 24 "github.com/bananabytelabs/wazero/internal/gojs/run" 25 "github.com/bananabytelabs/wazero/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 }