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 }