diff options
Diffstat (limited to 'src/syscall/js')
-rw-r--r-- | src/syscall/js/export_test.go | 9 | ||||
-rw-r--r-- | src/syscall/js/func.go | 96 | ||||
-rw-r--r-- | src/syscall/js/js.go | 583 | ||||
-rw-r--r-- | src/syscall/js/js_js.s | 69 | ||||
-rw-r--r-- | src/syscall/js/js_test.go | 604 |
5 files changed, 1361 insertions, 0 deletions
diff --git a/src/syscall/js/export_test.go b/src/syscall/js/export_test.go new file mode 100644 index 0000000..fb61dae --- /dev/null +++ b/src/syscall/js/export_test.go @@ -0,0 +1,9 @@ +// 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. + +//go:build js && wasm + +package js + +var JSGo = jsGo diff --git a/src/syscall/js/func.go b/src/syscall/js/func.go new file mode 100644 index 0000000..cc94972 --- /dev/null +++ b/src/syscall/js/func.go @@ -0,0 +1,96 @@ +// 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. + +//go:build js && wasm + +package js + +import "sync" + +var ( + funcsMu sync.Mutex + funcs = make(map[uint32]func(Value, []Value) any) + nextFuncID uint32 = 1 +) + +// Func is a wrapped Go function to be called by JavaScript. +type Func struct { + Value // the JavaScript function that invokes the Go function + id uint32 +} + +// FuncOf returns a function to be used by JavaScript. +// +// The Go function fn is called with the value of JavaScript's "this" keyword and the +// arguments of the invocation. The return value of the invocation is +// the result of the Go function mapped back to JavaScript according to ValueOf. +// +// Invoking the wrapped Go function from JavaScript will +// pause the event loop and spawn a new goroutine. +// Other wrapped functions which are triggered during a call from Go to JavaScript +// get executed on the same goroutine. +// +// As a consequence, if one wrapped function blocks, JavaScript's event loop +// is blocked until that function returns. Hence, calling any async JavaScript +// API, which requires the event loop, like fetch (http.Client), will cause an +// immediate deadlock. Therefore a blocking function should explicitly start a +// new goroutine. +// +// Func.Release must be called to free up resources when the function will not be invoked any more. +func FuncOf(fn func(this Value, args []Value) any) Func { + funcsMu.Lock() + id := nextFuncID + nextFuncID++ + funcs[id] = fn + funcsMu.Unlock() + return Func{ + id: id, + Value: jsGo.Call("_makeFuncWrapper", id), + } +} + +// Release frees up resources allocated for the function. +// The function must not be invoked after calling Release. +// It is allowed to call Release while the function is still running. +func (c Func) Release() { + funcsMu.Lock() + delete(funcs, c.id) + funcsMu.Unlock() +} + +// setEventHandler is defined in the runtime package. +func setEventHandler(fn func()) + +func init() { + setEventHandler(handleEvent) +} + +func handleEvent() { + cb := jsGo.Get("_pendingEvent") + if cb.IsNull() { + return + } + jsGo.Set("_pendingEvent", Null()) + + id := uint32(cb.Get("id").Int()) + if id == 0 { // zero indicates deadlock + select {} + } + funcsMu.Lock() + f, ok := funcs[id] + funcsMu.Unlock() + if !ok { + Global().Get("console").Call("error", "call to released function") + return + } + + this := cb.Get("this") + argsObj := cb.Get("args") + args := make([]Value, argsObj.Length()) + for i := range args { + args[i] = argsObj.Index(i) + } + result := f(this, args) + cb.Set("result", result) +} diff --git a/src/syscall/js/js.go b/src/syscall/js/js.go new file mode 100644 index 0000000..2f4f5ad --- /dev/null +++ b/src/syscall/js/js.go @@ -0,0 +1,583 @@ +// 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. + +//go:build js && wasm + +// Package js gives access to the WebAssembly host environment when using the js/wasm architecture. +// Its API is based on JavaScript semantics. +// +// This package is EXPERIMENTAL. Its current scope is only to allow tests to run, but not yet to provide a +// comprehensive API for users. It is exempt from the Go compatibility promise. +package js + +import ( + "runtime" + "unsafe" +) + +// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly. +// +// 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-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). +const nanHead = 0x7FF80000 + +// 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 { + _ [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 +) + +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 finalizeRef(r ref) + +func predefValue(id uint32, typeFlag byte) Value { + return Value{ref: (nanHead|ref(typeFlag))<<32 | ref(id)} +} + +func floatValue(f float64) Value { + if f == 0 { + return valueZero + } + 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. + Value +} + +// Error implements the error interface. +func (e Error) Error() string { + return "JavaScript error: " + e.Get("message").String() +} + +var ( + valueUndefined = Value{ref: 0} + 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 +} + +// ValueOf returns x as a JavaScript value: +// +// | Go | JavaScript | +// | ---------------------- | ---------------------- | +// | js.Value | [its value] | +// | js.Func | function | +// | nil | null | +// | bool | boolean | +// | integers and floats | number | +// | string | string | +// | []interface{} | new array | +// | map[string]interface{} | new object | +// +// Panics if x is not one of the expected types. +func ValueOf(x any) Value { + switch x := x.(type) { + case Value: + return x + case Func: + return x.Value + case nil: + return valueNull + case bool: + if x { + return valueTrue + } else { + return valueFalse + } + case int: + return floatValue(float64(x)) + case int8: + return floatValue(float64(x)) + case int16: + return floatValue(float64(x)) + case int32: + return floatValue(float64(x)) + case int64: + return floatValue(float64(x)) + case uint: + return floatValue(float64(x)) + case uint8: + return floatValue(float64(x)) + case uint16: + return floatValue(float64(x)) + case uint32: + return floatValue(float64(x)) + case uint64: + return floatValue(float64(x)) + case uintptr: + return floatValue(float64(x)) + case unsafe.Pointer: + return floatValue(float64(uintptr(x))) + case float32: + return floatValue(float64(x)) + case float64: + return floatValue(x) + case string: + return makeValue(stringVal(x)) + case []any: + a := arrayConstructor.New(len(x)) + for i, s := range x { + a.SetIndex(i, s) + } + return a + case map[string]any: + o := objectConstructor.New() + for k, v := range x { + o.Set(k, v) + } + return o + default: + panic("ValueOf: invalid value") + } +} + +func stringVal(x string) ref + +// Type represents the JavaScript type of a Value. +type Type int + +const ( + TypeUndefined Type = iota + TypeNull + TypeBoolean + TypeNumber + TypeString + TypeSymbol + TypeObject + TypeFunction +) + +func (t Type) String() string { + switch t { + case TypeUndefined: + return "undefined" + case TypeNull: + return "null" + case TypeBoolean: + return "boolean" + case TypeNumber: + return "number" + case TypeString: + return "string" + case TypeSymbol: + return "symbol" + case TypeObject: + return "object" + case TypeFunction: + return "function" + default: + panic("bad type") + } +} + +func (t Type) isObject() bool { + return t == TypeObject || t == TypeFunction +} + +// Type returns the JavaScript type of the value v. It is similar to JavaScript's typeof operator, +// except that it returns TypeNull instead of TypeObject for null. +func (v Value) Type() Type { + switch v.ref { + case valueUndefined.ref: + return TypeUndefined + case valueNull.ref: + return TypeNull + case valueTrue.ref, valueFalse.ref: + return TypeBoolean + } + if v.isNumber() { + return TypeNumber + } + typeFlag := (v.ref >> 32) & 7 + switch typeFlag { + case typeFlagObject: + return TypeObject + case typeFlagString: + return TypeString + case typeFlagSymbol: + return TypeSymbol + case typeFlagFunction: + return TypeFunction + default: + panic("bad type flag") + } +} + +// Get returns the JavaScript property p of value v. +// It panics if v is not a JavaScript object. +func (v Value) Get(p string) Value { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.Get", vType}) + } + r := makeValue(valueGet(v.ref, p)) + runtime.KeepAlive(v) + return r +} + +func valueGet(v ref, p string) ref + +// Set sets the JavaScript property p of value v to ValueOf(x). +// It panics if v is not a JavaScript object. +func (v Value) Set(p string, x any) { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.Set", vType}) + } + xv := ValueOf(x) + valueSet(v.ref, p, xv.ref) + runtime.KeepAlive(v) + runtime.KeepAlive(xv) +} + +func valueSet(v ref, p string, x ref) + +// Delete deletes the JavaScript property p of value v. +// It panics if v is not a JavaScript object. +func (v Value) Delete(p string) { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.Delete", vType}) + } + valueDelete(v.ref, p) + runtime.KeepAlive(v) +} + +func valueDelete(v ref, p string) + +// Index returns JavaScript index i of value v. +// It panics if v is not a JavaScript object. +func (v Value) Index(i int) Value { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.Index", vType}) + } + r := makeValue(valueIndex(v.ref, i)) + runtime.KeepAlive(v) + return r +} + +func valueIndex(v ref, i int) ref + +// SetIndex sets the JavaScript index i of value v to ValueOf(x). +// It panics if v is not a JavaScript object. +func (v Value) SetIndex(i int, x any) { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.SetIndex", vType}) + } + 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 []any) ([]Value, []ref) { + argVals := make([]Value, len(args)) + argRefs := make([]ref, len(args)) + for i, arg := range args { + v := ValueOf(arg) + argVals[i] = v + argRefs[i] = v.ref + } + return argVals, argRefs +} + +// Length returns the JavaScript property "length" of v. +// It panics if v is not a JavaScript object. +func (v Value) Length() int { + if vType := v.Type(); !vType.isObject() { + panic(&ValueError{"Value.SetIndex", vType}) + } + r := valueLength(v.ref) + runtime.KeepAlive(v) + return r +} + +func valueLength(v ref) int + +// Call does a JavaScript call to the method m of value v with the given arguments. +// 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 ...any) Value { + 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}) + } + if propType := v.Get(m).Type(); propType != TypeFunction { + panic("syscall/js: Value.Call: property " + m + " is not a function, got " + propType.String()) + } + panic(Error{makeValue(res)}) + } + return makeValue(res) +} + +func valueCall(v ref, m string, args []ref) (ref, bool) + +// Invoke does a JavaScript call of the value v with the given arguments. +// 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 ...any) Value { + 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}) + } + panic(Error{makeValue(res)}) + } + return makeValue(res) +} + +func valueInvoke(v ref, args []ref) (ref, bool) + +// New uses JavaScript's "new" operator with value v as constructor and the given arguments. +// 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 ...any) Value { + 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}) + } + panic(Error{makeValue(res)}) + } + return makeValue(res) +} + +func valueNew(v ref, args []ref) (ref, bool) + +func (v Value) isNumber() bool { + return v.ref == valueZero.ref || + v.ref == valueNaN.ref || + (v.ref != valueUndefined.ref && (v.ref>>32)&nanHead != nanHead) +} + +func (v Value) float(method string) float64 { + if !v.isNumber() { + panic(&ValueError{method, v.Type()}) + } + if v.ref == valueZero.ref { + return 0 + } + return *(*float64)(unsafe.Pointer(&v.ref)) +} + +// Float returns the value v as a float64. +// It panics if v is not a JavaScript number. +func (v Value) Float() float64 { + return v.float("Value.Float") +} + +// Int returns the value v truncated to an int. +// It panics if v is not a JavaScript number. +func (v Value) Int() int { + return int(v.float("Value.Int")) +} + +// Bool returns the value v as a bool. +// It panics if v is not a JavaScript boolean. +func (v Value) Bool() bool { + switch v.ref { + case valueTrue.ref: + return true + case valueFalse.ref: + return false + default: + panic(&ValueError{"Value.Bool", v.Type()}) + } +} + +// Truthy returns the JavaScript "truthiness" of the value v. In JavaScript, +// false, 0, "", null, undefined, and NaN are "falsy", and everything else is +// "truthy". See https://developer.mozilla.org/en-US/docs/Glossary/Truthy. +func (v Value) Truthy() bool { + switch v.Type() { + case TypeUndefined, TypeNull: + return false + case TypeBoolean: + return v.Bool() + case TypeNumber: + return v.ref != valueNaN.ref && v.ref != valueZero.ref + case TypeString: + return v.String() != "" + case TypeSymbol, TypeFunction, TypeObject: + return true + default: + panic("bad type") + } +} + +// String returns the value v as a string. +// String is a special case because of Go's String method convention. Unlike the other getters, +// it does not panic if v's Type is not TypeString. Instead, it returns a string of the form "<T>" +// or "<T: V>" where T is v's type and V is a string representation of v's value. +func (v Value) String() string { + switch v.Type() { + case TypeString: + return jsString(v) + case TypeUndefined: + return "<undefined>" + case TypeNull: + return "<null>" + case TypeBoolean: + return "<boolean: " + jsString(v) + ">" + case TypeNumber: + return "<number: " + jsString(v) + ">" + case TypeSymbol: + return "<symbol>" + case TypeObject: + return "<object>" + case TypeFunction: + return "<function>" + default: + panic("bad type") + } +} + +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) +} + +func valuePrepareString(v ref) (ref, int) + +func valueLoadString(v ref, b []byte) + +// InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator. +func (v Value) InstanceOf(t Value) bool { + r := valueInstanceOf(v.ref, t.ref) + runtime.KeepAlive(v) + runtime.KeepAlive(t) + return r +} + +func valueInstanceOf(v ref, t ref) bool + +// A ValueError occurs when a Value method is invoked on +// a Value that does not support it. Such cases are documented +// in the description of each method. +type ValueError struct { + Method string + Type Type +} + +func (e *ValueError) Error() string { + return "syscall/js: call of " + e.Method + " on " + e.Type.String() +} + +// CopyBytesToGo copies bytes from src to dst. +// It panics if src is not an Uint8Array or Uint8ClampedArray. +// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst. +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 or Uint8ClampedArray") + } + return n +} + +func copyBytesToGo(dst []byte, src ref) (int, bool) + +// CopyBytesToJS copies bytes from src to dst. +// It panics if dst is not an Uint8Array or Uint8ClampedArray. +// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst. +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 or Uint8ClampedArray") + } + return n +} + +func copyBytesToJS(dst ref, src []byte) (int, bool) diff --git a/src/syscall/js/js_js.s b/src/syscall/js/js_js.s new file mode 100644 index 0000000..47ad6b8 --- /dev/null +++ b/src/syscall/js/js_js.s @@ -0,0 +1,69 @@ +// 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. + +#include "textflag.h" + +TEXT ·finalizeRef(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·stringVal(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueGet(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueSet(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueDelete(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueIndex(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueSetIndex(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueCall(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueInvoke(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueNew(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueLength(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valuePrepareString(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueLoadString(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·valueInstanceOf(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·copyBytesToGo(SB), NOSPLIT, $0 + CallImport + RET + +TEXT ·copyBytesToJS(SB), NOSPLIT, $0 + CallImport + RET diff --git a/src/syscall/js/js_test.go b/src/syscall/js/js_test.go new file mode 100644 index 0000000..f860a5b --- /dev/null +++ b/src/syscall/js/js_test.go @@ -0,0 +1,604 @@ +// 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. + +//go:build js && wasm + +// To run these tests: +// +// - Install Node +// - Add /path/to/go/misc/wasm to your $PATH (so that "go test" can find +// "go_js_wasm_exec"). +// - GOOS=js GOARCH=wasm go test +// +// See -exec in "go help test", and "go help run" for details. + +package js_test + +import ( + "fmt" + "math" + "runtime" + "syscall/js" + "testing" +) + +var dummys = js.Global().Call("eval", `({ + someBool: true, + someString: "abc\u1234", + someInt: 42, + someFloat: 42.123, + someArray: [41, 42, 43], + someDate: new Date(), + add: function(a, b) { + return a + b; + }, + zero: 0, + stringZero: "0", + NaN: NaN, + emptyObj: {}, + emptyArray: [], + Infinity: Infinity, + NegInfinity: -Infinity, + objNumber0: new Number(0), + objBooleanFalse: new Boolean(false), +})`) + +func TestBool(t *testing.T) { + want := true + o := dummys.Get("someBool") + if got := o.Bool(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + dummys.Set("otherBool", want) + if got := dummys.Get("otherBool").Bool(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if !dummys.Get("someBool").Equal(dummys.Get("someBool")) { + t.Errorf("same value not equal") + } +} + +func TestString(t *testing.T) { + want := "abc\u1234" + o := dummys.Get("someString") + if got := o.String(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + dummys.Set("otherString", want) + if got := dummys.Get("otherString").String(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if !dummys.Get("someString").Equal(dummys.Get("someString")) { + t.Errorf("same value not equal") + } + + if got, want := js.Undefined().String(), "<undefined>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.Null().String(), "<null>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.ValueOf(true).String(), "<boolean: true>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.ValueOf(42.5).String(), "<number: 42.5>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.Global().Call("Symbol").String(), "<symbol>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.Global().String(), "<object>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := js.Global().Get("setTimeout").String(), "<function>"; got != want { + t.Errorf("got %#v, want %#v", got, want) + } +} + +func TestInt(t *testing.T) { + want := 42 + o := dummys.Get("someInt") + if got := o.Int(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + dummys.Set("otherInt", want) + if got := dummys.Get("otherInt").Int(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if !dummys.Get("someInt").Equal(dummys.Get("someInt")) { + t.Errorf("same value not equal") + } + if got := dummys.Get("zero").Int(); got != 0 { + t.Errorf("got %#v, want %#v", got, 0) + } +} + +func TestIntConversion(t *testing.T) { + testIntConversion(t, 0) + testIntConversion(t, 1) + testIntConversion(t, -1) + testIntConversion(t, 1<<20) + testIntConversion(t, -1<<20) + testIntConversion(t, 1<<40) + testIntConversion(t, -1<<40) + testIntConversion(t, 1<<60) + testIntConversion(t, -1<<60) +} + +func testIntConversion(t *testing.T, want int) { + if got := js.ValueOf(want).Int(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } +} + +func TestFloat(t *testing.T) { + want := 42.123 + o := dummys.Get("someFloat") + if got := o.Float(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + dummys.Set("otherFloat", want) + if got := dummys.Get("otherFloat").Float(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) { + t.Errorf("same value not equal") + } +} + +func TestObject(t *testing.T) { + 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.Equal(o) { + t.Errorf("object equals to its prototype") + } +} + +func TestFrozenObject(t *testing.T) { + o := js.Global().Call("eval", "(function () { let o = new Object(); o.field = 5; Object.freeze(o); return o; })()") + want := 5 + if got := o.Get("field").Int(); want != got { + t.Errorf("got %#v, want %#v", got, want) + } +} + +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) { + 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) { + 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) { + 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 TestLength(t *testing.T) { + if got := dummys.Get("someArray").Length(); got != 3 { + t.Errorf("got %#v, want %#v", got, 3) + } +} + +func TestGet(t *testing.T) { + // positive cases get tested per type + + expectValueError(t, func() { + dummys.Get("zero").Get("badField") + }) +} + +func TestSet(t *testing.T) { + // positive cases get tested per type + + expectValueError(t, func() { + dummys.Get("zero").Set("badField", 42) + }) +} + +func TestDelete(t *testing.T) { + dummys.Set("test", 42) + dummys.Delete("test") + if dummys.Call("hasOwnProperty", "test").Bool() { + t.Errorf("property still exists") + } + + expectValueError(t, func() { + dummys.Get("zero").Delete("badField") + }) +} + +func TestIndex(t *testing.T) { + if got := dummys.Get("someArray").Index(1).Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + + expectValueError(t, func() { + dummys.Get("zero").Index(1) + }) +} + +func TestSetIndex(t *testing.T) { + dummys.Get("someArray").SetIndex(2, 99) + if got := dummys.Get("someArray").Index(2).Int(); got != 99 { + t.Errorf("got %#v, want %#v", got, 99) + } + + expectValueError(t, func() { + dummys.Get("zero").SetIndex(2, 99) + }) +} + +func TestCall(t *testing.T) { + var i int64 = 40 + if got := dummys.Call("add", i, 2).Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + if got := dummys.Call("add", js.Global().Call("eval", "40"), 2).Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + + expectPanic(t, func() { + dummys.Call("zero") + }) + expectValueError(t, func() { + dummys.Get("zero").Call("badMethod") + }) +} + +func TestInvoke(t *testing.T) { + var i int64 = 40 + if got := dummys.Get("add").Invoke(i, 2).Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + + expectValueError(t, func() { + dummys.Get("zero").Invoke() + }) +} + +func TestNew(t *testing.T) { + if got := js.Global().Get("Array").New(42).Length(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + + expectValueError(t, func() { + dummys.Get("zero").New() + }) +} + +func TestInstanceOf(t *testing.T) { + someArray := js.Global().Get("Array").New() + if got, want := someArray.InstanceOf(js.Global().Get("Array")), true; got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got, want := someArray.InstanceOf(js.Global().Get("Function")), false; got != want { + t.Errorf("got %#v, want %#v", got, want) + } +} + +func TestType(t *testing.T) { + if got, want := js.Undefined().Type(), js.TypeUndefined; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.Null().Type(), js.TypeNull; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.ValueOf(true).Type(), js.TypeBoolean; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.ValueOf(0).Type(), js.TypeNumber; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.ValueOf(42).Type(), js.TypeNumber; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.ValueOf("test").Type(), js.TypeString; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.Global().Get("Symbol").Invoke("test").Type(), js.TypeSymbol; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.Global().Get("Array").New().Type(), js.TypeObject; got != want { + t.Errorf("got %s, want %s", got, want) + } + if got, want := js.Global().Get("Array").Type(), js.TypeFunction; got != want { + t.Errorf("got %s, want %s", got, want) + } +} + +type object = map[string]any +type array = []any + +func TestValueOf(t *testing.T) { + a := js.ValueOf(array{0, array{0, 42, 0}, 0}) + if got := a.Index(1).Index(1).Int(); got != 42 { + t.Errorf("got %v, want %v", got, 42) + } + + o := js.ValueOf(object{"x": object{"y": 42}}) + if got := o.Get("x").Get("y").Int(); got != 42 { + t.Errorf("got %v, want %v", got, 42) + } +} + +func TestZeroValue(t *testing.T) { + var v js.Value + if !v.IsUndefined() { + t.Error("zero js.Value is not js.Undefined()") + } +} + +func TestFuncOf(t *testing.T) { + c := make(chan struct{}) + cb := js.FuncOf(func(this js.Value, args []js.Value) any { + if got := args[0].Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + c <- struct{}{} + return nil + }) + defer cb.Release() + js.Global().Call("setTimeout", cb, 0, 42) + <-c +} + +func TestInvokeFunction(t *testing.T) { + called := false + cb := js.FuncOf(func(this js.Value, args []js.Value) any { + cb2 := js.FuncOf(func(this js.Value, args []js.Value) any { + called = true + return 42 + }) + defer cb2.Release() + return cb2.Invoke() + }) + defer cb.Release() + if got := cb.Invoke().Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + if !called { + t.Error("function not called") + } +} + +func TestInterleavedFunctions(t *testing.T) { + c1 := make(chan struct{}) + c2 := make(chan struct{}) + + js.Global().Get("setTimeout").Invoke(js.FuncOf(func(this js.Value, args []js.Value) any { + c1 <- struct{}{} + <-c2 + return nil + }), 0) + + <-c1 + c2 <- struct{}{} + // this goroutine is running, but the callback of setTimeout did not return yet, invoke another function now + f := js.FuncOf(func(this js.Value, args []js.Value) any { + return nil + }) + f.Invoke() +} + +func ExampleFuncOf() { + var cb js.Func + cb = js.FuncOf(func(this js.Value, args []js.Value) any { + fmt.Println("button clicked") + cb.Release() // release the function if the button will not be clicked again + return nil + }) + js.Global().Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb) +} + +// See +// - https://developer.mozilla.org/en-US/docs/Glossary/Truthy +// - https://stackoverflow.com/questions/19839952/all-falsey-values-in-javascript/19839953#19839953 +// - http://www.ecma-international.org/ecma-262/5.1/#sec-9.2 +func TestTruthy(t *testing.T) { + want := true + for _, key := range []string{ + "someBool", "someString", "someInt", "someFloat", "someArray", "someDate", + "stringZero", // "0" is truthy + "add", // functions are truthy + "emptyObj", "emptyArray", "Infinity", "NegInfinity", + // All objects are truthy, even if they're Number(0) or Boolean(false). + "objNumber0", "objBooleanFalse", + } { + if got := dummys.Get(key).Truthy(); got != want { + t.Errorf("%s: got %#v, want %#v", key, got, want) + } + } + + want = false + if got := dummys.Get("zero").Truthy(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got := dummys.Get("NaN").Truthy(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got := js.ValueOf("").Truthy(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got := js.Null().Truthy(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } + if got := js.Undefined().Truthy(); got != want { + t.Errorf("got %#v, want %#v", got, want) + } +} + +func expectValueError(t *testing.T, fn func()) { + defer func() { + err := recover() + if _, ok := err.(*js.ValueError); !ok { + t.Errorf("expected *js.ValueError, got %T", err) + } + }() + fn() +} + +func expectPanic(t *testing.T, fn func()) { + defer func() { + err := recover() + if err == nil { + t.Errorf("expected panic") + } + }() + fn() +} + +var copyTests = []struct { + srcLen int + dstLen int + copyLen int +}{ + {5, 3, 3}, + {3, 5, 3}, + {0, 0, 0}, +} + +func TestCopyBytesToGo(t *testing.T) { + for _, tt := range copyTests { + t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) { + src := js.Global().Get("Uint8Array").New(tt.srcLen) + if tt.srcLen >= 2 { + src.SetIndex(1, 42) + } + dst := make([]byte, tt.dstLen) + + if got, want := js.CopyBytesToGo(dst, src), tt.copyLen; got != want { + t.Errorf("copied %d, want %d", got, want) + } + if tt.dstLen >= 2 { + if got, want := int(dst[1]), 42; got != want { + t.Errorf("got %d, want %d", got, want) + } + } + }) + } +} + +func TestCopyBytesToJS(t *testing.T) { + for _, tt := range copyTests { + t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) { + src := make([]byte, tt.srcLen) + if tt.srcLen >= 2 { + src[1] = 42 + } + dst := js.Global().Get("Uint8Array").New(tt.dstLen) + + if got, want := js.CopyBytesToJS(dst, src), tt.copyLen; got != want { + t.Errorf("copied %d, want %d", got, want) + } + if tt.dstLen >= 2 { + if got, want := dst.Index(1).Int(), 42; got != want { + t.Errorf("got %d, want %d", got, want) + } + } + }) + } +} + +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.IsUndefined() { + b.Skip("Not a browser environment. Skipping.") + } + const data = "someString" + for i := 0; i < b.N; i++ { + div := document.Call("createElement", "div") + div.Call("setAttribute", "id", "myDiv") + document.Get("body").Call("appendChild", div) + myDiv := document.Call("getElementById", "myDiv") + myDiv.Set("innerHTML", data) + + if got, want := myDiv.Get("innerHTML").String(), data; got != want { + b.Errorf("got %s, want %s", got, want) + } + document.Get("body").Call("removeChild", div) + } +} + +func TestGlobal(t *testing.T) { + ident := js.FuncOf(func(this js.Value, args []js.Value) any { + return args[0] + }) + defer ident.Release() + + if got := ident.Invoke(js.Global()); !got.Equal(js.Global()) { + t.Errorf("got %#v, want %#v", got, js.Global()) + } +} |