brainbaking/content/post/2021/04/stop-limiting-yourself-to-j...

9.8 KiB

title subtitle date tags categories
Stop limiting yourself to JS in browsers Did you know that almost anything compiles to JavaScript? 2021-04-21
go
C++
javascript
programming

While sniffling through various documentation records, trying my best in understanding the ramifications of LLVM's license model compared to the older GCC toolchain, I dare to say that I actually found something interesting instead of falling asleep in-between the countless and needless pro or contra Richard Stallman debates1. Here's what I found:

Emscripten. Instead of calling it a day, let's take a closer look. Emscripten is a complete compiler toolchain that targets WebAssembly (and JavaScript), and is able to compile any language that uses LLVM - to work in browsers, Node.js, or wasm runtimes. Hold on, what where how? Okay, let's rephrase that. It acts as a drop-in replacement for gcc, just like clang does for GNU's gcc itself. Instead of outputting native code, it spews out a wasm file - wrapped in js or html if you prefer. That means it's possible to compile C and C++ code... To... JS?

C++ and JavaScript, sitting in a tree...

Most of the time, when I stumble on GitHub projects of that scale, they're in pre-alpha state and end up going nowhere. Yet, Emscripten is surprisingly robust and easy to use. It really is a "drop-in" replacement. Open your Makefile, locate CC=g++, replace it with CC=em++, and call it a day. It interprets popular gcc flags, just like clang does with GNU/gcc. For instance, adding optimization flag -O3 results in a longer compile time, but the binary size was halved, and the execution speed increased. Even "native" threading support (-lpthread) or C++14 constructs (-std=c++14) work.

An example might do wonders. For a system C course at our faculty, we introduce students to the concept of unit testing using Google Test. GTest is C++ code. You first compile the library yourself, and then statically link to its main executer. Your Makefile might end up looking like this:

compile:
    $(CC) -std=c++11 -O3 -I$(GTEST_DIR)/include -c cpp-testing-main.cpp 
    $(CC) -std=c++11 -O3 -I$(GTEST_DIR)/include -c cpp-testing.cpp 
link:
    $(CC) -std=c++11 -O3 cpp-testing-main.o cpp-testing.o $(GTEST_DIR)/build/lib/libgtest.a $(GTEST_DIR)/build/lib/libgtest_main.a -lpthread -o mytests

Building the .a files does not require a lot of work, as GTest comes with the needed CMake script files. Simply calling upon Emscripten's C++ compiler in the above Makefile will not be enough, as it will compile, but not link successfully. For that, we also need to compile GTest with Emscripten. Luckily, the toolchain provides convenient wrappers that automagically replaces the necessary things after CMake is done generating:

emcmake cmake ../
make

That was surprisingly simple. I did hold my breath. It was not needed. Now we can proceed to link the em-native libs. Using -o mytests.html creates a simple document that bootstraps the needed JS and WebAssembly. Suddenly, I find myself executing students' Google Tests in the browser:

But that's not all, the real fun starts once you take your time to look at the Emscripten documentation and realize its potential. For instance, Embind makes it possible to expose C++ classes and functions to native JS code. C++. C interop is usually "easy enough", but C++ interop is notoriously difficult to get right thanks to the name mangling after assembling. This complicated thing works:

#include <string>
#include <vector>
#include <emscripten/bind.h>

class Itm {
private:
    std::string itm;
public:
    Itm(std::string i) : itm(i) {}
    inline std::string unwrap() { return itm; }
};
class Wonky {
private:
    std::vector<std::unique_ptr<Itm> > items;
public:
    Wonky() {}
    inline void add(std::string s) {
        items.push_back(std::unique_ptr<Itm>(new Itm(s)));
    }
    inline std::string print() {
            std::string res;
            for(auto& itm : items) {
                res = res + " " + itm->unwrap();
            }
            return res;
    }
};
using namespace emscripten;
// Binding code
EMSCRIPTEN_BINDINGS(wonky) {
  class_<Wonky>("Wonky")
    .constructor<>()
    .function("add", &Wonky::add)
    .function("print", &Wonky::print);
}
// compile with em++ -std=c++11 --bind -o wonky.js wonky.cpp

