lib.go (8961B)
1 // Package webp provides a libwebp backend that runs the codec as a 2 // WebAssembly module under wazero. 3 // 4 // libwebp is compiled once to wasm32-wasi (see tools/build-wasm.sh) and 5 // embedded here, so a single artifact serves every GOOS/GOARCH combination 6 // rather than one transpiled Go file per target. 7 package webp 8 9 import ( 10 "context" 11 _ "embed" 12 "fmt" 13 "image" 14 "io" 15 "sync" 16 17 "github.com/tetratelabs/wazero" 18 "github.com/tetratelabs/wazero/api" 19 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 20 ) 21 22 //go:embed libwebp.wasm 23 var moduleBytes []byte 24 25 var ( 26 initOnce sync.Once 27 initErr error 28 embedded *Compiled 29 ) 30 31 // Compiled is a module compiled to machine code: the expensive, immutable, 32 // shareable half. Instantiating it is cheap by comparison. 33 type Compiled struct { 34 runtime wazero.Runtime 35 compiled wazero.CompiledModule 36 } 37 38 // CompileBytes compiles an arbitrary libwebp wasm module. It exists so 39 // modules built by different toolchains can be compared side by side; most 40 // callers want the embedded one via Init. 41 func CompileBytes(ctx context.Context, wasm []byte) (*Compiled, error) { 42 // The codec touches no host resources, so the compiler needs no 43 // filesystem or clock access. WASI is instantiated only because 44 // wasi-libc's abort path imports proc_exit. 45 rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig()) 46 if _, err := wasi_snapshot_preview1.Instantiate(ctx, rt); err != nil { 47 return nil, fmt.Errorf("instantiating wasi: %w", err) 48 } 49 50 // Emscripten's standalone output imports a single memory-growth callback 51 // that only matters to its JS glue. wasi-sdk builds import nothing here, 52 // so defining it unconditionally costs them nothing. 53 if _, err := rt.NewHostModuleBuilder("env"). 54 NewFunctionBuilder(). 55 WithFunc(func(context.Context, uint32) {}). 56 Export("emscripten_notify_memory_growth"). 57 Instantiate(ctx); err != nil { 58 return nil, fmt.Errorf("instantiating env stub: %w", err) 59 } 60 cm, err := rt.CompileModule(ctx, wasm) 61 if err != nil { 62 return nil, fmt.Errorf("compiling module: %w", err) 63 } 64 return &Compiled{runtime: rt, compiled: cm}, nil 65 } 66 67 // Init compiles the embedded module. It is safe to call repeatedly; the 68 // compile happens once. This is the expensive step (machine code generation), 69 // as distinct from instantiation, which allocates a fresh linear memory. 70 func Init() error { 71 initOnce.Do(func() { 72 embedded, initErr = CompileBytes(context.Background(), moduleBytes) 73 }) 74 return initErr 75 } 76 77 // instance is a single module instantiation: one linear memory, one copy of 78 // libwebp's globals, one shadow stack. Exactly one call may be in flight. 79 type instance struct { 80 mod api.Module 81 mem api.Memory 82 83 malloc api.Function 84 free api.Function 85 encodeRGBA api.Function 86 encodeLossless api.Function 87 decodeRGBA api.Function 88 webpFree api.Function 89 } 90 91 func newInstance(ctx context.Context) (*instance, error) { 92 if err := Init(); err != nil { 93 return nil, err 94 } 95 return embedded.newInstance(ctx) 96 } 97 98 func (c *Compiled) newInstance(ctx context.Context) (*instance, error) { 99 // Anonymous: not registered in the runtime's module namespace, so 100 // dropping it does not leak a reference. 101 mod, err := c.runtime.InstantiateModule(ctx, c.compiled, 102 wazero.NewModuleConfig().WithName("")) 103 if err != nil { 104 return nil, fmt.Errorf("instantiating module: %w", err) 105 } 106 i := &instance{ 107 mod: mod, 108 mem: mod.Memory(), 109 malloc: mod.ExportedFunction("malloc"), 110 free: mod.ExportedFunction("free"), 111 encodeRGBA: mod.ExportedFunction("WebPEncodeRGBA"), 112 encodeLossless: mod.ExportedFunction("WebPEncodeLosslessRGBA"), 113 decodeRGBA: mod.ExportedFunction("WebPDecodeRGBA"), 114 webpFree: mod.ExportedFunction("WebPFree"), 115 } 116 for name, fn := range map[string]api.Function{ 117 "malloc": i.malloc, "free": i.free, 118 "WebPEncodeRGBA": i.encodeRGBA, "WebPEncodeLosslessRGBA": i.encodeLossless, 119 "WebPDecodeRGBA": i.decodeRGBA, "WebPFree": i.webpFree, 120 } { 121 if fn == nil { 122 mod.Close(ctx) 123 return nil, fmt.Errorf("module missing export: %s", name) 124 } 125 } 126 return i, nil 127 } 128 129 func (i *instance) close(ctx context.Context) { i.mod.Close(ctx) } 130 131 // alloc reserves n bytes of guest memory and returns the offset. 132 func (i *instance) alloc(ctx context.Context, n int) (uint32, error) { 133 res, err := i.malloc.Call(ctx, uint64(n)) 134 if err != nil { 135 return 0, fmt.Errorf("guest malloc: %w", err) 136 } 137 if uint32(res[0]) == 0 { 138 return 0, fmt.Errorf("guest malloc: out of memory (%d bytes)", n) 139 } 140 return uint32(res[0]), nil 141 } 142 143 func (i *instance) readU32(off uint32) uint32 { 144 v, _ := i.mem.ReadUint32Le(off) 145 return v 146 } 147 148 func (i *instance) encode(ctx context.Context, m *image.NRGBA, q float32) ([]byte, error) { 149 b := m.Bounds() 150 // Guest pointers are 32-bit; an out-param needs 4 bytes of scratch. 151 in, err := i.alloc(ctx, len(m.Pix)) 152 if err != nil { 153 return nil, err 154 } 155 defer i.free.Call(ctx, uint64(in)) 156 157 outPtr, err := i.alloc(ctx, 4) 158 if err != nil { 159 return nil, err 160 } 161 defer i.free.Call(ctx, uint64(outPtr)) 162 163 if !i.mem.Write(in, m.Pix) { 164 return nil, fmt.Errorf("writing pixels into guest memory") 165 } 166 167 var res []uint64 168 if q >= 1.0 { 169 res, err = i.encodeLossless.Call(ctx, 170 uint64(in), uint64(b.Dx()), uint64(b.Dy()), uint64(m.Stride), uint64(outPtr)) 171 } else { 172 res, err = i.encodeRGBA.Call(ctx, 173 uint64(in), uint64(b.Dx()), uint64(b.Dy()), uint64(m.Stride), 174 uint64(api.EncodeF32(q*100)), uint64(outPtr)) 175 } 176 if err != nil { 177 return nil, fmt.Errorf("WebPEncode: %w", err) 178 } 179 180 size := uint32(res[0]) 181 if size == 0 { 182 return nil, fmt.Errorf("empty result") 183 } 184 out := i.readU32(outPtr) 185 if out == 0 { 186 return nil, fmt.Errorf("failed to allocate output buffer") 187 } 188 defer i.webpFree.Call(ctx, uint64(out)) 189 190 buf, ok := i.mem.Read(out, size) 191 if !ok { 192 return nil, fmt.Errorf("reading encoded data out of guest memory") 193 } 194 // mem.Read aliases the guest memory; copy before it is reused or grown. 195 cp := make([]byte, size) 196 copy(cp, buf) 197 return cp, nil 198 } 199 200 func (i *instance) decode(ctx context.Context, data []byte) (image.Image, error) { 201 in, err := i.alloc(ctx, len(data)) 202 if err != nil { 203 return nil, err 204 } 205 defer i.free.Call(ctx, uint64(in)) 206 207 dims, err := i.alloc(ctx, 8) // int width, int height 208 if err != nil { 209 return nil, err 210 } 211 defer i.free.Call(ctx, uint64(dims)) 212 213 if !i.mem.Write(in, data) { 214 return nil, fmt.Errorf("writing webp data into guest memory") 215 } 216 217 res, err := i.decodeRGBA.Call(ctx, 218 uint64(in), uint64(len(data)), uint64(dims), uint64(dims+4)) 219 if err != nil { 220 return nil, fmt.Errorf("WebPDecodeRGBA: %w", err) 221 } 222 223 samples := uint32(res[0]) 224 if samples == 0 { 225 return nil, fmt.Errorf("failed decoding webp into rgba") 226 } 227 defer i.webpFree.Call(ctx, uint64(samples)) 228 229 w, h := int(i.readU32(dims)), int(i.readU32(dims+4)) 230 raw, ok := i.mem.Read(samples, uint32(w*h*4)) 231 if !ok { 232 return nil, fmt.Errorf("reading pixels out of guest memory") 233 } 234 pix := make([]uint8, w*h*4) 235 copy(pix, raw) 236 237 return &image.NRGBA{ 238 Pix: pix, 239 Rect: image.Rectangle{Max: image.Point{X: w, Y: h}}, 240 Stride: w * 4, 241 }, nil 242 } 243 244 // EncodeImpl encodes m, instantiating a fresh module for the call. 245 func EncodeImpl(w io.Writer, m *image.NRGBA, quality float32) error { 246 ctx := context.Background() 247 i, err := newInstance(ctx) 248 if err != nil { 249 return err 250 } 251 defer i.close(ctx) 252 buf, err := i.encode(ctx, m, quality) 253 if err != nil { 254 return err 255 } 256 _, err = w.Write(buf) 257 return err 258 } 259 260 // DecodeImpl decodes buf, instantiating a fresh module for the call. 261 func DecodeImpl(buf []byte) (image.Image, error) { 262 ctx := context.Background() 263 i, err := newInstance(ctx) 264 if err != nil { 265 return nil, err 266 } 267 defer i.close(ctx) 268 return i.decode(ctx, buf) 269 } 270 271 // Instance is a reusable module instantiation. Exactly one call may be in 272 // flight on it at a time; callers needing concurrency should keep a pool. 273 // It exists so the cost of instantiation can be separated from the cost of 274 // the codec itself. 275 type Instance struct{ inner *instance } 276 277 // NewInstance instantiates this compiled module. 278 func (c *Compiled) NewInstance(ctx context.Context) (*Instance, error) { 279 i, err := c.newInstance(ctx) 280 if err != nil { 281 return nil, err 282 } 283 return &Instance{inner: i}, nil 284 } 285 286 func NewInstance(ctx context.Context) (*Instance, error) { 287 i, err := newInstance(ctx) 288 if err != nil { 289 return nil, err 290 } 291 return &Instance{inner: i}, nil 292 } 293 294 func (i *Instance) Encode(ctx context.Context, m *image.NRGBA, q float32) ([]byte, error) { 295 return i.inner.encode(ctx, m, q) 296 } 297 298 func (i *Instance) Decode(ctx context.Context, data []byte) (image.Image, error) { 299 return i.inner.decode(ctx, data) 300 } 301 302 // MemorySize reports the instance's current linear memory in bytes. It only 303 // ever grows, which is what makes pooled instances worth evicting. 304 func (i *Instance) MemorySize() uint32 { return i.inner.mem.Size() } 305 306 func (i *Instance) Close(ctx context.Context) { i.inner.close(ctx) }