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  `