Calling C functions in Python using WebAssembly
Using PythonMonkey and Emscripten to call C code from Python!
This article will cover how to use PythonMonkey and Emscripten to compile a C library to WebAssembly and use it in Python. Emscripten is a tool for compiling C code to WebAssembly(WASM). PythonMonkey on the other hand is a Python library for executing JavaScript and WASM in Python. Using both of these tools creates a powerful workflow for executing C in Python with the added benefits of WebAssembly.
All the code from this article is available on GitHub.
Required Software Installation
You’ll need to install Emscripten and PythonMonkey if you haven’t already.
Install Emscripten
Follow the installation instructions here: https://emscripten.org/docs/getting_started/downloads.html
Install PythonMonkey
PythonMonkey requires Python3.8 or higher. It can be installed using pip:
pip install pythonmonkey
Writing a C Library
We’ll write a basic C library called my_c_lib.c with two simple functions for addition and subtraction:
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
However, for us to export these functions as part of the API when the library gets compiled to WebAssembly, we’ll need to use a couple macros from the Emscripten C library.
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
int sub(int a, int b) {
return a - b;
}
The EMSCRIPTEN_KEEPALIVE
macro is used to ensure the function is exported and not eliminated as dead code by the compiler. For more information, refer to the Emscripten API.
Compiling C to WebAssembly .wasm Code
Next, we’ll use Emscripten’s emcc
tool to compile our C library to WASM by running the following command:
emcc my_c_lib.c -s WASM=1 -s SIDE_MODULE=1 -o my_c_lib.wasm
Running this command will create a new file my_c_lib.wasm.
Calling the C Library from Python with PythonMonkey
We’ll use PythonMonkey to load and make calls to the WebAssembly generated C library. PythonMonkey is a Python library powered by Mozilla’s SpiderMonkey JS engine for executing JavaScript, or WebAssembly, in Python.
Here’s a simple Python script that calls the C library functions we wrote using PythonMonkey:
import pythonmonkey as pm
# get the WebAssembly module from JS
WebAssembly = pm.eval('WebAssembly')
# read the wasm file
file = open('my_c_lib.wasm', 'rb')
wasm_bytes = bytearray(file.read())
# instantiate a new wasm module
wasm_module = pm.new(WebAssembly.Module)(wasm_bytes)
# create an import object which specifies the wasm environment
import_object = {
'env': {
'__memory_base': 0,
'__table_base': 0,
'memory': pm.eval('new WebAssembly.Memory({ initial: 256, maximum: 256 })'),
'table': pm.eval('new WebAssembly.Table({ initial: 0, maximum: 0, element: "anyfunc" })'),
'__indirect_function_table': pm.eval('new WebAssembly.Table({ initial: 0, maximum: 0, element: "anyfunc" })'),
'__stack_pointer': pm.eval('new WebAssembly.Global({ value: "i32", mutable: true }, 8192)'),
'abort': lambda: exec('raise Exception("Abort!")')
}
}
# call the add function from the library
my_c_lib = pm.new(WebAssembly.Instance)(wasm_module, import_object).exports;
print(my_c_lib.add(1,2)) # 3.0
print(my_c_lib.sub(1,2)) # -1.0
WebAssembly = pm.eval('WebAssembly')
grabs the WebAssembly global module from JavaScript and assigns it to a variable called WebAssembly in python.wasm_module = pm.new(WebAssembly.Module)(wasm_bytes)
instantiates a new WebAssembly module using theWebAssembly. Module
class and passeswasm_bytes
, a bytearray containing the bytes of the c library we compiled to WASM, to the constructor. We usepm.new
to instantiate it because Python lacks anew
keyword and JavaScript class constructors requirenew
to instantiate properly. Note: thepm.new
API was added in PythonMonkey version0.2.1
.import_object
specifies the environment for out WebAssembly to execute in such as its memory. We’ll also pass a function to be executed on abort.my_c_lib = pm.new(WebAssembly.Instance)(wasm_module, import_object).exports
assignsmy_c_lib
to an Object that contains all the functions from our C library- Then, to execute any of the functions, we can just do
my_c_lib.add(1,2)
ormy_c_lib.sub(1,2)
We can execute this script my typing python3 script.py
(replace “script.py” with whatever you called it).
Conclusion
In conclusion, we used PythonMonkey and Emscripten to execute C functions in Python really easily. PythonMonkey can do much more than execute WebAssembly. You can use it to load NPM packages in Python, improve Python’s speed using SpiderMonkey’s JIT, and more. Consider checking the project out on GitHub and giving it a star: https://github.com/Distributive-Network/PythonMonkey
Check out PythonMonkey’s documentation for more information: https://docs.pythonmonkey.io/
Also, if you’re interested in another article calling Rust from Python using a similar method, check out my other article: https://medium.com/@willkantorpringle/executing-rust-in-python-using-webassembly-d361eb5583da