github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/runtime.go (about) 1 package wazero 2 3 import ( 4 "context" 5 "fmt" 6 "sync/atomic" 7 8 "github.com/wasilibs/wazerox/api" 9 experimentalapi "github.com/wasilibs/wazerox/experimental" 10 internalclose "github.com/wasilibs/wazerox/internal/close" 11 internalsock "github.com/wasilibs/wazerox/internal/sock" 12 internalsys "github.com/wasilibs/wazerox/internal/sys" 13 "github.com/wasilibs/wazerox/internal/wasm" 14 binaryformat "github.com/wasilibs/wazerox/internal/wasm/binary" 15 "github.com/wasilibs/wazerox/sys" 16 ) 17 18 // Runtime allows embedding of WebAssembly modules. 19 // 20 // The below is an example of basic initialization: 21 // 22 // ctx := context.Background() 23 // r := wazero.NewRuntime(ctx) 24 // defer r.Close(ctx) // This closes everything this Runtime created. 25 // 26 // mod, _ := r.Instantiate(ctx, wasm) 27 // 28 // # Notes 29 // 30 // - This is an interface for decoupling, not third-party implementations. 31 // All implementations are in wazero. 32 // - Closing this closes any CompiledModule or Module it instantiated. 33 type Runtime interface { 34 // Instantiate instantiates a module from the WebAssembly binary (%.wasm) 35 // with default configuration, which notably calls the "_start" function, 36 // if it exists. 37 // 38 // Here's an example: 39 // ctx := context.Background() 40 // r := wazero.NewRuntime(ctx) 41 // defer r.Close(ctx) // This closes everything this Runtime created. 42 // 43 // mod, _ := r.Instantiate(ctx, wasm) 44 // 45 // # Notes 46 // 47 // - See notes on InstantiateModule for error scenarios. 48 // - See InstantiateWithConfig for configuration overrides. 49 Instantiate(ctx context.Context, source []byte) (api.Module, error) 50 51 // InstantiateWithConfig instantiates a module from the WebAssembly binary 52 // (%.wasm) or errs for reasons including exit or validation. 53 // 54 // Here's an example: 55 // ctx := context.Background() 56 // r := wazero.NewRuntime(ctx) 57 // defer r.Close(ctx) // This closes everything this Runtime created. 58 // 59 // mod, _ := r.InstantiateWithConfig(ctx, wasm, 60 // wazero.NewModuleConfig().WithName("rotate")) 61 // 62 // # Notes 63 // 64 // - See notes on InstantiateModule for error scenarios. 65 // - If you aren't overriding defaults, use Instantiate. 66 // - This is a convenience utility that chains CompileModule with 67 // InstantiateModule. To instantiate the same source multiple times, 68 // use CompileModule as InstantiateModule avoids redundant decoding 69 // and/or compilation. 70 InstantiateWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) 71 72 // NewHostModuleBuilder lets you create modules out of functions defined in Go. 73 // 74 // Below defines and instantiates a module named "env" with one function: 75 // 76 // ctx := context.Background() 77 // hello := func() { 78 // fmt.Fprintln(stdout, "hello!") 79 // } 80 // _, err := r.NewHostModuleBuilder("env"). 81 // NewFunctionBuilder().WithFunc(hello).Export("hello"). 82 // Instantiate(ctx, r) 83 // 84 // Note: empty `moduleName` is not allowed. 85 NewHostModuleBuilder(moduleName string) HostModuleBuilder 86 87 // CompileModule decodes the WebAssembly binary (%.wasm) or errs if invalid. 88 // Any pre-compilation done after decoding wasm is dependent on RuntimeConfig. 89 // 90 // There are two main reasons to use CompileModule instead of Instantiate: 91 // - Improve performance when the same module is instantiated multiple times under different names 92 // - Reduce the amount of errors that can occur during InstantiateModule. 93 // 94 // # Notes 95 // 96 // - The resulting module name defaults to what was binary from the custom name section. 97 // - Any pre-compilation done after decoding the source is dependent on RuntimeConfig. 98 // 99 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 100 CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) 101 102 // InstantiateModule instantiates the module or errs for reasons including 103 // exit or validation. 104 // 105 // Here's an example: 106 // mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig(). 107 // WithName("prod")) 108 // 109 // # Errors 110 // 111 // While CompiledModule is pre-validated, there are a few situations which 112 // can cause an error: 113 // - The module name is already in use. 114 // - The module has a table element initializer that resolves to an index 115 // outside the Table minimum size. 116 // - The module has a start function, and it failed to execute. 117 // - The module was compiled to WASI and exited with a non-zero exit 118 // code, you'll receive a sys.ExitError. 119 // - RuntimeConfig.WithCloseOnContextDone was enabled and a context 120 // cancellation or deadline triggered before a start function returned. 121 InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error) 122 123 // CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code. 124 // An error is returned if any module returns an error when closed. 125 // 126 // Here's an example: 127 // ctx := context.Background() 128 // r := wazero.NewRuntime(ctx) 129 // defer r.CloseWithExitCode(ctx, 2) // This closes everything this Runtime created. 130 // 131 // // Everything below here can be closed, but will anyway due to above. 132 // _, _ = wasi_snapshot_preview1.InstantiateSnapshotPreview1(ctx, r) 133 // mod, _ := r.Instantiate(ctx, wasm) 134 CloseWithExitCode(ctx context.Context, exitCode uint32) error 135 136 // Module returns an instantiated module in this runtime or nil if there aren't any. 137 Module(moduleName string) api.Module 138 139 // Closer closes all compiled code by delegating to CloseWithExitCode with an exit code of zero. 140 api.Closer 141 } 142 143 // NewRuntime returns a runtime with a configuration assigned by NewRuntimeConfig. 144 func NewRuntime(ctx context.Context) Runtime { 145 return NewRuntimeWithConfig(ctx, NewRuntimeConfig()) 146 } 147 148 // NewRuntimeWithConfig returns a runtime with the given configuration. 149 func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime { 150 config := rConfig.(*runtimeConfig) 151 var engine wasm.Engine 152 var cacheImpl *cache 153 if c := config.cache; c != nil { 154 // If the Cache is configured, we share the engine. 155 cacheImpl = c.(*cache) 156 engine = cacheImpl.initEngine(config.engineKind, config.newEngine, ctx, config.enabledFeatures) 157 } else { 158 // Otherwise, we create a new engine. 159 engine = config.newEngine(ctx, config.enabledFeatures, nil) 160 } 161 store := wasm.NewStore(config.enabledFeatures, engine) 162 return &runtime{ 163 cache: cacheImpl, 164 store: store, 165 enabledFeatures: config.enabledFeatures, 166 memoryLimitPages: config.memoryLimitPages, 167 memoryCapacityFromMax: config.memoryCapacityFromMax, 168 dwarfDisabled: config.dwarfDisabled, 169 storeCustomSections: config.storeCustomSections, 170 ensureTermination: config.ensureTermination, 171 } 172 } 173 174 // runtime allows decoupling of public interfaces from internal representation. 175 type runtime struct { 176 store *wasm.Store 177 cache *cache 178 enabledFeatures api.CoreFeatures 179 memoryLimitPages uint32 180 memoryCapacityFromMax bool 181 dwarfDisabled bool 182 storeCustomSections bool 183 184 // closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code. 185 // 186 // The update value is 1 + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed. 187 // 188 // Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations. 189 // See /RATIONALE.md 190 closed atomic.Uint64 191 192 ensureTermination bool 193 } 194 195 // Module implements Runtime.Module. 196 func (r *runtime) Module(moduleName string) api.Module { 197 if len(moduleName) == 0 { 198 return nil 199 } 200 return r.store.Module(moduleName) 201 } 202 203 // CompileModule implements Runtime.CompileModule 204 func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) { 205 if err := r.failIfClosed(); err != nil { 206 return nil, err 207 } 208 209 internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, 210 r.memoryLimitPages, r.memoryCapacityFromMax, !r.dwarfDisabled, r.storeCustomSections) 211 if err != nil { 212 return nil, err 213 } else if err = internal.Validate(r.enabledFeatures); err != nil { 214 // TODO: decoders should validate before returning, as that allows 215 // them to err with the correct position in the wasm binary. 216 return nil, err 217 } 218 219 // Now that the module is validated, cache the memory definitions. 220 // TODO: lazy initialization of memory definition. 221 internal.BuildMemoryDefinitions() 222 223 c := &compiledModule{module: internal, compiledEngine: r.store.Engine} 224 225 // typeIDs are static and compile-time known. 226 typeIDs, err := r.store.GetFunctionTypeIDs(internal.TypeSection) 227 if err != nil { 228 return nil, err 229 } 230 c.typeIDs = typeIDs 231 232 listeners, err := buildFunctionListeners(ctx, internal) 233 if err != nil { 234 return nil, err 235 } 236 internal.AssignModuleID(binary, listeners, r.ensureTermination) 237 if err = r.store.Engine.CompileModule(ctx, internal, listeners, r.ensureTermination); err != nil { 238 return nil, err 239 } 240 return c, nil 241 } 242 243 func buildFunctionListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) { 244 // Test to see if internal code are using an experimental feature. 245 fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{}) 246 if fnlf == nil { 247 return nil, nil 248 } 249 factory := fnlf.(experimentalapi.FunctionListenerFactory) 250 importCount := internal.ImportFunctionCount 251 listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection)) 252 for i := 0; i < len(listeners); i++ { 253 listeners[i] = factory.NewFunctionListener(internal.FunctionDefinition(uint32(i) + importCount)) 254 } 255 return listeners, nil 256 } 257 258 // failIfClosed returns an error if CloseWithExitCode was called implicitly (by Close) or explicitly. 259 func (r *runtime) failIfClosed() error { 260 if closed := r.closed.Load(); closed != 0 { 261 return fmt.Errorf("runtime closed with exit_code(%d)", uint32(closed>>32)) 262 } 263 return nil 264 } 265 266 // Instantiate implements Runtime.Instantiate 267 func (r *runtime) Instantiate(ctx context.Context, binary []byte) (api.Module, error) { 268 return r.InstantiateWithConfig(ctx, binary, NewModuleConfig()) 269 } 270 271 // InstantiateWithConfig implements Runtime.InstantiateWithConfig 272 func (r *runtime) InstantiateWithConfig(ctx context.Context, binary []byte, config ModuleConfig) (api.Module, error) { 273 if compiled, err := r.CompileModule(ctx, binary); err != nil { 274 return nil, err 275 } else { 276 compiled.(*compiledModule).closeWithModule = true 277 return r.InstantiateModule(ctx, compiled, config) 278 } 279 } 280 281 // InstantiateModule implements Runtime.InstantiateModule. 282 func (r *runtime) InstantiateModule( 283 ctx context.Context, 284 compiled CompiledModule, 285 mConfig ModuleConfig, 286 ) (mod api.Module, err error) { 287 if err = r.failIfClosed(); err != nil { 288 return nil, err 289 } 290 291 code := compiled.(*compiledModule) 292 config := mConfig.(*moduleConfig) 293 294 // Only add guest module configuration to guests. 295 if !code.module.IsHostModule { 296 if sockConfig, ok := ctx.Value(internalsock.ConfigKey{}).(*internalsock.Config); ok { 297 config.sockConfig = sockConfig 298 } 299 } 300 301 var sysCtx *internalsys.Context 302 if sysCtx, err = config.toSysContext(); err != nil { 303 return 304 } 305 306 name := config.name 307 if !config.nameSet && code.module.NameSection != nil && code.module.NameSection.ModuleName != "" { 308 name = code.module.NameSection.ModuleName 309 } 310 311 // Instantiate the module. 312 mod, err = r.store.Instantiate(ctx, code.module, name, sysCtx, code.typeIDs) 313 if err != nil { 314 // If there was an error, don't leak the compiled module. 315 if code.closeWithModule { 316 _ = code.Close(ctx) // don't overwrite the error 317 } 318 return 319 } 320 321 if closeNotifier, ok := ctx.Value(internalclose.NotifierKey{}).(internalclose.Notifier); ok { 322 mod.(*wasm.ModuleInstance).CloseNotifier = closeNotifier 323 } 324 325 // Attach the code closer so that anything afterward closes the compiled 326 // code when closing the module. 327 if code.closeWithModule { 328 mod.(*wasm.ModuleInstance).CodeCloser = code 329 } 330 331 // Now, invoke any start functions, failing at first error. 332 for _, fn := range config.startFunctions { 333 start := mod.ExportedFunction(fn) 334 if start == nil { 335 continue 336 } 337 if _, err = start.Call(ctx); err != nil { 338 _ = mod.Close(ctx) // Don't leak the module on error. 339 340 if se, ok := err.(*sys.ExitError); ok { 341 if se.ExitCode() == 0 { // Don't err on success. 342 err = nil 343 } 344 return // Don't wrap an exit error 345 } 346 err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err) 347 return 348 } 349 } 350 return 351 } 352 353 // Close implements api.Closer embedded in Runtime. 354 func (r *runtime) Close(ctx context.Context) error { 355 return r.CloseWithExitCode(ctx, 0) 356 } 357 358 // CloseWithExitCode implements Runtime.CloseWithExitCode 359 // 360 // Note: it also marks the internal `closed` field 361 func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error { 362 closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits. 363 if !r.closed.CompareAndSwap(0, closed) { 364 return nil 365 } 366 err := r.store.CloseWithExitCode(ctx, exitCode) 367 if r.cache == nil { 368 // Close the engine if the cache is not configured, which means that this engine is scoped in this runtime. 369 if errCloseEngine := r.store.Engine.Close(); errCloseEngine != nil { 370 return errCloseEngine 371 } 372 } 373 return err 374 }