I made up this silliness just to see what happens. Using the thing:

<!doctype html>
<html>
  <script>
    var Module = {
      onRuntimeInitialized: function() {
        const wonky = new Module.Wonky();
        wonky.add("good");
        wonky.add("day!");
        console.log(wonky.print()); // prints "good day!" in console.
        wonky.delete();
      }
    };
  </script>
  <script src="wonky.js"></script>
</html>

There. That begs the question: why write C++ when you can, in fact, write JS? Good point, C++ should die a painful death (Have you taken a tiny peek at the C++17/20 ISO standards? No? Keep it that way. It's horrible.) God, I miss Go. Hey, what about using Go in the browser? Check - have a "go" at fiddling with GopherJS over at the playground.

[insert language here] and JS

I'm not really sure where I'm going with this, except that Emscripten makes me excited since it provides countless of language transition opportunities. Using Emscripten, you can simply compile the compiler, as these are usually written in C. There's Lua.Space, there's a Python implementation, there's so many good stuff. Here's a list of languages that compile to JS. In fact, the list tells us that there are 20 different attempts at bringing Python code to the JS ecosystem! That brings us to the next problem: which project is still active, and which is not?

Of course, Emscripten is not the best solution when it comes to simplicity. I'm not sure if anyone Go is a very simple language, that is designed to be easily parsaeble, meaning it's fairly easy to generate basic JavaScript code. The hard part are Goroutines and other features, but even those are supported! No more promises, generators, etc. Just write plain Go - or Lua, as it comes with a similar concept called Coroutines - code.

A few other highlights, extracted from the GitHub list:

  • Opal - Ruby 💛 JavaScript
  • Rusthon - mixes many multiple languages and transpilers inside Markdown files
  • Haxe - a high-level language that compiles to multiple targets. Used before the advent of GopherJS to Compile to Go->Hax->C#
  • Nim - another language that has built-in JS output support
  • ClojureScript - a compiler for Clojure that targets JS
  • WebSharper - F#/C#-to-JavaScript: Full-stack web programming for .NET
  • Kotlin/JS - Kotlin's built-in JS support

And we didn't even touch static typing transpilers yet, such as TypeScript, Dart, Elm, Roy, PureScript, ...

Next time you plan on writing a module aimed at the browser or your Node server, consider this: if you don't want to, you don't have to stick with JavaScript...

But... is this production-ready?

That will depend on the pick you made from the list. But let's try to consider WebAssembly and Emscripten in general for a minute. I'm not sure if simply opting for wasm because it allows you to stick with your favorite language on the web is a good idea. Even though the WebAssembly security documentation states that "Each WebAssembly module executes within a sandboxed environment separated from the host runtime using fault isolation techniques", I'm more concerned with a more pressing issue: usability. To me, leaning heavy on wasm sounds like an indication that you're targeting the wrong platform. Why not create software... you know... that's not a website? As said before, the web sucks for many things, including 90% of what we nowadays call enterprise applications, and yet we still happily churn out "webapps".

A second consideration would be third party library support. Google Test compiled quite happily, and I'm glad it did, but I'm sure many libraries require more work to be 100% compatible. Quake 3.js and Doom3wasm are powered by Emscripten, but judging by the amount of commits, that wasn't a simple case of dropping in the compiler and opening a bottle of champagne.

Other lang-to-JS conversion projects also require a lot of work to be fully compliant. As stated in the GoperJS Compatibility document, the project is fully compatible with the Go language specs, but not so with the standard library, because of perhaps obvious OS-level file access and the unsafe package. This means your reliance on packages like that might be a bit of a problem.

Still, it's great to see these things exist, and they might come in handy some day!


  1. For the uninitiated, read the open letter to remove Richard M. Stallman from all leadership positions. Another open letter actively supporting him also exists. I honestly don't think it's worth reading or signing either. ↩︎