-
Notifications
You must be signed in to change notification settings - Fork 353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Call pseudo function from native function #199
Comments
I've modified interpreter.js to add methods for directly calling pseudo functions from native code. Here's the pull request for it: #201 Examples: Synchronous callback from native function:
Synchronous callback from native AsyncFunction:
Queued via queueFunction: (func is called last.)
(See: #201 for more) Please let me know if there's a better way to do this. |
Here's a full example of how to implement native timers (setTimeout, etc.) using PR #201
|
Hi Webifi, Great that you've been so busy making improvements to one area of JavaScript Interpreter that could really use some attention! I'm going to try to address your work as follows: first I'll make some comments on this bug about the overall issues and possible approaches to solving them, then I'll review the actual code you've submitted in PR #201, initially looking at a high level and, if/when the overall approach is good, also nit-picking anything that would need to be fixed before accepting it. (No guarantees about the latter, unfortunately: although Neil and I work quite closely on other projects, this one is entirely his.) More to follow. |
Some background, much of which you evidently already aware of, but which I want to lay out just so we are on the same page (and for the benefit of anyone else reading this bug). Callbacks in JavaScriptIgnoring completely for the moment JS Interpreter and it's slightly quirky terminology, but instead just looking at the JavaScript language itself, there are two quite distinct kinds of callbacks:
Callbacks in JS InterpreterThere are three sort of callbacks to discuss: the two above, and, separately, the special AsyncFunction callbacks. Synchronous callbacksAs presently implemented, JS Interpreter makes it easy to write synchronous callbacks from interpreted code to interpreted code, or from interpreted code to native code: you don't need to do anything special, you just call the function in the usual way. See the implementation of As you know, it does not provide any (straight forward) way for native code to call interpreted code. Unfortunately there is no trivial solution here; because of the step-by-step nature of the interpreter, any native function calling interpreted code (i.e., a pseudofunction) will need to:
This is all possible to do at the moment on a one-off basis, but the hackery involved is pretty awful so it would be great to close this bug by providing some nice, straight-forward (and well-documented) mechanisms for doing all that—which is exactly what you appear to be trying to do in #201. Without yet having looked at your code in that PR, my general suggestion would be to take an approach similar to the one I have taken in the Code City interpreter (which is based on JS interpreter), specifically:
Then you can write native functions that call interpreted code and though they will end up being hard-to-read state machines, they will be able to successfully call interpreted code (and—faint praise!—be no more incomprehensible than the step functions are!) Asynchronous callbacksJS Interpreter also provides no mechanism for doing asynchronous callbacks, but fortunately this is much easier: you just need:
Native functions that create async callbacks will remain nice and straight-forward: they don't need to be written as state machines, because they will return long before the callback is ever run. (Of course they also don't get to find out the return value of the callback, but such is the nature of life in a universe with unidirectional time.) PR #102 is a pretty good example of one way this might be done, albeit that there are few issues that need to be thought through:
AsyncFunction callbacksI mention these here mainly to make sure everyone is clear that these are quite distinct from the above. These "AF callbacks" are callbacks only from the point of view of the embedder (i.e., the person writing the program that calls One important point about these, however, is that there should only ever be at most one outstanding AF callback pending. This is because, when an AsyncFunction is running no other interpreted JavaScript code should run, so there is nowhere that another AsyncFunction could be called from. This is just like regular non-async NativeFunctions. (One small caveat: there's no problem in principle with an AsyncFunction making a synchronous callback to interpreted code (or scheduling an ordinary async callback) before running whatever native async function it wraps, or after the underlying native async function has called its callback but before the AF callback is invoked to terminate the AsyncFunction, although there is a problem that any such synchronous callbacks wouldn't actually run because the interpreter is paused. This issue can probably be ignored, because there are vanishingly few cases where someone authoring a native function would want to both call interpreted code synchronously and wrap an underlying async function in a way that makes it look synchronous to the interpreted code, and the changes outlined above to implement synchronous callbacks should make it easy enough to do either one without the special machinery provided by |
@Webifi: to try to answer some of your specific questions:
You'll need to look at the (horribly, horribly long and hairy)
For a synchronous call this will then be pushed on to the top of the stateStack, or for an async callback it will be (somehow, see previous comment for lack of detail) attached to the bottom of the stateStack to be run once the current
That is absolutely the right approach, but of course only part of it.
Looking at your examples only (not yet the implementation):
Since the interpreter is (intentionally) not reentrant, This
This long-winded thing could have been pollyfilled as (Actually, I've just realised that, because
I must admit that I am not entirely sure what the motivating use-case for this would be, but if this was a common pattern I note that, in the state-machine example above, the call to
Not sure what the intention here is, but calling
This looks like a straightforward asynchronous callback. Looks good to me, with two minor nit-picks:
|
Thanks for taking the time to look things over.
There's the rub. Not being reentrant is what makes it difficult to get a value back out of the interpreter. I made it basically reentrant by recording the current state index, adding an additional call state, then stepping until we're back to the recorded index. Looks like I'll need to approach it a different way.
That wouldn't return the value of the pseudo function, that could end up using a native async function, for use in the native async function. My example didn't make that use case very clear.
[Edit] See below... |
Okay, to no longer be reentrant, I've modified the Synchronous callbacks to use something more analogous to a Promise. Examples of use: Synchronous callback from native function:
Synchronous callback from native AsyncFunction: (That makes my brain hurt.)
In both cases above, the Additional pseudo functions can be called by simply returning another Callback in the For example:
or:
In addition, I added the ability to easily throw exceptions from Asyn Functions, so could close #178 & #189 if accepted: Throwing exception in native AsyncFunction:
And as a side effect, added an additional way to throw exception from native functions: Throwing exception in native function: (Alternate to interpreter.throwException(...))
And added catch to function calls: Catching exceptions in pseudo functions calls from native:
(See detailed examples in #201 thread) Names for the methods may need to change. For example, "callFunction" probably should be something like "createCallback", and "queueFunction" could be changed to "appendCall". Then there's the arguments for the interpreted callbacks. I currently just use variable arguments, but perhaps others prefer an array? |
cache invalidation and naming things... I've used "pseudo function" to refer to both interpreted functions and native functions wrapped in a FUNCTION_PROTO. I'm uncertain what's the correct, terse, way to refer to them. For attribute names, perhaps just 'func', as you recommend, is best, since I usually use 'fn' for native JavaScript functions, but when referring to sandboxed functions in code comments? Maybe "sandboxed function"? |
Ugh yes, so true.
Yeah, actually upon reflection that is a pretty reasonable thing to do. The obvious question to ask is: what does the existing codebase do? I actually don't remember. (Or more accurately: whatever memory I do have is doubtless corrupted by all the refactoring I've done on my derived codebase…) |
interpreter.js uses "interpreted function" for purely interpreted functions, "native function" for native functions wrapped in a FUNCTION_PROTO and "native asynchronous function" for native functions wrapped in a FUNCTION_PROTO that will pause the interpreter instance until completion. And that makes perfect sense from inside the interpreter sandbox where there needs to be a distinction between them. But for methods that expose the functions to callbacks from outside the sandbox, I guess it's probably best to just call them all something like "sandboxed function". I've been using "pseudo function", but perhaps that too confusing? |
This looks really interesting. I'm currently occupied in a conversion project, but will tackle this as soon as that's complete. |
Absent any clear precedent I think that is absolutely fine. |
@NeilFraser Just a ping to see if you've had a chance to look this over. |
@NeilFraser Looks like I'll need to resolve some conflicts... Any hope of this, or something like it, being merged? Any do's and don'ts I should keep in mind while refactoring my pull request / resolving conflicts that could make it more likely to be accepted? |
I came up with a walk around for my use case, where I only need to call functions declared in the pseudo code while passing in objects as arguments which can actually control my code. Not sure if it is a desired approach but I will post it here. Code to create a sandbox that we could invoke functions on: const createSandbox = (src) => {
// Create interpreter from user code
const interpreter = new Interpreter(src);
// Retrieve global object from interpreter
const globalScope = interpreter.getGlobalScope().object;
// Define sandbox state that will be shared between pseudo code and the sandbox
const state = {
running: true,
fInvoked: false,
fInvokeTarget: "",
fInvokeArgs: [],
};
// Wrap the sand box state (puts it in closure) since
// interpreter.nativeToPseudo kinda copies the object values.
const stateWrapped = {
getInvokeTarget: () => {
return sandboxState.fInvokeTarget;
},
getInvokeArgs: () => {
return sandboxState.fInvokeArgs;
},
isRunning: () => {
return sandboxState.running;
},
setInvoked: () => {
state.fInvoked = true;
state.fInvokeTarget = EMPTY_INVOKE_TARGET;
state.fInvokeArgs = EMPTY_INVODE_ARGUMENTS;
}
};
// Define function that executes pseudo code, which is private
const execute = () => {
let steps = 0;
// Either the code executes to the end or a function gets invoked, break.
while (interpreter.step() && !state.fInvoked) {
steps++;
if (steps > 1000 /* some arbitrary limit you set */ ) {
throw Error("Are you trying to infinite loop?");
}
}
}
// Execute user code first (there's nothing in the global scope yet)
execute();
// Append our sandbox code now.
interpreter.appendCode(`
// Self invoking function here, not accessible from user code :)
(function () {
// Get the sandbox state from the global scope.
var state = window.state;
// Now we have the state, delete it from the global scope
// so the user code won't be able to get it and cause chaos :)
delete window.state;
// A regular loop to invoke functions
while (state.isRunning()) {
// Get the function we would like to invoke.
var f = window[state.getInvokeTarget()];
// Get the args as well
var args = state.getInvokeArgs();
// Call the function if user code defined it.
if (typeof f === "function") {
f.apply(this, args);
// Probably could also put the return value into the sandbox state
// so the 'invoke' API function could return it.
}
// Set fInvoked to true so the execution would stop.
state.setInvoked();
}
// Remove the sandbox state reference when we are done.
state = undefined;
})();
`);
// Bind the sandbox state that the sandbox code will use.
interpreter.setProperty(globalScope, "state", interpreter.nativeToPseudo(stateWrapped));
// Execute sandbox code
execute();
// Now we are in the function invoking loop, return the API object.
return {
// API to bind objects to pseudo code's global scope
bind: (name, obj) => {
interpreter.setProperty(globalScope, name, interpreter.nativeToPseudo(obj));
},
// API to invoke a function defined by the pseudo code
invoke: (functionName, ...args) => {
state.fInvoked = false;
state.fInvokeTarget = functionName;
state.fInvokeArgs = args;
execute();
}
// API to dispose the sandbox
dispose: () => {
state.running = false;
state.fInvokeTarget = "";
execute();
interpreter.setProperty(globalScope, "state", undefined);
}
}
} Example of using the sandbox: // Suppose we would like to let user write code to control a robot
const userCode = `
function onSomethingHappened(robot) {
robot.doSomething();
console.log("Function 'onSomethingHappened' called.");
}
`;
// Define robot object
const robot = {
doSomething: () => {
console.log("Robot did something.");
}
}
// Wrap robot object so it goes into the wrapper object's closure which makes it
// impossible for the user code to obtain (hide private APIs and states)
const robotWrapper = {
doSomething: () => {
robot.doSomething();
}
}
// Profit.
const sandbox = createSandbox(userCode);
sandbox.bind("console", console);
sandbox.invoke("onSomethingHappened", robotWrapper);
sandbox.dispose(); |
Edit: There is a pull request for this now, ( #201 )
Trying to figure out how to implement calling an interpreted function from a native function, and be able to pass arguments to that function without having to essentially serialize the argument values and pass them back through .appendCode.
After reading previous issues, #130, #153, etc., it appears that what I'd like to do is non-trivial, but I'd still like to give it a shot. Problem is, I'm not sure where to start.
An old pull request, #102 , seems close to what I'd like, but unfortunately the version of interpreter.js at that time is so different from the current that I can't even use the PR as a guide.
Any pointers on how to build a valid Interpreter.State from a Function object passed to a native function, and pass some arguments to that Interpreter.State? Or, if that's the wrong approach, what's the correct one?
The text was updated successfully, but these errors were encountered: