Run wasm built with wasm-pack with pythonmonkey

2025-01-03

I have been trying to run wasm code built with wasm-pack with pythonmonkey,  a Mozilla SpiderMonkey JavaScript engine embedded into the Python Runtime.

Consider a lib.rs as follow:

use wasm_bindgen::prelude::*;  
  
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {  
  return a + b; 
}

With wasm-pack, we can build the src code to wasm

wasm-pack build --target nodejs --no-typescript

We will get a .wasm file and a .js wrapper file. The case here is that we can not instantiate the .wasm directly, which will get us errors like No module named '__wbindgen_placeholder__', or import object field '__wbindgen_init_externref_table' is not a Function. We should instead import the .js wrapper file, and use exported interfaces.

But import the .js wrapper in pythonmonkey would be a problem, the generated .js wrapper contains the following code:

const path = require('path').join(__dirname, '<your_module_name_bg>.wasm');  
const bytes = require('fs').readFileSync(path);

But pythonmonkey doesn’t provide those libs. So we have to monkey patch those modules and methods. For example, a fs module with readFileSync method in fs.py:

def readFileSync(path):
    with open(path, 'rb') as fd:  
        return bytearray(fd.read())  
  
exports = {
  'readFileSync': readFileSync  
}

We can require fs.py in pythonmonkey by require('./fs.py'). And one more step, we have to hack require.cache to make sure we can require('fs') in the future code. So we we can write a load function for this:

import os  
import pythonmonkey as pm  
  
require = pm.createRequire(os.path.join(os.getcwd(),
										f'{__name__}__virtual__'))  
pm.globalThis['require'] = require
  
def load(module, path=None):
    if path is None:
        path = f'./{module}.py'
    pm.eval(f'''
require('{path}')
require.cache['{module}'] = require.cache[require.resolve('{path}')]  
''')

Finally, we can run our add(1, 1) wasm code in pythonmonkey:

load('fs')  
load('path')

pm.eval('''  
const wasm = require('./pkg/wasm_add.js');  
console.log(wasm.add(1, 1));

But if your code is more complex than a simple add function, especially if you expose more interfaces and more types, the generated .js wrapper file would be much more complex. More modules and functions need to be patched. One more thing, pythonmonkey doesn’t support object destruction(const { a } = {a: 1, b: 2}) currently(version 1.1.0), so the generated .js wrapper need to be edited.