Design#

Main Idea#

pybind11#

Pybind11 is a library that exposes C++ types in Python. It is a wrapper around the Python C API that allows for seamless integration of C++ and Python. To export a C++ class like the following to Python, you would use pybind11:

// foo.h
class Foo {
public:
    void say_hello() {
        std::cout << "Hello, World!" << std::endl;
    }
};
// main.cpp
#include <foo.h>
#include <pybind11/pybind11.h>

PYBIND11_MODULE(example, m) {
    py::class_<Foo>(m, "Foo")
        .def(py::init<>())
        .def("say_hello", &Foo::say_hello);
}

Not only can Python call C++ functions, but C++ can also call Python functions. In particular, one can interact with Python objects. An object is represented by the py::object type on the C++ side.

// main.cpp
py::object sys = py::module::import("sys");
py::object version = sys.attr("version");
std::string version_string = version.cast<std::string>();
std::cout << "Python version: " << version_string << std::endl;

embind#

There is a similar library for emscripten called embind. It allows you to expose C++ types to JavaScript.

// main.cpp
#include <foo.h>
#include <emscripten/bind.h>
using namespace emscripten;
// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
  class_<Foo>("Foo")
    .constructor<>()
    .function("say_hello", &Foo::say_hello)
    ;
}

To access JavaScript from C++, you would use the emscripten::val type. This is the counterpart of py::object in pybind11.

emscripten::val console = emscripten::val::global("console");
console.call<void>("log", "Hello, World!");

pyjs#

The main idea of pyjs is to export emscripten emscripten::val objects to Python with pybind11 and to export pybind11 py::object objects to JavaScript with embind.

That way, we get a seamless integration of Python and JavaScript with relatively little effort and high level C++ code.

Error Handling#

To catch JavaScript exceptions from Python, we wrap all JavaScript code in a try-catch block. If an exception is thrown, we catch it and raise a Python exception with the same message. The Python exceptions are directly translated to JavaScript exceptions.

Memory Management#

Any C++ class that is exported via embind needs to be deleted by hand with delete method. This is because the JavaScript garbage collector does not know about the C++ objects. Therefore all py::object objects that are created from javascript objects need to be deleted by hand. This is done by calling the delete method on the pyobject object JavaScript side.

Performance#

Compared to pyodide, pyjs is slower when crossing the language barrier. Yet it is fast enough for all practical purposes. The FFI and the asyncio event loop are benchmarked, see benchmarks.

Packaging#

Pyjs is distributed exclusively via emscripten-forge.

Testing#

To test pyjs without manually inspecting a web page, we use pyjs-code-runner. This is a tool that runs a Python script in a headless browser and returns the output. See tests.