Calling C functions in Python using WebAssembly

Will Pringle
4 min readJul 25, 2023

--

Using PythonMonkey and Emscripten to call C code from Python!

Youtube video version of this article

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.

Emscripten compiles C to WebAssembly & PythonMonkey runs WebAssembly in Python.

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 the WebAssembly. Module class and passes wasm_bytes, a bytearray containing the bytes of the c library we compiled to WASM, to the constructor. We use pm.new to instantiate it because Python lacks a new keyword and JavaScript class constructors require new to instantiate properly. Note: thepm.new API was added in PythonMonkey version 0.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 assigns my_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) or my_c_lib.sub(1,2)

We can execute this script my typing python3 script.py (replace “script.py” with whatever you called it).

Conclusion

Pretty picture that doesn’t really explain that much.

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

--

--