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  `