github.com/tetratelabs/wazero@v1.2.1/runtime.go (about)

     1  package wazero
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync/atomic"
     7  
     8  	"github.com/tetratelabs/wazero/api"
     9  	experimentalapi "github.com/tetratelabs/wazero/experimental"
    10  	internalsock "github.com/tetratelabs/wazero/internal/sock"
    11  	internalsys "github.com/tetratelabs/wazero/internal/sys"
    12  	"github.com/tetratelabs/wazero/internal/wasm"
    13  	binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
    14  	"github.com/tetratelabs/wazero/sys"
    15  )
    16  
    17  // Runtime allows embedding of WebAssembly modules.
    18  //
    19  // The below is an example of basic initialization:
    20  //
    21  //	ctx := context.Background()
    22  //	r := wazero.NewRuntime(ctx)
    23  //	defer r.Close(ctx) // This closes everything this Runtime created.
    24  //
    25  //	mod, _ := r.Instantiate(ctx, wasm)
    26  //
    27  // # Notes
    28  //
    29  //   - This is an interface for decoupling, not third-party implementations.
    30  //     All implementations are in wazero.
    31  //   - Closing this closes any CompiledModule or Module it instantiated.
    32  type Runtime interface {
    33  	// Instantiate instantiates a module from the WebAssembly binary (%.wasm)
    34  	// with default configuration.
    35  	//
    36  	// Here's an example:
    37  	//	ctx := context.Background()
    38  	//	r := wazero.NewRuntime(ctx)
    39  	//	defer r.Close(ctx) // This closes everything this Runtime created.
    40  	//
    41  	//	mod, _ := r.Instantiate(ctx, wasm)
    42  	//
    43  	// # Notes
    44  	//
    45  	//   - See notes on InstantiateModule for error scenarios.
    46  	//   - See InstantiateWithConfig for configuration overrides.
    47  	Instantiate(ctx context.Context, source []byte) (api.Module, error)
    48  
    49  	// InstantiateWithConfig instantiates a module from the WebAssembly binary
    50  	// (%.wasm) or errs for reasons including exit or validation.
    51  	//
    52  	// Here's an example:
    53  	//	ctx := context.Background()
    54  	//	r := wazero.NewRuntime(ctx)
    55  	//	defer r.Close(ctx) // This closes everything this Runtime created.
    56  	//
    57  	//	mod, _ := r.InstantiateWithConfig(ctx, wasm,
    58  	//		wazero.NewModuleConfig().WithName("rotate"))
    59  	//
    60  	// # Notes
    61  	//
    62  	//   - See notes on InstantiateModule for error scenarios.
    63  	//   - If you aren't overriding defaults, use Instantiate.
    64  	//   - This is a convenience utility that chains CompileModule with
    65  	//     InstantiateModule. To instantiate the same source multiple times,
    66  	//     use CompileModule as InstantiateModule avoids redundant decoding
    67  	//     and/or compilation.
    68  	InstantiateWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error)
    69  
    70  	// NewHostModuleBuilder lets you create modules out of functions defined in Go.
    71  	//
    72  	// Below defines and instantiates a module named "env" with one function:
    73  	//
    74  	//	ctx := context.Background()
    75  	//	hello := func() {
    76  	//		fmt.Fprintln(stdout, "hello!")
    77  	//	}
    78  	//	_, err := r.NewHostModuleBuilder("env").
    79  	//		NewFunctionBuilder().WithFunc(hello).Export("hello").
    80  	//		Instantiate(ctx, r)
    81  	//
    82  	// Note: empty `moduleName` is not allowed.
    83  	NewHostModuleBuilder(moduleName string) HostModuleBuilder
    84  
    85  	// CompileModule decodes the WebAssembly binary (%.wasm) or errs if invalid.
    86  	// Any pre-compilation done after decoding wasm is dependent on RuntimeConfig.
    87  	//
    88  	// There are two main reasons to use CompileModule instead of Instantiate:
    89  	//   - Improve performance when the same module is instantiated multiple times under different names
    90  	//   - Reduce the amount of errors that can occur during InstantiateModule.
    91  	//
    92  	// # Notes
    93  	//
    94  	//   - The resulting module name defaults to what was binary from the custom name section.
    95  	//   - Any pre-compilation done after decoding the source is dependent on RuntimeConfig.
    96  	//
    97  	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
    98  	CompileModule(ctx context.Context, binary []byte) (CompiledModule, error)
    99  
   100  	// InstantiateModule instantiates the module or errs for reasons including
   101  	// exit or validation.
   102  	//
   103  	// Here's an example:
   104  	//	mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().
   105  	//		WithName("prod"))
   106  	//
   107  	// # Errors
   108  	//
   109  	// While CompiledModule is pre-validated, there are a few situations which
   110  	// can cause an error:
   111  	//   - The module name is already in use.
   112  	//   - The module has a table element initializer that resolves to an index
   113  	//     outside the Table minimum size.
   114  	//   - The module has a start function, and it failed to execute.
   115  	//   - The module was compiled to WASI and exited with a non-zero exit
   116  	//     code, you'll receive a sys.ExitError.
   117  	//   - RuntimeConfig.WithCloseOnContextDone was enabled and a context
   118  	//     cancellation or deadline triggered before a start function returned.
   119  	InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error)
   120  
   121  	// CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code.
   122  	// An error is returned if any module returns an error when closed.
   123  	//
   124  	// Here's an example:
   125  	//	ctx := context.Background()
   126  	//	r := wazero.NewRuntime(ctx)
   127  	//	defer r.CloseWithExitCode(ctx, 2) // This closes everything this Runtime created.
   128  	//
   129  	//	// Everything below here can be closed, but will anyway due to above.
   130  	//	_, _ = wasi_snapshot_preview1.InstantiateSnapshotPreview1(ctx, r)
   131  	//	mod, _ := r.Instantiate(ctx, wasm)
   132  	CloseWithExitCode(ctx context.Context, exitCode uint32) error
   133  
   134  	// Module returns an instantiated module in this runtime or nil if there aren't any.
   135  	Module(moduleName string) api.Module
   136  
   137  	// Closer closes all compiled code by delegating to CloseWithExitCode with an exit code of zero.
   138  	api.Closer
   139  }
   140  
   141  // NewRuntime returns a runtime with a configuration assigned by NewRuntimeConfig.
   142  func NewRuntime(ctx context.Context) Runtime {
   143  	return NewRuntimeWithConfig(ctx, NewRuntimeConfig())
   144  }
   145  
   146  // NewRuntimeWithConfig returns a runtime with the given configuration.
   147  func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
   148  	config := rConfig.(*runtimeConfig)
   149  	var engine wasm.Engine
   150  	var cacheImpl *cache
   151  	if c := config.cache; c != nil {
   152  		// If the Cache is configured, we share the engine.
   153  		cacheImpl = c.(*cache)
   154  		engine = cacheImpl.initEngine(config.engineKind, config.newEngine, ctx, config.enabledFeatures)
   155  	} else {
   156  		// Otherwise, we create a new engine.
   157  		engine = config.newEngine(ctx, config.enabledFeatures, nil)
   158  	}
   159  	store := wasm.NewStore(config.enabledFeatures, engine)
   160  	zero := uint64(0)
   161  	return &runtime{
   162  		cache:                 cacheImpl,
   163  		store:                 store,
   164  		enabledFeatures:       config.enabledFeatures,
   165  		memoryLimitPages:      config.memoryLimitPages,
   166  		memoryCapacityFromMax: config.memoryCapacityFromMax,
   167  		dwarfDisabled:         config.dwarfDisabled,
   168  		storeCustomSections:   config.storeCustomSections,
   169  		closed:                &zero,
   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 *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, len(listeners) > 0, 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 := atomic.LoadUint64(r.closed); 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 build listeners on a guest module. A host module doesn't have
   295  	// memory, and a guest without memory can't use listeners anyway.
   296  	if !code.module.IsHostModule {
   297  		if sockConfig, ok := ctx.Value(internalsock.ConfigKey{}).(*internalsock.Config); ok {
   298  			config.sockConfig = sockConfig
   299  		}
   300  	}
   301  
   302  	var sysCtx *internalsys.Context
   303  	if sysCtx, err = config.toSysContext(); err != nil {
   304  		return
   305  	}
   306  
   307  	name := config.name
   308  	if !config.nameSet && code.module.NameSection != nil && code.module.NameSection.ModuleName != "" {
   309  		name = code.module.NameSection.ModuleName
   310  	}
   311  
   312  	// Instantiate the module.
   313  	mod, err = r.store.Instantiate(ctx, code.module, name, sysCtx, code.typeIDs)
   314  	if err != nil {
   315  		// If there was an error, don't leak the compiled module.
   316  		if code.closeWithModule {
   317  			_ = code.Close(ctx) // don't overwrite the error
   318  		}
   319  		return
   320  	}
   321  
   322  	// Attach the code closer so that anything afterwards closes the compiled code when closing the module.
   323  	if code.closeWithModule {
   324  		mod.(*wasm.ModuleInstance).CodeCloser = code
   325  	}
   326  
   327  	// Now, invoke any start functions, failing at first error.
   328  	for _, fn := range config.startFunctions {
   329  		start := mod.ExportedFunction(fn)
   330  		if start == nil {
   331  			continue
   332  		}
   333  		if _, err = start.Call(ctx); err != nil {
   334  			_ = mod.Close(ctx) // Don't leak the module on error.
   335  
   336  			if se, ok := err.(*sys.ExitError); ok {
   337  				if se.ExitCode() == 0 { // Don't err on success.
   338  					err = nil
   339  				}
   340  				return // Don't wrap an exit error
   341  			}
   342  			err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err)
   343  			return
   344  		}
   345  	}
   346  	return
   347  }
   348  
   349  // Close implements api.Closer embedded in Runtime.
   350  func (r *runtime) Close(ctx context.Context) error {
   351  	return r.CloseWithExitCode(ctx, 0)
   352  }
   353  
   354  // CloseWithExitCode implements Runtime.CloseWithExitCode
   355  //
   356  // Note: it also marks the internal `closed` field
   357  func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
   358  	closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits.
   359  	if !atomic.CompareAndSwapUint64(r.closed, 0, closed) {
   360  		return nil
   361  	}
   362  	err := r.store.CloseWithExitCode(ctx, exitCode)
   363  	if r.cache == nil {
   364  		// Close the engine if the cache is not configured, which means that this engine is scoped in this runtime.
   365  		if errCloseEngine := r.store.Engine.Close(); errCloseEngine != nil {
   366  			return errCloseEngine
   367  		}
   368  	}
   369  	return err
   370  }