]> Cypherpunks.ru repositories - gostls13.git/commitdiff
syscall/js: use stable references to JavaScript values
authorRichard Musiol <mail@richard-musiol.de>
Sun, 24 Jun 2018 15:23:38 +0000 (17:23 +0200)
committerPaul Jolly <paul@myitcv.org.uk>
Tue, 26 Jun 2018 16:40:09 +0000 (16:40 +0000)
This commit changes how JavaScript values are referenced by Go code.
After this change, a JavaScript value is always represented by the same
ref, even if passed multiple times from JavaScript to Go. This allows
Go's == operator to work as expected on js.Value (strict equality).
Additionally, the performance of some operations of the syscall/js
package got improved by saving additional roundtrips to JavaScript code.

Fixes #25802.

Change-Id: Ide6ffe66c6aa1caf5327a2d3ddbe48fe7c180461
Reviewed-on: https://go-review.googlesource.com/120561
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
misc/wasm/wasm_exec.js
src/cmd/vet/all/whitelist/wasm.txt
src/syscall/js/js.go
src/syscall/js/js_js.s
src/syscall/js/js_test.go

index 4c29109766de870a0dfd0184cc8a4c6d5e41e819..ecb096509fb7ebdac281a97405c86201bffbba32 100644 (file)
@@ -31,7 +31,7 @@
 
                let outputBuf = "";
                global.fs = {
-                       constants: {},
+                       constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_NONBLOCK: -1, O_SYNC: -1 }, // unused
                        writeSync(fd, buf) {
                                outputBuf += decoder.decode(buf);
                                const nl = outputBuf.lastIndexOf("\n");
                        }
 
                        const loadValue = (addr) => {
+                               const f = mem().getFloat64(addr, true);
+                               if (!isNaN(f)) {
+                                       return f;
+                               }
+
                                const id = mem().getUint32(addr, true);
                                return this._values[id];
                        }
 
                        const storeValue = (addr, v) => {
-                               if (v === undefined) {
-                                       mem().setUint32(addr, 0, true);
+                               if (typeof v === "number") {
+                                       if (isNaN(v)) {
+                                               mem().setUint32(addr + 4, 0x7FF80000, true); // NaN
+                                               mem().setUint32(addr, 0, true);
+                                               return;
+                                       }
+                                       mem().setFloat64(addr, v, true);
                                        return;
                                }
-                               if (v === null) {
-                                       mem().setUint32(addr, 1, true);
+
+                               mem().setUint32(addr + 4, 0x7FF80000, true); // NaN
+
+                               switch (v) {
+                                       case undefined:
+                                               mem().setUint32(addr, 1, true);
+                                               return;
+                                       case null:
+                                               mem().setUint32(addr, 2, true);
+                                               return;
+                                       case true:
+                                               mem().setUint32(addr, 3, true);
+                                               return;
+                                       case false:
+                                               mem().setUint32(addr, 4, true);
+                                               return;
+                               }
+
+                               if (typeof v === "string") {
+                                       let ref = this._stringRefs.get(v);
+                                       if (ref === undefined) {
+                                               ref = this._values.length;
+                                               this._values.push(v);
+                                               this._stringRefs.set(v, ref);
+                                       }
+                                       mem().setUint32(addr, ref, true);
                                        return;
                                }
-                               this._values.push(v);
-                               mem().setUint32(addr, this._values.length - 1, true);
+
+                               if (typeof v === "symbol") {
+                                       let ref = this._symbolRefs.get(v);
+                                       if (ref === undefined) {
+                                               ref = this._values.length;
+                                               this._values.push(v);
+                                               this._symbolRefs.set(v, ref);
+                                       }
+                                       mem().setUint32(addr, ref, true);
+                                       return;
+                               }
+
+                               let ref = v[this._refProp];
+                               if (ref === undefined) {
+                                       ref = this._values.length;
+                                       this._values.push(v);
+                                       v[this._refProp] = ref;
+                               }
+                               mem().setUint32(addr, ref, true);
                        }
 
                        const loadSlice = (addr) => {
                                const len = getInt64(addr + 8);
                                const a = new Array(len);
                                for (let i = 0; i < len; i++) {
-                                       const id = mem().getUint32(array + i * 4, true);
-                                       a[i] = this._values[id];
+                                       a[i] = loadValue(array + i * 8);
                                }
                                return a;
                        }
                                                crypto.getRandomValues(loadSlice(sp + 8));
                                        },
 
-                                       // func boolVal(value bool) ref
-                                       "syscall/js.boolVal": (sp) => {
-                                               storeValue(sp + 16, mem().getUint8(sp + 8) !== 0);
-                                       },
-
-                                       // func intVal(value int) ref
-                                       "syscall/js.intVal": (sp) => {
-                                               storeValue(sp + 16, getInt64(sp + 8));
-                                       },
-
-                                       // func floatVal(value float64) ref
-                                       "syscall/js.floatVal": (sp) => {
-                                               storeValue(sp + 16, mem().getFloat64(sp + 8, true));
-                                       },
-
                                        // func stringVal(value string) ref
                                        "syscall/js.stringVal": (sp) => {
                                                storeValue(sp + 24, loadString(sp + 8));
                                                        const m = Reflect.get(v, loadString(sp + 16));
                                                        const args = loadSliceOfValues(sp + 32);
                                                        storeValue(sp + 56, Reflect.apply(m, v, args));
-                                                       mem().setUint8(sp + 60, 1);
+                                                       mem().setUint8(sp + 64, 1);
                                                } catch (err) {
                                                        storeValue(sp + 56, err);
-                                                       mem().setUint8(sp + 60, 0);
+                                                       mem().setUint8(sp + 64, 0);
                                                }
                                        },
 
                                                        const v = loadValue(sp + 8);
                                                        const args = loadSliceOfValues(sp + 16);
                                                        storeValue(sp + 40, Reflect.apply(v, undefined, args));
