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  }