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