Emscripten, Callbacks, and C++11 Lambdas

The web is an asynchronous world built on asynchronous APIs. Thus, typical web applications are full of callbacks. onload and onerror for XMLHttpRequest, the callback argument to setTimeout, and messages from Web Workers are common examples.

Using asynchronous APIs is relatively natural in JavaScript. JavaScript is garbage-collected and supports anonymous functions that close over their scope.

However, when writing Emscripten-compiled C++ to interact with asynchronous web APIs, callbacks are less natural. First, JavaScript must call into a C++ interface. Embind works well for this purpose. Then, the C++ must respond to the callback appropriately. This post is about the latter component: writing efficient and clean asynchronous code in C++11.

I won’t go into detail here about how that works, but imagine you have an interface for fetching URLs with XMLHttpRequest.

class XHRCallback {
    virtual void onLoaded(const Response& response) = 0;
};
void fetch(const std::string& url, XHRCallback* callback);

Imagine Response is a struct that embodies the HTTP response and onLoaded runs when the XMLHttpRequest ‘load’ event fires.

To fetch data from the network, you would instantiate an implementation of the XHRCallback interface and pass it into the XHR object. I’m not going to cover in this article how to connect these interfaces up to JavaScript, but instead we will look at various various implementations of XHRCallback on the C++ side.

For the purposes of this example, let’s imagine we want to fetch some JSON, parse it, and store the result in a model.

Approach 1

A simple approach is to write an implementation of the interface that knows about the destination Model and updates it after parsing the body. Something like:

class MyXHRCallback : public XHRCallback {
public:
    Model* model;

    MyXHRCallback(Model* model) : model(model) {}

    virtual void onLoaded(const Response& response) override {
        model->update(parseJSON(response.body));
    }
};

void updateModelFromURL(const std::string& url, Model* model) {
    fetch(url, new MyXHRCallback(model));
}

This is quite doable, but it’s a real pain to write a new class instance, field list, and constructor for every callback.

What if we tried to simplify the API with C++11 lambdas?

Approach 2

class LambdaXHRCallback : public XHRCallback {
public:
    std::function<void(const Response&)> onLoadedFunction;
    virtual void onLoaded(const Response& response) override {
        if (onLoadedFunction) {
            onLoadedFunction(response);
        }
    }
};

Above is boilerplate per interface. Below is use.

void updateModelFromURL(const std::string& url, Model* model) {
    auto callback = new LambdaXHRCallback;
    callback->onLoaded = [model](const Response& response) {
        model->update(parseJSON(response.body));
    };
    fetch(url, callback);
}

Ignoring the implementation of LambdaXHRCallback, the API’s a little cleaner to use. This approach requires backing the callback interface with an implementation that delegates to a std::function. The std::function can be bound to a local lambda, keeping the callback logic lexically near the code issuing the request.

From a clarity perspective, this is an improvement. However, because Emscripten requires that your customers download and parse the entire program during page load (in some browsers, parsing happens on every pageload!), code size is a huge deal. Even code in rarely-used code paths is worth paying attention to.

std::function, being implemented with its own abstract “implementation-knowledge-erasing” interface that is allocated upon initialization or assignment, tends to result in rather fat code. The default 16-byte backing storage in 32-bit libc++ doesn’t help either.

Can we achieve clear asynchronous code without paying the std::function penalty? Yes, in fact!

Approach 3

template<typename OnLoad>
class LambdaXHRCallback : public XHRCallback {
public:
    // perfect forwarding
    LambdaXHRCallback(OnLoad&& onLoad)
    : onLoad_(std::forward<OnLoad>(onLoad))
    {}

    virtual void onLoaded(const Response& response) override {
        onLoad_(response);
    }
    
private:
    OnLoad onLoad_;
};

// this function exists so OnLoad’s type can be inferred,
// as lambdas have anonymous types
template<typename OnLoad>
LambdaXHRCallback<OnLoad>* makeXHRCallback(OnLoad&& onLoad) {
    return new LambdaXHRCallback(std::forward<OnLoad>(onLoad));
}

Above is boilerplate per interface. Below is use.

void updateModelFromURL(const std::string& url, Model* model) {
    fetch(url, makeXHRCallback(
        [model](const Response& response) {
            model->update(parseJSON(response.body));
        }
    ));
}

But… but… there are templates here, how is that any better than std::function? Well, first of all, now we only have one virtual call: the XHRCallback interface itself. Previously, we would have a virtual call into LambdaXHRCallback and then again through the std::function.

Second, in C++11, lambdas are syntax sugar for an anonymous class type with an operator(). Since the lambda’s immediately given to the LambdaXHRCallback template and stored directly as a member variable, in practice, the types are merged during link-time optimization.

I ported a dozen or so network callbacks from std::function to the template lambda implementation and saw a 39 KB reduction in the size of the resulting minified JavaScript.

I won’t go so far as to recommend avoiding std::function in Emscripten projects, but I would suggest asking whether there are better ways to accomplish your goals.

Leave a Reply

Your email address will not be published. Required fields are marked *