Go: syscall/js: request: want to convert slices from/to TypedArray directly

Created on 3 Jun 2019  路  9Comments  路  Source: golang/go

As TypedArray no longer exists as of Go 1.13, there is no way to convert slices other than []byte from/to TypedArray objects other than Uint8Array. It is technically possible to do this by using encoding/binary, but I'm worried this conversion is not efficient.

As there are Web APIs that take such TypedArray objects (e.g., AudioBuffer.copyToChannel takes a Float32Array [1]), it would be very useful if syscall/js had such conversion functions.

CC @neelance

[1] https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer/copyToChannel

Arch-Wasm FeatureRequest NeedsInvestigation

Most helpful comment

@dennwc Thanks. Now I've succeeded to do what I wanted:

func sliceToByteSlice(s interface{}) []byte {
        switch s := s.(type) {
        case []int8:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                return *(*[]byte)(unsafe.Pointer(h))
        case []int16:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 2
                h.Cap *= 2
                return *(*[]byte)(unsafe.Pointer(h))
        case []int32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []int64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint8:
                return s
        case []uint16:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 2
                h.Cap *= 2
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        case []float32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []float64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        default:
                panic(fmt.Sprintf("jsutil: unexpected value at sliceToBytesSlice: %T", s))
        }
}

func SliceToTypedArray(s interface{}) js.Value {
        switch s := s.(type) {
        case []int8:
                a := js.Global().Get("Uint8Array").New(len(s))
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int8Array").New(buf, a.Get("byteOffset"), a.Get("byteLength"))
        case []int16:
                a := js.Global().Get("Uint8Array").New(len(s) * 2)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
        case []int32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []uint8:
                a := js.Global().Get("Uint8Array").New(len(s))
                js.CopyBytesToJS(a, s)
                runtime.KeepAlive(s)
                return a
        case []uint16:
                a := js.Global().Get("Uint8Array").New(len(s) * 2)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Uint16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
        case []uint32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Uint32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []float32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []float64:
                a := js.Global().Get("Uint8Array").New(len(s) * 8)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Float64Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/8)
        default:
                panic(fmt.Sprintf("jsutil: unexpected value at SliceToTypedArray: %T", s))
        }
}

All 9 comments

A possible workaround, for now, is to cast slices with unsafe and reflect.SliceHeader.

@dennwc Thanks. Now I've succeeded to do what I wanted:

func sliceToByteSlice(s interface{}) []byte {
        switch s := s.(type) {
        case []int8:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                return *(*[]byte)(unsafe.Pointer(h))
        case []int16:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 2
                h.Cap *= 2
                return *(*[]byte)(unsafe.Pointer(h))
        case []int32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []int64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint8:
                return s
        case []uint16:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 2
                h.Cap *= 2
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []uint64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        case []float32:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 4
                h.Cap *= 4
                return *(*[]byte)(unsafe.Pointer(h))
        case []float64:
                h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
                h.Len *= 8
                h.Cap *= 8
                return *(*[]byte)(unsafe.Pointer(h))
        default:
                panic(fmt.Sprintf("jsutil: unexpected value at sliceToBytesSlice: %T", s))
        }
}

func SliceToTypedArray(s interface{}) js.Value {
        switch s := s.(type) {
        case []int8:
                a := js.Global().Get("Uint8Array").New(len(s))
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int8Array").New(buf, a.Get("byteOffset"), a.Get("byteLength"))
        case []int16:
                a := js.Global().Get("Uint8Array").New(len(s) * 2)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
        case []int32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Int32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []uint8:
                a := js.Global().Get("Uint8Array").New(len(s))
                js.CopyBytesToJS(a, s)
                runtime.KeepAlive(s)
                return a
        case []uint16:
                a := js.Global().Get("Uint8Array").New(len(s) * 2)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Uint16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
        case []uint32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Uint32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []float32:
                a := js.Global().Get("Uint8Array").New(len(s) * 4)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
        case []float64:
                a := js.Global().Get("Uint8Array").New(len(s) * 8)
                js.CopyBytesToJS(a, sliceToByteSlice(s))
                runtime.KeepAlive(s)
                buf := a.Get("buffer")
                return js.Global().Get("Float64Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/8)
        default:
                panic(fmt.Sprintf("jsutil: unexpected value at SliceToTypedArray: %T", s))
        }
}

@hajimehoshi Is this available as a library somewhere?

No so far. Ebiten has an internal package though: https://github.com/hajimehoshi/ebiten/tree/master/internal/jsutil

I resorted to patching wasm_exec.js with:

const loadFloatSlice = (addr) => {
    const array = getInt64(addr + 0);
    const len = getInt64(addr + 8);
    return new Float32Array(this._inst.exports.mem.buffer, array, len);
}
...
"syscall/js.copyBytesToJS": (sp) => {
    const dst = loadValue(sp + 8);
    let src;
    if (dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) {
        src = loadSlice(sp + 16);
    } else if (dst instanceof Float32Array) {
        src = loadFloatSlice(sp + 16)
    } else {
        this.mem.setUint8(sp + 48, 0);
        return;
    }
    const toCopy = src.subarray(0, dst.length);
    dst.set(toCopy);
    setInt64(sp + 40, toCopy.length);
    this.mem.setUint8(sp + 48, 1);
},

These methods get called quite often in my WebGL game and I can't afford indirection or extra syscalls used here:
https://github.com/hajimehoshi/ebiten/blob/982a68e5a21f3b2dff494132b34639084fd3e9ac/internal/jsutil/go113_js.go#L104

Looking for something similar to be included in the standard syscall/js library (supporting all other typed arrays too, not just Float32 which I am currently restricting myself to). An API like CopyFloat32sToJS would be perfect!

@finnbear For performance critical code you probably want to reuse a single buffer anyways. So first create a Uint8Array and Float32Array that share the same buffer and then use copyBytesToJS with sliceToByteSlice (see above) multiple times. This should have no additional overhead.

@hajimehoshi @finnbear Are you both happy with what is already possible? Shall we close this issue?

@neelance I'm fine with CopyBytesToJS as-is, mostly because the option to use multiple typed arrays referencing the same buffer is available. I highly suggest you document that, although I'm still using the hack I showed above because my application commonly copies float32s and bytes. I'm fine with this issue being closed 馃憤

I'm fine with closing this issue.

Was this page helpful?
0 / 5 - 0 ratings