-                                                       mem().setUint8(sp + 44, 1);
+                                                       mem().setUint8(sp + 48, 1);
                                                } catch (err) {
                                                        storeValue(sp + 40, err);
-                                                       mem().setUint8(sp + 44, 0);
+                                                       mem().setUint8(sp + 48, 0);
                                                }
                                        },
 
                                                        const v = loadValue(sp + 8);
                                                        const args = loadSliceOfValues(sp + 16);
                                                        storeValue(sp + 40, Reflect.construct(v, args));
-                                                       mem().setUint8(sp + 44, 1);
+                                                       mem().setUint8(sp + 48, 1);
                                                } catch (err) {
                                                        storeValue(sp + 40, err);
-                                                       mem().setUint8(sp + 44, 0);
+                                                       mem().setUint8(sp + 48, 0);
                                                }
                                        },
 
-                                       // func valueFloat(v ref) float64
-                                       "syscall/js.valueFloat": (sp) => {
-                                               mem().setFloat64(sp + 16, parseFloat(loadValue(sp + 8)), true);
-                                       },
-
-                                       // func valueInt(v ref) int
-                                       "syscall/js.valueInt": (sp) => {
-                                               setInt64(sp + 16, parseInt(loadValue(sp + 8)));
-                                       },
-
-                                       // func valueBool(v ref) bool
-                                       "syscall/js.valueBool": (sp) => {
-                                               mem().setUint8(sp + 16, !!loadValue(sp + 8));
-                                       },
-
                                        // func valueLength(v ref) int
                                        "syscall/js.valueLength": (sp) => {
                                                setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
 
                                        // func valueInstanceOf(v ref, t ref) bool
                                        "syscall/js.valueInstanceOf": (sp) => {
-                                               mem().setUint8(sp + 16, loadValue(sp + 8) instanceof loadValue(sp + 12));
+                                               mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
                                        },
 
                                        "debug": (value) => {
                async run(instance) {
                        this._inst = instance;
                        this._values = [ // TODO: garbage collection
+                               NaN,
                                undefined,
                                null,
+                               true,
+                               false,
                                global,
                                this._inst.exports.mem,
                                () => { // resolveCallbackPromise
                                        setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
                                },
                        ];
+                       this._stringRefs = new Map();
+                       this._symbolRefs = new Map();
+                       this._refProp = Symbol();
                        this.exited = false;
 
                        const mem = new DataView(this._inst.exports.mem.buffer)
index 2b59e5a700d6c078be4b426b25688821eb99b91a..7a8037f085ed671db37e7865f378f3de1103e13e 100644 (file)
@@ -17,18 +17,12 @@ runtime/asm_wasm.s: [wasm] rt0_go: use of 8(SP) points beyond argument frame
 // Calling WebAssembly import. No write from Go assembly.
 runtime/sys_wasm.s: [wasm] nanotime: RET without writing to 8-byte ret+0(FP)
 runtime/sys_wasm.s: [wasm] scheduleCallback: RET without writing to 4-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] boolVal: RET without writing to 4-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] intVal: RET without writing to 4-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] floatVal: RET without writing to 4-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] stringVal: RET without writing to 4-byte ret+16(FP)
