github.com/tetratelabs/wazero@v1.7.1/experimental/features_example_test.go (about)

     1  package experimental_test
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"log"
     8  	"runtime"
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	"github.com/tetratelabs/wazero"
    13  	"github.com/tetratelabs/wazero/api"
    14  	"github.com/tetratelabs/wazero/experimental"
    15  	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
    16  )
    17  
    18  // pthreadWasm was generated by the following:
    19  //
    20  //	docker run -it --rm -v `pwd`/testdata:/workspace ghcr.io/webassembly/wasi-sdk:wasi-sdk-20 sh -c '$CC -o /workspace/pthread.wasm /workspace/pthread.c --target=wasm32-wasi-threads --sysroot=/wasi-sysroot -pthread -mexec-model=reactor -Wl,--export=run -Wl,--export=get'
    21  //
    22  // TODO: Use zig cc instead of wasi-sdk to compile when it supports wasm32-wasi-threads
    23  // https://github.com/ziglang/zig/issues/15484
    24  //
    25  //go:embed testdata/pthread.wasm
    26  var pthreadWasm []byte
    27  
    28  //go:embed testdata/memory.wasm
    29  var memoryWasm []byte
    30  
    31  // This shows how to use a WebAssembly module compiled with the threads feature with wasi sdk.
    32  func ExampleCoreFeaturesThreads() {
    33  	// Use a default context
    34  	ctx := context.Background()
    35  
    36  	// Threads support must be enabled explicitly in addition to standard V2 features.
    37  	cfg := wazero.NewRuntimeConfig().WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads)
    38  
    39  	r := wazero.NewRuntimeWithConfig(ctx, cfg)
    40  	defer r.Close(ctx)
    41  
    42  	wasmCompiled, err := r.CompileModule(ctx, pthreadWasm)
    43  	if err != nil {
    44  		log.Panicln(err)
    45  	}
    46  
    47  	// Because we are using wasi-sdk to compile the guest, we must initialize WASI.
    48  	wasi_snapshot_preview1.MustInstantiate(ctx, r)
    49  
    50  	if _, err := r.InstantiateWithConfig(ctx, memoryWasm, wazero.NewModuleConfig().WithName("env")); err != nil {
    51  		log.Panicln(err)
    52  	}
    53  
    54  	mod, err := r.InstantiateModule(ctx, wasmCompiled, wazero.NewModuleConfig().WithStartFunctions("_initialize"))
    55  	if err != nil {
    56  		log.Panicln(err)
    57  	}
    58  
    59  	// Channel to synchronize start of goroutines before running.
    60  	startCh := make(chan struct{})
    61  	// Channel to synchronize end of goroutines.
    62  	endCh := make(chan struct{})
    63  
    64  	// We start up 8 goroutines and run for 100000 iterations each. The count should reach
    65  	// 800000, at the end, but it would not if threads weren't working!
    66  	for i := 0; i < 8; i++ {
    67  		go func() {
    68  			defer func() { endCh <- struct{}{} }()
    69  			<-startCh
    70  
    71  			// We must instantiate a child per simultaneous thread. This should normally be pooled
    72  			// among arbitrary goroutine invocations.
    73  			child := createChildModule(r, mod, wasmCompiled)
    74  			fn := child.mod.ExportedFunction("run")
    75  			for i := 0; i < 100000; i++ {
    76  				_, err := fn.Call(ctx)
    77  				if err != nil {
    78  					log.Panicln(err)
    79  				}
    80  			}
    81  			runtime.KeepAlive(child)
    82  		}()
    83  	}
    84  	for i := 0; i < 8; i++ {
    85  		startCh <- struct{}{}
    86  	}
    87  	for i := 0; i < 8; i++ {
    88  		<-endCh
    89  	}
    90  
    91  	res, err := mod.ExportedFunction("get").Call(ctx)
    92  	if err != nil {
    93  		log.Panicln(err)
    94  	}
    95  	fmt.Println(res[0])
    96  	// Output: 800000
    97  }
    98  
    99  type childModule struct {
   100  	mod        api.Module
   101  	tlsBasePtr uint32
   102  }
   103  
   104  var prevTID uint32
   105  
   106  var childModuleMu sync.Mutex
   107  
   108  // wasi sdk maintains a stack per thread within memory, so we must allocate one separately per child
   109  // module, corresponding to a host thread, or the stack accesses would collide. wasi sdk does not
   110  // currently plan to implement this so we must implement it ourselves. We allocate memory for a stack,
   111  // initialize a pthread struct at the beginning of the stack, and set globals to reference it.
   112  // https://github.com/WebAssembly/wasi-threads/issues/45
   113  func createChildModule(rt wazero.Runtime, root api.Module, wasmCompiled wazero.CompiledModule) *childModule {
   114  	childModuleMu.Lock()
   115  	defer childModuleMu.Unlock()
   116  
   117  	ctx := context.Background()
   118  
   119  	// Not executing function so the current stack pointer is end of stack
   120  	stackPointer := root.ExportedGlobal("__stack_pointer").Get()
   121  	tlsBase := root.ExportedGlobal("__tls_base").Get()
   122  
   123  	// Thread-local-storage for the main thread is from __tls_base to __stack_pointer
   124  	size := stackPointer - tlsBase
   125  
   126  	malloc := root.ExportedFunction("malloc")
   127  
   128  	// Allocate memory for the child thread stack. We go ahead and use the same size
   129  	// as the root stack.
   130  	res, err := malloc.Call(ctx, size)
   131  	if err != nil {
   132  		panic(err)
   133  	}
   134  	ptr := uint32(res[0])
   135  
   136  	child, err := rt.InstantiateModule(ctx, wasmCompiled, wazero.NewModuleConfig().
   137  		// Don't need to execute start functions again in child, it crashes anyways because
   138  		// LLVM only allows calling them once.
   139  		WithStartFunctions())
   140  	if err != nil {
   141  		panic(err)
   142  	}
   143  
   144  	// Call LLVM's TLS initialiation.
   145  	initTLS := child.ExportedFunction("__wasm_init_tls")
   146  	if _, err := initTLS.Call(ctx, uint64(ptr)); err != nil {
   147  		panic(err)
   148  	}
   149  
   150  	// Populate the stack pointer and thread ID into the pthread struct at the beginning of stack.
   151  	// This is relying on libc implementation details. The structure has been stable for a long time
   152  	// though it is possible it could change if compiling with a different version of wasi sdk.
   153  	tid := atomic.AddUint32(&prevTID, 1)
   154  	child.Memory().WriteUint32Le(ptr, ptr)
   155  	child.Memory().WriteUint32Le(ptr+20, tid)
   156  	child.ExportedGlobal("__stack_pointer").(api.MutableGlobal).Set(uint64(ptr) + size)
   157  
   158  	ret := &childModule{
   159  		mod:        child,
   160  		tlsBasePtr: ptr,
   161  	}
   162  
   163  	// Set a finalizer to clear the allocated stack if the module gets collected.
   164  	runtime.SetFinalizer(ret, func(obj interface{}) {
   165  		cm := obj.(*childModule)
   166  		free := cm.mod.ExportedFunction("free")
   167  		// Ignore errors since runtime may have been closed before this is called.
   168  		_, _ = free.Call(ctx, uint64(cm.tlsBasePtr))
   169  		_ = cm.mod.Close(context.Background())
   170  	})
   171  	return ret
   172  }