Commit 54e6ba67 authored by Richard Musiol's avatar Richard Musiol Committed by Brad Fitzpatrick

syscall/js: garbage collect references to JavaScript values

The js.Value struct now contains a pointer, so a finalizer can
determine if the value is not referenced by Go any more.

Unfortunately this breaks Go's == operator with js.Value. This change
adds a new Equal method to check for the equality of two Values.
This is a breaking change. The == operator is now disallowed to
not silently break code.

Additionally the helper methods IsUndefined, IsNull and IsNaN got added.

Fixes #35111

Change-Id: I58a50ca18f477bf51a259c668a8ba15bfa76c955
Reviewed-on: https://go-review.googlesource.com/c/go/+/203600
Run-TryBot: Richard Musiol <neelance@gmail.com>
Reviewed-by: default avatarCherry Zhang <cherryyz@google.com>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 063d0f11
...@@ -205,26 +205,31 @@ ...@@ -205,26 +205,31 @@
return; return;
} }
let ref = this._refs.get(v); let id = this._ids.get(v);
if (ref === undefined) { if (id === undefined) {
ref = this._values.length; id = this._idPool.pop();
this._values.push(v); if (id === undefined) {
this._refs.set(v, ref); 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) { switch (typeof v) {
case "string": case "string":
typeFlag = 1; typeFlag = 2;
break; break;
case "symbol": case "symbol":
typeFlag = 2; typeFlag = 3;
break; break;
case "function": case "function":
typeFlag = 3; typeFlag = 4;
break; break;
} }
this.mem.setUint32(addr + 4, nanHead | typeFlag, true); this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, ref, true); this.mem.setUint32(addr, id, true);
} }
const loadSlice = (addr) => { const loadSlice = (addr) => {
...@@ -263,7 +268,9 @@ ...@@ -263,7 +268,9 @@
this.exited = true; this.exited = true;
delete this._inst; delete this._inst;
delete this._values; delete this._values;
delete this._refs; delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code); this.exit(code);
}, },
...@@ -323,6 +330,18 @@ ...@@ -323,6 +330,18 @@
crypto.getRandomValues(loadSlice(sp + 8)); 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 // 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));
...@@ -462,7 +481,7 @@ ...@@ -462,7 +481,7 @@
async run(instance) { async run(instance) {
this._inst = instance; this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer); 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, NaN,
0, 0,
null, null,
...@@ -471,8 +490,10 @@ ...@@ -471,8 +490,10 @@
global, global,
this, this,
]; ];
this._refs = new Map(); this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this.exited = false; 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. // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096; let offset = 4096;
......
...@@ -41,7 +41,7 @@ const jsFetchCreds = "js.fetch:credentials" ...@@ -41,7 +41,7 @@ const jsFetchCreds = "js.fetch:credentials"
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
const jsFetchRedirect = "js.fetch:redirect" 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. // RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
func (t *Transport) RoundTrip(req *Request) (*Response, error) { func (t *Transport) RoundTrip(req *Request) (*Response, error) {
...@@ -50,7 +50,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) { ...@@ -50,7 +50,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
} }
ac := js.Global().Get("AbortController") ac := js.Global().Get("AbortController")
if ac != js.Undefined() { if !ac.IsUndefined() {
// Some browsers that support WASM don't necessarily support // Some browsers that support WASM don't necessarily support
// the AbortController. See // the AbortController. See
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility. // https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
...@@ -74,7 +74,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) { ...@@ -74,7 +74,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
opt.Set("redirect", h) opt.Set("redirect", h)
req.Header.Del(jsFetchRedirect) req.Header.Del(jsFetchRedirect)
} }
if ac != js.Undefined() { if !ac.IsUndefined() {
opt.Set("signal", ac.Get("signal")) opt.Set("signal", ac.Get("signal"))
} }
headers := js.Global().Get("Headers").New() headers := js.Global().Get("Headers").New()
...@@ -132,7 +132,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) { ...@@ -132,7 +132,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
var body io.ReadCloser var body io.ReadCloser
// The body is undefined when the browser does not support streaming response bodies (Firefox), // 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. // 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")} body = &streamReader{stream: b.Call("getReader")}
} else { } else {
// Fall back to using ArrayBuffer // Fall back to using ArrayBuffer
...@@ -168,7 +168,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) { ...@@ -168,7 +168,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
respPromise.Call("then", success, failure) respPromise.Call("then", success, failure)
select { select {
case <-req.Context().Done(): case <-req.Context().Done():
if ac != js.Undefined() { if !ac.IsUndefined() {
// Abort the Fetch request // Abort the Fetch request
ac.Call("abort") ac.Call("abort")
} }
......
...@@ -259,7 +259,7 @@ func Lchown(path string, uid, gid int) error { ...@@ -259,7 +259,7 @@ func Lchown(path string, uid, gid int) error {
if err := checkPath(path); err != nil { if err := checkPath(path); err != nil {
return err 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 // fs.lchown is unavailable on Linux until Node.js 10.6.0
// TODO(neelance): remove when we require at least this Node.js version // TODO(neelance): remove when we require at least this Node.js version
return ENOSYS return ENOSYS
...@@ -497,7 +497,7 @@ func fsCall(name string, args ...interface{}) (js.Value, error) { ...@@ -497,7 +497,7 @@ func fsCall(name string, args ...interface{}) (js.Value, error) {
var res callResult var res callResult
if len(args) >= 1 { // on Node.js 8, fs.utimes calls the callback without any arguments 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) res.err = mapJSError(jsErr)
} }
} }
......
// 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
...@@ -64,7 +64,7 @@ func init() { ...@@ -64,7 +64,7 @@ func init() {
func handleEvent() { func handleEvent() {
cb := jsGo.Get("_pendingEvent") cb := jsGo.Get("_pendingEvent")
if cb == Null() { if cb.IsNull() {
return return
} }
jsGo.Set("_pendingEvent", Null()) jsGo.Set("_pendingEvent", Null())
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
package js package js
import ( import (
"runtime"
"unsafe" "unsafe"
) )
...@@ -20,7 +21,7 @@ import ( ...@@ -20,7 +21,7 @@ import (
// The JavaScript value "undefined" is represented by the value 0. // 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. // 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 // 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 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). // 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).
...@@ -33,21 +34,45 @@ type Wrapper interface { ...@@ -33,21 +34,45 @@ type Wrapper interface {
} }
// Value represents a JavaScript value. The zero value is the JavaScript value "undefined". // 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 { 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. // JSValue implements Wrapper interface.
func (v Value) JSValue() Value { func (v Value) JSValue() Value {
return v return v
} }
func makeValue(v ref) Value { func makeValue(r ref) Value {
return Value{ref: v} 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 { func finalizeRef(r ref)
return Value{ref: nanHead<<32 | ref(id)}
func predefValue(id uint32, typeFlag byte) Value {
return Value{ref: (nanHead|ref(typeFlag))<<32 | ref(id)}
} }
func floatValue(f float64) Value { func floatValue(f float64) Value {
...@@ -73,28 +98,48 @@ func (e Error) Error() string { ...@@ -73,28 +98,48 @@ func (e Error) Error() string {
var ( var (
valueUndefined = Value{ref: 0} valueUndefined = Value{ref: 0}
valueNaN = predefValue(0) valueNaN = predefValue(0, typeFlagNone)
valueZero = predefValue(1) valueZero = predefValue(1, typeFlagNone)
valueNull = predefValue(2) valueNull = predefValue(2, typeFlagNone)
valueTrue = predefValue(3) valueTrue = predefValue(3, typeFlagNone)
valueFalse = predefValue(4) valueFalse = predefValue(4, typeFlagNone)
valueGlobal = predefValue(5) valueGlobal = predefValue(5, typeFlagObject)
jsGo = predefValue(6) // instance of the Go class in JavaScript jsGo = predefValue(6, typeFlagObject) // instance of the Go class in JavaScript
objectConstructor = valueGlobal.Get("Object") objectConstructor = valueGlobal.Get("Object")
arrayConstructor = valueGlobal.Get("Array") 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". // Undefined returns the JavaScript value "undefined".
func Undefined() Value { func Undefined() Value {
return valueUndefined 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". // Null returns the JavaScript value "null".
func Null() Value { func Null() Value {
return valueNull 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". // Global returns the JavaScript global object, usually "window" or "global".
func Global() Value { func Global() Value {
return valueGlobal return valueGlobal
...@@ -232,16 +277,18 @@ func (v Value) Type() Type { ...@@ -232,16 +277,18 @@ func (v Value) Type() Type {
if v.isNumber() { if v.isNumber() {
return TypeNumber return TypeNumber
} }
typeFlag := v.ref >> 32 & 3 typeFlag := (v.ref >> 32) & 7
switch typeFlag { switch typeFlag {
case 1: case typeFlagObject:
return TypeObject
case typeFlagString:
return TypeString return TypeString
case 2: case typeFlagSymbol:
return TypeSymbol return TypeSymbol
case 3: case typeFlagFunction:
return TypeFunction return TypeFunction
default: default:
return TypeObject panic("bad type flag")
} }
} }
...@@ -251,7 +298,9 @@ func (v Value) Get(p string) Value { ...@@ -251,7 +298,9 @@ func (v Value) Get(p string) Value {
if vType := v.Type(); !vType.isObject() { if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Get", vType}) 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 func valueGet(v ref, p string) ref
...@@ -262,7 +311,10 @@ func (v Value) Set(p string, x interface{}) { ...@@ -262,7 +311,10 @@ func (v Value) Set(p string, x interface{}) {
if vType := v.Type(); !vType.isObject() { if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Set", vType}) 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) func valueSet(v ref, p string, x ref)
...@@ -274,6 +326,7 @@ func (v Value) Delete(p string) { ...@@ -274,6 +326,7 @@ func (v Value) Delete(p string) {
panic(&ValueError{"Value.Delete", vType}) panic(&ValueError{"Value.Delete", vType})
} }
valueDelete(v.ref, p) valueDelete(v.ref, p)
runtime.KeepAlive(v)
} }
func valueDelete(v ref, p string) func valueDelete(v ref, p string)
...@@ -284,7 +337,9 @@ func (v Value) Index(i int) Value { ...@@ -284,7 +337,9 @@ func (v Value) Index(i int) Value {
if vType := v.Type(); !vType.isObject() { if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Index", vType}) 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 func valueIndex(v ref, i int) ref
...@@ -295,17 +350,23 @@ func (v Value) SetIndex(i int, x interface{}) { ...@@ -295,17 +350,23 @@ func (v Value) SetIndex(i int, x interface{}) {
if vType := v.Type(); !vType.isObject() { if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.SetIndex", vType}) 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 valueSetIndex(v ref, i int, x ref)
func makeArgs(args []interface{}) []ref { func makeArgs(args []interface{}) ([]Value, []ref) {
argVals := make([]ref, len(args)) argVals := make([]Value, len(args))
argRefs := make([]ref, len(args))
for i, arg := range 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. // Length returns the JavaScript property "length" of v.
...@@ -314,7 +375,9 @@ func (v Value) Length() int { ...@@ -314,7 +375,9 @@ func (v Value) Length() int {
if vType := v.Type(); !vType.isObject() { if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.SetIndex", vType}) panic(&ValueError{"Value.SetIndex", vType})
} }
return valueLength(v.ref) r := valueLength(v.ref)
runtime.KeepAlive(v)
return r
} }
func valueLength(v ref) int func valueLength(v ref) int
...@@ -323,7 +386,10 @@ func valueLength(v ref) int ...@@ -323,7 +386,10 @@ func valueLength(v ref) int
// It panics if v has no method m. // It panics if v has no method m.
// The arguments get mapped to JavaScript values according to the ValueOf function. // The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) Call(m string, args ...interface{}) Value { 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 !ok {
if vType := v.Type(); !vType.isObject() { // check here to avoid overhead in success case if vType := v.Type(); !vType.isObject() { // check here to avoid overhead in success case
panic(&ValueError{"Value.Call", vType}) panic(&ValueError{"Value.Call", vType})
...@@ -342,7 +408,10 @@ func valueCall(v ref, m string, args []ref) (ref, bool) ...@@ -342,7 +408,10 @@ func valueCall(v ref, m string, args []ref) (ref, bool)
// It panics if v is not a JavaScript function. // It panics if v is not a JavaScript function.
// The arguments get mapped to JavaScript values according to the ValueOf function. // The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) Invoke(args ...interface{}) Value { 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 !ok {
if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
panic(&ValueError{"Value.Invoke", vType}) panic(&ValueError{"Value.Invoke", vType})
...@@ -358,7 +427,10 @@ func valueInvoke(v ref, args []ref) (ref, bool) ...@@ -358,7 +427,10 @@ func valueInvoke(v ref, args []ref) (ref, bool)
// It panics if v is not a JavaScript function. // It panics if v is not a JavaScript function.
// The arguments get mapped to JavaScript values according to the ValueOf function. // The arguments get mapped to JavaScript values according to the ValueOf function.
func (v Value) New(args ...interface{}) Value { 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 !ok {
if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
panic(&ValueError{"Value.Invoke", vType}) panic(&ValueError{"Value.Invoke", vType})
...@@ -373,7 +445,7 @@ func valueNew(v ref, args []ref) (ref, bool) ...@@ -373,7 +445,7 @@ func valueNew(v ref, args []ref) (ref, bool)
func (v Value) isNumber() bool { func (v Value) isNumber() bool {
return v.ref == valueZero.ref || return v.ref == valueZero.ref ||
v.ref == valueNaN.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) float(method string) float64 {
...@@ -438,15 +510,15 @@ func (v Value) Truthy() bool { ...@@ -438,15 +510,15 @@ func (v Value) Truthy() bool {
func (v Value) String() string { func (v Value) String() string {
switch v.Type() { switch v.Type() {
case TypeString: case TypeString:
return jsString(v.ref) return jsString(v)
case TypeUndefined: case TypeUndefined:
return "<undefined>" return "<undefined>"
case TypeNull: case TypeNull:
return "<null>" return "<null>"
case TypeBoolean: case TypeBoolean:
return "<boolean: " + jsString(v.ref) + ">" return "<boolean: " + jsString(v) + ">"
case TypeNumber: case TypeNumber:
return "<number: " + jsString(v.ref) + ">" return "<number: " + jsString(v) + ">"
case TypeSymbol: case TypeSymbol:
return "<symbol>" return "<symbol>"
case TypeObject: case TypeObject:
...@@ -458,10 +530,12 @@ func (v Value) String() string { ...@@ -458,10 +530,12 @@ func (v Value) String() string {
} }
} }
func jsString(v ref) string { func jsString(v Value) string {
str, length := valuePrepareString(v) str, length := valuePrepareString(v.ref)
runtime.KeepAlive(v)
b := make([]byte, length) b := make([]byte, length)
valueLoadString(str, b) valueLoadString(str, b)
finalizeRef(str)
return string(b) return string(b)
} }
...@@ -471,7 +545,10 @@ func valueLoadString(v ref, b []byte) ...@@ -471,7 +545,10 @@ func valueLoadString(v ref, b []byte)
// InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator. // InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
func (v Value) InstanceOf(t Value) bool { 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 func valueInstanceOf(v ref, t ref) bool
...@@ -493,6 +570,7 @@ func (e *ValueError) Error() string { ...@@ -493,6 +570,7 @@ func (e *ValueError) Error() string {
// CopyBytesToGo panics if src is not an Uint8Array. // CopyBytesToGo panics if src is not an Uint8Array.
func CopyBytesToGo(dst []byte, src Value) int { func CopyBytesToGo(dst []byte, src Value) int {
n, ok := copyBytesToGo(dst, src.ref) n, ok := copyBytesToGo(dst, src.ref)
runtime.KeepAlive(src)
if !ok { if !ok {
panic("syscall/js: CopyBytesToGo: expected src to be an Uint8Array") panic("syscall/js: CopyBytesToGo: expected src to be an Uint8Array")
} }
...@@ -506,6 +584,7 @@ func copyBytesToGo(dst []byte, src ref) (int, bool) ...@@ -506,6 +584,7 @@ func copyBytesToGo(dst []byte, src ref) (int, bool)
// CopyBytesToJS panics if dst is not an Uint8Array. // CopyBytesToJS panics if dst is not an Uint8Array.
func CopyBytesToJS(dst Value, src []byte) int { func CopyBytesToJS(dst Value, src []byte) int {
n, ok := copyBytesToJS(dst.ref, src) n, ok := copyBytesToJS(dst.ref, src)
runtime.KeepAlive(dst)
if !ok { if !ok {
panic("syscall/js: CopyBytesToJS: expected dst to be an Uint8Array") panic("syscall/js: CopyBytesToJS: expected dst to be an Uint8Array")
} }
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
#include "textflag.h" #include "textflag.h"
TEXT ·finalizeRef(SB), NOSPLIT, $0
CallImport
RET
TEXT ·stringVal(SB), NOSPLIT, $0 TEXT ·stringVal(SB), NOSPLIT, $0
CallImport CallImport
RET RET
......
...@@ -18,6 +18,7 @@ package js_test ...@@ -18,6 +18,7 @@ package js_test
import ( import (
"fmt" "fmt"
"math" "math"
"runtime"
"syscall/js" "syscall/js"
"testing" "testing"
) )
...@@ -53,7 +54,7 @@ func TestBool(t *testing.T) { ...@@ -53,7 +54,7 @@ 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") { if !dummys.Get("someBool").Equal(dummys.Get("someBool")) {
t.Errorf("same value not equal") t.Errorf("same value not equal")
} }
} }
...@@ -68,7 +69,7 @@ func TestString(t *testing.T) { ...@@ -68,7 +69,7 @@ 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") { if !dummys.Get("someString").Equal(dummys.Get("someString")) {
t.Errorf("same value not equal") t.Errorf("same value not equal")
} }
...@@ -105,7 +106,7 @@ func TestInt(t *testing.T) { ...@@ -105,7 +106,7 @@ 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") { if !dummys.Get("someInt").Equal(dummys.Get("someInt")) {
t.Errorf("same value not equal") t.Errorf("same value not equal")
} }
if got := dummys.Get("zero").Int(); got != 0 { if got := dummys.Get("zero").Int(); got != 0 {
...@@ -141,20 +142,20 @@ func TestFloat(t *testing.T) { ...@@ -141,20 +142,20 @@ 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") { if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
t.Errorf("same value not equal") t.Errorf("same value not equal")
} }
} }
func TestObject(t *testing.T) { 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") t.Errorf("same value not equal")
} }
// An object and its prototype should not be equal. // An object and its prototype should not be equal.
proto := js.Global().Get("Object").Get("prototype") proto := js.Global().Get("Object").Get("prototype")
o := js.Global().Call("eval", "new Object()") o := js.Global().Call("eval", "new Object()")
if proto == o { if proto.Equal(o) {
t.Errorf("object equals to its prototype") t.Errorf("object equals to its prototype")
} }
} }
...@@ -167,26 +168,66 @@ func TestFrozenObject(t *testing.T) { ...@@ -167,26 +168,66 @@ func TestFrozenObject(t *testing.T) {
} }
} }
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) { func TestNaN(t *testing.T) {
want := js.ValueOf(math.NaN()) if !dummys.Get("NaN").IsNaN() {
got := dummys.Get("NaN") t.Errorf("JS NaN is not NaN")
if got != want { }
t.Errorf("got %#v, want %#v", got, want) 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) { func TestUndefined(t *testing.T) {
dummys.Set("test", js.Undefined()) if !js.Undefined().IsUndefined() {
if dummys == js.Undefined() || dummys.Get("test") != js.Undefined() || dummys.Get("xyz") != js.Undefined() { t.Errorf("undefined is not undefined")
t.Errorf("js.Undefined expected") }
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) { func TestNull(t *testing.T) {
dummys.Set("test1", nil) if !js.Null().IsNull() {
dummys.Set("test2", js.Null()) t.Errorf("null is not null")
if dummys == js.Null() || dummys.Get("test1") != js.Null() || dummys.Get("test2") != js.Null() { }
t.Errorf("js.Null expected") 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")
} }
} }
...@@ -340,7 +381,7 @@ func TestValueOf(t *testing.T) { ...@@ -340,7 +381,7 @@ func TestValueOf(t *testing.T) {
func TestZeroValue(t *testing.T) { func TestZeroValue(t *testing.T) {
var v js.Value var v js.Value
if v != js.Undefined() { if !v.IsUndefined() {
t.Error("zero js.Value is not js.Undefined()") t.Error("zero js.Value is not js.Undefined()")
} }
} }
...@@ -497,12 +538,24 @@ func TestCopyBytesToJS(t *testing.T) { ...@@ -497,12 +538,24 @@ func TestCopyBytesToJS(t *testing.T) {
} }
} }
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. // 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. // It creates a div, and sets its id. Then searches by that id and sets some data.
// Finally it removes that div. // Finally it removes that div.
func BenchmarkDOM(b *testing.B) { func BenchmarkDOM(b *testing.B) {
document := js.Global().Get("document") document := js.Global().Get("document")
if document == js.Undefined() { if document.IsUndefined() {
b.Skip("Not a browser environment. Skipping.") b.Skip("Not a browser environment. Skipping.")
} }
const data = "someString" const data = "someString"
......
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