cuelang.org/go@v0.13.0/cue/interpreter/wasm/runtime.go (about)

     1  // Copyright 2023 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package wasm
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"os"
    22  	"sync"
    23  
    24  	"github.com/tetratelabs/wazero"
    25  	"github.com/tetratelabs/wazero/api"
    26  	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
    27  )
    28  
    29  // A runtime is a Wasm runtime that can compile, load, and execute
    30  // Wasm code.
    31  type runtime struct {
    32  	// ctx exists so that we have something to pass to Wazero
    33  	// functions, but it's unused otherwise.
    34  	ctx context.Context
    35  
    36  	wazero.Runtime
    37  }
    38  
    39  func newRuntime() runtime {
    40  	ctx := context.Background()
    41  	r := wazero.NewRuntime(ctx)
    42  	wasi_snapshot_preview1.MustInstantiate(ctx, r)
    43  
    44  	return runtime{
    45  		ctx:     ctx,
    46  		Runtime: r,
    47  	}
    48  }
    49  
    50  // compile takes the name of a Wasm module, and returns its compiled
    51  // form, or an error.
    52  func (r *runtime) compile(name string) (*module, error) {
    53  	buf, err := os.ReadFile(name)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("can't compile Wasm module: %w", err)
    56  	}
    57  
    58  	mod, err := r.Runtime.CompileModule(r.ctx, buf)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("can't compile Wasm module: %w", err)
    61  	}
    62  	return &module{
    63  		runtime:        r,
    64  		name:           name,
    65  		CompiledModule: mod,
    66  	}, nil
    67  }
    68  
    69  // compileAndLoad is a convenience method that compiles a module then
    70  // loads it into memory returning the loaded instance, or an error.
    71  func (r *runtime) compileAndLoad(name string) (*instance, error) {
    72  	m, err := r.compile(name)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	i, err := m.load()
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	return i, nil
    81  }
    82  
    83  // A module is a compiled Wasm module.
    84  type module struct {
    85  	*runtime
    86  	name string
    87  	wazero.CompiledModule
    88  }
    89  
    90  // load loads the compiled module into memory, returning a new instance
    91  // that can be called into, or an error. Different instances of the
    92  // same module do not share memory.
    93  func (m *module) load() (*instance, error) {
    94  	cfg := wazero.NewModuleConfig().WithName(m.name)
    95  	wInst, err := m.Runtime.InstantiateModule(m.ctx, m.CompiledModule, cfg)
    96  	if err != nil {
    97  		return nil, fmt.Errorf("can't instantiate Wasm module: %w", err)
    98  	}
    99  
   100  	inst := instance{
   101  		module:   m,
   102  		instance: wInst,
   103  		alloc:    wInst.ExportedFunction("allocate"),
   104  		free:     wInst.ExportedFunction("deallocate"),
   105  	}
   106  	return &inst, nil
   107  }
   108  
   109  // An instance is a Wasm module loaded into memory.
   110  type instance struct {
   111  	// mu serializes access the whole struct.
   112  	mu sync.Mutex
   113  
   114  	*module
   115  	instance api.Module
   116  
   117  	// alloc is a guest function that allocates guest memory on
   118  	// behalf of the host.
   119  	alloc api.Function
   120  
   121  	// free is a guest function that frees guest memory on
   122  	// behalf of the host.
   123  	free api.Function
   124  }
   125  
   126  // load attempts to load the named function from the instance, returning
   127  // it if found, or an error.
   128  func (i *instance) load(funcName string) (api.Function, error) {
   129  	i.mu.Lock()
   130  	defer i.mu.Unlock()
   131  
   132  	f := i.instance.ExportedFunction(funcName)
   133  	if f == nil {
   134  		return nil, fmt.Errorf("can't find function %q in Wasm module %v", funcName, i.module.Name())
   135  	}
   136  	return f, nil
   137  }
   138  
   139  // Alloc returns a reference to newly allocated guest memory that spans
   140  // the provided size.
   141  func (i *instance) Alloc(size uint32) (*memory, error) {
   142  	i.mu.Lock()
   143  	defer i.mu.Unlock()
   144  
   145  	res, err := i.alloc.Call(i.ctx, uint64(size))
   146  	if err != nil {
   147  		return nil, fmt.Errorf("can't allocate memory: requested %d bytes", size)
   148  	}
   149  	return &memory{
   150  		i:   i,
   151  		ptr: uint32(res[0]),
   152  		len: size,
   153  	}, nil
   154  }
   155  
   156  // Free frees previously allocated guest memory.
   157  func (i *instance) Free(m *memory) {
   158  	i.mu.Lock()
   159  	defer i.mu.Unlock()
   160  
   161  	i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len))
   162  }
   163  
   164  // Free frees several previously allocated guest memories.
   165  func (i *instance) FreeAll(ms []*memory) {
   166  	i.mu.Lock()
   167  	defer i.mu.Unlock()
   168  
   169  	for _, m := range ms {
   170  		i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len))
   171  	}
   172  }
   173  
   174  // memory is a read and write reference to guest memory that the host
   175  // requested.
   176  type memory struct {
   177  	i   *instance
   178  	ptr uint32
   179  	len uint32
   180  }
   181  
   182  // Bytes return a copy of the contents of the guest memory to the host.
   183  func (m *memory) Bytes() []byte {
   184  	m.i.mu.Lock()
   185  	defer m.i.mu.Unlock()
   186  
   187  	p, ok := m.i.instance.Memory().Read(m.ptr, m.len)
   188  	if !ok {
   189  		panic(fmt.Sprintf("can't read %d bytes from Wasm address %#x", m.len, m.ptr))
   190  	}
   191  	return bytes.Clone(p)
   192  }
   193  
   194  // WriteAt writes p at the given relative offset within m.
   195  // It panics if buf doesn't fit into m, or if off is out of bounds.
   196  func (m *memory) WriteAt(p []byte, off int64) (int, error) {
   197  	if (off < 0) || (off >= 1<<32-1) {
   198  		panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr))
   199  	}
   200  
   201  	m.i.mu.Lock()
   202  	defer m.i.mu.Unlock()
   203  
   204  	ok := m.i.instance.Memory().Write(m.ptr+uint32(off), p)
   205  	if !ok {
   206  		panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr))
   207  	}
   208  	return len(p), nil
   209  }
   210  
   211  // Args returns a memory in the form of pair of arguments directly
   212  // passable to Wasm.
   213  func (m *memory) Args() []uint64 {
   214  	return []uint64{uint64(m.ptr), uint64(m.len)}
   215  }