mirror of
				https://github.com/golang/go.git
				synced 2025-10-31 08:40:55 +00:00 
			
		
		
		
	runtime, sycall/js: add support for callbacks from JavaScript
This commit adds support for JavaScript callbacks back into WebAssembly. This is experimental API, just like the rest of the syscall/js package. The time package now also uses this mechanism to properly support timers without resorting to a busy loop. JavaScript code can call into the same entry point multiple times. The new RUN register is used to keep track of the program's run state. Possible values are: starting, running, paused and exited. If no goroutine is ready any more, the scheduler can put the program into the "paused" state and the WebAssembly code will stop running. When a callback occurs, the JavaScript code puts the callback data into a queue and then calls into WebAssembly to allow the Go code to continue running. Updates #18892 Updates #25506 Change-Id: Ib8701cfa0536d10d69bd541c85b0e2a754eb54fb Reviewed-on: https://go-review.googlesource.com/114197 Reviewed-by: Austin Clements <austin@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
		
							parent
							
								
									5fdacfa89f
								
							
						
					
					
						commit
						e083dc6307
					
				
					 18 changed files with 482 additions and 49 deletions
				
			
		|  | @ -56,6 +56,8 @@ | ||||||
| 					console.warn("exit code:", code); | 					console.warn("exit code:", code); | ||||||
| 				} | 				} | ||||||
| 			}; | 			}; | ||||||
|  | 			this._callbackTimeouts = new Map(); | ||||||
|  | 			this._nextCallbackTimeoutID = 1; | ||||||
| 
 | 
 | ||||||
| 			const mem = () => { | 			const mem = () => { | ||||||
| 				// The buffer may change when requesting more memory.
 | 				// The buffer may change when requesting more memory.
 | ||||||
|  | @ -119,6 +121,7 @@ | ||||||
| 				go: { | 				go: { | ||||||
| 					// func wasmExit(code int32)
 | 					// func wasmExit(code int32)
 | ||||||
| 					"runtime.wasmExit": (sp) => { | 					"runtime.wasmExit": (sp) => { | ||||||
|  | 						this.exited = true; | ||||||
| 						this.exit(mem().getInt32(sp + 8, true)); | 						this.exit(mem().getInt32(sp + 8, true)); | ||||||
| 					}, | 					}, | ||||||
| 
 | 
 | ||||||
|  | @ -142,6 +145,24 @@ | ||||||
| 						mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); | 						mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); | ||||||
| 					}, | 					}, | ||||||
| 
 | 
 | ||||||
|  | 					// func scheduleCallback(delay int64) int32
 | ||||||
|  | 					"runtime.scheduleCallback": (sp) => { | ||||||
|  | 						const id = this._nextCallbackTimeoutID; | ||||||
|  | 						this._nextCallbackTimeoutID++; | ||||||
|  | 						this._callbackTimeouts.set(id, setTimeout( | ||||||
|  | 							() => { this._resolveCallbackPromise(); }, | ||||||
|  | 							getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
 | ||||||
|  | 						)); | ||||||
|  | 						mem().setInt32(sp + 16, id, true); | ||||||
|  | 					}, | ||||||
|  | 
 | ||||||
|  | 					// func clearScheduledCallback(id int32)
 | ||||||
|  | 					"runtime.clearScheduledCallback": (sp) => { | ||||||
|  | 						const id = mem().getInt32(sp + 8, true); | ||||||
|  | 						clearTimeout(this._callbackTimeouts.get(id)); | ||||||
|  | 						this._callbackTimeouts.delete(id); | ||||||
|  | 					}, | ||||||
|  | 
 | ||||||
| 					// func getRandomData(r []byte)
 | 					// func getRandomData(r []byte)
 | ||||||
| 					"runtime.getRandomData": (sp) => { | 					"runtime.getRandomData": (sp) => { | ||||||
| 						crypto.getRandomValues(loadSlice(sp + 8)); | 						crypto.getRandomValues(loadSlice(sp + 8)); | ||||||
|  | @ -269,7 +290,19 @@ | ||||||
| 
 | 
 | ||||||
| 		async run(instance) { | 		async run(instance) { | ||||||
| 			this._inst = instance; | 			this._inst = instance; | ||||||
| 			this._values = [undefined, null, global, this._inst.exports.mem]; // TODO: garbage collection
 | 			this._values = [ // TODO: garbage collection
 | ||||||
|  | 				undefined, | ||||||
|  | 				null, | ||||||
|  | 				global, | ||||||
|  | 				this._inst.exports.mem, | ||||||
|  | 				() => { // resolveCallbackPromise
 | ||||||
|  | 					if (this.exited) { | ||||||
|  | 						throw new Error("bad callback: Go program has already exited"); | ||||||
|  | 					} | ||||||
|  | 					setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
 | ||||||
|  | 				}, | ||||||
|  | 			]; | ||||||
|  | 			this.exited = false; | ||||||
| 
 | 
 | ||||||
| 			const mem = new DataView(this._inst.exports.mem.buffer) | 			const mem = new DataView(this._inst.exports.mem.buffer) | ||||||
| 
 | 
 | ||||||
|  | @ -303,7 +336,16 @@ | ||||||
| 				offset += 8; | 				offset += 8; | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			this._inst.exports.run(argc, argv); | 			while (true) { | ||||||
|  | 				const callbackPromise = new Promise((resolve) => { | ||||||
|  | 					this._resolveCallbackPromise = resolve; | ||||||
|  | 				}); | ||||||
|  | 				this._inst.exports.run(argc, argv); | ||||||
|  | 				if (this.exited) { | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 				await callbackPromise; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -318,9 +360,16 @@ | ||||||
| 		go.env = process.env; | 		go.env = process.env; | ||||||
| 		go.exit = process.exit; | 		go.exit = process.exit; | ||||||
| 		WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { | 		WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { | ||||||
|  | 			process.on("exit", () => { // Node.js exits if no callback is pending
 | ||||||
|  | 				if (!go.exited) { | ||||||
|  | 					console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!"); | ||||||
|  | 					process.exit(1); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
| 			return go.run(result.instance); | 			return go.run(result.instance); | ||||||
| 		}).catch((err) => { | 		}).catch((err) => { | ||||||
| 			console.error(err); | 			console.error(err); | ||||||
|  | 			go.exited = true; | ||||||
| 			process.exit(1); | 			process.exit(1); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -219,6 +219,8 @@ const ( | ||||||
| 	// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call. | 	// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call. | ||||||
| 	ACALLNORESUME | 	ACALLNORESUME | ||||||
| 
 | 
 | ||||||
|  | 	ARETUNWIND | ||||||
|  | 
 | ||||||
| 	AMOVB | 	AMOVB | ||||||
| 	AMOVH | 	AMOVH | ||||||
| 	AMOVW | 	AMOVW | ||||||
|  | @ -244,6 +246,7 @@ const ( | ||||||
| 	REG_RET1 | 	REG_RET1 | ||||||
| 	REG_RET2 | 	REG_RET2 | ||||||
| 	REG_RET3 | 	REG_RET3 | ||||||
|  | 	REG_RUN | ||||||
| 
 | 
 | ||||||
| 	// locals | 	// locals | ||||||
| 	REG_R0 | 	REG_R0 | ||||||
|  |  | ||||||
|  | @ -180,6 +180,7 @@ var Anames = []string{ | ||||||
| 	"F64ReinterpretI64", | 	"F64ReinterpretI64", | ||||||
| 	"RESUMEPOINT", | 	"RESUMEPOINT", | ||||||
| 	"CALLNORESUME", | 	"CALLNORESUME", | ||||||
|  | 	"RETUNWIND", | ||||||
| 	"MOVB", | 	"MOVB", | ||||||
| 	"MOVH", | 	"MOVH", | ||||||
| 	"MOVW", | 	"MOVW", | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ var Register = map[string]int16{ | ||||||
| 	"RET1": REG_RET1, | 	"RET1": REG_RET1, | ||||||
| 	"RET2": REG_RET2, | 	"RET2": REG_RET2, | ||||||
| 	"RET3": REG_RET3, | 	"RET3": REG_RET3, | ||||||
|  | 	"RUN":  REG_RUN, | ||||||
| 
 | 
 | ||||||
| 	"R0":  REG_R0, | 	"R0":  REG_R0, | ||||||
| 	"R1":  REG_R1, | 	"R1":  REG_R1, | ||||||
|  | @ -487,7 +488,7 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { | ||||||
| 				p = appendp(p, AEnd) // end of Loop | 				p = appendp(p, AEnd) // end of Loop | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 		case obj.ARET: | 		case obj.ARET, ARETUNWIND: | ||||||
| 			ret := *p | 			ret := *p | ||||||
| 			p.As = obj.ANOP | 			p.As = obj.ANOP | ||||||
| 
 | 
 | ||||||
|  | @ -528,7 +529,14 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { | ||||||
| 			p = appendp(p, AI32Add) | 			p = appendp(p, AI32Add) | ||||||
| 			p = appendp(p, ASet, regAddr(REG_SP)) | 			p = appendp(p, ASet, regAddr(REG_SP)) | ||||||
| 
 | 
 | ||||||
| 			// not switching goroutine, return 0 | 			if ret.As == ARETUNWIND { | ||||||
|  | 				// function needs to unwind the WebAssembly stack, return 1 | ||||||
|  | 				p = appendp(p, AI32Const, constAddr(1)) | ||||||
|  | 				p = appendp(p, AReturn) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// not unwinding the WebAssembly stack, return 0 | ||||||
| 			p = appendp(p, AI32Const, constAddr(0)) | 			p = appendp(p, AI32Const, constAddr(0)) | ||||||
| 			p = appendp(p, AReturn) | 			p = appendp(p, AReturn) | ||||||
| 		} | 		} | ||||||
|  | @ -726,7 +734,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { | ||||||
| 			} | 			} | ||||||
| 			reg := p.From.Reg | 			reg := p.From.Reg | ||||||
| 			switch { | 			switch { | ||||||
| 			case reg >= REG_PC_F && reg <= REG_RET3: | 			case reg >= REG_PC_F && reg <= REG_RUN: | ||||||
| 				w.WriteByte(0x23) // get_global | 				w.WriteByte(0x23) // get_global | ||||||
| 				writeUleb128(w, uint64(reg-REG_PC_F)) | 				writeUleb128(w, uint64(reg-REG_PC_F)) | ||||||
| 			case reg >= REG_R0 && reg <= REG_F15: | 			case reg >= REG_R0 && reg <= REG_F15: | ||||||
|  | @ -743,7 +751,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { | ||||||
| 			} | 			} | ||||||
| 			reg := p.To.Reg | 			reg := p.To.Reg | ||||||
| 			switch { | 			switch { | ||||||
| 			case reg >= REG_PC_F && reg <= REG_RET3: | 			case reg >= REG_PC_F && reg <= REG_RUN: | ||||||
| 				w.WriteByte(0x24) // set_global | 				w.WriteByte(0x24) // set_global | ||||||
| 				writeUleb128(w, uint64(reg-REG_PC_F)) | 				writeUleb128(w, uint64(reg-REG_PC_F)) | ||||||
| 			case reg >= REG_R0 && reg <= REG_F15: | 			case reg >= REG_R0 && reg <= REG_F15: | ||||||
|  |  | ||||||
|  | @ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) { | ||||||
| 		I64, // 6: RET1 | 		I64, // 6: RET1 | ||||||
| 		I64, // 7: RET2 | 		I64, // 7: RET2 | ||||||
| 		I64, // 8: RET3 | 		I64, // 8: RET3 | ||||||
|  | 		I32, // 9: RUN | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals | 	writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals | ||||||
|  |  | ||||||
|  | @ -1,3 +1,7 @@ | ||||||
|  | // 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. | ||||||
|  | 
 | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  |  | ||||||
|  | @ -1,3 +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. | ||||||
|  | 
 | ||||||
|  | // +build !js | ||||||
|  | 
 | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ | ||||||
| // Use of this source code is governed by a BSD-style | // Use of this source code is governed by a BSD-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
| 
 | 
 | ||||||
|  | // +build !js | ||||||
|  | 
 | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  |  | ||||||
|  | @ -139,7 +139,7 @@ var pkgDeps = map[string][]string{ | ||||||
| 
 | 
 | ||||||
| 	// Operating system access. | 	// Operating system access. | ||||||
| 	"syscall":                           {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"}, | 	"syscall":                           {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"}, | ||||||
| 	"syscall/js":                        {"unsafe"}, | 	"syscall/js":                        {"L0"}, | ||||||
| 	"internal/syscall/unix":             {"L0", "syscall"}, | 	"internal/syscall/unix":             {"L0", "syscall"}, | ||||||
| 	"internal/syscall/windows":          {"L0", "syscall", "internal/syscall/windows/sysdll"}, | 	"internal/syscall/windows":          {"L0", "syscall", "internal/syscall/windows/sysdll"}, | ||||||
| 	"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"}, | 	"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"}, | ||||||
|  |  | ||||||
|  | @ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool { | ||||||
| 	exitsyscall() | 	exitsyscall() | ||||||
| 	return ok | 	return ok | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func pauseSchedulerUntilCallback() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkTimeouts() {} | ||||||
|  |  | ||||||
|  | @ -6,14 +6,22 @@ | ||||||
| 
 | 
 | ||||||
| package runtime | package runtime | ||||||
| 
 | 
 | ||||||
|  | import ( | ||||||
|  | 	_ "unsafe" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // js/wasm has no support for threads yet. There is no preemption. | // js/wasm has no support for threads yet. There is no preemption. | ||||||
| // Waiting for a mutex or timeout is implemented as a busy loop | // Waiting for a mutex is implemented by allowing other goroutines | ||||||
| // while allowing other goroutines to run. | // to run until the mutex gets unlocked. | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	mutex_unlocked = 0 | 	mutex_unlocked = 0 | ||||||
| 	mutex_locked   = 1 | 	mutex_locked   = 1 | ||||||
| 
 | 
 | ||||||
|  | 	note_cleared = 0 | ||||||
|  | 	note_woken   = 1 | ||||||
|  | 	note_timeout = 2 | ||||||
|  | 
 | ||||||
| 	active_spin     = 4 | 	active_spin     = 4 | ||||||
| 	active_spin_cnt = 30 | 	active_spin_cnt = 30 | ||||||
| 	passive_spin    = 1 | 	passive_spin    = 1 | ||||||
|  | @ -21,7 +29,7 @@ const ( | ||||||
| 
 | 
 | ||||||
| func lock(l *mutex) { | func lock(l *mutex) { | ||||||
| 	for l.key == mutex_locked { | 	for l.key == mutex_locked { | ||||||
| 		Gosched() | 		mcall(gosched_m) | ||||||
| 	} | 	} | ||||||
| 	l.key = mutex_locked | 	l.key = mutex_locked | ||||||
| } | } | ||||||
|  | @ -34,16 +42,31 @@ func unlock(l *mutex) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // One-time notifications. | // One-time notifications. | ||||||
|  | 
 | ||||||
|  | type noteWithTimeout struct { | ||||||
|  | 	gp       *g | ||||||
|  | 	deadline int64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	notes            = make(map[*note]*g) | ||||||
|  | 	notesWithTimeout = make(map[*note]noteWithTimeout) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func noteclear(n *note) { | func noteclear(n *note) { | ||||||
| 	n.key = 0 | 	n.key = note_cleared | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func notewakeup(n *note) { | func notewakeup(n *note) { | ||||||
| 	if n.key != 0 { | 	// gp := getg() | ||||||
| 		print("notewakeup - double wakeup (", n.key, ")\n") | 	if n.key == note_woken { | ||||||
| 		throw("notewakeup - double wakeup") | 		throw("notewakeup - double wakeup") | ||||||
| 	} | 	} | ||||||
| 	n.key = 1 | 	cleared := n.key == note_cleared | ||||||
|  | 	n.key = note_woken | ||||||
|  | 	if cleared { | ||||||
|  | 		goready(notes[n], 1) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func notesleep(n *note) { | func notesleep(n *note) { | ||||||
|  | @ -62,14 +85,88 @@ func notetsleepg(n *note, ns int64) bool { | ||||||
| 		throw("notetsleepg on g0") | 		throw("notetsleepg on g0") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	deadline := nanotime() + ns | 	if ns >= 0 { | ||||||
| 	for { | 		deadline := nanotime() + ns | ||||||
| 		if n.key != 0 { | 		delay := ns/1000000 + 1 // round up | ||||||
| 			return true | 		if delay > 1<<31-1 { | ||||||
|  | 			delay = 1<<31 - 1 // cap to max int32 | ||||||
| 		} | 		} | ||||||
| 		Gosched() | 
 | ||||||
| 		if ns >= 0 && nanotime() >= deadline { | 		id := scheduleCallback(delay) | ||||||
| 			return false | 		mp := acquirem() | ||||||
|  | 		notes[n] = gp | ||||||
|  | 		notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline} | ||||||
|  | 		releasem(mp) | ||||||
|  | 
 | ||||||
|  | 		gopark(nil, nil, waitReasonSleep, traceEvNone, 1) | ||||||
|  | 
 | ||||||
|  | 		clearScheduledCallback(id) // note might have woken early, clear timeout | ||||||
|  | 		mp = acquirem() | ||||||
|  | 		delete(notes, n) | ||||||
|  | 		delete(notesWithTimeout, n) | ||||||
|  | 		releasem(mp) | ||||||
|  | 
 | ||||||
|  | 		return n.key == note_woken | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for n.key != note_woken { | ||||||
|  | 		mp := acquirem() | ||||||
|  | 		notes[n] = gp | ||||||
|  | 		releasem(mp) | ||||||
|  | 
 | ||||||
|  | 		gopark(nil, nil, waitReasonZero, traceEvNone, 1) | ||||||
|  | 
 | ||||||
|  | 		mp = acquirem() | ||||||
|  | 		delete(notes, n) | ||||||
|  | 		releasem(mp) | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // checkTimeouts resumes goroutines that are waiting on a note which has reached its deadline. | ||||||
|  | func checkTimeouts() { | ||||||
|  | 	now := nanotime() | ||||||
|  | 	for n, nt := range notesWithTimeout { | ||||||
|  | 		if n.key == note_cleared && now > nt.deadline { | ||||||
|  | 			n.key = note_timeout | ||||||
|  | 			goready(nt.gp, 1) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | var waitingForCallback *g | ||||||
|  | 
 | ||||||
|  | // sleepUntilCallback puts the current goroutine to sleep until a callback is triggered. | ||||||
|  | // It is currently only used by the callback routine of the syscall/js package. | ||||||
|  | //go:linkname sleepUntilCallback syscall/js.sleepUntilCallback | ||||||
|  | func sleepUntilCallback() { | ||||||
|  | 	waitingForCallback = getg() | ||||||
|  | 	gopark(nil, nil, waitReasonZero, traceEvNone, 1) | ||||||
|  | 	waitingForCallback = nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // pauseSchedulerUntilCallback gets called from the scheduler and pauses the execution | ||||||
|  | // of Go's WebAssembly code until a callback is triggered. Then it checks for note timeouts | ||||||
|  | // and resumes goroutines that are waiting for a callback. | ||||||
|  | func pauseSchedulerUntilCallback() bool { | ||||||
|  | 	if waitingForCallback == nil && len(notesWithTimeout) == 0 { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pause() | ||||||
|  | 	checkTimeouts() | ||||||
|  | 	if waitingForCallback != nil { | ||||||
|  | 		goready(waitingForCallback, 1) | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // pause pauses the execution of Go's WebAssembly code until a callback is triggered. | ||||||
|  | func pause() | ||||||
|  | 
 | ||||||
|  | // scheduleCallback tells the WebAssembly environment to trigger a callback after ms milliseconds. | ||||||
|  | // It returns a timer id that can be used with clearScheduledCallback. | ||||||
|  | func scheduleCallback(ms int64) int32 | ||||||
|  | 
 | ||||||
|  | // clearScheduledCallback clears a callback scheduled by scheduleCallback. | ||||||
|  | func clearScheduledCallback(id int32) | ||||||
|  |  | ||||||
|  | @ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool { | ||||||
| 	exitsyscall() | 	exitsyscall() | ||||||
| 	return ok | 	return ok | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func pauseSchedulerUntilCallback() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkTimeouts() {} | ||||||
|  |  | ||||||
|  | @ -263,6 +263,7 @@ func forcegchelper() { | ||||||
| // Gosched yields the processor, allowing other goroutines to run. It does not | // Gosched yields the processor, allowing other goroutines to run. It does not | ||||||
| // suspend the current goroutine, so execution resumes automatically. | // suspend the current goroutine, so execution resumes automatically. | ||||||
| func Gosched() { | func Gosched() { | ||||||
|  | 	checkTimeouts() | ||||||
| 	mcall(gosched_m) | 	mcall(gosched_m) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -282,6 +283,9 @@ func goschedguarded() { | ||||||
| // Reasons should be unique and descriptive. | // Reasons should be unique and descriptive. | ||||||
| // Do not re-use reasons, add new ones. | // Do not re-use reasons, add new ones. | ||||||
| func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { | func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { | ||||||
|  | 	if reason != waitReasonSleep { | ||||||
|  | 		checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy | ||||||
|  | 	} | ||||||
| 	mp := acquirem() | 	mp := acquirem() | ||||||
| 	gp := mp.curg | 	gp := mp.curg | ||||||
| 	status := readgstatus(gp) | 	status := readgstatus(gp) | ||||||
|  | @ -2361,6 +2365,14 @@ stop: | ||||||
| 		return gp, false | 		return gp, false | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// wasm only: | ||||||
|  | 	// Check if a goroutine is waiting for a callback from the WebAssembly host. | ||||||
|  | 	// If yes, pause the execution until a callback was triggered. | ||||||
|  | 	if pauseSchedulerUntilCallback() { | ||||||
|  | 		// A callback was triggered and caused at least one goroutine to wake up. | ||||||
|  | 		goto top | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Before we drop our P, make a snapshot of the allp slice, | 	// Before we drop our P, make a snapshot of the allp slice, | ||||||
| 	// which can change underfoot once we no longer block | 	// which can change underfoot once we no longer block | ||||||
| 	// safe-points. We don't need to snapshot the contents because | 	// safe-points. We don't need to snapshot the contents because | ||||||
|  |  | ||||||
|  | @ -5,45 +5,81 @@ | ||||||
| #include "go_asm.h" | #include "go_asm.h" | ||||||
| #include "textflag.h" | #include "textflag.h" | ||||||
| 
 | 
 | ||||||
|  | // The register RUN indicates the current run state of the program. | ||||||
|  | // Possible values are: | ||||||
|  | #define RUN_STARTING 0 | ||||||
|  | #define RUN_RUNNING 1 | ||||||
|  | #define RUN_PAUSED 2 | ||||||
|  | #define RUN_EXITED 3 | ||||||
|  | 
 | ||||||
| // _rt0_wasm_js does NOT follow the Go ABI. It has two WebAssembly parameters: | // _rt0_wasm_js does NOT follow the Go ABI. It has two WebAssembly parameters: | ||||||
| // R0: argc (i32) | // R0: argc (i32) | ||||||
| // R1: argv (i32) | // R1: argv (i32) | ||||||
| TEXT _rt0_wasm_js(SB),NOSPLIT,$0 | TEXT _rt0_wasm_js(SB),NOSPLIT,$0 | ||||||
| 	MOVD $runtime·wasmStack+m0Stack__size(SB), SP | 	Get RUN | ||||||
|  | 	I32Const $RUN_STARTING | ||||||
|  | 	I32Eq | ||||||
|  | 	If | ||||||
|  | 		MOVD $runtime·wasmStack+m0Stack__size(SB), SP | ||||||
| 
 | 
 | ||||||
| 	Get SP | 		Get SP | ||||||
| 	Get R0 // argc | 		Get R0 // argc | ||||||
| 	I64ExtendUI32 | 		I64ExtendUI32 | ||||||
| 	I64Store $0 | 		I64Store $0 | ||||||
| 
 | 
 | ||||||
| 	Get SP | 		Get SP | ||||||
| 	Get R1 // argv | 		Get R1 // argv | ||||||
| 	I64ExtendUI32 | 		I64ExtendUI32 | ||||||
| 	I64Store $8 | 		I64Store $8 | ||||||
| 
 | 
 | ||||||
| 	I32Const $runtime·rt0_go(SB) | 		I32Const $runtime·rt0_go(SB) | ||||||
| 	I32Const $16 | 		I32Const $16 | ||||||
| 	I32ShrU | 		I32ShrU | ||||||
| 	Set PC_F | 		Set PC_F | ||||||
| 
 | 
 | ||||||
| // Call the function for the current PC_F. Repeat until SP=0 indicates program end. | 		I32Const $RUN_RUNNING | ||||||
|  | 		Set RUN | ||||||
|  | 	Else | ||||||
|  | 		Get RUN | ||||||
|  | 		I32Const $RUN_PAUSED | ||||||
|  | 		I32Eq | ||||||
|  | 		If | ||||||
|  | 			I32Const $RUN_RUNNING | ||||||
|  | 			Set RUN | ||||||
|  | 		Else | ||||||
|  | 			Unreachable | ||||||
|  | 		End | ||||||
|  | 	End | ||||||
|  | 
 | ||||||
|  | // Call the function for the current PC_F. Repeat until RUN != 0 indicates pause or exit. | ||||||
| // The WebAssembly stack may unwind, e.g. when switching goroutines. | // The WebAssembly stack may unwind, e.g. when switching goroutines. | ||||||
| // The Go stack on the linear memory is then used to jump to the correct functions | // The Go stack on the linear memory is then used to jump to the correct functions | ||||||
| // with this loop, without having to restore the full WebAssembly stack. | // with this loop, without having to restore the full WebAssembly stack. | ||||||
| loop: | loop: | ||||||
| 	Loop | 	Loop | ||||||
| 		Get SP |  | ||||||
| 		I32Eqz |  | ||||||
| 		If |  | ||||||
| 			Return |  | ||||||
| 		End |  | ||||||
| 
 |  | ||||||
| 		Get PC_F | 		Get PC_F | ||||||
| 		CallIndirect $0 | 		CallIndirect $0 | ||||||
| 		Drop | 		Drop | ||||||
| 
 | 
 | ||||||
| 		Br loop | 		Get RUN | ||||||
|  | 		I32Const $RUN_RUNNING | ||||||
|  | 		I32Eq | ||||||
|  | 		BrIf loop | ||||||
| 	End | 	End | ||||||
| 
 | 
 | ||||||
|  | 	Return | ||||||
|  | 
 | ||||||
|  | TEXT runtime·pause(SB), NOSPLIT, $0 | ||||||
|  | 	I32Const $RUN_PAUSED | ||||||
|  | 	Set RUN | ||||||
|  | 	RETUNWIND | ||||||
|  | 
 | ||||||
|  | TEXT runtime·exit(SB), NOSPLIT, $0-8 | ||||||
|  | 	Call runtime·wasmExit(SB) | ||||||
|  | 	Drop | ||||||
|  | 	I32Const $RUN_EXITED | ||||||
|  | 	Set RUN | ||||||
|  | 	RETUNWIND | ||||||
|  | 
 | ||||||
| TEXT _rt0_wasm_js_lib(SB),NOSPLIT,$0 | TEXT _rt0_wasm_js_lib(SB),NOSPLIT,$0 | ||||||
| 	UNDEF | 	UNDEF | ||||||
|  |  | ||||||
|  | @ -149,13 +149,6 @@ TEXT runtime·wasmTruncU(SB), NOSPLIT, $0-0 | ||||||
| 	I64TruncUF64 | 	I64TruncUF64 | ||||||
| 	Return | 	Return | ||||||
| 
 | 
 | ||||||
| TEXT runtime·exit(SB), NOSPLIT, $0-8 |  | ||||||
| 	Call runtime·wasmExit(SB) |  | ||||||
| 	Drop |  | ||||||
| 	I32Const $0 |  | ||||||
| 	Set SP |  | ||||||
| 	I32Const $1 |  | ||||||
| 
 |  | ||||||
| TEXT runtime·exitThread(SB), NOSPLIT, $0-0 | TEXT runtime·exitThread(SB), NOSPLIT, $0-0 | ||||||
| 	UNDEF | 	UNDEF | ||||||
| 
 | 
 | ||||||
|  | @ -194,6 +187,14 @@ TEXT ·walltime(SB), NOSPLIT, $0 | ||||||
| 	CallImport | 	CallImport | ||||||
| 	RET | 	RET | ||||||
| 
 | 
 | ||||||
|  | TEXT ·scheduleCallback(SB), NOSPLIT, $0 | ||||||
|  | 	CallImport | ||||||
|  | 	RET | ||||||
|  | 
 | ||||||
|  | TEXT ·clearScheduledCallback(SB), NOSPLIT, $0 | ||||||
|  | 	CallImport | ||||||
|  | 	RET | ||||||
|  | 
 | ||||||
| TEXT ·getRandomData(SB), NOSPLIT, $0 | TEXT ·getRandomData(SB), NOSPLIT, $0 | ||||||
| 	CallImport | 	CallImport | ||||||
| 	RET | 	RET | ||||||
|  |  | ||||||
							
								
								
									
										145
									
								
								src/syscall/js/callback.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/syscall/js/callback.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,145 @@ | ||||||
|  | // 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 | ||||||
|  | 
 | ||||||
|  | import "sync" | ||||||
|  | 
 | ||||||
|  | var pendingCallbacks = Global.Get("Array").New() | ||||||
|  | 
 | ||||||
|  | var makeCallbackHelper = Global.Call("eval", ` | ||||||
|  | 	(function(id, pendingCallbacks, resolveCallbackPromise) { | ||||||
|  | 		return function() { | ||||||
|  | 			pendingCallbacks.push({ id: id, args: arguments }); | ||||||
|  | 			resolveCallbackPromise(); | ||||||
|  | 		}; | ||||||
|  | 	}) | ||||||
|  | `) | ||||||
|  | 
 | ||||||
|  | var makeEventCallbackHelper = Global.Call("eval", ` | ||||||
|  | 	(function(preventDefault, stopPropagation, stopImmediatePropagation, fn) { | ||||||
|  | 		return function(event) { | ||||||
|  | 			if (preventDefault) { | ||||||
|  | 				event.preventDefault(); | ||||||
|  | 			} | ||||||
|  | 			if (stopPropagation) { | ||||||
|  | 				event.stopPropagation(); | ||||||
|  | 			} | ||||||
|  | 			if (stopImmediatePropagation) { | ||||||
|  | 				event.stopImmediatePropagation(); | ||||||
|  | 			} | ||||||
|  | 			fn(event); | ||||||
|  | 		}; | ||||||
|  | 	}) | ||||||
|  | `) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	callbacksMu    sync.Mutex | ||||||
|  | 	callbacks             = make(map[uint32]func([]Value)) | ||||||
|  | 	nextCallbackID uint32 = 1 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Callback is a Go function that got wrapped for use as a JavaScript callback. | ||||||
|  | // A Callback can be passed to functions of this package that accept interface{}, | ||||||
|  | // for example Value.Set and Value.Call. | ||||||
|  | type Callback struct { | ||||||
|  | 	id        uint32 | ||||||
|  | 	enqueueFn Value // the JavaScript function that queues the callback for execution | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewCallback returns a wrapped callback function. It can be passed to functions of this package | ||||||
|  | // that accept interface{}, for example Value.Set and Value.Call. | ||||||
|  | // | ||||||
|  | // Invoking the callback in JavaScript will queue the Go function fn for execution. | ||||||
|  | // This execution happens asynchronously on a special goroutine that handles all callbacks and preserves | ||||||
|  | // the order in which the callbacks got called. | ||||||
|  | // As a consequence, if one callback blocks this goroutine, other callbacks will not be processed. | ||||||
|  | // A blocking callback should therefore explicitly start a new goroutine. | ||||||
|  | // | ||||||
|  | // Callback.Close must be called to free up resources when the callback will not be used any more. | ||||||
|  | func NewCallback(fn func(args []Value)) Callback { | ||||||
|  | 	callbackLoopOnce.Do(func() { | ||||||
|  | 		go callbackLoop() | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	callbacksMu.Lock() | ||||||
|  | 	id := nextCallbackID | ||||||
|  | 	nextCallbackID++ | ||||||
|  | 	callbacks[id] = fn | ||||||
|  | 	callbacksMu.Unlock() | ||||||
|  | 	return Callback{ | ||||||
|  | 		id:        id, | ||||||
|  | 		enqueueFn: makeCallbackHelper.Invoke(id, pendingCallbacks, resolveCallbackPromise), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type EventCallbackFlag int | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// PreventDefault can be used with NewEventCallback to call event.preventDefault synchronously. | ||||||
|  | 	PreventDefault EventCallbackFlag = 1 << iota | ||||||
|  | 	// StopPropagation can be used with NewEventCallback to call event.stopPropagation synchronously. | ||||||
|  | 	StopPropagation | ||||||
|  | 	// StopImmediatePropagation can be used with NewEventCallback to call event.stopImmediatePropagation synchronously. | ||||||
|  | 	StopImmediatePropagation | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewEventCallback returns a wrapped callback function, just like NewCallback, but the callback expects to have | ||||||
|  | // exactly one argument, the event. Depending on flags, it will synchronously call event.preventDefault, | ||||||
|  | // event.stopPropagation and/or event.stopImmediatePropagation before queuing the Go function fn for execution. | ||||||
|  | func NewEventCallback(flags EventCallbackFlag, fn func(event Value)) Callback { | ||||||
|  | 	c := NewCallback(func(args []Value) { | ||||||
|  | 		fn(args[0]) | ||||||
|  | 	}) | ||||||
|  | 	return Callback{ | ||||||
|  | 		id: c.id, | ||||||
|  | 		enqueueFn: makeEventCallbackHelper.Invoke( | ||||||
|  | 			flags&PreventDefault != 0, | ||||||
|  | 			flags&StopPropagation != 0, | ||||||
|  | 			flags&StopImmediatePropagation != 0, | ||||||
|  | 			c, | ||||||
|  | 		), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c Callback) Close() { | ||||||
|  | 	callbacksMu.Lock() | ||||||
|  | 	delete(callbacks, c.id) | ||||||
|  | 	callbacksMu.Unlock() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var callbackLoopOnce sync.Once | ||||||
|  | 
 | ||||||
|  | func callbackLoop() { | ||||||
|  | 	for { | ||||||
|  | 		sleepUntilCallback() | ||||||
|  | 		for { | ||||||
|  | 			cb := pendingCallbacks.Call("shift") | ||||||
|  | 			if cb == Undefined { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			id := uint32(cb.Get("id").Int()) | ||||||
|  | 			callbacksMu.Lock() | ||||||
|  | 			f, ok := callbacks[id] | ||||||
|  | 			callbacksMu.Unlock() | ||||||
|  | 			if !ok { | ||||||
|  | 				Global.Get("console").Call("error", "call to closed callback") | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			argsObj := cb.Get("args") | ||||||
|  | 			args := make([]Value, argsObj.Length()) | ||||||
|  | 			for i := range args { | ||||||
|  | 				args[i] = argsObj.Index(i) | ||||||
|  | 			} | ||||||
|  | 			f(args) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // sleepUntilCallback is defined in the runtime package | ||||||
|  | func sleepUntilCallback() | ||||||
|  | @ -39,7 +39,11 @@ var ( | ||||||
| 	// Global is the JavaScript global object, usually "window" or "global". | 	// Global is the JavaScript global object, usually "window" or "global". | ||||||
| 	Global = Value{2} | 	Global = Value{2} | ||||||
| 
 | 
 | ||||||
|  | 	// memory is the WebAssembly linear memory. | ||||||
| 	memory = Value{3} | 	memory = Value{3} | ||||||
|  | 
 | ||||||
|  | 	// resolveCallbackPromise is a function that the callback helper uses to resume the execution of Go's WebAssembly code. | ||||||
|  | 	resolveCallbackPromise = Value{4} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var uint8Array = Global.Get("Uint8Array") | var uint8Array = Global.Get("Uint8Array") | ||||||
|  | @ -49,6 +53,8 @@ func ValueOf(x interface{}) Value { | ||||||
| 	switch x := x.(type) { | 	switch x := x.(type) { | ||||||
| 	case Value: | 	case Value: | ||||||
| 		return x | 		return x | ||||||
|  | 	case Callback: | ||||||
|  | 		return x.enqueueFn | ||||||
| 	case nil: | 	case nil: | ||||||
| 		return Null | 		return Null | ||||||
| 	case bool: | 	case bool: | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
| package js_test | package js_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"syscall/js" | 	"syscall/js" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  | @ -144,3 +145,52 @@ func TestNew(t *testing.T) { | ||||||
| 		t.Errorf("got %#v, want %#v", got, 42) | 		t.Errorf("got %#v, want %#v", got, 42) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestCallback(t *testing.T) { | ||||||
|  | 	c := make(chan struct{}) | ||||||
|  | 	cb := js.NewCallback(func(args []js.Value) { | ||||||
|  | 		if got := args[0].Int(); got != 42 { | ||||||
|  | 			t.Errorf("got %#v, want %#v", got, 42) | ||||||
|  | 		} | ||||||
|  | 		c <- struct{}{} | ||||||
|  | 	}) | ||||||
|  | 	defer cb.Close() | ||||||
|  | 	js.Global.Call("setTimeout", cb, 0, 42) | ||||||
|  | 	<-c | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestEventCallback(t *testing.T) { | ||||||
|  | 	for _, name := range []string{"preventDefault", "stopPropagation", "stopImmediatePropagation"} { | ||||||
|  | 		c := make(chan struct{}) | ||||||
|  | 		var flags js.EventCallbackFlag | ||||||
|  | 		switch name { | ||||||
|  | 		case "preventDefault": | ||||||
|  | 			flags = js.PreventDefault | ||||||
|  | 		case "stopPropagation": | ||||||
|  | 			flags = js.StopPropagation | ||||||
|  | 		case "stopImmediatePropagation": | ||||||
|  | 			flags = js.StopImmediatePropagation | ||||||
|  | 		} | ||||||
|  | 		cb := js.NewEventCallback(flags, func(event js.Value) { | ||||||
|  | 			c <- struct{}{} | ||||||
|  | 		}) | ||||||
|  | 		defer cb.Close() | ||||||
|  | 
 | ||||||
|  | 		event := js.Global.Call("eval", fmt.Sprintf("({ called: false, %s: function() { this.called = true; } })", name)) | ||||||
|  | 		js.ValueOf(cb).Invoke(event) | ||||||
|  | 		if !event.Get("called").Bool() { | ||||||
|  | 			t.Errorf("%s not called", name) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		<-c | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ExampleNewCallback() { | ||||||
|  | 	var cb js.Callback | ||||||
|  | 	cb = js.NewCallback(func(args []js.Value) { | ||||||
|  | 		fmt.Println("button clicked") | ||||||
|  | 		cb.Close() // close the callback if the button will not be clicked again | ||||||
|  | 	}) | ||||||
|  | 	js.Global.Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Richard Musiol
						Richard Musiol