wa-lang.org/wazero@v1.0.2/runtime.go (about)

     1  package wazero
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  
     8  	"wa-lang.org/wazero/api"
     9  	experimentalapi "wa-lang.org/wazero/experimental"
    10  	"wa-lang.org/wazero/internal/version"
    11  	"wa-lang.org/wazero/internal/wasm"
    12  	binaryformat "wa-lang.org/wazero/internal/wasm/binary"
    13  )
    14  
    15  // Runtime allows embedding of WebAssembly modules.
    16  //
    17  // The below is an example of basic initialization:
    18  //
    19  //	ctx := context.Background()
    20  //	r := wazero.NewRuntime(ctx)
    21  //	defer r.Close(ctx) // This closes everything this Runtime created.
    22  //
    23  //	module, _ := r.InstantiateModuleFromBinary(ctx, wasm)
    24  type Runtime interface {
    25  	// NewHostModuleBuilder lets you create modules out of functions defined in Go.
    26  	//
    27  	// Below defines and instantiates a module named "env" with one function:
    28  	//
    29  	//	ctx := context.Background()
    30  	//	hello := func() {
    31  	//		fmt.Fprintln(stdout, "hello!")
    32  	//	}
    33  	//	_, err := r.NewHostModuleBuilder("env").
    34  	//		NewFunctionBuilder().WithFunc(hello).Export("hello").
    35  	//		Instantiate(ctx, r)
    36  	NewHostModuleBuilder(moduleName string) HostModuleBuilder
    37  
    38  	// CompileModule decodes the WebAssembly binary (%.wasm) or errs if invalid.
    39  	// Any pre-compilation done after decoding wasm is dependent on RuntimeConfig.
    40  	//
    41  	// There are two main reasons to use CompileModule instead of InstantiateModuleFromBinary:
    42  	//   - Improve performance when the same module is instantiated multiple times under different names
    43  	//   - Reduce the amount of errors that can occur during InstantiateModule.
    44  	//
    45  	// # Notes
    46  	//
    47  	//   - The resulting module name defaults to what was binary from the custom name section.
    48  	//   - Any pre-compilation done after decoding the source is dependent on RuntimeConfig.
    49  	//
    50  	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
    51  	CompileModule(ctx context.Context, binary []byte) (CompiledModule, error)
    52  
    53  	// InstantiateModuleFromBinary instantiates a module from the WebAssembly binary (%.wasm) or errs if invalid.
    54  	//
    55  	// Here's an example:
    56  	//	ctx := context.Background()
    57  	//	r := wazero.NewRuntime(ctx)
    58  	//	defer r.Close(ctx) // This closes everything this Runtime created.
    59  	//
    60  	//	module, _ := r.InstantiateModuleFromBinary(ctx, wasm)
    61  	//
    62  	// # Notes
    63  	//
    64  	//   - This is a convenience utility that chains CompileModule with InstantiateModule. To instantiate the same
    65  	//	source multiple times, use CompileModule as InstantiateModule avoids redundant decoding and/or compilation.
    66  	//   - To avoid using configuration defaults, use InstantiateModule instead.
    67  	InstantiateModuleFromBinary(ctx context.Context, source []byte) (api.Module, error)
    68  
    69  	// Namespace is the default namespace of this runtime, and is embedded for convenience. Most users will only use the
    70  	// default namespace.
    71  	//
    72  	// Advanced use cases can use NewNamespace to redefine modules of the same name. For example, to allow different
    73  	// modules to define their own stateful "env" module.
    74  	Namespace
    75  
    76  	// NewNamespace creates an empty namespace which won't conflict with any other namespace including the default.
    77  	// This is more efficient than multiple runtimes, as namespaces share a compiler cache.
    78  	//
    79  	// In simplest case, a namespace won't conflict if another has a module with the same name:
    80  	//	b := assemblyscript.NewBuilder(r)
    81  	//	m1, _ := b.InstantiateModule(ctx, r.NewNamespace(ctx))
    82  	//	m2, _ := b.InstantiateModule(ctx, r.NewNamespace(ctx))
    83  	//
    84  	// This is also useful for different modules that import the same module name (like "env"), but need different
    85  	// configuration or state. For example, one with trace logging enabled and another disabled:
    86  	//	b := assemblyscript.NewBuilder(r)
    87  	//
    88  	//	// m1 has trace logging disabled
    89  	//	ns1 := r.NewNamespace(ctx)
    90  	//	_ = b.InstantiateModule(ctx, ns1)
    91  	//	m1, _ := ns1.InstantiateModule(ctx, compiled, config)
    92  	//
    93  	//	// m2 has trace logging enabled
    94  	//	ns2 := r.NewNamespace(ctx)
    95  	//	_ = b.WithTraceToStdout().InstantiateModule(ctx, ns2)
    96  	//	m2, _ := ns2.InstantiateModule(ctx, compiled, config)
    97  	//
    98  	// # Notes
    99  	//
   100  	//   - The returned namespace does not inherit any modules from the runtime default namespace.
   101  	//   - Closing the returned namespace closes any modules in it.
   102  	//   - Closing this runtime also closes the namespace returned from this function.
   103  	NewNamespace(context.Context) Namespace
   104  
   105  	// CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code.
   106  	// An error is returned if any module returns an error when closed.
   107  	//
   108  	// Here's an example:
   109  	//	ctx := context.Background()
   110  	//	r := wazero.NewRuntime(ctx)
   111  	//	defer r.CloseWithExitCode(ctx, 2) // This closes everything this Runtime created.
   112  	//
   113  	//	// Everything below here can be closed, but will anyway due to above.
   114  	//	_, _ = wasi_snapshot_preview1.InstantiateSnapshotPreview1(ctx, r)
   115  	//	mod, _ := r.InstantiateModuleFromBinary(ctx, wasm)
   116  	CloseWithExitCode(ctx context.Context, exitCode uint32) error
   117  
   118  	// Closer closes all namespace and compiled code by delegating to CloseWithExitCode with an exit code of zero.
   119  	api.Closer
   120  }
   121  
   122  // NewRuntime returns a runtime with a configuration assigned by NewRuntimeConfig.
   123  func NewRuntime(ctx context.Context) Runtime {
   124  	return NewRuntimeWithConfig(ctx, NewRuntimeConfig())
   125  }
   126  
   127  // NewRuntimeWithConfig returns a runtime with the given configuration.
   128  func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
   129  	if v := ctx.Value(version.WazeroVersionKey{}); v == nil {
   130  		ctx = context.WithValue(ctx, version.WazeroVersionKey{}, wazeroVersion)
   131  	}
   132  	config := rConfig.(*runtimeConfig)
   133  	store, ns := wasm.NewStore(config.enabledFeatures, config.newEngine(ctx, config.enabledFeatures))
   134  	return &runtime{
   135  		store:                 store,
   136  		ns:                    &namespace{store: store, ns: ns},
   137  		enabledFeatures:       config.enabledFeatures,
   138  		memoryLimitPages:      config.memoryLimitPages,
   139  		memoryCapacityFromMax: config.memoryCapacityFromMax,
   140  		isInterpreter:         config.isInterpreter,
   141  	}
   142  }
   143  
   144  // runtime allows decoupling of public interfaces from internal representation.
   145  type runtime struct {
   146  	store                 *wasm.Store
   147  	ns                    *namespace
   148  	enabledFeatures       api.CoreFeatures
   149  	memoryLimitPages      uint32
   150  	memoryCapacityFromMax bool
   151  	isInterpreter         bool
   152  	compiledModules       []*compiledModule
   153  }
   154  
   155  // NewNamespace implements Runtime.NewNamespace.
   156  func (r *runtime) NewNamespace(ctx context.Context) Namespace {
   157  	return &namespace{store: r.store, ns: r.store.NewNamespace(ctx)}
   158  }
   159  
   160  // Module implements Namespace.Module embedded by Runtime.
   161  func (r *runtime) Module(moduleName string) api.Module {
   162  	return r.ns.Module(moduleName)
   163  }
   164  
   165  // CompileModule implements Runtime.CompileModule
   166  func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) {
   167  	if binary == nil {
   168  		return nil, errors.New("binary == nil")
   169  	}
   170  
   171  	if len(binary) < 4 || !bytes.Equal(binary[0:4], binaryformat.Magic) {
   172  		return nil, errors.New("invalid binary")
   173  	}
   174  
   175  	internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax)
   176  	if err != nil {
   177  		return nil, err
   178  	} else if err = internal.Validate(r.enabledFeatures); err != nil {
   179  		// TODO: decoders should validate before returning, as that allows
   180  		// them to err with the correct position in the wasm binary.
   181  		return nil, err
   182  	}
   183  
   184  	internal.AssignModuleID(binary)
   185  
   186  	// Now that the module is validated, cache the function and memory definitions.
   187  	internal.BuildFunctionDefinitions()
   188  	internal.BuildMemoryDefinitions()
   189  
   190  	c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
   191  
   192  	listeners, err := buildListeners(ctx, internal)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	if err = r.store.Engine.CompileModule(ctx, internal, listeners); err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	r.compiledModules = append(r.compiledModules, c)
   202  	return c, nil
   203  }
   204  
   205  func buildListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
   206  	// Test to see if internal code are using an experimental feature.
   207  	fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{})
   208  	if fnlf == nil {
   209  		return nil, nil
   210  	}
   211  	factory := fnlf.(experimentalapi.FunctionListenerFactory)
   212  	importCount := internal.ImportFuncCount()
   213  	listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection))
   214  	for i := 0; i < len(listeners); i++ {
   215  		listeners[i] = factory.NewListener(internal.FunctionDefinitionSection[uint32(i)+importCount])
   216  	}
   217  	return listeners, nil
   218  }
   219  
   220  // InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
   221  func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
   222  	if compiled, err := r.CompileModule(ctx, binary); err != nil {
   223  		return nil, err
   224  	} else {
   225  		compiled.(*compiledModule).closeWithModule = true
   226  		return r.InstantiateModule(ctx, compiled, NewModuleConfig())
   227  	}
   228  }
   229  
   230  // InstantiateModule implements Namespace.InstantiateModule embedded by Runtime.
   231  func (r *runtime) InstantiateModule(
   232  	ctx context.Context,
   233  	compiled CompiledModule,
   234  	mConfig ModuleConfig,
   235  ) (api.Module, error) {
   236  	return r.ns.InstantiateModule(ctx, compiled, mConfig)
   237  }
   238  
   239  // Close implements api.Closer embedded in Runtime.
   240  func (r *runtime) Close(ctx context.Context) error {
   241  	return r.CloseWithExitCode(ctx, 0)
   242  }
   243  
   244  // CloseWithExitCode implements Runtime.CloseWithExitCode
   245  func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
   246  	err := r.store.CloseWithExitCode(ctx, exitCode)
   247  	for _, c := range r.compiledModules {
   248  		if e := c.Close(ctx); e != nil && err == nil {
   249  			err = e
   250  		}
   251  	}
   252  	return err
   253  }