return;
}
- let ref = this._refs.get(v);
- if (ref === undefined) {
- ref = this._values.length;
- this._values.push(v);
- this._refs.set(v, ref);
+ let id = this._ids.get(v);
+ if (id === undefined) {
+ id = this._idPool.pop();
+ if (id === undefined) {
+ id = this._values.length;
+ }
+ this._values[id] = v;
+ this._goRefCounts[id] = 0;
+ this._ids.set(v, id);
}
- let typeFlag = 0;
+ this._goRefCounts[id]++;
+ let typeFlag = 1;
switch (typeof v) {
case "string":
- typeFlag = 1;
+ typeFlag = 2;
break;
case "symbol":
- typeFlag = 2;
+ typeFlag = 3;
break;
case "function":
- typeFlag = 3;
+ typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
- this.mem.setUint32(addr, ref, true);
+ this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
this.exited = true;
delete this._inst;
delete this._values;
- delete this._refs;
+ delete this._goRefCounts;
+ delete this._ids;
+ delete this._idPool;
this.exit(code);
},
crypto.getRandomValues(loadSlice(sp + 8));
},
+ // func finalizeRef(v ref)
+ "syscall/js.finalizeRef": (sp) => {
+ const id = this.mem.getUint32(sp + 8, true);
+ this._goRefCounts[id]--;
+ if (this._goRefCounts[id] === 0) {
+ const v = this._values[id];
+ this._values[id] = null;
+ this._ids.delete(v);
+ this._idPool.push(id);
+ }
+ },
+
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
storeValue(sp + 24, loadString(sp + 8));
async run(instance) {
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
- this._values = [ // TODO: garbage collection
+ this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
global,
this,
];
- this._refs = new Map();
- this.exited = false;
+ this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
+ this._ids = new Map(); // mapping from JS values to reference ids
+ this._idPool = []; // unused ids that have been garbage collected
+ this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
const jsFetchRedirect = "js.fetch:redirect"
-var useFakeNetwork = js.Global().Get("fetch") == js.Undefined()
+var useFakeNetwork = js.Global().Get("fetch").IsUndefined()
// RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
}
ac := js.Global().Get("AbortController")
- if ac != js.Undefined() {
+ if !ac.IsUndefined() {
// Some browsers that support WASM don't necessarily support
// the AbortController. See
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
opt.Set("redirect", h)
req.Header.Del(jsFetchRedirect)
}
- if ac != js.Undefined() {
+ if !ac.IsUndefined() {
opt.Set("signal", ac.Get("signal"))
}
headers := js.Global().Get("Headers").New()
var body io.ReadCloser
// The body is undefined when the browser does not support streaming response bodies (Firefox),
// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
- if b != js.Undefined() && b != js.Null() {
+ if !b.IsUndefined() && !b.IsNull() {
body = &streamReader{stream: b.Call("getReader")}
} else {
// Fall back to using ArrayBuffer
respPromise.Call("then", success, failure)
select {
case <-req.Context().Done():
- if ac != js.Undefined() {
+ if !ac.IsUndefined() {
// Abort the Fetch request
ac.Call("abort")
}
if err := checkPath(path); err != nil {
return err
}
- if jsFS.Get("lchown") == js.Undefined() {
+ if jsFS.Get("lchown").IsUndefined() {
// fs.lchown is unavailable on Linux until Node.js 10.6.0
// TODO(neelance): remove when we require at least this Node.js version
return ENOSYS
var res callResult
if len(args) >= 1 { // on Node.js 8, fs.utimes calls the callback without any arguments
- if jsErr := args[0]; jsErr != js.Null() {
+ if jsErr := args[0]; !jsErr.IsNull() {
res.err = mapJSError(jsErr)
}
}
--- /dev/null
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build js,wasm
+
+package js
+
+var JSGo = jsGo
func handleEvent() {
cb := jsGo.Get("_pendingEvent")
- if cb == Null() {
+ if cb.IsNull() {
return
}
jsGo.Set("_pendingEvent", Null())
package js
import (
+ "runtime"
"unsafe"
)
// The JavaScript value "undefined" is represented by the value 0.
// A JavaScript number (64-bit float, except 0 and NaN) is represented by its IEEE 754 binary representation.
// All other values are represented as an IEEE 754 binary representation of NaN with bits 0-31 used as
-// an ID and bits 32-33 used to differentiate between string, symbol, function and object.
+// an ID and bits 32-34 used to differentiate between string, symbol, function and object.
type ref uint64
// nanHead are the upper 32 bits of a ref which are set if the value is not encoded as an IEEE 754 number (see above).
}
// Value represents a JavaScript value. The zero value is the JavaScript value "undefined".
+// Values can be checked for equality with the Equal method.
type Value struct {
- ref ref
+ _ [0]func() // uncomparable; to make == not compile
+ ref ref // identifies a JavaScript value, see ref type
+ gcPtr *ref // used to trigger the finalizer when the Value is not referenced any more
}
+const (
+ // the type flags need to be in sync with wasm_exec.js
+ typeFlagNone = iota
+ typeFlagObject
+ typeFlagString
+ typeFlagSymbol
+ typeFlagFunction
+)
+
// JSValue implements Wrapper interface.
func (v Value) JSValue() Value {
return v
}
-func makeValue(v ref) Value {
- return Value{ref: v}
+func makeValue(r ref) Value {
+ var gcPtr *ref
+ typeFlag := (r >> 32) & 7
+ if (r>>32)&nanHead == nanHead && typeFlag != typeFlagNone {
+ gcPtr = new(ref)
+ *gcPtr = r
+ runtime.SetFinalizer(gcPtr, func(p *ref) {
+ finalizeRef(*p)
+ })
+ }
+
+ return Value{ref: r, gcPtr: gcPtr}
}
-func predefValue(id uint32) Value {
- return Value{ref: nanHead<<32 | ref(id)}
+func finalizeRef(r ref)
+
+func predefValue(id uint32, typeFlag byte) Value {
+ return Value{ref: (nanHead|ref(typeFlag))<<32 | ref(id)}
}
func floatValue(f float64) Value {
var (
valueUndefined = Value{ref: 0}
- valueNaN = predefValue(0)
- valueZero = predefValue(1)
- valueNull = predefValue(2)
- valueTrue = predefValue(3)
- valueFalse = predefValue(4)
- valueGlobal = predefValue(5)
- jsGo = predefValue(6) // instance of the Go class in JavaScript
+ valueNaN = predefValue(0, typeFlagNone)
+ valueZero = predefValue(1, typeFlagNone)
+ valueNull = predefValue(2, typeFlagNone)
+ valueTrue = predefValue(3, typeFlagNone)
+ valueFalse = predefValue(4, typeFlagNone)
+ valueGlobal = predefValue(5, typeFlagObject)
+ jsGo = predefValue(6, typeFlagObject) // instance of the Go class in JavaScript
objectConstructor = valueGlobal.Get("Object")
arrayConstructor = valueGlobal.Get("Array")
)
+// Equal reports whether v and w are equal according to JavaScript's === operator.
+func (v Value) Equal(w Value) bool {
+ return v.ref == w.ref && v.ref != valueNaN.ref
+}
+
// Undefined returns the JavaScript value "undefined".
func Undefined() Value {
return valueUndefined
}
+// IsUndefined reports whether v is the JavaScript value "undefined".
+func (v Value) IsUndefined() bool {
+ return v.ref == valueUndefined.ref
+}
+
// Null returns the JavaScript value "null".
func Null() Value {
return valueNull
}
+// IsNull reports whether v is the JavaScript value "null".
+func (v Value) IsNull() bool {
+ return v.ref == valueNull.ref
+}
+
+// IsNaN reports whether v is the JavaScript value "NaN".
+func (v Value) IsNaN() bool {
+ return v.ref == valueNaN.ref
+}
+
// Global returns the JavaScript global object, usually "window" or "global".
func Global() Value {
return valueGlobal
if v.isNumber() {
return TypeNumber
}
- typeFlag := v.ref >> 32 & 3
+ typeFlag := (v.ref >> 32) & 7
switch typeFlag {
- case 1:
+ case typeFlagObject:
+ return TypeObject
+ case typeFlagString:
return TypeString
- case 2:
+ case typeFlagSymbol:
return TypeSymbol
- case 3:
+ case typeFlagFunction:
return TypeFunction
default:
- return TypeObject
+ panic("bad type flag")
}
}
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Get", vType})
}
- return makeValue(valueGet(v.ref, p))
+ r := makeValue(valueGet(v.ref, p))
+ runtime.KeepAlive(v)
+ return r
}
func valueGet(v ref, p string) ref
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Set", vType})
}
- valueSet(v.ref, p, ValueOf(x).ref)
+ xv := ValueOf(x)
+ valueSet(v.ref, p, xv.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(xv)
}
func valueSet(v ref, p string, x ref)
panic(&ValueError{"Value.Delete", vType})
}
valueDelete(v.ref, p)
+ runtime.KeepAlive(v)
}
func valueDelete(v ref, p string)
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Index", vType})
}
- return makeValue(valueIndex(v.ref, i))
+ r := makeValue(valueIndex(v.ref, i))
+ runtime.KeepAlive(v)
+ return r
}
func valueIndex(v ref, i int) ref
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.SetIndex", vType})
}
- valueSetIndex(v.ref, i, ValueOf(x).ref)
+ xv := ValueOf(x)
+ valueSetIndex(v.ref, i, xv.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(xv)
}
func valueSetIndex(v ref, i int, x ref)
-func makeArgs(args []interface{}) []ref {
- argVals := make([]ref, len(args))
+func makeArgs(args []interface{}) ([]Value, []ref) {
+ argVals := make([]Value, len(args))
+ argRefs := make([]ref, len(args))
for i, arg := range args {
- argVals[i] = ValueOf(arg).ref
+ v := ValueOf(arg)
+ argVals[i] = v
+ argRefs[i] = v.ref
}
- return argVals
+ return argVals, argRefs
}
// Length returns the JavaScript property "length" of v.
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.SetIndex", vType})
}
- return valueLength(v.ref)
+ r := valueLength(v.ref)
+ runtime.KeepAlive(v)
+ return r
}
func valueLength(v ref) int
// It panics if v has no method m.
// The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) Call(m string, args ...interface{}) Value {
- res, ok := valueCall(v.ref, m, makeArgs(args))
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueCall(v.ref, m, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
if !ok {
if vType := v.Type(); !vType.isObject() { // check here to avoid overhead in success case
panic(&ValueError{"Value.Call", vType})
// It panics if v is not a JavaScript function.
// The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) Invoke(args ...interface{}) Value {
- res, ok := valueInvoke(v.ref, makeArgs(args))
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueInvoke(v.ref, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
if !ok {
if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
panic(&ValueError{"Value.Invoke", vType})
// It panics if v is not a JavaScript function.
// The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) New(args ...interface{}) Value {
- res, ok := valueNew(v.ref, makeArgs(args))
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueNew(v.ref, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
if !ok {
if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
panic(&ValueError{"Value.Invoke", vType})
func (v Value) isNumber() bool {
return v.ref == valueZero.ref ||
v.ref == valueNaN.ref ||
- (v.ref != valueUndefined.ref && v.ref>>32&nanHead != nanHead)
+ (v.ref != valueUndefined.ref && (v.ref>>32)&nanHead != nanHead)
}
func (v Value) float(method string) float64 {
func (v Value) String() string {
switch v.Type() {
case TypeString:
- return jsString(v.ref)
+ return jsString(v)
case TypeUndefined:
return "<undefined>"
case TypeNull:
return "<null>"
case TypeBoolean:
- return "<boolean: " + jsString(v.ref) + ">"
+ return "<boolean: " + jsString(v) + ">"
case TypeNumber:
- return "<number: " + jsString(v.ref) + ">"
+ return "<number: " + jsString(v) + ">"
case TypeSymbol:
return "<symbol>"
case TypeObject:
}
}
-func jsString(v ref) string {
- str, length := valuePrepareString(v)
+func jsString(v Value) string {
+ str, length := valuePrepareString(v.ref)
+ runtime.KeepAlive(v)
b := make([]byte, length)
valueLoadString(str, b)
+ finalizeRef(str)
return string(b)
}
// InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
func (v Value) InstanceOf(t Value) bool {
- return valueInstanceOf(v.ref, t.ref)
+ r := valueInstanceOf(v.ref, t.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(t)
+ return r
}
func valueInstanceOf(v ref, t ref) bool
// CopyBytesToGo panics if src is not an Uint8Array.
func CopyBytesToGo(dst []byte, src Value) int {
n, ok := copyBytesToGo(dst, src.ref)
+ runtime.KeepAlive(src)
if !ok {
panic("syscall/js: CopyBytesToGo: expected src to be an Uint8Array")
}
// CopyBytesToJS panics if dst is not an Uint8Array.
func CopyBytesToJS(dst Value, src []byte) int {
n, ok := copyBytesToJS(dst.ref, src)
+ runtime.KeepAlive(dst)
if !ok {
panic("syscall/js: CopyBytesToJS: expected dst to be an Uint8Array")
}
#include "textflag.h"
+TEXT ·finalizeRef(SB), NOSPLIT, $0
+ CallImport
+ RET
+
TEXT ·stringVal(SB), NOSPLIT, $0
CallImport
RET
import (
"fmt"
"math"
+ "runtime"
"syscall/js"
"testing"
)
if got := dummys.Get("otherBool").Bool(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
- if dummys.Get("someBool") != dummys.Get("someBool") {
+ if !dummys.Get("someBool").Equal(dummys.Get("someBool")) {
t.Errorf("same value not equal")
}
}
if got := dummys.Get("otherString").String(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
- if dummys.Get("someString") != dummys.Get("someString") {
+ if !dummys.Get("someString").Equal(dummys.Get("someString")) {
t.Errorf("same value not equal")
}
if got := dummys.Get("otherInt").Int(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
- if dummys.Get("someInt") != dummys.Get("someInt") {
+ if !dummys.Get("someInt").Equal(dummys.Get("someInt")) {
t.Errorf("same value not equal")
}
if got := dummys.Get("zero").Int(); got != 0 {
if got := dummys.Get("otherFloat").Float(); got != want {
t.Errorf("got %#v, want %#v", got, want)
}
- if dummys.Get("someFloat") != dummys.Get("someFloat") {
+ if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
t.Errorf("same value not equal")
}
}
func TestObject(t *testing.T) {
- if dummys.Get("someArray") != dummys.Get("someArray") {
+ if !dummys.Get("someArray").Equal(dummys.Get("someArray")) {
t.Errorf("same value not equal")
}
// An object and its prototype should not be equal.
proto := js.Global().Get("Object").Get("prototype")
o := js.Global().Call("eval", "new Object()")
- if proto == o {
+ if proto.Equal(o) {
t.Errorf("object equals to its prototype")
}
}
}
}
+func TestEqual(t *testing.T) {
+ if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
+ t.Errorf("same float is not equal")
+ }
+ if !dummys.Get("emptyObj").Equal(dummys.Get("emptyObj")) {
+ t.Errorf("same object is not equal")
+ }
+ if dummys.Get("someFloat").Equal(dummys.Get("someInt")) {
+ t.Errorf("different values are not unequal")
+ }
+}
+
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)
+ if !dummys.Get("NaN").IsNaN() {
+ t.Errorf("JS NaN is not NaN")
+ }
+ if !js.ValueOf(math.NaN()).IsNaN() {
+ t.Errorf("Go NaN is not NaN")
+ }
+ if dummys.Get("NaN").Equal(dummys.Get("NaN")) {
+ t.Errorf("NaN is equal to NaN")
}
}
func TestUndefined(t *testing.T) {
- dummys.Set("test", js.Undefined())
- if dummys == js.Undefined() || dummys.Get("test") != js.Undefined() || dummys.Get("xyz") != js.Undefined() {
- t.Errorf("js.Undefined expected")
+ if !js.Undefined().IsUndefined() {
+ t.Errorf("undefined is not undefined")
+ }
+ if !js.Undefined().Equal(js.Undefined()) {
+ t.Errorf("undefined is not equal to undefined")
+ }
+ if dummys.IsUndefined() {
+ t.Errorf("object is undefined")
+ }
+ if js.Undefined().IsNull() {
+ t.Errorf("undefined is null")
+ }
+ if dummys.Set("test", js.Undefined()); !dummys.Get("test").IsUndefined() {
+ t.Errorf("could not set undefined")
}
}
func TestNull(t *testing.T) {
- dummys.Set("test1", nil)
- dummys.Set("test2", js.Null())
- if dummys == js.Null() || dummys.Get("test1") != js.Null() || dummys.Get("test2") != js.Null() {
- t.Errorf("js.Null expected")
+ if !js.Null().IsNull() {
+ t.Errorf("null is not null")
+ }
+ if !js.Null().Equal(js.Null()) {
+ t.Errorf("null is not equal to null")
+ }
+ if dummys.IsNull() {
+ t.Errorf("object is null")
+ }
+ if js.Null().IsUndefined() {
+ t.Errorf("null is undefined")
+ }
+ if dummys.Set("test", js.Null()); !dummys.Get("test").IsNull() {
+ t.Errorf("could not set null")
+ }
+ if dummys.Set("test", nil); !dummys.Get("test").IsNull() {
+ t.Errorf("could not set nil")
}
}
func TestZeroValue(t *testing.T) {
var v js.Value
- if v != js.Undefined() {
+ if !v.IsUndefined() {
t.Error("zero js.Value is not js.Undefined()")
}
}
}
}
+func TestGarbageCollection(t *testing.T) {
+ before := js.JSGo.Get("_values").Length()
+ for i := 0; i < 1000; i++ {
+ _ = js.Global().Get("Object").New().Call("toString").String()
+ runtime.GC()
+ }
+ after := js.JSGo.Get("_values").Length()
+ if after-before > 500 {
+ t.Errorf("garbage collection ineffective")
+ }
+}
+
// BenchmarkDOM is a simple benchmark which emulates a webapp making DOM operations.
// It creates a div, and sets its id. Then searches by that id and sets some data.
// Finally it removes that div.
func BenchmarkDOM(b *testing.B) {
document := js.Global().Get("document")
- if document == js.Undefined() {
+ if document.IsUndefined() {
b.Skip("Not a browser environment. Skipping.")
}
const data = "someString"