github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/fnruntime/jsglue.go (about) 1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package fnruntime 16 17 // The glue code below has been tested with node.js v18.6.0. 18 // It may need some small changes to work with v14 and v16. 19 20 var glueCode = `// The code below run the wasm function. 21 const go = new Go(); 22 // The path to the wasm artifact is provided by env var to enable the reusibility of this script. 23 const wasmPath = process.env.` + WasmPathEnv + ` 24 if (!wasmPath) { 25 throw 'environment variable KPT_FN_WASM_PATH is expected to be set by the kpt wasm runtime' 26 } 27 const wasmBuffer = fs.readFileSync(wasmPath) 28 // Read the resourceList from stdin 29 var rl = fs.readFileSync(0, 'utf-8') 30 WebAssembly.instantiate(wasmBuffer, go.importObject).then((result) => { 31 go.run(result.instance); 32 console.log(` + jsEntrypointFunction + `(rl)); 33 });` 34 35 const golangWasmJSCode = `const fs = require('fs'); 36 const { 37 performance 38 } = require('perf_hooks'); 39 const crypto = require('crypto'); 40 41 // The JS glue code below mostly comes from 42 // https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js with minor 43 // changes. "misc/wasm/wasm_exec.js" is shipped with all golang installation. 44 45 "use strict"; 46 (() => { 47 const enosys = () => { 48 const err = new Error("not implemented"); 49 err.code = "ENOSYS"; 50 return err; 51 }; 52 if (!globalThis.fs) { 53 let outputBuf = ""; 54 globalThis.fs = { 55 constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 56 writeSync(fd, buf) { 57 outputBuf += decoder.decode(buf); 58 const nl = outputBuf.lastIndexOf("\n"); 59 if (nl != -1) { 60 console.log(outputBuf.substr(0, nl)); 61 outputBuf = outputBuf.substr(nl + 1); 62 } 63 return buf.length; 64 }, 65 write(fd, buf, offset, length, position, callback) { 66 if (offset !== 0 || length !== buf.length || position !== null) { 67 callback(enosys()); 68 return; 69 } 70 const n = this.writeSync(fd, buf); 71 callback(null, n); 72 }, 73 chmod(path, mode, callback) { callback(enosys()); }, 74 chown(path, uid, gid, callback) { callback(enosys()); }, 75 close(fd, callback) { callback(enosys()); }, 76 fchmod(fd, mode, callback) { callback(enosys()); }, 77 fchown(fd, uid, gid, callback) { callback(enosys()); }, 78 fstat(fd, callback) { callback(enosys()); }, 79 fsync(fd, callback) { callback(null); }, 80 ftruncate(fd, length, callback) { callback(enosys()); }, 81 lchown(path, uid, gid, callback) { callback(enosys()); }, 82 link(path, link, callback) { callback(enosys()); }, 83 lstat(path, callback) { callback(enosys()); }, 84 mkdir(path, perm, callback) { callback(enosys()); }, 85 open(path, flags, mode, callback) { callback(enosys()); }, 86 read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 87 readdir(path, callback) { callback(enosys()); }, 88 readlink(path, callback) { callback(enosys()); }, 89 rename(from, to, callback) { callback(enosys()); }, 90 rmdir(path, callback) { callback(enosys()); }, 91 stat(path, callback) { callback(enosys()); }, 92 symlink(path, link, callback) { callback(enosys()); }, 93 truncate(path, length, callback) { callback(enosys()); }, 94 unlink(path, callback) { callback(enosys()); }, 95 utimes(path, atime, mtime, callback) { callback(enosys()); }, 96 }; 97 } 98 if (!globalThis.process) { 99 globalThis.process = { 100 getuid() { return -1; }, 101 getgid() { return -1; }, 102 geteuid() { return -1; }, 103 getegid() { return -1; }, 104 getgroups() { throw enosys(); }, 105 pid: -1, 106 ppid: -1, 107 umask() { throw enosys(); }, 108 cwd() { throw enosys(); }, 109 chdir() { throw enosys(); }, 110 } 111 } 112 /* 113 if (!globalThis.crypto) { 114 throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 115 } 116 if (!globalThis.performance) { 117 throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 118 } 119 */ 120 if (!globalThis.TextEncoder) { 121 throw new Error("globalThis.TextEncoder is not available, polyfill required"); 122 } 123 if (!globalThis.TextDecoder) { 124 throw new Error("globalThis.TextDecoder is not available, polyfill required"); 125 } 126 const encoder = new TextEncoder("utf-8"); 127 const decoder = new TextDecoder("utf-8"); 128 globalThis.Go = class { 129 constructor() { 130 this.argv = ["js"]; 131 this.env = {}; 132 this.exit = (code) => { 133 if (code !== 0) { 134 console.warn("exit code:", code); 135 } 136 }; 137 this._exitPromise = new Promise((resolve) => { 138 this._resolveExitPromise = resolve; 139 }); 140 this._pendingEvent = null; 141 this._scheduledTimeouts = new Map(); 142 this._nextCallbackTimeoutID = 1; 143 const setInt64 = (addr, v) => { 144 this.mem.setUint32(addr + 0, v, true); 145 this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 146 } 147 const getInt64 = (addr) => { 148 const low = this.mem.getUint32(addr + 0, true); 149 const high = this.mem.getInt32(addr + 4, true); 150 return low + high * 4294967296; 151 } 152 const loadValue = (addr) => { 153 const f = this.mem.getFloat64(addr, true); 154 if (f === 0) { 155 return undefined; 156 } 157 if (!isNaN(f)) { 158 return f; 159 } 160 const id = this.mem.getUint32(addr, true); 161 return this._values[id]; 162 } 163 const storeValue = (addr, v) => { 164 const nanHead = 0x7FF80000; 165 if (typeof v === "number" && v !== 0) { 166 if (isNaN(v)) { 167 this.mem.setUint32(addr + 4, nanHead, true); 168 this.mem.setUint32(addr, 0, true); 169 return; 170 } 171 this.mem.setFloat64(addr, v, true); 172 return; 173 } 174 if (v === undefined) { 175 this.mem.setFloat64(addr, 0, true); 176 return; 177 } 178 let id = this._ids.get(v); 179 if (id === undefined) { 180 id = this._idPool.pop(); 181 if (id === undefined) { 182 id = this._values.length; 183 } 184 this._values[id] = v; 185 this._goRefCounts[id] = 0; 186 this._ids.set(v, id); 187 } 188 this._goRefCounts[id]++; 189 let typeFlag = 0; 190 switch (typeof v) { 191 case "object": 192 if (v !== null) { 193 typeFlag = 1; 194 } 195 break; 196 case "string": 197 typeFlag = 2; 198 break; 199 case "symbol": 200 typeFlag = 3; 201 break; 202 case "function": 203 typeFlag = 4; 204 break; 205 } 206 this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 207 this.mem.setUint32(addr, id, true); 208 } 209 const loadSlice = (addr) => { 210 const array = getInt64(addr + 0); 211 const len = getInt64(addr + 8); 212 return new Uint8Array(this._inst.exports.mem.buffer, array, len); 213 } 214 const loadSliceOfValues = (addr) => { 215 const array = getInt64(addr + 0); 216 const len = getInt64(addr + 8); 217 const a = new Array(len); 218 for (let i = 0; i < len; i++) { 219 a[i] = loadValue(array + i * 8); 220 } 221 return a; 222 } 223 const loadString = (addr) => { 224 const saddr = getInt64(addr + 0); 225 const len = getInt64(addr + 8); 226 return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 227 } 228 const timeOrigin = Date.now() - performance.now(); 229 this.importObject = { 230 go: { 231 // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 232 // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 233 // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 234 // This changes the SP, thus we have to update the SP used by the imported function. 235 // func wasmExit(code int32) 236 "runtime.wasmExit": (sp) => { 237 sp >>>= 0; 238 const code = this.mem.getInt32(sp + 8, true); 239 this.exited = true; 240 delete this._inst; 241 delete this._values; 242 delete this._goRefCounts; 243 delete this._ids; 244 delete this._idPool; 245 this.exit(code); 246 }, 247 // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 248 "runtime.wasmWrite": (sp) => { 249 sp >>>= 0; 250 const fd = getInt64(sp + 8); 251 const p = getInt64(sp + 16); 252 const n = this.mem.getInt32(sp + 24, true); 253 fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 254 }, 255 // func resetMemoryDataView() 256 "runtime.resetMemoryDataView": (sp) => { 257 sp >>>= 0; 258 this.mem = new DataView(this._inst.exports.mem.buffer); 259 }, 260 // func nanotime1() int64 261 "runtime.nanotime1": (sp) => { 262 sp >>>= 0; 263 setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 264 }, 265 // func walltime() (sec int64, nsec int32) 266 "runtime.walltime": (sp) => { 267 sp >>>= 0; 268 const msec = (new Date).getTime(); 269 setInt64(sp + 8, msec / 1000); 270 this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 271 }, 272 // func scheduleTimeoutEvent(delay int64) int32 273 "runtime.scheduleTimeoutEvent": (sp) => { 274 sp >>>= 0; 275 const id = this._nextCallbackTimeoutID; 276 this._nextCallbackTimeoutID++; 277 this._scheduledTimeouts.set(id, setTimeout( 278 () => { 279 this._resume(); 280 while (this._scheduledTimeouts.has(id)) { 281 // for some reason Go failed to register the timeout event, log and try again 282 // (temporary workaround for https://github.com/golang/go/issues/28975) 283 console.warn("scheduleTimeoutEvent: missed timeout event"); 284 this._resume(); 285 } 286 }, 287 getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 288 )); 289 this.mem.setInt32(sp + 16, id, true); 290 }, 291 // func clearTimeoutEvent(id int32) 292 "runtime.clearTimeoutEvent": (sp) => { 293 sp >>>= 0; 294 const id = this.mem.getInt32(sp + 8, true); 295 clearTimeout(this._scheduledTimeouts.get(id)); 296 this._scheduledTimeouts.delete(id); 297 }, 298 // func getRandomData(r []byte) 299 "runtime.getRandomData": (sp) => { 300 sp >>>= 0; 301 crypto.getRandomValues(loadSlice(sp + 8)); 302 }, 303 // func finalizeRef(v ref) 304 "syscall/js.finalizeRef": (sp) => { 305 sp >>>= 0; 306 const id = this.mem.getUint32(sp + 8, true); 307 this._goRefCounts[id]--; 308 if (this._goRefCounts[id] === 0) { 309 const v = this._values[id]; 310 this._values[id] = null; 311 this._ids.delete(v); 312 this._idPool.push(id); 313 } 314 }, 315 // func stringVal(value string) ref 316 "syscall/js.stringVal": (sp) => { 317 sp >>>= 0; 318 storeValue(sp + 24, loadString(sp + 8)); 319 }, 320 // func valueGet(v ref, p string) ref 321 "syscall/js.valueGet": (sp) => { 322 sp >>>= 0; 323 const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 324 sp = this._inst.exports.getsp() >>> 0; // see comment above 325 storeValue(sp + 32, result); 326 }, 327 // func valueSet(v ref, p string, x ref) 328 "syscall/js.valueSet": (sp) => { 329 sp >>>= 0; 330 Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 331 }, 332 // func valueDelete(v ref, p string) 333 "syscall/js.valueDelete": (sp) => { 334 sp >>>= 0; 335 Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 336 }, 337 // func valueIndex(v ref, i int) ref 338 "syscall/js.valueIndex": (sp) => { 339 sp >>>= 0; 340 storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 341 }, 342 // valueSetIndex(v ref, i int, x ref) 343 "syscall/js.valueSetIndex": (sp) => { 344 sp >>>= 0; 345 Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 346 }, 347 // func valueCall(v ref, m string, args []ref) (ref, bool) 348 "syscall/js.valueCall": (sp) => { 349 sp >>>= 0; 350 try { 351 const v = loadValue(sp + 8); 352 const m = Reflect.get(v, loadString(sp + 16)); 353 const args = loadSliceOfValues(sp + 32); 354 const result = Reflect.apply(m, v, args); 355 sp = this._inst.exports.getsp() >>> 0; // see comment above 356 storeValue(sp + 56, result); 357 this.mem.setUint8(sp + 64, 1); 358 } catch (err) { 359 sp = this._inst.exports.getsp() >>> 0; // see comment above 360 storeValue(sp + 56, err); 361 this.mem.setUint8(sp + 64, 0); 362 } 363 }, 364 // func valueInvoke(v ref, args []ref) (ref, bool) 365 "syscall/js.valueInvoke": (sp) => { 366 sp >>>= 0; 367 try { 368 const v = loadValue(sp + 8); 369 const args = loadSliceOfValues(sp + 16); 370 const result = Reflect.apply(v, undefined, args); 371 sp = this._inst.exports.getsp() >>> 0; // see comment above 372 storeValue(sp + 40, result); 373 this.mem.setUint8(sp + 48, 1); 374 } catch (err) { 375 sp = this._inst.exports.getsp() >>> 0; // see comment above 376 storeValue(sp + 40, err); 377 this.mem.setUint8(sp + 48, 0); 378 } 379 }, 380 // func valueNew(v ref, args []ref) (ref, bool) 381 "syscall/js.valueNew": (sp) => { 382 sp >>>= 0; 383 try { 384 const v = loadValue(sp + 8); 385 const args = loadSliceOfValues(sp + 16); 386 const result = Reflect.construct(v, args); 387 sp = this._inst.exports.getsp() >>> 0; // see comment above 388 storeValue(sp + 40, result); 389 this.mem.setUint8(sp + 48, 1); 390 } catch (err) { 391 sp = this._inst.exports.getsp() >>> 0; // see comment above 392 storeValue(sp + 40, err); 393 this.mem.setUint8(sp + 48, 0); 394 } 395 }, 396 // func valueLength(v ref) int 397 "syscall/js.valueLength": (sp) => { 398 sp >>>= 0; 399 setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 400 }, 401 // valuePrepareString(v ref) (ref, int) 402 "syscall/js.valuePrepareString": (sp) => { 403 sp >>>= 0; 404 const str = encoder.encode(String(loadValue(sp + 8))); 405 storeValue(sp + 16, str); 406 setInt64(sp + 24, str.length); 407 }, 408 // valueLoadString(v ref, b []byte) 409 "syscall/js.valueLoadString": (sp) => { 410 sp >>>= 0; 411 const str = loadValue(sp + 8); 412 loadSlice(sp + 16).set(str); 413 }, 414 // func valueInstanceOf(v ref, t ref) bool 415 "syscall/js.valueInstanceOf": (sp) => { 416 sp >>>= 0; 417 this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 418 }, 419 // func copyBytesToGo(dst []byte, src ref) (int, bool) 420 "syscall/js.copyBytesToGo": (sp) => { 421 sp >>>= 0; 422 const dst = loadSlice(sp + 8); 423 const src = loadValue(sp + 32); 424 if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 425 this.mem.setUint8(sp + 48, 0); 426 return; 427 } 428 const toCopy = src.subarray(0, dst.length); 429 dst.set(toCopy); 430 setInt64(sp + 40, toCopy.length); 431 this.mem.setUint8(sp + 48, 1); 432 }, 433 // func copyBytesToJS(dst ref, src []byte) (int, bool) 434 "syscall/js.copyBytesToJS": (sp) => { 435 sp >>>= 0; 436 const dst = loadValue(sp + 8); 437 const src = loadSlice(sp + 16); 438 if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 439 this.mem.setUint8(sp + 48, 0); 440 return; 441 } 442 const toCopy = src.subarray(0, dst.length); 443 dst.set(toCopy); 444 setInt64(sp + 40, toCopy.length); 445 this.mem.setUint8(sp + 48, 1); 446 }, 447 "debug": (value) => { 448 console.log(value); 449 }, 450 } 451 }; 452 } 453 async run(instance) { 454 if (!(instance instanceof WebAssembly.Instance)) { 455 throw new Error("Go.run: WebAssembly.Instance expected"); 456 } 457 this._inst = instance; 458 this.mem = new DataView(this._inst.exports.mem.buffer); 459 this._values = [ // JS values that Go currently has references to, indexed by reference id 460 NaN, 461 0, 462 null, 463 true, 464 false, 465 globalThis, 466 this, 467 ]; 468 this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 469 this._ids = new Map([ // mapping from JS values to reference ids 470 [0, 1], 471 [null, 2], 472 [true, 3], 473 [false, 4], 474 [globalThis, 5], 475 [this, 6], 476 ]); 477 this._idPool = []; // unused ids that have been garbage collected 478 this.exited = false; // whether the Go program has exited 479 // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 480 let offset = 4096; 481 const strPtr = (str) => { 482 const ptr = offset; 483 const bytes = encoder.encode(str + "\0"); 484 new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 485 offset += bytes.length; 486 if (offset % 8 !== 0) { 487 offset += 8 - (offset % 8); 488 } 489 return ptr; 490 }; 491 const argc = this.argv.length; 492 const argvPtrs = []; 493 this.argv.forEach((arg) => { 494 argvPtrs.push(strPtr(arg)); 495 }); 496 argvPtrs.push(0); 497 const keys = Object.keys(this.env).sort(); 498 keys.forEach((key) => { 499 argvPtrs.push(strPtr(` + "`" + `${key}=${this.env[key]}` + "`" + `)); 500 }); 501 argvPtrs.push(0); 502 const argv = offset; 503 argvPtrs.forEach((ptr) => { 504 this.mem.setUint32(offset, ptr, true); 505 this.mem.setUint32(offset + 4, 0, true); 506 offset += 8; 507 }); 508 // The linker guarantees global data starts from at least wasmMinDataAddr. 509 // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 510 const wasmMinDataAddr = 4096 + 8192; 511 if (offset >= wasmMinDataAddr) { 512 throw new Error("total length of command line and environment variables exceeds limit"); 513 } 514 this._inst.exports.run(argc, argv); 515 if (this.exited) { 516 this._resolveExitPromise(); 517 } 518 await this._exitPromise; 519 } 520 _resume() { 521 if (this.exited) { 522 throw new Error("Go program has already exited"); 523 } 524 this._inst.exports.resume(); 525 if (this.exited) { 526 this._resolveExitPromise(); 527 } 528 } 529 _makeFuncWrapper(id) { 530 const go = this; 531 return function () { 532 const event = { id: id, this: this, args: arguments }; 533 go._pendingEvent = event; 534 go._resume(); 535 return event.result; 536 }; 537 } 538 } 539 })(); 540 `