go-libwebp

Experimental translation from libwebp to Go source.
Log | Files | Refs | README | LICENSE

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) }