blob: 7300d2c7695fc902b809f46bebb5c1122274333e [file] [log] [blame]
// 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 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 (
"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-33 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
// Wrapper is implemented by types that are backed by a JavaScript value.
type Wrapper interface {
// JSValue returns a JavaScript value associated with an object.
JSValue() Value
}
// Value represents a JavaScript value. The zero value is the JavaScript value "undefined".
type Value struct {
ref ref
}
// JSValue implements Wrapper interface.
func (v Value) JSValue() Value {
return v
}
func makeValue(v ref) Value {
return Value{ref: v}
}
func predefValue(id uint32) Value {
return Value{ref: nanHead<<32 | ref(id)}
}
func floatValue(f float64) Value {
if f == 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)
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
objectConstructor = valueGlobal.Get("Object")
arrayConstructor = valueGlobal.Get("Array")
)
// Undefined returns the JavaScript value "undefined".
func Undefined() Value {
return valueUndefined
}
// Null returns the JavaScript value "null".
func Null() Value {
return valueNull
}
// 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 interface{}) Value {
switch x := x.(type) {
case Value: // should precede Wrapper to avoid a loop
return x
case Wrapper:
return x.JSValue()
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 []interface{}:
a := arrayConstructor.New(len(x))
for i, s := range x {
a.SetIndex(i, s)
}
return a
case map[string]interface{}:
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 & 3
switch typeFlag {
case 1:
return TypeString
case 2:
return TypeSymbol
case 3:
return TypeFunction
default:
return TypeObject
}
}
// 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})
}
return makeValue(valueGet(v.ref, p))
}
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 interface{}) {
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.Set", vType})
}
valueSet(v.ref, p, ValueOf(x).ref)
}
func valueSet(v ref, p string, x ref)
// 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})
}
return makeValue(valueIndex(v.ref, i))
}
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 interface{}) {
if vType := v.Type(); !vType.isObject() {
panic(&ValueError{"Value.SetIndex", vType})
}
valueSetIndex(v.ref, i, ValueOf(x).ref)
}
func valueSetIndex(v ref, i int, x ref)
func makeArgs(args []interface{}) []ref {
argVals := make([]ref, len(args))
for i, arg := range args {
argVals[i] = ValueOf(arg).ref
}
return argVals
}
// 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})
}
return valueLength(v.ref)
}
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 ...interface{}) Value {
res, ok := valueCall(v.ref, m, makeArgs(args))
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 ...interface{}) Value {
res, ok := valueInvoke(v.ref, makeArgs(args))
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 ...interface{}) Value {
res, ok := valueNew(v.ref, makeArgs(args))
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.ref)
case TypeUndefined:
return "<undefined>"
case TypeNull:
return "<null>"
case TypeBoolean:
return "<boolean: " + jsString(v.ref) + ">"
case TypeNumber:
return "<number: " + jsString(v.ref) + ">"
case TypeSymbol:
return "<symbol>"
case TypeObject:
return "<object>"
case TypeFunction:
return "<function>"
default:
panic("bad type")
}
}
func jsString(v ref) string {
str, length := valuePrepareString(v)
b := make([]byte, length)
valueLoadString(str, b)
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 {
return valueInstanceOf(v.ref, t.ref)
}
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 the Uint8Array src to dst.
// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst.
// CopyBytesToGo panics if src is not an Uint8Array.
func CopyBytesToGo(dst []byte, src Value) int {
n, ok := copyBytesToGo(dst, src.ref)
if !ok {
panic("syscall/js: CopyBytesToGo: expected src to be an Uint8Array")
}
return n
}
func copyBytesToGo(dst []byte, src ref) (int, bool)
// CopyBytesToJS copies bytes from src to the Uint8Array dst.
// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst.
// CopyBytesToJS panics if dst is not an Uint8Array.
func CopyBytesToJS(dst Value, src []byte) int {
n, ok := copyBytesToJS(dst.ref, src)
if !ok {
panic("syscall/js: CopyBytesToJS: expected dst to be an Uint8Array")
}
return n
}
func copyBytesToJS(dst ref, src []byte) (int, bool)