-
Notifications
You must be signed in to change notification settings - Fork 13
Description
I'm writing this down now as an idea for future work. I probably won't be able to get to this soon, but I might later, or someone else might be able to contribute it.
PyMiniRacer runs code in the V8 sandbox. The JS code, by design, has no (intended!) way to reach out and touch the system it runs in. I.e., it cannot make network calls, read and write files, etc. It would be nice if a PyMiniRacer user could write a custom JavaScript API in Python and present it to JS code. For inspiration, the NodeJS standard library is full of such APIs: a bunch of things from network I/O to file I/O to subprocess management, all specific to the Node server and useful for JS code. (Of course, the NodeJS library is implemented in C++ and exposed to JS. Here we're talking about implementing an API in Python and exposing it to JS. Also creating a standard library as extensive as NodeJS's would be an enormous undertaking; this issue is just talking about making it possible to create JS-callable APIs in Python, not about creating any specific "PyMiniRacer standard library".)
Obviously using such a feature would expose the PyMiniRacer user to security concerns if running unstrusted JS code as it would create a breach of the sandbox. However, it could be useful if running trusted code, or if the user is very careful about the APIs they expose. We wouldn't want to include any "PyMiniRacer standard library" by default (unless its contents were totally safe; e.g., we currently ship a setTimeout and clearTimeout which just simplify what one can already do with Atomics.waitAsync, and that's it).
Unfortunately, as of this writing there's no way for JavaScript under PyMiniRacer to call back into Python, and thus no way for a PyMiniRacer user to create a Python-native API and expose it to JS. One could do a hacky solution like polling a "work queue" (e.g., a JS array) from Python and servicing it in Python, but there's no true callback mechanism. The one place PyMiniRacer actually already supports callbacks from JS is Promise objects, which you can create in JS and await in Python. However, those are one-shot by design and thus would be very awkward to convert into a general-purpose callback API.
Here's a sketch of how a general-purpose callback API might work:
async def read_arbitrary_file(name: str) -> str: # args and return can be any of PythonJSConvertedTypes
with aiofiles.open(name) as f:
return await f.read()
# Wrap and install the API:
mr = MiniRacer()
wrapped_api = mr.wrap_api(my_api)
mr.eval("this")["read_arbitrary_file"] = wrapped_api
promise = mr.eval("""
let get_reversed_contents = async (name) => {
let contents = await read_arbitrary_file(name); # note the API is async in JS!
return contents.split("").reverse().join("");
};
get_reversed_contents("/usr/share/dict/words")
""")
print(promise.get()) # print the dictionary backwards!A design key point is that at least on the JS side, if not also the Python side, the API must be async. This is because callbacks from V8 hold the V8 Isolate lock, so the most we should do in a callback (especially an arbitrary one like proposed here) is to create and return a promise. (We want to avoid the mistake of attempting to re-enter V8 from within the callback. That would deadlock, at least under PyMiniRacer's current design.) We can then fulfill the promise in another thread which doesn't hold the v8 isolate lock. (This is somewhat similar to how the NodeJS standard library works; NodeJS APIs generally either return Promises or take in callback functions. They don't usually give you synchronous results.)
Implementation idea: To make this work, the mr.wrap_api Python API would call C++ code which instantiates and returns generic C++ callback function. That C++ callback function would do nothing other than create a Promise, then call a Python callback with the arguments array plus the Promise resolve+reject functions, then return the Promise to the JS caller. The Python callback would just decode the arguments and resolve+reject and stuff them onto a work queue (probably an asyncio.Queue). Then another loop, probably an asyncio loop (TODO: managed by the PyMiniRacer user? or magically by PyMiniRacer?) would service that queue by popping the args+resolve+reject, calling the user-specified Python callback with the args, and stuffing the return (or exception) into the resolve or reject function. In that way, JS would be immediately triggering work in Python, by way of a asyncio.Queue wakeup, but Python would not do any serious work on V8's message loop thread.