Skip to content

Commit

Permalink
[x/programs] Add Engine and Store abstractions (#657)
Browse files Browse the repository at this point in the history
* Move fixtures into tests directory

Signed-off-by: Sam Batschelet <[email protected]>

* Abstract engine from runtime

Signed-off-by: Sam Batschelet <[email protected]>

* React to changes

Signed-off-by: Sam Batschelet <[email protected]>

* Nits

Signed-off-by: Sam Batschelet <[email protected]>

* Review comments

Signed-off-by: Sam Batschelet <[email protected]>

* Clarify debug mode

Signed-off-by: Sam Batschelet <[email protected]>

* Nit

Signed-off-by: Sam Batschelet <[email protected]>

---------

Signed-off-by: Sam Batschelet <[email protected]>
  • Loading branch information
hexfusion authored Jan 5, 2024
1 parent ad3ac94 commit 1c21cfd
Show file tree
Hide file tree
Showing 22 changed files with 1,044 additions and 659 deletions.
321 changes: 321 additions & 0 deletions x/programs/engine/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package engine

import (
"github.com/ava-labs/avalanchego/utils/units"

"github.com/bytecodealliance/wasmtime-go/v14"
)

type CompileStrategy uint8

const (
// CompileWasm will compile the wasm module before instantiating it.
CompileWasm CompileStrategy = iota
// PrecompiledWasm accepts a precompiled wasm module serialized by an Engine.
PrecompiledWasm
)

var (
DefaultMaxWasmStack = 256 * units.MiB // 256 MiB
DefaultLimitMaxMemory = uint32(18 * 64 * units.KiB) // 18 pages
DefaultSIMD = false
DefaultEnableReferenceTypes = false
DefaultEnableBulkMemory = false
DefaultProfilingStrategy = wasmtime.ProfilingStrategyNone
DefaultMultiValue = false
DefaultCompileStrategy = CompileWasm

defaultWasmThreads = false
defaultFuelMetering = true
defaultWasmMultiMemory = false
defaultWasmMemory64 = false
defaultCompilerStrategy = wasmtime.StrategyCranelift
defaultEpochInterruption = true
defaultNaNCanonicalization = "true"
defaultCraneliftOptLevel = wasmtime.OptLevelSpeed
defaultEnableCraneliftDebugVerifier = false
defaultEnableDebugInfo = false
)

// NewConfig creates a new engine config with default settings
func NewConfig() *Config {
return &Config{
wasmConfig: DefaultWasmtimeConfig(),
}
}

// Config is wrapper for wasmtime.Config
type Config struct {
wasmConfig *wasmtime.Config
}

// Get returns the underlying wasmtime config.
func (c *Config) Get() *wasmtime.Config {
return c.wasmConfig
}

// CacheConfigLoad enables compiled code caching for this `Config` using the
// settings specified in the configuration file `path`.
func (c *Config) CacheConfigLoad(path string) error {
return c.wasmConfig.CacheConfigLoad(path)
}

// CacheConfigLoadDefault enables compiled code caching for this `Config` using
// the default settings configuration can be found.
func (c *Config) CacheConfigLoadDefault() error {
return c.wasmConfig.CacheConfigLoadDefault()
}

// EnableCraneliftFlag enables a target-specific flag in Cranelift.
func (c *Config) EnableCraneliftFlag(flag string) {
c.wasmConfig.EnableCraneliftFlag(flag)
}

// SetConsumFuel configures whether fuel is enabled.
func (c *Config) SetConsumeFuel(enabled bool) {
c.wasmConfig.SetConsumeFuel(enabled)
}

// SetCraneliftDebugVerifier configures whether the cranelift debug verifier
// will be active when cranelift is used to compile wasm code.
func (c *Config) SetCraneliftDebugVerifier(enabled bool) {
c.wasmConfig.SetCraneliftDebugVerifier(enabled)
}

// SetCraneliftFlag sets a target-specific flag in Cranelift to the specified value.
func (c *Config) SetCraneliftFlag(name string, value string) {
c.wasmConfig.SetCraneliftFlag(name, value)
}

// SetCraneliftOptLevel configures the cranelift optimization level for generated code.
func (c *Config) SetCraneliftOptLevel(level wasmtime.OptLevel) {
c.wasmConfig.SetCraneliftOptLevel(level)
}

// SetDebugInfo configures whether dwarf debug information for JIT code is enabled
func (c *Config) SetDebugInfo(enabled bool) {
c.wasmConfig.SetDebugInfo(enabled)
}

// SetEpochInterruption enables epoch-based instrumentation of generated code to
// interrupt WebAssembly execution when the current engine epoch exceeds a
// defined threshold.
func (c *Config) SetEpochInterruption(enable bool) {
c.wasmConfig.SetEpochInterruption(enable)
}

// SetMaxWasmStack configures the maximum stack size, in bytes, that JIT code can use.
func (c *Config) SetMaxWasmStack(size int) {
c.wasmConfig.SetMaxWasmStack(size)
}

// SetProfiler configures what profiler strategy to use for generated code.
func (c *Config) SetProfiler(profiler wasmtime.ProfilingStrategy) {
c.wasmConfig.SetProfiler(profiler)
}

// SetStrategy configures what compilation strategy is used to compile wasm code.
func (c *Config) SetStrategy(strategy wasmtime.Strategy) {
c.wasmConfig.SetStrategy(strategy)
}

// SetTarget configures the target triple that this configuration will produce machine code for.
func (c *Config) SetTarget(target string) error {
return c.wasmConfig.SetTarget(target)
}

// SetWasmBulkMemory configures whether the wasm bulk memory proposal is enabled.
func (c *Config) SetWasmBulkMemory(enabled bool) {
c.wasmConfig.SetWasmBulkMemory(enabled)
}

// SetWasmMemory64 configures whether the wasm memory64 proposal is enabled.
func (c *Config) SetWasmMemory64(enabled bool) {
c.wasmConfig.SetWasmMemory64(enabled)
}

// SetWasmMultiMemory configures whether the wasm multi memory proposal is enabled.
func (c *Config) SetWasmMultiMemory(enabled bool) {
c.wasmConfig.SetWasmMultiMemory(enabled)
}

// SetWasmMultiValue configures whether the wasm multi value proposal is enabled.
func (c *Config) SetWasmMultiValue(enabled bool) {
c.wasmConfig.SetWasmMultiValue(enabled)
}

// SetWasmReferenceTypes configures whether the wasm reference types proposal is enabled.
func (c *Config) SetWasmReferenceTypes(enabled bool) {
c.wasmConfig.SetWasmReferenceTypes(enabled)
}

// SetWasmSIMD configures whether the wasm SIMD proposal is enabled.
func (c *Config) SetWasmSIMD(enabled bool) {
c.wasmConfig.SetWasmSIMD(enabled)
}

// SetWasmThreads configures whether the wasm threads proposal is enabled.
func (c *Config) SetWasmThreads(enabled bool) {
c.wasmConfig.SetWasmThreads(enabled)
}

// DefaultWasmtimeConfig returns a new wasmtime config with default settings.
func DefaultWasmtimeConfig() *wasmtime.Config {
cfg := wasmtime.NewConfig()

// non configurable defaults
cfg.SetCraneliftOptLevel(defaultCraneliftOptLevel)
cfg.SetConsumeFuel(defaultFuelMetering)
cfg.SetWasmThreads(defaultWasmThreads)
cfg.SetWasmMultiMemory(defaultWasmMultiMemory)
cfg.SetWasmMemory64(defaultWasmMemory64)
cfg.SetStrategy(defaultCompilerStrategy)
cfg.SetEpochInterruption(defaultEpochInterruption)
cfg.SetCraneliftFlag("enable_nan_canonicalization", defaultNaNCanonicalization)

// TODO: expose these knobs for developers
cfg.SetCraneliftDebugVerifier(defaultEnableCraneliftDebugVerifier)
cfg.SetDebugInfo(defaultEnableDebugInfo)
return cfg
}

// NewConfigBuilder returns a new engine configuration builder with default settings.
// All instances of ConfigBuilder should be created with this constructor.
func NewConfigBuilder() *ConfigBuilder {
return &ConfigBuilder{
EnableBulkMemory: DefaultEnableBulkMemory,
EnableWasmMultiValue: DefaultMultiValue,
EnableWasmReferenceTypes: DefaultEnableReferenceTypes,
EnableWasmSIMD: DefaultSIMD,
MaxWasmStack: DefaultMaxWasmStack,
ProfilingStrategy: DefaultProfilingStrategy,
EnableDefaultCache: false,
}
}

type ConfigBuilder struct {
// Configures whether the WebAssembly bulk memory operations proposal will
// be enabled for compilation. This feature gates items such as the
// memory.copy instruction, passive data/table segments, etc, being in a
// module.
// This is false by default.
EnableBulkMemory bool `yaml:"enable_bulk_memory,omitempty" json:"enableBulkMemory,omitempty"`
// Configures whether the WebAssembly multi-value proposal will be enabled for compilation.
// This feature gates functions and blocks returning multiple values in a module, for example.
// This is false by default.
EnableWasmMultiValue bool `yaml:"enable_wasm_multi_value,omitempty" json:"enableWasmMultiValue,omitempty"`
// Configures whether the WebAssembly reference types proposal will be
// enabled for compilation. This feature gates items such as the externref
// and funcref types as well as allowing a module to define multiple tables.
// Note that the reference types proposal depends on the bulk memory
// proposal.
// This is false by default.
EnableWasmReferenceTypes bool `yaml:"enable_wasm_reference_types,omitempty" json:"enableWasmReferenceTypes,omitempty"`
// Configures whether the WebAssembly SIMD proposal will be enabled for
// compilation. The WebAssembly SIMD proposal. This feature gates items
// such as the v128 type and all of its operators being in a module. Note
// that this does not enable the relaxed simd proposal.
// This is false by default.
EnableWasmSIMD bool `yaml:"enable_wasm_simd,omitempty" json:"enableWasmSIMD,omitempty"`
// EnableDefaultCache enables compiled code caching for this `Config` using the default settings
// configuration can be found.
//
// For more information about caching see
// https://bytecodealliance.github.io/wasmtime/cli-cache.html
// This is false by default.
EnableDefaultCache bool `yaml:"enable_default_cache,omitempty" json:"enableDefaultCache,omitempty"`
// SetMaxWasmStack configures the maximum stack size, in bytes, that JIT code can use.
// The amount of stack space that wasm takes is always relative to the first invocation of wasm on the stack.
// Recursive calls with host frames in the middle will all need to fit within this setting.
// Note that this setting is not interpreted with 100% precision.
// This is 256 MiB by default.
MaxWasmStack int `yaml:"max_wasm_stack,omitempty" json:"maxWasmStack,omitempty"`
// ProfilingStrategy decides what sort of profiling to enable, if any.
// Default is `wasmtime.ProfilingStrategyNone`.
ProfilingStrategy wasmtime.ProfilingStrategy
}

// WithMaxWasmStack defines the maximum amount of stack space available for
// executing WebAssembly code.
//
// Default is 256 MiB.
func (c *ConfigBuilder) WithMaxWasmStack(max int) *ConfigBuilder {
c.MaxWasmStack = max
return c
}

// WithMultiValue enables modules that can return multiple values.
// ref. https://github.com/webassembly/multi-value
//
// Default is false.
func (c *ConfigBuilder) WithMultiValue(enable bool) *ConfigBuilder {
c.EnableWasmMultiValue = enable
return c
}

// WithBulkMemory enables `memory.copy` instruction, tables and passive data.
// ref. https://github.com/WebAssembly/bulk-memory-operations
//
// Default is false.
func (c *ConfigBuilder) WithBulkMemory(enable bool) *ConfigBuilder {
c.EnableBulkMemory = enable
return c
}

// WithReferenceTypes Enables the `externref` and `funcref` types as well as
// allowing a module to define multiple tables.
// ref. https://github.com/webassembly/reference-types
//
// Note: depends on bulk memory being enabled.
//
// Default is false.
func (c *ConfigBuilder) WithReferenceTypes(enable bool) *ConfigBuilder {
c.EnableWasmReferenceTypes = enable
return c
}

// WithSIMD enables SIMD instructions including v128.
// ref. https://github.com/webassembly/simd
//
// Default is false.
func (c *ConfigBuilder) WithSIMD(enable bool) *ConfigBuilder {
c.EnableWasmSIMD = enable
return c
}

// WithProfilingStrategy defines the profiling strategy used for defining the
// default profiler.
//
// Default is `wasmtime.ProfilingStrategyNone`.
func (c *ConfigBuilder) WithProfilingStrategy(strategy wasmtime.ProfilingStrategy) *ConfigBuilder {
c.ProfilingStrategy = strategy
return c
}

// WithDefaultCache enables the default caching strategy.
//
// Default is false.
func (c *ConfigBuilder) WithDefaultCache(enabled bool) *ConfigBuilder {
c.EnableDefaultCache = enabled
return c
}

func (c *ConfigBuilder) Build() (*Config, error) {
cfg := NewConfig()
cfg.SetWasmBulkMemory(c.EnableBulkMemory)
cfg.SetWasmMultiValue(c.EnableWasmMultiValue)
cfg.SetWasmReferenceTypes(c.EnableWasmReferenceTypes)
cfg.SetWasmSIMD(c.EnableWasmSIMD)
cfg.SetMaxWasmStack(c.MaxWasmStack)
cfg.SetProfiler(c.ProfilingStrategy)
if c.EnableDefaultCache {
if err := cfg.CacheConfigLoadDefault(); err != nil {
return nil, err
}
}

return cfg, nil
}
72 changes: 72 additions & 0 deletions x/programs/engine/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package engine

import (
"fmt"

"github.com/bytecodealliance/wasmtime-go/v14"
)

// Engine is a wrapper around a wasmtime.Engine and manages the lifecycle of a
// programs execution. It is expected that a single engine can have multiple
// stores. For example in the case of program to program calls.
type Engine struct {
wasmEngine *wasmtime.Engine
}

// New creates a new Wasm engine.
func New(cfg *Config) *Engine {
return &Engine{
wasmEngine: wasmtime.NewEngineWithConfig(cfg.Get()),
}
}

// Stop will increase the current epoch number by 1 within the current
// engine which will cause any connected stores to be interrupted.
func (e *Engine) Stop() {
e.wasmEngine.IncrementEpoch()
}

// PreCompileModule will deserialize a precompiled module.
func (e *Engine) PreCompileModule(bytes []byte) (*wasmtime.Module, error) {
// Note: that to deserialize successfully the bytes provided must have been
// produced with an `Engine` that has the same compilation options as the
// provided engine, and from the same version of this library.
//
// A precompile is not something we would store on chain.
// Instead we would prefetch programs and precompile them.
return wasmtime.NewModuleDeserialize(e.wasmEngine, bytes)
}

// CompileModule will compile a module.
func (e *Engine) CompileModule(bytes []byte) (*wasmtime.Module, error) {
return wasmtime.NewModule(e.wasmEngine, bytes)
}

// PreCompileWasm returns a precompiled wasm module.
//
// Note: these bytes can be deserialized by an `Engine` that has the same version.
// For that reason precompiled wasm modules should not be stored on chain.
func PreCompileWasmBytes(engine *Engine, programBytes []byte, limitMaxMemory uint32) ([]byte, error) {
store := NewStore(engine, NewStoreConfig().SetLimitMaxMemory(limitMaxMemory))
module, err := wasmtime.NewModule(store.GetEngine(), programBytes)
if err != nil {
return nil, err
}

return module.Serialize()
}

// NewModule creates a new wasmtime module and handles the Wasm bytes based on compile strategy.
func NewModule(engine *Engine, bytes []byte, strategy CompileStrategy) (*wasmtime.Module, error) {
switch strategy {
case CompileWasm:
return engine.CompileModule(bytes)
case PrecompiledWasm:
return engine.PreCompileModule(bytes)
default:
return nil, fmt.Errorf("unknown compile strategy")
}
}
Loading

0 comments on commit 1c21cfd

Please sign in to comment.