NeilFraser / JS-Interpreter

A sandboxed JavaScript interpreter in JavaScript.
Apache License 2.0
1.98k stars 353 forks source link

Add methods for calling interpreted functions #201

Open Webifi opened 3 years ago

Webifi commented 3 years ago

Allows calling anonymous interpreted functions from native code. (Includes changes in #193)

fixes #192, resolves #199, closes #147, closes #189,

Examples:

Synchronous callback from native function:

function nativeFunction (func) {
    return interpreter.callFunction(func, this, 25).then(function(v) {
        console.log('got psudo result', v)
        return v + 5
    })
}

// Called via interpreted code
var test = nativeFunction(function(v){return v + 2})
// Should produce 32

Synchronous callback from native AsyncFunction:

function nativeAsyncFunction(func, callback) {
    callback(interpreter.callFunction(func, this, 25).then(function(v, callback2) {
        console.log('got psudo result', v)
        callback2(v + 5)
    }))
}

// Called via interpreted code
var test = nativeAsyncFunction(function(v){return v + 2})
// Should produce 32

In both cases above, the .then(...) callback value handler is optional. If omitted, the value of the called interpreted function will be returned.

Additional pseudo functions can be called by simply returning another Callback in the .then() handler via return interpreter.callFunction(...)

For example:

function nativeAsyncFunction(func, func2, callback) {
    callback(interpreter.callFunction(func, this).then(function(v, callback) {
        callback(interpreter.callFunction(func2, this).then(function(v2, callback) {
                callback("I'm done with " + v + " and " +  v2)
            }))
    }))
}

or:

function nativeFunction(func, func2) {
    return interpreter.callFunction(func, this).then(function(v) {
        return interpreter.callFunction(func2, this).then(function(v2) {
                return "I'm done with " + v + " and " +  v2
            })
    })
}

Queued via queueFunction: (func is called last.)

function nativeFunction(func) {
  interpreter.queueFunction(func, this, pseudoarg1, pseudoarg2);
}

Queued Callbacks:

function nativeFunction(func1, func2) {
  interpreter.queueFunction(func1, this).then(val => {
    console.log('func1 returned:', val);
  });
  interpreter.queueFunction(func2, this).then(val => {
    console.log('func2 returned:', val);
  });
}

Throwing exception in native AsyncFunction:

function nativeAsyncFunction(val, callback) {
        if (val < 2) return callback(interpreter.createThrowable(
            interpreter.RANGE_ERROR,
           'Value must be greater than 2'
        ))
        callback(val + 2)
}

Throwing exception in native function: (Alternate to interpreter.throwException(...))

function nativeFunction(val) {
        if (val < 2) return interpreter.createThrowable(
            interpreter.RANGE_ERROR,
           'Value must be greater than 2'
        )
        return val + 2
}

Catching exceptions in pseudo function calls from native:

function nativeFunction(func1, func2) {
   // Will be called later
   interpreter.queueFunction(func2, this).then(val => {
    console.log('func2 returned:', val);
  }).catch(e => {
    console.log('Got an error in func2:', interpreter.getProperty(e, 'message'); e);
  });
  // Will be called on next step
  return interpreter.callFunction(func1, this).then(val => {
    console.log('func1 returned:', val);
    return val + 25; // will return val + 25 to caller
  }).catch(e => {
    console.log('Got an error in func1:', interpreter.getProperty(e, 'message'); e);
    return 42; // will return 42 to caller
  });
}

Catching exceptions in pseudo function calls from native async:

function nativeAsyncFunction(func1, func2, callback) {
   // Will be called later
   interpreter.queueFunction(func2, this).then(val => {
    console.log('func2 returned:', val);
  }).catch(e => {
    console.log('Got an error in func2:', interpreter.getProperty(e, 'message); e);
  });
  // Will be called on next step
  callback(interpreter.callFunction(func1, this).then((val, callback) => {
    console.log('func1 returned:', val);
    callback(val + 25); // will return val + 25 to caller
  }).catch((e, callback) => {
    console.log('Got an error in func1:', interpreter.getProperty(e, 'message); e);
    callback(42); // will return 42 to caller
  }));
}

Chaining thens in functions calls from native:

function nativeFunction(func1, func2) {
  return interpreter.callFunction(func1, this).then(val => {
    console.log('func1 returned:', val);
    return val + 25; // will return val + 25 to caller
  }).then(val => {
    return val + 2;
  }).then(val => {
    return val * 4;
  }).catch(e => {
    return 0;
  });
  // Note:  Returning an interpreter.callFunction(...) in a then will effectively terminate the chain.
  //            All remaining then operations will be ignored and the callback will be executed.
}

Implementing native timers example:

const interpreter = new Interpreter("", (interpreter, globalObject) => {
  const timeouts = {};
  let timeoutCounter = 0;

  const intervals = {};
  let intervalCounter = 0;

  const frames = {};
  let frameCounter = 0;

  interpreter.setProperty(
    globalObject,
    "setTimeout",
    interpreter.createNativeFunction(function (fn, time) {
      const tid = ++timeoutCounter;
      const _this = this;
      timeouts[tid] = setTimeout(function () {
        if (timeouts[tid]) {
          delete timeouts[tid];
          interpreter.queueFunction(fn, _this);
          interpreter.run(); // Keep running
        }
      }, time);
      return tid;
    })
  );

  interpreter.setProperty(
    globalObject,
    "clearTimeout",
    interpreter.createNativeFunction((tid) => {
      clearTimeout(timeouts[tid]);
      delete timeouts[tid];
    })
  );

  interpreter.setProperty(
    globalObject,
    "setInterval",
    interpreter.createNativeFunction(function (fn, time) {
      const tid = ++intervalCounter;
      const _this = this;
      intervals[tid] = setInterval(function () {
        interpreter.queueFunction(fn, _this);
        interpreter.run(); // Keep running
      }, time);
      return tid;
    })
  );

  interpreter.setProperty(
    globalObject,
    "clearInterval",
    interpreter.createNativeFunction((tid) => {
      clearInterval(intervals[tid]);
      delete intervals[tid];
    })
  );

  interpreter.setProperty(
    globalObject,
    "requestAnimationFrame",
    interpreter.createNativeFunction(function (fn, time) {
      const tid = ++frameCounter;
      const _this = this;
      frames[tid] = requestAnimationFrame(function () {
        if (frames[tid]) {
          delete frames[tid];
          interpreter.queueFunction(fn, _this);
          interpreter.run(); // Keep running
        }
      }, time);
      return tid;
    })
  );

  interpreter.setProperty(
    globalObject,
    "cancelAnimationFrame",
    interpreter.createNativeFunction((tid) => {
      cancelAnimationFrame(frames[tid]);
      delete frames[tid];
    })
  );
});

interpreter.appendCode(`
  var interval = setInterval(function() {
    console.log('Yay! Intervals!');
  }, 1000);
  setTimeout(function() {
    console.log('Yay! Timeouts!');
    clearInterval(interval);
  }, 5000);
`);
interpreter.run();

Use Callbacks in AsyncFuntion, full example:

const interpreter = new Interpreter('', (interpreter, globalObject) => {
  interpreter.setProperty(
    globalObject,
    'doThing',
    interpreter.createAsyncFunction(function(
      urlProvider,
      success,
      error,
      callback
    ) {
      var _this = this
      callback(
        interpreter.callFunction(urlProvider, _this).then(url => {
          fetch(url)
            .then(response => response.json())
            .then(data => {
              callback(interpreter.callFunction(success, _this, data))
              interpreter.run()
            })
            .catch(e => {
              callback(interpreter.callFunction(error, _this, e.toString()))
              interpreter.run()
            })
        })
      )
    })
  )
})

interpreter.appendCode(`
function onSuccess(json) {
  console.log(json.fruit);
}
function onError(message) {
  console.log(message);
}
function urlProvider() {
  return 'https://support.oneskyapp.com/hc/en-us/article_attachments/202761627/example_1.json'
}
doThing(urlProvider, onSuccess, onError);
`)
interpreter.run()

Use Callbacks in Native Function, full example:

const interpreter = new Interpreter('', (interpreter, globalObject) => {
  interpreter.setProperty(
    globalObject,
    'doThing',
    interpreter.createNativeFunction(function(urlProvider, success, error) {
      var _this = this
      return interpreter.callFunction(urlProvider, _this).then(url => {
        fetch(url)
          .then(response => response.json())
          .then(data => {
            interpreter.queueFunction(success, _this, data)
            interpreter.run()
          })
          .catch(e => {
            interpreter.queueFunction(error, _this, e.toString())
            interpreter.run()
          })
      })
    })
  )
})

interpreter.appendCode(`
function onSuccess(json) {
  console.log(json.fruit);
}
function onError(message) {
  console.log(message);
}
function urlProvider() {
  return 'https://support.oneskyapp.com/hc/en-us/article_attachments/202761627/example_1.json'
}
doThing(urlProvider, onSuccess, onError);
`)
interpreter.run()
Webifi commented 3 years ago

[EDIT] No longer applicable

Webifi commented 3 years ago

EDIT: No longer applicable

Webifi commented 3 years ago

Currently broken...

Webifi commented 3 years ago

Should be working now

Webifi commented 3 years ago

Points @cpcallen made here should all be addressed now.

Naming of new exported methods, callFunction, queueFunction and createThrowable have yet to be finalized.

Webifi commented 3 years ago

I think I ironed out all the bugs now.

cpcallen commented 1 year ago

@NeilFraser: do you want to consider this PR? The topic of implementing setTimeout in JS Interpreter has come up in the Blockly forum again.

If you're interested in considering it I'm happy to give it a careful review as a second pair of eyes if you like.

Webifi commented 1 year ago

I merged it with all the recent changes, but I'm not sure it's working correctly. Don't have time to test right now.