Commit 8997ec1c authored by Richard Musiol's avatar Richard Musiol Committed by Paul Jolly

syscall/js: use stable references to JavaScript values

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: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
parent d89efc3c
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
let outputBuf = ""; let outputBuf = "";
global.fs = { 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) { writeSync(fd, buf) {
outputBuf += decoder.decode(buf); outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n"); const nl = outputBuf.lastIndexOf("\n");
...@@ -81,21 +81,72 @@ ...@@ -81,21 +81,72 @@
} }
const loadValue = (addr) => { const loadValue = (addr) => {
const f = mem().getFloat64(addr, true);
if (!isNaN(f)) {
return f;
}
const id = mem().getUint32(addr, true); const id = mem().getUint32(addr, true);
return this._values[id]; return this._values[id];
} }
const storeValue = (addr, v) => { const storeValue = (addr, v) => {
if (v === undefined) { if (typeof v === "number") {
mem().setUint32(addr, 0, true); if (isNaN(v)) {
mem().setUint32(addr + 4, 0x7FF80000, true); // NaN
mem().setUint32(addr, 0, true);
return;
}
mem().setFloat64(addr, v, true);
return; 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; 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 loadSlice = (addr) => {
...@@ -109,8 +160,7 @@ ...@@ -109,8 +160,7 @@
const len = getInt64(addr + 8); const len = getInt64(addr + 8);
const a = new Array(len); const a = new Array(len);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const id = mem().getUint32(array + i * 4, true); a[i] = loadValue(array + i * 8);
a[i] = this._values[id];
} }
return a; return a;
} }
...@@ -173,21 +223,6 @@ ...@@ -173,21 +223,6 @@
crypto.getRandomValues(loadSlice(sp + 8)); 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 // func stringVal(value string) ref
"syscall/js.stringVal": (sp) => { "syscall/js.stringVal": (sp) => {
storeValue(sp + 24, loadString(sp + 8)); storeValue(sp + 24, loadString(sp + 8));
...@@ -220,10 +255,10 @@ ...@@ -220,10 +255,10 @@
const m = Reflect.get(v, loadString(sp + 16)); const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32); const args = loadSliceOfValues(sp + 32);
storeValue(sp + 56, Reflect.apply(m, v, args)); storeValue(sp + 56, Reflect.apply(m, v, args));
mem().setUint8(sp + 60, 1); mem().setUint8(sp + 64, 1);
} catch (err) { } catch (err) {
storeValue(sp + 56, err); storeValue(sp + 56, err);
mem().setUint8(sp + 60, 0); mem().setUint8(sp + 64, 0);
} }
}, },
...@@ -233,10 +268,10 @@ ...@@ -233,10 +268,10 @@
const v = loadValue(sp + 8); const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16); const args = loadSliceOfValues(sp + 16);
storeValue(sp + 40, Reflect.apply(v, undefined, args)); storeValue(sp + 40, Reflect.apply(v, undefined, args));
mem().setUint8(sp + 44, 1); mem().setUint8(sp + 48, 1);
} catch (err) { } catch (err) {
storeValue(sp + 40, err); storeValue(sp + 40, err);
mem().setUint8(sp + 44, 0); mem().setUint8(sp + 48, 0);
} }
}, },
...@@ -246,28 +281,13 @@ ...@@ -246,28 +281,13 @@
const v = loadValue(sp + 8); const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16); const args = loadSliceOfValues(sp + 16);
storeValue(sp + 40, Reflect.construct(v, args)); storeValue(sp + 40, Reflect.construct(v, args));
mem().setUint8(sp + 44, 1); mem().setUint8(sp + 48, 1);
} catch (err) { } catch (err) {
storeValue(sp + 40, 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 // func valueLength(v ref) int
"syscall/js.valueLength": (sp) => { "syscall/js.valueLength": (sp) => {
setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
...@@ -288,7 +308,7 @@ ...@@ -288,7 +308,7 @@
// func valueInstanceOf(v ref, t ref) bool // func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => { "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) => { "debug": (value) => {
...@@ -301,8 +321,11 @@ ...@@ -301,8 +321,11 @@
async run(instance) { async run(instance) {
this._inst = instance; this._inst = instance;
this._values = [ // TODO: garbage collection this._values = [ // TODO: garbage collection
NaN,
undefined, undefined,
null, null,
true,
false,
global, global,
this._inst.exports.mem, this._inst.exports.mem,
() => { // resolveCallbackPromise () => { // resolveCallbackPromise
...@@ -312,6 +335,9 @@ ...@@ -312,6 +335,9 @@
setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
}, },
]; ];
this._stringRefs = new Map();
this._symbolRefs = new Map();
this._refProp = Symbol();
this.exited = false; this.exited = false;
const mem = new DataView(this._inst.exports.mem.buffer) const mem = new DataView(this._inst.exports.mem.buffer)
......
...@@ -17,18 +17,12 @@ runtime/asm_wasm.s: [wasm] rt0_go: use of 8(SP) points beyond argument frame ...@@ -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. // 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] 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) 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] stringVal: RET without writing to 8-byte ret+16(FP)
syscall/js/js_js.s: [wasm] intVal: RET without writing to 4-byte ret+8(FP) syscall/js/js_js.s: [wasm] valueGet: RET without writing to 8-byte ret+24(FP)
syscall/js/js_js.s: [wasm] floatVal: RET without writing to 4-byte ret+8(FP) syscall/js/js_js.s: [wasm] valueIndex: RET without writing to 8-byte ret+16(FP)
syscall/js/js_js.s: [wasm] stringVal: RET without writing to 4-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] valueGet: RET without writing to 4-byte ret+24(FP) syscall/js/js_js.s: [wasm] valueInvoke: RET without writing to 8-byte ret+32(FP)
syscall/js/js_js.s: [wasm] valueIndex: RET without writing to 4-byte ret+16(FP) syscall/js/js_js.s: [wasm] valueNew: RET without writing to 8-byte ret+32(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] valueLength: RET without writing to 8-byte ret+8(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] valuePrepareString: RET without writing to 8-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] valueInstanceOf: RET without writing to 1-byte ret+16(FP)
...@@ -15,8 +15,14 @@ import ( ...@@ -15,8 +15,14 @@ import (
"unsafe" "unsafe"
) )
// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly itself. // ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly.
type ref uint32 // 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. // Value represents a JavaScript value.
type Value struct { type Value struct {
...@@ -27,6 +33,17 @@ func makeValue(v ref) Value { ...@@ -27,6 +33,17 @@ func makeValue(v ref) Value {
return Value{ref: v} 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. // Error wraps a JavaScript error.
type Error struct { type Error struct {
// Value is the underlying JavaScript error value. // Value is the underlying JavaScript error value.
...@@ -39,11 +56,14 @@ func (e Error) Error() string { ...@@ -39,11 +56,14 @@ func (e Error) Error() string {
} }
var ( var (
valueUndefined = makeValue(0) valueNaN = predefValue(0)
valueNull = makeValue(1) valueUndefined = predefValue(1)
valueGlobal = makeValue(2) valueNull = predefValue(2)
memory = makeValue(3) // WebAssembly linear memory valueTrue = predefValue(3)
resolveCallbackPromise = makeValue(4) // function that the callback helper uses to resume the execution of Go's WebAssembly code 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". // Undefined returns the JavaScript value "undefined".
...@@ -73,35 +93,39 @@ func ValueOf(x interface{}) Value { ...@@ -73,35 +93,39 @@ func ValueOf(x interface{}) Value {
case nil: case nil:
return valueNull return valueNull
case bool: case bool:
return makeValue(boolVal(x)) if x {
return valueTrue
} else {
return valueFalse
}
case int: case int:
return makeValue(intVal(x)) return floatValue(float64(x))
case int8: case int8:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case int16: case int16:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case int32: case int32:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case int64: case int64:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uint: case uint:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uint8: case uint8:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uint16: case uint16:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uint32: case uint32:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uint64: case uint64:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case uintptr: case uintptr:
return makeValue(intVal(int(x))) return floatValue(float64(x))
case unsafe.Pointer: case unsafe.Pointer:
return makeValue(intVal(int(uintptr(x)))) return floatValue(float64(uintptr(x)))
case float32: case float32:
return makeValue(floatVal(float64(x))) return floatValue(float64(x))
case float64: case float64:
return makeValue(floatVal(x)) return floatValue(x)
case string: case string:
return makeValue(stringVal(x)) return makeValue(stringVal(x))
case []byte: case []byte:
...@@ -114,12 +138,6 @@ func ValueOf(x interface{}) Value { ...@@ -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 func stringVal(x string) ref
// Get returns the JavaScript property p of value v. // Get returns the JavaScript property p of value v.
...@@ -201,27 +219,35 @@ func (v Value) New(args ...interface{}) Value { ...@@ -201,27 +219,35 @@ func (v Value) New(args ...interface{}) Value {
func valueNew(v ref, args []ref) (ref, bool) 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) isNumber() bool {
func (v Value) Float() float64 { return v.ref>>32 != nanHead || v.ref == valueNaN.ref
return valueFloat(v.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 { func (v Value) Int() int {
return valueInt(v.ref) return int(v.Float())
} }
func valueInt(v ref) int // Bool returns the value v as a bool. It panics if v is not a JavaScript boolean.
// Bool returns the value v converted to bool according to JavaScript type conversions.
func (v Value) Bool() bool { 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. // String returns the value v converted to string according to JavaScript type conversions.
func (v Value) String() string { func (v Value) String() string {
str, length := valuePrepareString(v.ref) str, length := valuePrepareString(v.ref)
......
...@@ -4,18 +4,6 @@ ...@@ -4,18 +4,6 @@
#include "textflag.h" #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 TEXT ·stringVal(SB), NOSPLIT, $0
CallImport CallImport
RET RET
...@@ -48,18 +36,6 @@ TEXT ·valueNew(SB), NOSPLIT, $0 ...@@ -48,18 +36,6 @@ TEXT ·valueNew(SB), NOSPLIT, $0
CallImport CallImport
RET 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 TEXT ·valueLength(SB), NOSPLIT, $0
CallImport CallImport
RET RET
......
...@@ -8,6 +8,7 @@ package js_test ...@@ -8,6 +8,7 @@ package js_test
import ( import (
"fmt" "fmt"
"math"
"syscall/js" "syscall/js"
"testing" "testing"
) )
...@@ -21,6 +22,7 @@ var dummys = js.Global().Call("eval", `({ ...@@ -21,6 +22,7 @@ var dummys = js.Global().Call("eval", `({
add: function(a, b) { add: function(a, b) {
return a + b; return a + b;
}, },
NaN: NaN,
})`) })`)
func TestBool(t *testing.T) { func TestBool(t *testing.T) {
...@@ -33,6 +35,9 @@ func TestBool(t *testing.T) { ...@@ -33,6 +35,9 @@ func TestBool(t *testing.T) {
if got := dummys.Get("otherBool").Bool(); got != want { if got := dummys.Get("otherBool").Bool(); got != want {
t.Errorf("got %#v, want %#v", 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) { func TestString(t *testing.T) {
...@@ -45,6 +50,9 @@ func TestString(t *testing.T) { ...@@ -45,6 +50,9 @@ func TestString(t *testing.T) {
if got := dummys.Get("otherString").String(); got != want { if got := dummys.Get("otherString").String(); got != want {
t.Errorf("got %#v, want %#v", 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) { func TestInt(t *testing.T) {
...@@ -57,6 +65,9 @@ func TestInt(t *testing.T) { ...@@ -57,6 +65,9 @@ func TestInt(t *testing.T) {
if got := dummys.Get("otherInt").Int(); got != want { if got := dummys.Get("otherInt").Int(); got != want {
t.Errorf("got %#v, want %#v", 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) { func TestIntConversion(t *testing.T) {
...@@ -87,6 +98,23 @@ func TestFloat(t *testing.T) { ...@@ -87,6 +98,23 @@ func TestFloat(t *testing.T) {
if got := dummys.Get("otherFloat").Float(); got != want { if got := dummys.Get("otherFloat").Float(); got != want {
t.Errorf("got %#v, want %#v", 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) { func TestUndefined(t *testing.T) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment