@plasticity/coldbrew
v0.0.74
Published
Coldbrew is Python compiled into JavaScript using Emscripten. It is the easiest way to run Python code in JavaScript and is developed by Plasticity.
Downloads
3,720
Maintainers
Readme
Coldbrew: Run Python in JavaScript
Coldbrew is Python compiled into JavaScript using Emscripten and WebAssembly. It is the easiest way to run Python code in JavaScript (a web browser or Node.js) and is developed by Plasticity.
Coldbrew currently uses CPython 3.8.0 and supports Python modules utilizing native C extensions (such as the built-in sqlite3
). There are a ton of neat features like HTTP support in Python and bridge variables to share native objects between the two languages using ES6 Proxies in JavaScript and operator overloads in Python.
Coldbrew also allows you to bundle your own Python application, script, library along with its required modules and dependencies to the environment. See the section on building a custom Coldbrew Python environment. The build is Docker-ized so it is very easy to build! 🐳
Table of Contents
- Demo
- Installation
- Using the Library
- Loading the Environment
- Running Python in JavaScript
- Running JavaScript in Python
- Running Python Files in JavaScript
- Communicating between JavaScript and Python
- Bridge Variables
- Error Handling
- Accessing HTTP in Python
- Using Multiple Threads in Python
- Accessing the Virtual File System
- Extra Performance by using Workers
- Unloading the Environment
- Creating a New Instance of the Environment
- Accessing the Environment
- Get the Current Working Directory
- Change Current Working Directory
- Set Environment Variable
- Delete an Environment Variable
- Get Environment Variables
- Resetting Environment Variables
- Access Standard Output
- Access Standard Error
- Respond to Standard Input with a Buffer
- Respond to Standard Input Interactively
- Respond to Standard Input Interactively and Asynchronously
- Change the Asynchronous Yield Rate
- Resetting Coldbrew Environment
- Can I install third-party Python modules at run time?
- Building a Custom Coldbrew Python Environment
- 1. Changing the Module Name and Other Settings
- 2. Adding Python Module Dependencies and Requirements to the Environment
- 3. Bundling files
- 4. Building
- 5. Running
- 6. Deploying
- 7. Saving space (Optional, but recommended)
- 8. Saving even more space (Optional, but recommended)
- 9. Customizing the Export (Optional)
- Example Use Cases
- Known Limitations
- Security
- Performance and Size Benchmarks
- Contributing
- LICENSE and Attribution
Demo
You can find a demo with some examples of Coldbrew at http://coldbrew.plasticity.ai.
We also have a demo project that ports the Star Wars API's Python Library, swapi-python, to JavaScript using Coldbrew at plasticityai/coldbrew-star-wars-api-demo. It is a good example of a custom build of Coldbrew.
Installation
You can use the library in a browser by embedding coldbrew.js
via our CDN:
<script src="https://cdn.jsdelivr.net/gh/plasticityai/coldbrew@latest/dist/coldbrew.js"></script>
You can use the library in Node.js by installing it using npm
:
npm install @plasticity/coldbrew@latest
and then importing it with:
var Coldbrew = require('@plasticity/coldbrew');
To use a specific version, replace @latest
with @VERSION
where VERSION
is one of the versions listed on the releases page.
If you would like to self host the files, you can also download the files from dist
(web) or dist-node
(Node.js). You can also build it yourself from source and use resulting dist
folder.
Using the Library
Loading the Environment
You can load the Python environment with Coldbrew.load
:
Coldbrew.load(options).then(function() {
console.log("Finished loading Coldbrew!");
});
The load
method optionally takes a dictionary of options and returns a Promise that resolves when Coldbrew is done loading. The options
dictionary can contain some or all of the following options (when omitted the values default to the ones shown below):
var options = {
fsOptions: {
persistHome: false, /* Makes the /home/ directory persistable to browser storage */
persistTmp: false, /* Makes the /tmp/ directory persistable to browser storage */
},
hideWarnings: false, /* Hides warnings */
monitorFileUsage: false, /* Monitors file usage for slimming the data bundle for a custom Coldbrew Python environment */
asyncYieldRate: null, /* Allows you to override the default asynchronous yield rate (see setAsyncYieldRate()) */
worker: false, /* Runs the Coldbrew Python interpreter in a separate Web Worker or Worker thread */
transformVariableCasing: true, /* Transforms bridge variable properties and methods to camel case or snake case automatically */
threadWorkers: 1, /* The amount of workers in a pool that will execute Python spawned threads */
};
Running Python in JavaScript
You can run Python in JavaScript like so:
Coldbrew.run("import sys");
Coldbrew.run("print('The current Python version is:', sys.version)");
The run
function returns 0
when successful or throws a Coldbrew.PythonError
if there was an error.
Running Python Asynchronously in JavaScript
Python code is run synchronously and will lock up the browser for long running code. To run Python code asynchronously and concurrently with JavaScript, you can run it like so:
for(var i=0; i<5; i++) {
setTimeout(function() {
console.log("Every 1 second for 5 seconds from JavaScript.")
}, (i+1)*1000);
}
Coldbrew.runAsync(
`from time import sleep
for i in range(5):
sleep(1)
print("Every 1 second for 5 seconds from Python.")
`);
The runAsync
function returns a Promise that resolves to 0
when successful or rejects with a Coldbrew.PythonError
if there was an error.
Running JavaScript in Python
You can run JavaScript in Python like so:
Coldbrew.run("Coldbrew.run('x = 5;')");
console.log(x); // Prints 5
The run
function returns 0
when successful or throws a Coldbrew.JavaScriptError
if there was an error.
Note: This runs in the scope of window
(browser) or global
(Node.js).
Running Python Files in JavaScript
You can also run Python files like add.py in JavaScript:
Coldbrew.runFile('add.py', {
cwd: '/coldbrew/examples',
env: {},
args: ['5', '15', '-v']
});
The cwd
option is the path to the folder containing the Python file, the env
option is a dictionary of environment variables, the args
is a list of arguments to pass to the Python file.
The runFile
function returns 0
when successful or throws a Coldbrew.PythonError
if there was an error.
Running Python Files Asynchronously in JavaScript
You can also run Python files like add.py in JavaScript asynchronously:
Coldbrew.runFileAsync('add.py', {
cwd: '/coldbrew/examples',
env: {},
args: ['5', '15', '-v']
});
The runFileAsync
function returns a Promise that resolves to 0
when successful or rejects with a Coldbrew.PythonError
if there was an error.
Communicating between JavaScript and Python
You can communicate between the two languages by tranferring variables and calling functions. While only simple examples are shown below, this can be very powerful with "bridge variables" that allow you to transfer not just "primitive" types between the two languages, but all types.
Get Python Variable in JavaScript
You can access a variable from Python in JavaScript like so:
Coldbrew.run("x = 5**2");
Coldbrew.getVariable("x"); // Returns 25
Get JavaScript Variable in Python
You can access a variable from JavaScript in Python like so:
var x = Math.pow(5, 2);
Coldbrew.run("print(Coldbrew.get_variable('x'))") // Prints 25
Note: This only works if the variable is in the scope of window
(browser) or global
(Node.js).
Note: If the variable is a Promise
, it is automatically resolved to the value (requires running in asynchronous mode).
Run Python Function in JavaScript
You can run a function from Python in JavaScript like so:
Coldbrew.run(
`def foo(x, y):
return x**y
`);
Coldbrew.runFunction("foo", 5, 2); // Returns 25
You can also run a function with keywords from Python in JavaScript like so:
Coldbrew.run(
`def foo(x, y=0):
return x**y
`);
Coldbrew.runFunction("foo", 5, Coldbrew.PythonKeywords({y: 2})); // Returns 25
Run Python Function Asynchronously in JavaScript
You can run a function from Python asynchronously in JavaScript like so:
Coldbrew.run(
`def foo(x, y):
return x**y
`);
Coldbrew.runFunctionAsync("foo", 5, 2); // Resolves to 25
Run JavaScript Function in Python
You can run a function from JavaScript in Python like so:
function foo(x, y) {
return Math.pow(x, y);
}
Coldbrew.run("print(Coldbrew.run_function('foo', 5, 2))"); // Prints 25
Note: This only works if the function is in the scope of window
(browser) or global
(Node.js).
Note: If the return value is a Promise
, it is automatically resolved to the value (requires running in asynchronous mode).
Bridge Variables
What are Bridge Variables?
When you call getVariable
or get_variable
, if the type of the variable is a "primitive" type, the data is JSON-serialized, passed to the other language and then JSON-unserialized. When the type of the variable is a non "primitive" type, a bridge variable is returned instead that allows you to interact with a variable from the other language as if it were a native variable by essentially performing RPC-style communication between the two languages.
The bridge variable uses ECMAScript 6 Proxies in JavaScript and Object Customization in Python to make it seem like a Python variable is natively accessible in JavaScript and a JavaScript variable is natively accessible in Python.
For example, with bridge variables you can export a Python class like the Counter
class from Python to JavaScript and it will behave like it were a JavaScript class:
Coldbrew.run("from collections import Counter");
var Counter = Coldbrew.getVariable("Counter");
var c = new Counter(['red', 'blue', 'red', 'green', 'blue', 'blue']);
console.log(c.toString()); // Prints "Counter({'blue': 3, 'red': 2, 'green': 1})"
console.log(c.blue); // Prints "3"
console.log(c.mostCommon(2)); // Prints "[('blue', 3), ('red', 2)]"
If you use Coldbrew.getVariableAsync
you can get the variable asynchronously. Meaning all interaction and methods run on the underlying Python object will be run asynchronously. For example, making HTTP requests requires running asynchronously:
Coldbrew.run("import urllib.request");
var urllib = Coldbrew.getVariableAsync('urllib');
urllib.request.urlopen("http://coldbrew.plasticity.ai/remote/example.txt").read().toString().then(function(contents) {
console.log(contents);
});
You can also go the other way too. You can export a JavaScript class like the Date
class from JavaScript to Python and it will behave like it were a Python class:
Coldbrew.run("Date = Coldbrew.get_variable('Date')");
Coldbrew.run("d = Date(1234567890000)");
Coldbrew.run("print(d)"); // Prints "Fri Feb 13 2009 15:31:30 GMT-0800 (Pacific Standard Time)"
Coldbrew.run("print(d.to_iso_string())"); // Prints "2009-02-13T23:31:30.000Z"
You can do this with pretty much any type of variable (classes, object instances, functions, modules, primitives, etc.) between both languages.
Interacting with Bridge Variables
You can interact with bridge variables as if they were a native variable. Method names or property names are automatically transformed to camel case from snake case when using a Python bridge variable in JavaScript and vice-versa. You can turn this off by passing transformVariableCasing: false
to the Coldbrew load
method.
Using introspection can help you determine what properties or methods on the object can be accessed. But if you want more detailed documentation on how syntax in one language will be translated to the other, you can find more information here.
Bridge Variable Introspection
You can check if a variable is a bridge variable in JavaScript like so:
Coldbrew.PythonVariable.isPythonVariable(...) // Returns true or false
or in Python like so:
Coldbrew.run("print(Coldbrew.JavaScriptVariable.is_javascript_variable(...))") // Prints true or false
In both languages, you can use the .__type__
property to find out the actual type of a bridge variable and you can call the .__inspect__()
method to return a list of attributes you can access on the object.
Implicit Bridge Variables
We've shown how bridge variables are returned when using getVariable
and get_variable
. There are many other places where bridge variables are implicitly returned or created.
For example, runFunction
, runFunctionAsync
, and run_function
can all return bridge variables if the data is not JSON-serializable. Also, any property or method on a bridge variable that is not JSON-serializable is returned as a bridge variable. Moreover, if you pass a bridge variable as an argument to a function that will be run in the other language, it will recieve a reference to the original variable and it should "just work". If you pass a non JSON-serializable variable to a function that will be run in the other language, it will automatically be converted and accessible as a bridge variable in the other language.
Bridge Variable Garbage Collection
When creating a bridge variable, a reference to the "real" variable is held in the other language's interpreter to prevent the value from being garbage collected.
In JavaScript, there are no hooks for the garbage collector so when a bridge variable is created with getVariable()
, it needs to be explicitly told when it is done being used to remove the reference in the Python interpreter so the memory can be released. You can do this by calling the __destroy__()
method on the bridge variable. Once destroyed, you should not access the bridge variable anymore and it should be considered invalid. You can check if a variable is already destroyed, by using the __destroyed__
property. If you want to destroy all bridge variables created at once, run Coldbrew.destroyAllVariables()
.
In Python, there are hooks for the garbage collector so we know when a bridge variable created by get_variable()
is garbage collected and can de-reference the variable in JavaScript as well, so there are no memory leaks.
We still expose __destroy__()
, __destroyed__
, and Coldbrew.destroy_all_variables()
in Python for manual garbage collection, if needed.
Error Handling
Catching Python Errors in JavaScript
If you are running a Python using Coldbrew that encounters an error, a Coldbrew.PythonError
will be thrown in JavaScript. You can then use e.errorData
on the error object e
to get more information about the Python context where the error occurred. You can also get information about the last Python exception thrown in JavaScript like so:
Coldbrew.getExceptionInfo(); // Returns a dictionary with information from the last Python exception
Catching JavaScript Errors in Python
If you are running a JavaScript function in Python that encounters an error, a Coldbrew.JavaScriptError
will be raised in Python. You can then use e.error_data
on the error object e
to get more information about the JavaScript context where the error occurred. You can also access the last JavaScript error in Python like so:
Coldbrew.run('print(Coldbrew.get_exception_info())');
Accessing HTTP in Python
Python is able to access HTTP connections in Coldbrew and the requests will be shimmed by JavaScript's XHR requests:
Coldbrew.runAsync(
`import urllib.request
print(urllib.request.urlopen("http://coldbrew.plasticity.ai/remote/example.txt").read())
`);
Using Multiple Threads in Python
The Python threading
library is supported and each individual Python thread will run inside a Web Worker for true parallelism. To make threads efficient, a pool of Web Workers are spawned when first loading Coldbrew. You can control the number of workers in the pool using the threadWorkers
setting on Coldbrew.load
. Ideally, threadWorkers
should be set to the maximum number of concurrently running threads your Python application will ever run.
To see an example of multi-threaded Python code running, you can run the threads.py file:
Coldbrew.runFile('threads.py', { cwd: '/coldbrew/examples' });
Note: Node.js and some browsers do not support threading as it relies on a JavaScript feature called SharedArrayBuffer
s which were disabled across many JavaScript engines due to the Spectre attacks. Chrome has re-enabled them after fixing the issue, fixes for other browsers and Node.js are pending.
Accessing the Virtual File System
Python is not able to access the system's actual file system due to the limitations of the browser, but it can access a virtual file system.
Listing files under a directory
You can list files under a directory like so:
Coldbrew.listFiles('/')
Create a Folder
You create a folder like so:
Coldbrew.createFolder('/home/myfolder/');
Adding a File
You can add a file like so:
Coldbrew.addFile('/test.txt', 'Hello World!')
Check if a Path Exists
You can get the information of a path like so:
Coldbrew.pathExists('/home');
If it exists, a dictionary of path information is returned. If it doesn't exist, null
is returned.
Reading a File
You can read a file like so:
Coldbrew.addFile('/test.txt', 'Hello World!');
Coldbrew.readFile('/test.txt');
You can also read file back in bytes for binary files like so:
Coldbrew.addFile('/test.txt', 'Hello World!');
Coldbrew.readBinaryFile('/test.txt');
Deleting a Path
You can delete a file or path to a folder like so:
Coldbrew.addFile('/test.txt', 'Hello World!');
Coldbrew.deletePath('/test.txt');
Adding a .zip
File
You can bulk add files from a ZIP file like so:
Coldbrew.addFilesFromZip('/home', 'http://coldbrew.plasticity.ai/remote/example_project.zip').then(function() {
Coldbrew.runFile('multiply.py', {
cwd: '/home/example_project',
env: {},
args: ['5', '15', '-v']
});
});
Downloading files to a .zip
File
You can download files and folders from the file system to a ZIP file like so:
Coldbrew.downloadPathToZip('/', 'download.zip');
Saving and Loading Files to Browser Storage
If using persistTmp
or persistHome
options, you can save files to the browser storage like so:
Coldbrew.saveFiles();
To load saved files from browser storage back to the virtual file system later you can use:
Coldbrew.loadFiles();
Extra Performance by using Workers
You can run the Coldbrew library in a separate worker thread (Web Workers in the browser and Worker threads in Node.js) by passing worker: true
to the Coldbrew load
method.
This will make it so that even synchronous Python execution doesn't block the browser's UI thread or the main JavaScript event loop as it will be running in its own thread. This also means it can utilize multiple cores on a machine with multiple cores. When running in asynchronous mode, the yield rate is automatically set to infinity, since you never need to yield back as it is running on a separate thread. A high yield rate increases performance as well.
Note that when running in workers, every method on the Coldbrew
object returns a Promise
since the main thread has to asynchronously communicate back and forth with the worker thread. For example, you would get return values like so:
Coldbrew.unload();
Coldbrew.load({worker: true}).then(function() { // Load Coldbrew using workers
// Done loading!
// This gets executed in a separate worker thread
Coldbrew.run("x = 5**2");
// This will fetch the variable 'x' from the separate worker thread
Coldbrew.getVariable("x").then(function(result) {
console.log(result); // Prints 25
});
});
Unloading the Environment
You can unload the Python environment with Coldbrew.unload
:
Coldbrew.unload();
This makes it as if load()
was never called.
Creating a New Instance of the Environment
You can create a new instance of the Coldbrew object with Coldbrew.createNewInstance
:
var Coldbrew2 = Coldbrew.createNewInstance();
Coldbrew2.load().then(function() {
console.log("Finished loading another Coldbrew!");
});
Accessing the Environment
Get the Current Working Directory
You can get the current working directory path like so:
Coldbrew.getcwd();
Change Current Working Directory
You can change the current working directory path like so:
Coldbrew.chdir('/home');
Set Environment Variable
You can add/overwrite an environment variable like so:
Coldbrew.setenv('FOO', '1');
Delete an Environment Variable
You can unset an environment variable like so:
Coldbrew.unsetenv('FOO');
Get Environment Variables
You can get back a dictionary of all environment variables like so:
Coldbrew.getenv();
Resetting Environment Variables
You can reset environment variables like so:
Coldbrew.resetenv();
Access Standard Output
By default, standard output is line buffered and each line is sent to console.log
. You can override this behavior by setting:
Coldbrew.onStandardOut = function(line) {
// Do what you want with each line of standard output
};
Access Standard Error
By default, standard error is line buffered and each line is sent to console.warn
. You can override this behavior by setting:
Coldbrew.onStandardErr = function(line) {
// Do what you want with each line of standard error
};
Respond to Standard Input with a Buffer
If you know ahead of time what your entire standard input buffer is you can set it like so:
Coldbrew.standardInBuffer = "This line of text is what will be read from standard input.\n";
Respond to Standard Input Interactively
You can also respond to standard input interactively by implementing:
Coldbrew.onStandardInRead = function(size) {
// Return a string of `size` bytes (or characters)
};
Respond to Standard Input Interactively and Asynchronously
If running Python asynchronously, you can respond to standard input asynchronously as well:
Coldbrew.onStandardInReadAsync = function(size) {
// Return a Promise that resolves to a string of `size` bytes (or characters)
};
Change the Asynchronous Yield Rate
By default, the Python interpreter yields back to the JavaScript event loop every 100 Python bytecode instructions when running asynchronously. You change the number of bytecode instructions from the default to something else with:
Coldbrew.setAsyncYieldRate(...)
As the number is set higher, Python code will run faster (since it yields back to the JavaScript event loop less frequently), but the JavaScript VM will feel more locked up. The lower the number, the opposite is true.
You can retrieve the current yield rate like so:
Coldbrew.getAsyncYieldRate()
You can also set/get the yield rate in Python with:
Coldbrew.run("Coldbrew.set_async_yield_rate(...)");
Coldbrew.run("print(Coldbrew.get_async_yield_rate())");
Can I install third-party Python modules at run time?
Coldbrew only comes with a standard Python installation, with no third-party modules. There is no way to add modules at run time to this environment using something like pip install
. You can, however, instead build a custom Coldbrew Python environment yourself (it's easy!) which allows you to specify a requirements.txt
file to pre-load your third-party modules into the environment.
Building a Custom Coldbrew Python Environment
You can build a custom Coldbrew Python Environment to access more functionality of Coldbrew and customize various settings.
1. Changing the Module Name and Other Settings
To change the global name of the library from Coldbrew
to something else in JavaScript (or to run multiple custom Coldbrew Python environments on the same page without their names conflicting in the global namespace), you can modify the MODULE_NAME
setting in customize/coldbrew_settings.py
. If you are building for Node.js, you will also want to set NODE = True
. Various other settings can also be modified in customize/coldbrew_settings.py
like the initial allocated memory size, however, the defaults are generally acceptable if you don't want to modify them.
2. Adding Python Module Dependencies and Requirements to the Environment
Add any requirements of your project in the standard Python requirements.txt
format.
Note: Python modules that are pure Python will almost certainly work. Python modules that use C code may or may not work properly depending on if they have external dependencies or rely heavily on specific operating system functionality.
3. Bundling files
To bundle additional files or scripts into the virtual file system, simply add them to the /customize/files
folder. They will be available at runtime under /files
in the virtual file system.
4. Building
To build, simply navigate to the project root in terminal and run ./build.sh
. This will kick off a build process within a Docker container. The first build will take quite some time as it will have to compile Python from source. Subsequent builds will be much faster with caches. If you need to clean all build caches for some reason, run ./clean.sh
.
5. Running
To test, simply navigate to the project root in terminal and run ./serve.sh
. This will run a web server at http://localhost:8000 that will embed your custom Coldbrew Python environment for use.
6. Deploying
Deployment files can be found under the dist
folder. An example index.html
in the dist
folder shows how to import and use the library on a page. A package.json
file will be added to the dist
folder when building for Node.js, which you can modify as needed.
7. Saving space (Optional, but recommended)
The version of Coldbrew distributed by CDN can be quite large since it makes an AJAX request to download the entire Python standard library (coldbrew.data
file). Your particular Python program / application / script, however, may not need all of these files. We have included an easy way to slim down the data bundle. Modify customize/keeplist.txt
to include paths to all files of the standard library that your application uses to keep them (sort of like a reverse .gitignore
), any other files will not be bundled at runtime.
You can easily generate this list of files to keep by passing monitorFileUsage: true
to the Coldbrew load
method and then calling Coldbrew.getUsedFiles()
in the JavaScript console after running your Python program / application / script (to make sure that all files imported by your code were monitored). This saves a significant amount of space, so it is recommended!
8. Saving even more space (Optional, but recommended)
The WebAssembly binary (coldbrew.wasm
) contains code for all of Python's compiled built-in modules. However, you may not need all of them. There is a very simple way to remove them during the build process to make the coldbrew.wasm
file smaller. Modify the UNUSED_BUILTIN_MODULES
setting in customize/coldbrew_settings.py
to be a list of unused module names to not include and compile during the build process.
You can easily generate this list of unused modules by calling Coldbrew.getUnusedModules()
in the JavaScript console after running your Python program / application / script (to make sure that all modules your code uses are accounted for).
9. Customizing the Export (Optional)
You may want to change what value will be exported by the library. By default, the Coldbrew Python Environment is exported. You can modify what value is exported by setting the EXPORT
variable in customize/export.js
.
Whatever you set the EXPORT
variable to, is what the global variable named MODULE_NAME
(the same MODULE_NAME
from coldbrew_settings.py
) will be set to in browsers or what will be exported in Node.js when require()
-ing your library.
Example Use Cases
This isn't the most efficient way to run code in a JavaScript. It is an interpreted language (Python), within another interpreted language (JavaScript). However, with the emitted WebAssembly code from Emscripten, it actually runs fairly quickly. Emscripten can get fairly close to native machine code speeds, because browsers will compile WebAssembly to machine code. It would, of course, be better if you hand re-wrote your Python code for the browser. However, there are be a few legitimate cases where this library would be useful:
An online demo for a Python library that makes it easy for people to quickly experiment with it in the browser / Node.js.
A quick-and-dirty solution to porting over Python code to the browser / Node.js that isn't worth hand re-writing, is a legacy codebase, or would be too difficult to maintain multiple versions of in two different languages. (Write once. Deploy anywhere.)
Access the Python Standard Library and thousands of Python libraries that are not available in JavaScript.
An online IDE or coding tutorial website that wants to let people run Python code in a sandbox. It is better to run the Python code in the user's own browser so you don't have execute arbitrary Python code on your servers (for security reasons). It also means you don't have to run servers to execute Python code server side and send the result back to the browser (which is more cost efficient).
Running Python code at the edge, on-device, instead of on a backend server for user privacy reasons.
Known Limitations
Any limitations imposed by the browser will be imposed by Python running in the browser (of course). Here are some known limitations:
Python can access the system's file system. JavaScript cannot. However, a virtual file system is created and Python is run at the files directory of that virtual file system (
/files
).Python can access low-level networking like sockets. JavaScript cannot. We have, however, shimmed HTTP support into Python.
Python can interface with the operating system and shell. JavaScript cannot. Functions like
os.system
,os.fork
, ormultiprocessing
are not appropriate as there is no notion of a host environment shell or processes as these are operating system level constructs.
A lot of these things can be "shimmed" in the future just like the virtual file system. Things like /dev/random
and /dev/urandom
are actually available even though there is no operating system because Emscripten shims these devices.
Even though on Node.js, it is possible to access some of these features, we still limit the ability of Coldbrew to access these features to maintain compatibility with the browser version of Coldbrew.
Security
When using Coldbrew in the browser, all Python code does execute in the browser, so it is fairly safe to execute arbitrary Python code in comparison to executing arbitrary Python code server-side on your backend servers. However, you should treat any Python code running on a page in the browser as " eval()
-ed JavaScript". That means the Python code is not sandboxed from manipulating elements on the page, reading cookies, accessing the network, or accessing other site resources.
When using Coldbrew in Node.js, you similarly want to protect against executing arbitrary Python code server-side as there is now an additional risk of code executing on your backend server performing Denial-of-Service attacks or accessing your backend system resources.
Coldbrew is not sandboxed by design, however, if you need that functionality you could use other JavaScript sandboxing solutions like sandboxed iframes or Realms-shim to create a JavaScript sandbox and run Coldbrew within that.
Performance and Size Benchmarks
To give a quick sense of how much of a performance hit running Python in JavaScript takes, a few different tests of running /coldbrew/examples/fib.py
are shown below. All tests were run on a MacBook Pro (Retina, 15-inch, Mid 2014) 2.2GHz quad-core Intel Core i7 @ 16GB RAM on SSD with the Google Chrome Browser:
| Benchmark Test | Native (macOS) CPython Execution | Coldbrew Synchronous Execution | Coldbrew Asynchronous Execution (yield rate = 100) (default) | Coldbrew Asynchronous Execution (yield rate = 1000) | Coldbrew Asynchronous Execution (yield rate = 10000) | Coldbrew Asynchronous Execution (yield rate = 100000) |
| -------------------------------------------------------------------------- | :--------------------------------: | :------------------------------: | :---------------------------------------------------------------------------------: | :------------------------------------------------------------------------: | :------------------------------------------------------------------------: | :--------------------------------------------------------------------------: |
| fib.py 100000 -i -v
(compute intensive workload) | 0.149s | 0.505s (3.39x) | 11.62s (77.98x) | 2.518s (16.89x) | 1.223s (8.20x) | 1.115s (7.48x) |
| fib.py 25 -v
(recursive & function call heavy workload) | 0.040s | 0.154s (3.85x) | 59.24s (1481.00x) | 9.699s (242.47x) | 3.747s (93.67x) | 3.130s (78.25x) |
| fib.py 1000 -f -v
(file system intensive workload) | 0.298s | 0.405s (1.36x) | 11.84s (39.73x) | 2.463s (8.26x) | 1.268s (4.25x) | 1.181s (3.96x) |
These tests are surely not exhausitive or representative, but should give some sense of how the performance compares at a glance.
Synchronous execution performs fairly well, with only a minor slowdown. Asynchronous execution is considerably slower. Therefore, we recommended running most code synchronously, if possible, and only running the sections of your Python program that need to be asynchronous (such as code that may be accessing an HTTP connection) in asynchronous mode. We also recommened experimenting with the yield rate and setting it to a number as high as possible if your application doesn't feel locked up.
Contributing
The main repository for this project can be found on GitLab. The GitHub repository is only a mirror. Pull requests for more tests, better error-checking, bug fixes, performance improvements, or documentation or adding additional utilties / functionalities are welcome on GitLab.
Please note: This library is being actively maintained as we use it. However, due to the nature of compiling a whole other language's interpreter into JavaScript using a tool like Emscripten, certain things might not work or be accessible due to Emscripten's limitations. If you want to contribute a PR to fix something, please feel free to, but if we have no need for whatever happens to be broken we will likely not fix or provide support for those issues ourselves. If it is simply a matter of exposing a library / function that wasn't exposed or another simple change, open an issue or PR and we'll see if we can do our best to add support.
You can contact us at [email protected].
LICENSE and Attribution
This repository is licensed under the license found here.