-syscall/js/js_js.s: [wasm] valueGet: RET without writing to 4-byte ret+24(FP)
-syscall/js/js_js.s: [wasm] valueIndex: RET without writing to 4-byte ret+16(FP)
-syscall/js/js_js.s: [wasm] valueCall: RET without writing to 4-byte ret+48(FP)
-syscall/js/js_js.s: [wasm] valueInvoke: RET without writing to 4-byte ret+32(FP)
-syscall/js/js_js.s: [wasm] valueNew: RET without writing to 4-byte ret+32(FP)
-syscall/js/js_js.s: [wasm] valueFloat: RET without writing to 8-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] valueInt: RET without writing to 8-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] valueBool: RET without writing to 1-byte ret+8(FP)
+syscall/js/js_js.s: [wasm] stringVal: RET without writing to 8-byte ret+16(FP)
+syscall/js/js_js.s: [wasm] valueGet: RET without writing to 8-byte ret+24(FP)
+syscall/js/js_js.s: [wasm] valueIndex: RET without writing to 8-byte ret+16(FP)
+syscall/js/js_js.s: [wasm] valueCall: RET without writing to 8-byte ret+48(FP)
+syscall/js/js_js.s: [wasm] valueInvoke: RET without writing to 8-byte ret+32(FP)
+syscall/js/js_js.s: [wasm] valueNew: RET without writing to 8-byte ret+32(FP)
 syscall/js/js_js.s: [wasm] valueLength: RET without writing to 8-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] valuePrepareString: RET without writing to 4-byte ret+8(FP)
-syscall/js/js_js.s: [wasm] valueInstanceOf: RET without writing to 1-byte ret+8(FP)
+syscall/js/js_js.s: [wasm] valuePrepareString: RET without writing to 8-byte ret+8(FP)
+syscall/js/js_js.s: [wasm] valueInstanceOf: RET without writing to 1-byte ret+16(FP)
index 93c39652466b3d7644cf07b8e79fa8feef8564d7..8217c24c5e3f4319a6a3f81ef0c7e8aaa6deb409 100644 (file)
@@ -15,8 +15,14 @@ import (
        "unsafe"
 )
 
-// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly itself.
-type ref uint32
+// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly.
+// A JavaScript number (64-bit float, except NaN) is represented by its IEEE 754 binary representation.
+// All other values are represented as an IEEE 754 binary representation of NaN with the low 32 bits
+// used as an ID.
+type ref uint64
+
+// nanHead are the upper 32 bits of a ref if the value is not a JavaScript number or NaN itself.
+const nanHead = 0x7FF80000
 
 // Value represents a JavaScript value.
 type Value struct {
@@ -27,6 +33,17 @@ func makeValue(v ref) Value {
        return Value{ref: v}
 }
 
+func predefValue(id uint32) Value {
+       return Value{ref: nanHead<<32 | ref(id)}
+}
+
+func floatValue(f float64) Value {
+       if f != f {
+               return valueNaN
+       }
+       return Value{ref: *(*ref)(unsafe.Pointer(&f))}
+}
+
 // Error wraps a JavaScript error.
 type Error struct {
        // Value is the underlying JavaScript error value.
@@ -39,11 +56,14 @@ func (e Error) Error() string {
 }
 
 var (
-       valueUndefined         = makeValue(0)
-       valueNull              = makeValue(1)
-       valueGlobal            = makeValue(2)
-       memory                 = makeValue(3) // WebAssembly linear memory
-       resolveCallbackPromise = makeValue(4) // function that the callback helper uses to resume the execution of Go's WebAssembly code
+       valueNaN               = predefValue(0)
+       valueUndefined         = predefValue(1)
+       valueNull              = predefValue(2)
+       valueTrue              = predefValue(3)
+       valueFalse             = predefValue(4)
+       valueGlobal            = predefValue(5)
+       memory                 = predefValue(6) // WebAssembly linear memory
+       resolveCallbackPromise = predefValue(7) // function that the callback helper uses to resume the execution of Go's WebAssembly code
 )
 
 // Undefined returns the JavaScript value "undefined".
@@ -73,35 +93,39 @@ func ValueOf(x interface{}) Value {
        case nil:
                return valueNull
        case bool:
-               return makeValue(boolVal(x))
+               if x {
+                       return valueTrue
+               } else {
+                       return valueFalse
+               }
        case int:
-               return makeValue(intVal(x))
+               return floatValue(float64(x))
        case int8:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case int16:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case int32:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case int64:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uint:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uint8:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uint16:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uint32:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uint64:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case uintptr:
-               return makeValue(intVal(int(x)))
+               return floatValue(float64(x))
        case unsafe.Pointer:
-               return makeValue(intVal(int(uintptr(x))))
+               return floatValue(float64(uintptr(x)))
        case float32:
-               return makeValue(floatVal(float64(x)))
+               return floatValue(float64(x))
        case float64:
-               return makeValue(floatVal(x))
+               return floatValue(x)
        case string:
                return makeValue(stringVal(x))
        case []byte:
@@ -114,12 +138,6 @@ func ValueOf(x interface{}) Value {
        }
 }
 
-func boolVal(x bool) ref
-
-func intVal(x int) ref
-
-func floatVal(x float64) ref
-
 func stringVal(x string) ref
 
 // Get returns the JavaScript property p of value v.
@@ -201,27 +219,35 @@ func (v Value) New(args ...interface{}) Value {
 
 func valueNew(v ref, args []ref) (ref, bool)
 
-// Float returns the value v converted to float64 according to JavaScript type conversions (parseFloat).
-func (v Value) Float() float64 {
-       return valueFloat(v.ref)
+func (v Value) isNumber() bool {
+       return v.ref>>32 != nanHead || v.ref == valueNaN.ref
 }
 
-func valueFloat(v ref) float64
+// Float returns the value v as a float64. It panics if v is not a JavaScript number.
+func (v Value) Float() float64 {
+       if !v.isNumber() {
+               panic("syscall/js: not a number")
+       }
+       return *(*float64)(unsafe.Pointer(&v.ref))
+}
 
-// Int returns the value v converted to int according to JavaScript type conversions (parseInt).
+// Int returns the value v truncated to an int. It panics if v is not a JavaScript number.
 func (v Value) Int() int {
-       return valueInt(v.ref)
+       return int(v.Float())
 }
 
-func valueInt(v ref) int
-
-// Bool returns the value v converted to bool according to JavaScript type conversions.
+// Bool returns the value v as a bool. It panics if v is not a JavaScript boolean.
 func (v Value) Bool() bool {
-       return valueBool(v.ref)
+       switch v.ref {
+       case valueTrue.ref:
+               return true
+       case valueFalse.ref:
+               return false
+       default:
+               panic("syscall/js: not a boolean")
+       }
 }
 
-func valueBool(v ref) bool
-
 // String returns the value v converted to string according to JavaScript type conversions.
 func (v Value) String() string {
        str, length := valuePrepareString(v.ref)
index cb90d88a6a132523bc9eff7ca5622ecb859dacf8..0ec164d5cbc1f2590b5a03c09c0944bd8984a723 100644 (file)
@@ -4,18 +4,6 @@
 
 #include "textflag.h"
 
-TEXT ·boolVal(SB), NOSPLIT, $0
-  CallImport
-  RET
-
-TEXT ·intVal(SB), NOSPLIT, $0
-  CallImport
-  RET
-
-TEXT ·floatVal(SB), NOSPLIT, $0
-  CallImport
-  RET
-
 TEXT ·stringVal(SB), NOSPLIT, $0
   CallImport
   RET
@@ -48,18 +36,6 @@ TEXT ·valueNew(SB), NOSPLIT, $0
   CallImport
   RET
 
-TEXT ·valueFloat(SB), NOSPLIT, $0
-  CallImport
-  RET
-
-TEXT ·valueInt(SB), NOSPLIT, $0
-  CallImport
-  RET
-
-TEXT ·valueBool(SB), NOSPLIT, $0
-  CallImport
-  RET
-
 TEXT ·valueLength(SB), NOSPLIT, $0
   CallImport
   RET
index e5e950f3a322885ccab0191f46cb9b1e5b09f3a4..c96ad82850721bdadcb2760eef251d1ffa418a3b 100644 (file)
@@ -8,6 +8,7 @@ package js_test
 
 import (
        "fmt"
+       "math"
        "syscall/js"
        "testing"
 )
@@ -21,6 +22,7 @@ var dummys = js.Global().Call("eval", `({
        add: function(a, b) {
                return a + b;
        },
+       NaN: NaN,
 })`)
 
 func TestBool(t *testing.T) {
@@ -33,6 +35,9 @@ func TestBool(t *testing.T) {
        if got := dummys.Get("otherBool").Bool(); got != want {
                t.Errorf("got %#v, want %#v", got, want)
        }
+       if dummys.Get("someBool") != dummys.Get("someBool") {
+               t.Errorf("same value not equal")
+       }
 }
 
 func TestString(t *testing.T) {
@@ -45,6 +50,9 @@ func TestString(t *testing.T) {
        if got := dummys.Get("otherString").String(); got != want {
                t.Errorf("got %#v, want %#v", got, want)
        }
+       if dummys.Get("someString") != dummys.Get("someString") {
+               t.Errorf("same value not equal")
+       }
 }
 
 func TestInt(t *testing.T) {
@@ -57,6 +65,9 @@ func TestInt(t *testing.T) {
        if got := dummys.Get("otherInt").Int(); got != want {
                t.Errorf("got %#v, want %#v", got, want)
        }
+       if dummys.Get("someInt") != dummys.Get("someInt") {
+               t.Errorf("same value not equal")
+       }
 }
 
 func TestIntConversion(t *testing.T) {
@@ -87,6 +98,23 @@ func TestFloat(t *testing.T) {
        if got := dummys.Get("otherFloat").Float(); got != want {
                t.Errorf("got %#v, want %#v", got, want)
        }
+       if dummys.Get("someFloat") != dummys.Get("someFloat") {
+               t.Errorf("same value not equal")
+       }
+}
+
+func TestObject(t *testing.T) {
+       if dummys.Get("someArray") != dummys.Get("someArray") {
+               t.Errorf("same value not equal")
+       }
+}
+
+func TestNaN(t *testing.T) {
+       want := js.ValueOf(math.NaN())
+       got := dummys.Get("NaN")
+       if got != want {
+               t.Errorf("got %#v, want %#v", got, want)
+       }
 }
 
 func TestUndefined(t *testing.T) {