github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/compiler/natives/src/runtime/runtime.go (about)

     1  //go:build js
     2  // +build js
     3  
     4  package runtime
     5  
     6  import (
     7  	"github.com/gopherjs/gopherjs/js"
     8  )
     9  
    10  const (
    11  	GOOS     = "js"
    12  	GOARCH   = "ecmascript"
    13  	Compiler = "gopherjs"
    14  )
    15  
    16  // The Error interface identifies a run time error.
    17  type Error interface {
    18  	error
    19  
    20  	// RuntimeError is a no-op function but
    21  	// serves to distinguish types that are run time
    22  	// errors from ordinary errors: a type is a
    23  	// run time error if it has a RuntimeError method.
    24  	RuntimeError()
    25  }
    26  
    27  // TODO(nevkontakte): In the upstream, this struct is meant to be compatible
    28  // with reflect.rtype, but here we use a minimal stub that satisfies the API
    29  // TypeAssertionError expects, which we dynamically instantiate in $assertType().
    30  type _type struct{ str string }
    31  
    32  func (t *_type) string() string  { return t.str }
    33  func (t *_type) pkgpath() string { return "" }
    34  
    35  // A TypeAssertionError explains a failed type assertion.
    36  type TypeAssertionError struct {
    37  	_interface    *_type
    38  	concrete      *_type
    39  	asserted      *_type
    40  	missingMethod string // one method needed by Interface, missing from Concrete
    41  }
    42  
    43  func (*TypeAssertionError) RuntimeError() {}
    44  
    45  func (e *TypeAssertionError) Error() string {
    46  	inter := "interface"
    47  	if e._interface != nil {
    48  		inter = e._interface.string()
    49  	}
    50  	as := e.asserted.string()
    51  	if e.concrete == nil {
    52  		return "interface conversion: " + inter + " is nil, not " + as
    53  	}
    54  	cs := e.concrete.string()
    55  	if e.missingMethod == "" {
    56  		msg := "interface conversion: " + inter + " is " + cs + ", not " + as
    57  		if cs == as {
    58  			// provide slightly clearer error message
    59  			if e.concrete.pkgpath() != e.asserted.pkgpath() {
    60  				msg += " (types from different packages)"
    61  			} else {
    62  				msg += " (types from different scopes)"
    63  			}
    64  		}
    65  		return msg
    66  	}
    67  	return "interface conversion: " + cs + " is not " + as +
    68  		": missing method " + e.missingMethod
    69  }
    70  
    71  func init() {
    72  	jsPkg := js.Global.Get("$packages").Get("github.com/gopherjs/gopherjs/js")
    73  	js.Global.Set("$jsObjectPtr", jsPkg.Get("Object").Get("ptr"))
    74  	js.Global.Set("$jsErrorPtr", jsPkg.Get("Error").Get("ptr"))
    75  	js.Global.Set("$throwRuntimeError", js.InternalObject(throw))
    76  	buildVersion = js.Global.Get("$goVersion").String()
    77  	// avoid dead code elimination
    78  	var e error
    79  	e = &TypeAssertionError{}
    80  	_ = e
    81  }
    82  
    83  func GOROOT() string {
    84  	process := js.Global.Get("process")
    85  	if process == js.Undefined || process.Get("env") == js.Undefined {
    86  		return "/"
    87  	}
    88  	if v := process.Get("env").Get("GOPHERJS_GOROOT"); v != js.Undefined && v.String() != "" {
    89  		// GopherJS-specific GOROOT value takes precedence.
    90  		return v.String()
    91  	} else if v := process.Get("env").Get("GOROOT"); v != js.Undefined && v.String() != "" {
    92  		return v.String()
    93  	}
    94  	// sys.DefaultGoroot is now gone, can't use it as fallback anymore.
    95  	// TODO: See if a better solution is needed.
    96  	return "/usr/local/go"
    97  }
    98  
    99  func Breakpoint() { js.Debugger() }
   100  
   101  var (
   102  	// JavaScript runtime doesn't provide access to low-level execution position
   103  	// counters, so we emulate them by recording positions we've encountered in
   104  	// Caller() and Callers() functions and assigning them arbitrary integer values.
   105  	//
   106  	// We use the map and the slice below to convert a "file:line" position
   107  	// into an integer position counter and then to a Func instance.
   108  	knownPositions   = map[string]uintptr{}
   109  	positionCounters = []*Func{}
   110  )
   111  
   112  func registerPosition(funcName string, file string, line int, col int) uintptr {
   113  	key := file + ":" + itoa(line) + ":" + itoa(col)
   114  	if pc, found := knownPositions[key]; found {
   115  		return pc
   116  	}
   117  	f := &Func{
   118  		name: funcName,
   119  		file: file,
   120  		line: line,
   121  	}
   122  	pc := uintptr(len(positionCounters))
   123  	positionCounters = append(positionCounters, f)
   124  	knownPositions[key] = pc
   125  	return pc
   126  }
   127  
   128  // itoa converts an integer to a string.
   129  //
   130  // Can't use strconv.Itoa() in the `runtime` package due to a cyclic dependency.
   131  func itoa(i int) string {
   132  	return js.Global.Get("String").New(i).String()
   133  }
   134  
   135  // basicFrame contains stack trace information extracted from JS stack trace.
   136  type basicFrame struct {
   137  	FuncName string
   138  	File     string
   139  	Line     int
   140  	Col      int
   141  }
   142  
   143  func callstack(skip, limit int) []basicFrame {
   144  	skip = skip + 1 /*skip error message*/ + 1 /*skip callstack's own frame*/
   145  	lines := js.Global.Get("Error").New().Get("stack").Call("split", "\n").Call("slice", skip, skip+limit)
   146  	return parseCallstack(lines)
   147  }
   148  
   149  var (
   150  	// These functions are GopherJS-specific and don't have counterparts in
   151  	// upstream Go runtime. To improve interoperability, we filter them out from
   152  	// the stack trace.
   153  	hiddenFrames = map[string]bool{
   154  		"$callDeferred": true,
   155  	}
   156  	// The following GopherJS prelude functions have differently-named
   157  	// counterparts in the upstream Go runtime. Some standard library code relies
   158  	// on the names matching, so we perform this substitution.
   159  	knownFrames = map[string]string{
   160  		"$panic":     "runtime.gopanic",
   161  		"$goroutine": "runtime.goexit",
   162  	}
   163  )
   164  
   165  func parseCallstack(lines *js.Object) []basicFrame {
   166  	frames := []basicFrame{}
   167  	l := lines.Length()
   168  	for i := 0; i < l; i++ {
   169  		frame := ParseCallFrame(lines.Index(i))
   170  		if hiddenFrames[frame.FuncName] {
   171  			continue
   172  		}
   173  		if alias, ok := knownFrames[frame.FuncName]; ok {
   174  			frame.FuncName = alias
   175  		}
   176  		frames = append(frames, frame)
   177  		if frame.FuncName == "runtime.goexit" {
   178  			break // We've reached the bottom of the goroutine stack.
   179  		}
   180  	}
   181  	return frames
   182  }
   183  
   184  // ParseCallFrame is exported for the sake of testing. See this discussion for context https://github.com/gopherjs/gopherjs/pull/1097/files/561e6381406f04ccb8e04ef4effedc5c7887b70f#r776063799
   185  //
   186  // TLDR; never use this function!
   187  func ParseCallFrame(info *js.Object) basicFrame {
   188  	// FireFox
   189  	if info.Call("indexOf", "@").Int() >= 0 {
   190  		split := js.Global.Get("RegExp").New("[@:]")
   191  		parts := info.Call("split", split)
   192  		return basicFrame{
   193  			File:     parts.Call("slice", 1, parts.Length()-2).Call("join", ":").String(),
   194  			Line:     parts.Index(parts.Length() - 2).Int(),
   195  			Col:      parts.Index(parts.Length() - 1).Int(),
   196  			FuncName: parts.Index(0).String(),
   197  		}
   198  	}
   199  
   200  	// Chrome / Node.js
   201  	openIdx := info.Call("lastIndexOf", "(").Int()
   202  	if openIdx == -1 {
   203  		parts := info.Call("split", ":")
   204  
   205  		return basicFrame{
   206  			File: parts.Call("slice", 0, parts.Length()-2).Call("join", ":").
   207  				Call("replace", js.Global.Get("RegExp").New(`^\s*at `), "").String(),
   208  			Line:     parts.Index(parts.Length() - 2).Int(),
   209  			Col:      parts.Index(parts.Length() - 1).Int(),
   210  			FuncName: "<none>",
   211  		}
   212  	}
   213  
   214  	var file, funcName string
   215  	var line, col int
   216  
   217  	pos := info.Call("substring", openIdx+1, info.Call("indexOf", ")").Int())
   218  	parts := pos.Call("split", ":")
   219  
   220  	if pos.String() == "<anonymous>" {
   221  		file = "<anonymous>"
   222  	} else {
   223  		file = parts.Call("slice", 0, parts.Length()-2).Call("join", ":").String()
   224  		line = parts.Index(parts.Length() - 2).Int()
   225  		col = parts.Index(parts.Length() - 1).Int()
   226  	}
   227  	fn := info.Call("substring", info.Call("indexOf", "at ").Int()+3, info.Call("indexOf", " (").Int())
   228  	if idx := fn.Call("indexOf", "[as ").Int(); idx > 0 {
   229  		fn = fn.Call("substring", idx+4, fn.Call("indexOf", "]"))
   230  	}
   231  	funcName = fn.String()
   232  
   233  	return basicFrame{
   234  		File:     file,
   235  		Line:     line,
   236  		Col:      col,
   237  		FuncName: funcName,
   238  	}
   239  }
   240  
   241  func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
   242  	skip = skip + 1 /*skip Caller's own frame*/
   243  	frames := callstack(skip, 1)
   244  	if len(frames) != 1 {
   245  		return 0, "", 0, false
   246  	}
   247  	pc = registerPosition(frames[0].FuncName, frames[0].File, frames[0].Line, frames[0].Col)
   248  	return pc, frames[0].File, frames[0].Line, true
   249  }
   250  
   251  // Callers fills the slice pc with the return program counters of function
   252  // invocations on the calling goroutine's stack. The argument skip is the number
   253  // of stack frames to skip before recording in pc, with 0 identifying the frame
   254  // for Callers itself and 1 identifying the caller of Callers. It returns the
   255  // number of entries written to pc.
   256  //
   257  // The returned call stack represents the logical Go call stack, which excludes
   258  // certain runtime-internal call frames that would be present in the raw
   259  // JavaScript stack trace. This is done to improve interoperability with the
   260  // upstream Go. Use JavaScript native APIs to access the raw call stack.
   261  //
   262  // To translate these PCs into symbolic information such as function names and
   263  // line numbers, use CallersFrames. CallersFrames accounts for inlined functions
   264  // and adjusts the return program counters into call program counters. Iterating
   265  // over the returned slice of PCs directly is discouraged, as is using FuncForPC
   266  // on any of the returned PCs, since these cannot account for inlining or return
   267  // program counter adjustment.
   268  func Callers(skip int, pc []uintptr) int {
   269  	frames := callstack(skip, len(pc))
   270  	for i, frame := range frames {
   271  		pc[i] = registerPosition(frame.FuncName, frame.File, frame.Line, frame.Col)
   272  	}
   273  	return len(frames)
   274  }
   275  
   276  func CallersFrames(callers []uintptr) *Frames {
   277  	result := Frames{}
   278  	for _, pc := range callers {
   279  		fun := FuncForPC(pc)
   280  		result.frames = append(result.frames, Frame{
   281  			PC:       pc,
   282  			Func:     fun,
   283  			Function: fun.name,
   284  			File:     fun.file,
   285  			Line:     fun.line,
   286  			Entry:    fun.Entry(),
   287  		})
   288  	}
   289  	return &result
   290  }
   291  
   292  type Frames struct {
   293  	frames  []Frame
   294  	current int
   295  }
   296  
   297  func (ci *Frames) Next() (frame Frame, more bool) {
   298  	if ci.current >= len(ci.frames) {
   299  		return Frame{}, false
   300  	}
   301  	f := ci.frames[ci.current]
   302  	ci.current++
   303  	return f, ci.current < len(ci.frames)
   304  }
   305  
   306  type Frame struct {
   307  	PC       uintptr
   308  	Func     *Func
   309  	Function string
   310  	File     string
   311  	Line     int
   312  	Entry    uintptr
   313  }
   314  
   315  func GC() {}
   316  
   317  func Goexit() {
   318  	js.Global.Get("$curGoroutine").Set("exit", true)
   319  	js.Global.Call("$throw", nil)
   320  }
   321  
   322  func GOMAXPROCS(int) int { return 1 }
   323  
   324  func Gosched() {
   325  	c := make(chan struct{})
   326  	js.Global.Call("$setTimeout", js.InternalObject(func() { close(c) }), 0)
   327  	<-c
   328  }
   329  
   330  func NumCPU() int { return 1 }
   331  
   332  func NumGoroutine() int {
   333  	return js.Global.Get("$totalGoroutines").Int()
   334  }
   335  
   336  type MemStats struct {
   337  	// General statistics.
   338  	Alloc      uint64 // bytes allocated and still in use
   339  	TotalAlloc uint64 // bytes allocated (even if freed)
   340  	Sys        uint64 // bytes obtained from system (sum of XxxSys below)
   341  	Lookups    uint64 // number of pointer lookups
   342  	Mallocs    uint64 // number of mallocs
   343  	Frees      uint64 // number of frees
   344  
   345  	// Main allocation heap statistics.
   346  	HeapAlloc    uint64 // bytes allocated and still in use
   347  	HeapSys      uint64 // bytes obtained from system
   348  	HeapIdle     uint64 // bytes in idle spans
   349  	HeapInuse    uint64 // bytes in non-idle span
   350  	HeapReleased uint64 // bytes released to the OS
   351  	HeapObjects  uint64 // total number of allocated objects
   352  
   353  	// Low-level fixed-size structure allocator statistics.
   354  	//	Inuse is bytes used now.
   355  	//	Sys is bytes obtained from system.
   356  	StackInuse  uint64 // bytes used by stack allocator
   357  	StackSys    uint64
   358  	MSpanInuse  uint64 // mspan structures
   359  	MSpanSys    uint64
   360  	MCacheInuse uint64 // mcache structures
   361  	MCacheSys   uint64
   362  	BuckHashSys uint64 // profiling bucket hash table
   363  	GCSys       uint64 // GC metadata
   364  	OtherSys    uint64 // other system allocations
   365  
   366  	// Garbage collector statistics.
   367  	NextGC        uint64 // next collection will happen when HeapAlloc ≥ this amount
   368  	LastGC        uint64 // end time of last collection (nanoseconds since 1970)
   369  	PauseTotalNs  uint64
   370  	PauseNs       [256]uint64 // circular buffer of recent GC pause durations, most recent at [(NumGC+255)%256]
   371  	PauseEnd      [256]uint64 // circular buffer of recent GC pause end times
   372  	NumGC         uint32
   373  	GCCPUFraction float64 // fraction of CPU time used by GC
   374  	EnableGC      bool
   375  	DebugGC       bool
   376  
   377  	// Per-size allocation statistics.
   378  	// 61 is NumSizeClasses in the C code.
   379  	BySize [61]struct {
   380  		Size    uint32
   381  		Mallocs uint64
   382  		Frees   uint64
   383  	}
   384  }
   385  
   386  func ReadMemStats(m *MemStats) {
   387  	// TODO(nevkontakte): This function is effectively unimplemented and may
   388  	// lead to silent unexpected behaviors. Consider panicing explicitly.
   389  }
   390  
   391  func SetFinalizer(x, f interface{}) {
   392  	// TODO(nevkontakte): This function is effectively unimplemented and may
   393  	// lead to silent unexpected behaviors. Consider panicing explicitly.
   394  }
   395  
   396  type Func struct {
   397  	name string
   398  	file string
   399  	line int
   400  
   401  	opaque struct{} // unexported field to disallow conversions
   402  }
   403  
   404  func (_ *Func) Entry() uintptr { return 0 }
   405  
   406  func (f *Func) FileLine(pc uintptr) (file string, line int) {
   407  	if f == nil {
   408  		return "", 0
   409  	}
   410  	return f.file, f.line
   411  }
   412  
   413  func (f *Func) Name() string {
   414  	if f == nil || f.name == "" {
   415  		return "<unknown>"
   416  	}
   417  	return f.name
   418  }
   419  
   420  func FuncForPC(pc uintptr) *Func {
   421  	ipc := int(pc)
   422  	if ipc >= len(positionCounters) {
   423  		// Since we are faking position counters, the only valid way to obtain one
   424  		// is through a Caller() or Callers() function. If pc is out of positionCounters
   425  		// bounds it must have been obtained in some other way, which is unexpected.
   426  		// If a panic proves problematic, we can return a nil *Func, which will
   427  		// present itself as a generic "unknown" function.
   428  		panic("GopherJS: pc=" + itoa(ipc) + " is out of range of known position counters")
   429  	}
   430  	return positionCounters[ipc]
   431  }
   432  
   433  var MemProfileRate int = 512 * 1024
   434  
   435  func SetBlockProfileRate(rate int) {
   436  }
   437  
   438  func SetMutexProfileFraction(rate int) int {
   439  	// TODO: Investigate this. If it's possible to implement, consider doing so, otherwise remove this comment.
   440  	return 0
   441  }
   442  
   443  // Stack formats a stack trace of the calling goroutine into buf and returns the
   444  // number of bytes written to buf. If all is true, Stack formats stack traces of
   445  // all other goroutines into buf after the trace for the current goroutine.
   446  //
   447  // Unlike runtime.Callers(), it returns an unprocessed, runtime-specific text
   448  // representation of the JavaScript stack trace.
   449  func Stack(buf []byte, all bool) int {
   450  	s := js.Global.Get("Error").New().Get("stack")
   451  	if s == js.Undefined {
   452  		return 0
   453  	}
   454  	return copy(buf, s.Call("substr", s.Call("indexOf", "\n").Int()+1).String())
   455  }
   456  
   457  func LockOSThread() {}
   458  
   459  func UnlockOSThread() {}
   460  
   461  var buildVersion string // Set by init()
   462  
   463  func Version() string {
   464  	return buildVersion
   465  }
   466  
   467  func StartTrace() error { return nil }
   468  func StopTrace()        {}
   469  func ReadTrace() []byte
   470  
   471  // We fake a cgo environment to catch errors. Therefore we have to implement this and always return 0
   472  func NumCgoCall() int64 {
   473  	return 0
   474  }
   475  
   476  func KeepAlive(interface{}) {}
   477  
   478  // An errorString represents a runtime error described by a single string.
   479  type errorString string
   480  
   481  func (e errorString) RuntimeError() {}
   482  
   483  func (e errorString) Error() string {
   484  	return "runtime error: " + string(e)
   485  }
   486  
   487  func throw(s string) {
   488  	panic(errorString(s))
   489  }
   490  
   491  func nanotime() int64 {
   492  	const millisecond = 1_000_000
   493  	return js.Global.Get("Date").New().Call("getTime").Int64() * millisecond
   494  }