Skip to content

Instantly share code, notes, and snippets.

@kripken
Last active December 30, 2022 06:54
Show Gist options
  • Save kripken/cbffda150b4e1583bdad832d070944da to your computer and use it in GitHub Desktop.
Save kripken/cbffda150b4e1583bdad832d070944da to your computer and use it in GitHub Desktop.

Sizes

(html+js+wasm)

Non-emscripten: 19,964 bytes
Emscripten:     11,086 bytes

The biggest factor here is that the emscripten version uses emscripten's standard minification features (like closure compiler, --low-memory-unused, etc.) which the other one doesn't (it was minified using Uglify3 as mentioned in that post).

Please let me know if I missed something or got anything wrong!

Building

Notes:

  • The non-emscripten version uses JS to do Math.asin etc., which is smaller than emscripten's default behavior which is to use compiled musl code, which is faster. For a more apples-to-apples comparison, the emscripten version sets JS_MATH which does something similar. This makes both versions about 25% smaller, but also both suffer a speed penalty because of this, so it's not recommended in general.

Summary

Emscripten aims to provide a simple interface for users (at the cost of internal complexity inside the tool), and to integrate the most powerful minification and optimization tools. So there are tradeoffs to using it versus a more barebones approach like in that post.

It's good we have options! And that post does a good job of explaining the technical details of one type of approach. We just need to be aware of the tradeoffs.

In this case, it's less work to set up the emscripten version (everything in a single compile command on one line; no need for the 100+ line Makefile), and it's smaller (and likely faster, see the note above on musl vs JS). On the other hand, the barebones approach gives you more control, and if you put in enough work, you can get interesting results for specific use cases.

/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
*/
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#define GL_GLEXT_PROTOTYPES
#define EGL_EGLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GLES2/gl2.h>
#include <math.h>
static const char* vertex_shader_text =
"precision lowp float;"
"uniform mat4 uMVP;"
"attribute vec4 aPos;"
"attribute vec3 aCol;"
"varying vec3 vCol;"
"void main()"
"{"
"vCol = aCol;"
"gl_Position = uMVP * aPos;"
"}";
static const char* fragment_shader_text =
"precision lowp float;"
"varying vec3 vCol;"
"void main()"
"{"
"gl_FragColor = vec4(vCol, 1.0);"
"}";
typedef struct Vertex { float x, y, r, g, b; } Vertex;
static GLuint program, vertex_buffer;
static GLint uMVP_location, aPos_location, aCol_location;
int WAFNDraw(double f, void*);
// This function is called at startup
int main(int argc, char *argv[])
{
emscripten_set_canvas_element_size("canvas", 640, 480);
EmscriptenWebGLContextAttributes attrs;
emscripten_webgl_init_context_attributes(&attrs);
attrs.alpha = 0;
auto glContext = emscripten_webgl_create_context("canvas", &attrs);
emscripten_webgl_make_context_current(glContext);
glViewport(0, 0, 640, 480);
GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
glCompileShader(vertex_shader);
GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
glCompileShader(fragment_shader);
program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
uMVP_location = glGetUniformLocation(program, "uMVP");
aPos_location = glGetAttribLocation(program, "aPos");
aCol_location = glGetAttribLocation(program, "aCol");
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glEnableVertexAttribArray(aPos_location);
glVertexAttribPointer(aPos_location, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
glEnableVertexAttribArray(aCol_location);
glVertexAttribPointer(aCol_location, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(float) * 2));
emscripten_request_animation_frame_loop(&WAFNDraw, 0);
return 0;
}
// This function is called by loader.js every frame
int WAFNDraw(double f, void*)
{
f /= 1000.0;
glClear(GL_COLOR_BUFFER_BIT);
Vertex vertices[3] =
{
{ -0.6f, -0.4f, 1.f, 0.f, 0.f },
{ 0.6f, -0.4f, 0.f, 0.f, 1.f },
{ 0.f, 0.6f, 1.f, 1.f, 1.f },
};
vertices[0].r = 0.5f + sinf(f * 3.14159f * 2.0f) * 0.5f;
vertices[1].b = 0.5f + cosf(f * 3.14159f * 2.0f) * 0.5f;
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GLfloat mvp[4*4] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1 };
glUseProgram(program);
glUniformMatrix4fv(uMVP_location, 1, GL_FALSE, mvp);
glDrawArrays(GL_TRIANGLES, 0, 3);
return EM_TRUE;
}
<!doctypehtml><html lang=en-us><head><meta charset=utf-8></head><body><canvas style=display:block;margin:auto></canvas><script>var Module={};function binary(e){return new Promise((n,r)=>{var a=new XMLHttpRequest;a.open("GET",e,!0),a.responseType="arraybuffer",a.onload=()=>{n(a.response)},a.send(null)})}function script(e){return new Promise((n,r)=>{var a=document.createElement("script");a.src=e,a.onload=()=>{n()},document.body.appendChild(a)})}Promise.all([binary("main_ems.js"),binary("main_ems.wasm")]).then(e=>{Module.wasm=e[1];var n=URL.createObjectURL(new Blob([e[0]],{type:"application/javascript"}));script(n).then(()=>{URL.revokeObjectURL(n)})})</script></body></html>
if(typeof Module==="undefined"){var Module={};}
var h;h||(h=Module);var k="function"===typeof read;if("object"===typeof process){var fs=require("fs");h.wasm=fs.readFileSync(__dirname+"/main_ems.wasm")}k&&(h.wasm=read("main_ems.wasm","binary"));var l="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;
function m(a,b){var c=n,d=a+b;for(b=a;c[b]&&!(b>=d);)++b;if(16<b-a&&c.subarray&&l)return l.decode(c.subarray(a,b));for(d="";a<b;){var e=c[a++];if(e&128){var f=c[a++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|f);else{var g=c[a++]&63;e=224==(e&240)?(e&15)<<12|f<<6|g:(e&7)<<18|f<<12|g<<6|c[a++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}
var p=new WebAssembly.Memory({initial:256,maximum:256}),r=p.buffer,t=new WebAssembly.Table({initial:2,maximum:2,element:"anyfunc"});new Int8Array(r);new Int16Array(r);var u=new Int32Array(r),n=new Uint8Array(r);new Uint16Array(r);new Uint32Array(r);var v=new Float32Array(r);new Float64Array(r);u[808]=5246272;function w(a){return a===a+0?a?m(a,void 0):"":a}var x=[0,"undefined"!==typeof document?document:0,"undefined"!==typeof window?window:0];
function z(a){return x[a]||("undefined"!==typeof document?document.querySelector(w(a)):void 0)}function A(a){var b=a.getExtension("ANGLE_instanced_arrays");b&&(a.vertexAttribDivisor=function(c,d){b.vertexAttribDivisorANGLE(c,d)},a.drawArraysInstanced=function(c,d,e,f){b.drawArraysInstancedANGLE(c,d,e,f)},a.drawElementsInstanced=function(c,d,e,f,g){b.drawElementsInstancedANGLE(c,d,e,f,g)})}
function B(a){var b=a.getExtension("OES_vertex_array_object");b&&(a.createVertexArray=function(){return b.createVertexArrayOES()},a.deleteVertexArray=function(c){b.deleteVertexArrayOES(c)},a.bindVertexArray=function(c){b.bindVertexArrayOES(c)},a.isVertexArray=function(c){return b.isVertexArrayOES(c)})}function D(a){var b=a.getExtension("WEBGL_draw_buffers");b&&(a.drawBuffers=function(c,d){b.drawBuffersWEBGL(c,d)})}var E=1,F=0,G=[],H=[],I=[],J=[],K=[],L=null,M={};
function N(a){for(var b=E++,c=a.length;c<b;c++)a[c]=null;return b}var O=[0];
function P(a){a||(a=L);if(!a.H){a.H=!0;var b=a.B;2>a.version&&(A(b),B(b),D(b));b.L=b.getExtension("EXT_disjoint_timer_query");var c="OES_texture_float OES_texture_half_float OES_standard_derivatives OES_vertex_array_object WEBGL_compressed_texture_s3tc WEBGL_depth_texture OES_element_index_uint EXT_texture_filter_anisotropic EXT_frag_depth WEBGL_draw_buffers ANGLE_instanced_arrays OES_texture_float_linear OES_texture_half_float_linear EXT_blend_minmax EXT_shader_texture_lod EXT_texture_norm16 WEBGL_compressed_texture_pvrtc EXT_color_buffer_half_float WEBGL_color_buffer_float EXT_sRGB WEBGL_compressed_texture_etc1 EXT_disjoint_timer_query WEBGL_compressed_texture_etc WEBGL_compressed_texture_astc EXT_color_buffer_float WEBGL_compressed_texture_s3tc_srgb EXT_disjoint_timer_query_webgl2 WEBKIT_WEBGL_compressed_texture_pvrtc".split(" ");(b.getSupportedExtensions()||
[]).forEach(function(d){-1!=c.indexOf(d)&&b.getExtension(d)})}}for(var Q=["default","low-power","high-performance"],R,S=new Float32Array(256),T=0;256>T;T++)O[T]=S.subarray(0,T+1);for(T=0;256>T;T++);var U,V;
WebAssembly.instantiate(h.wasm,{a:{m:function(a){return Math.cos(a)},l:function(a){return Math.sin(a)},t:function(a,b){function c(d){V(a,d,b)&&requestAnimationFrame(c)}return requestAnimationFrame(c)},y:function(a,b,c){a=z(a);if(!a)return-4;a.width=b;a.height=c;return 0},q:function(a,b){var c={};b>>=2;c.alpha=!!u[b];c.depth=!!u[b+1];c.stencil=!!u[b+2];c.antialias=!!u[b+3];c.premultipliedAlpha=!!u[b+4];c.preserveDrawingBuffer=!!u[b+5];c.powerPreference=Q[u[b+6]];c.failIfMajorPerformanceCaveat=!!u[b+
7];c.I=u[b+8];c.P=u[b+9];c.C=u[b+10];c.G=u[b+11];c.R=u[b+12];c.S=u[b+13];a=z(a);if(!a||c.G)c=0;else if(a=a.getContext("webgl",c)){b=N(K);var d={M:b,attributes:c,version:c.I,B:a};a.canvas&&(a.canvas.J=d);K[b]=d;("undefined"===typeof c.C||c.C)&&P(d);c=b}else c=0;return c},x:function(a){a>>=2;for(var b=0;14>b;++b)u[a+b]=0;u[a]=u[a+1]=u[a+3]=u[a+4]=u[a+8]=u[a+10]=1},k:function(a){L=K[a];h.K=R=L&&L.B;return!a||R?0:-5},a:function(a,b){R.attachShader(H[a],J[b])},g:function(a,b){R.bindBuffer(a,G[b])},r:function(a,
b,c,d){R.bufferData(a,c?n.subarray(c,c+b):b,d)},s:function(a){R.clear(a)},b:function(a){R.compileShader(J[a])},i:function(){var a=N(H),b=R.createProgram();b.name=a;H[a]=b;return a},d:function(a){var b=N(J);J[b]=R.createShader(a);return b},n:function(a,b,c){R.drawArrays(a,b,c)},f:function(a){R.enableVertexAttribArray(a)},u:function(a,b){for(var c=0;c<a;c++){var d=R.createBuffer(),e=d&&N(G);d?(d.name=e,G[e]=d):F||(F=1282);u[b+4*c>>2]=e}},h:function(a,b){return R.getAttribLocation(H[a],b?m(b,void 0):
"")},v:function(a,b){b=b?m(b,void 0):"";var c=0;if("]"==b[b.length-1]){var d=b.lastIndexOf("[");c="]"!=b[d+1]?parseInt(b.slice(d+1)):0;b=b.slice(0,d)}return(a=M[a]&&M[a].F[b])&&0<=c&&c<a[0]?a[1]+c:-1},w:function(a){R.linkProgram(H[a]);var b=H[a];a=M[a]={F:{},D:0,N:-1,O:-1};for(var c=a.F,d=R.getProgramParameter(b,35718),e=0;e<d;++e){var f=R.getActiveUniform(b,e),g=f.name;a.D=Math.max(a.D,g.length+1);"]"==g.slice(-1)&&(g=g.slice(0,g.lastIndexOf("[")));var q=R.getUniformLocation(b,g);if(q){var y=N(I);
c[g]=[f.size,y];I[y]=q;for(var C=1;C<f.size;++C)q=R.getUniformLocation(b,g+"["+C+"]"),y=N(I),I[y]=q}}},c:function(a,b,c,d){for(var e="",f=0;f<b;++f){var g=d?u[d+4*f>>2]:-1,q=u[c+4*f>>2];g=q?m(q,0>g?void 0:g):"";e+=g}R.shaderSource(J[a],e)},o:function(a,b,c,d){if(256>=16*b){var e=O[16*b-1];d>>=2;for(var f=0;f<16*b;f+=16){var g=d+f;e[f]=v[g];e[f+1]=v[g+1];e[f+2]=v[g+2];e[f+3]=v[g+3];e[f+4]=v[g+4];e[f+5]=v[g+5];e[f+6]=v[g+6];e[f+7]=v[g+7];e[f+8]=v[g+8];e[f+9]=v[g+9];e[f+10]=v[g+10];e[f+11]=v[g+11];e[f+
12]=v[g+12];e[f+13]=v[g+13];e[f+14]=v[g+14];e[f+15]=v[g+15]}}else e=v.subarray(d>>2,d+64*b>>2);R.uniformMatrix4fv(I[a],!!c,e)},p:function(a){R.useProgram(H[a])},e:function(a,b,c,d,e,f){R.vertexAttribPointer(a,b,c,!!d,e,f)},j:function(a,b,c,d){R.viewport(a,b,c,d)},memory:p,table:t}}).then(function(a){a=a.instance.exports;U=a.A;V=a.ya;a.z();U()});
@schellingb
Copy link

Here's some of the output that I build for where WASM is one of the build targets:

  • Games (File size mentioned is gzipped including all assets)
  • Samples (55kb ~ 170kb gzipped with assets).

These are 40 programs that I build each for 10+ targets (WASM, Emscripten (deprecated), NaCl (deprecated), Win32, Win64, Linux32, Linux64, macOS, iOS and Android (up to 4 separate builds for 4 ABIs). So compiling 400 times, linking 400 times, collecting assets and packing gzips/ZIPs/APKs/etc 400 times. And I do that all automated in a few minutes.
Yes those are not giant commercial games, just small things made by me in my spare time. But so are most things on the web.

Now until I switched to building without Emscripten, maintenance of the web target was a big pain. Builds were slow, modifying the JavaScript library meant I had to rerun emcc and updating Emscripten was never smooth (yes I'm on Windows). It also never ran without flaws (scaling the web site resized the canvas incorrectly, fullscreen barely worked, etc.) thus I also deployed NaCl builds for Chrome (never had any problems with both GL rendering and audio output there). Sure web standards and browser support grew along the way, too. But so did WebAssembly.

I still stand by that I think Emscripten should be just one tool not THE tool for running C/C++ on the web. I hoped that with llvm officially gaining support for wasm that the part that Emscripten does (generating the layer between browser APIs and the wasm) would be an optional step. Maybe as one of the Binaryen executables.
And it basically is (the point of my blog post)! Yet the public at large doesn't seem to realize/know that.

Also I think the source code for the SDL2, glfw, OpenAL, etc. JavaScript libraries should by now be in the respective projects source repositories and not in Emscripten's. It feels like this closed platform where these few libraries are chosen to be the supported (and maintained) ones without giving others a clear path how to embrace the web platform. Shoutouts to the ones that still do like the sokol headers by @floooh.

Btw. that last link 'greetings' is 3.7 MB 😉

@kripken
Copy link
Author

kripken commented Apr 6, 2020

@juj

It is probably also lacking in documentation, which is perhaps the root cause why these kinds of articles keep coming up? I think we will need to improve on that part.

Good point, I agree. How about working together on a blogpost about MINIMAL_RUNTIME and related things? I think that could be very useful!

@schellingb

Also I think the source code for the SDL2, glfw, OpenAL, etc. JavaScript libraries should by now be in the respective projects source repositories and not in Emscripten's.

I think that's reasonable. Emscripten support for SDL2 has already been upstreamed into the Emscripten backend there, and others do similar things. It really just depends on volunteers stepping up to do it for the rest.

edit: if you want a non-Emscripten upstreamed version, then there's no obvious way to do that: You need a way to integrate JS and wasm in a way that's convenient for those projects to use (like a C API such as emscripten_*, EM_ASM/EM_JS, etc.). Perhaps in time wasi and/or interface types will help here, but those are far off.

It feels like this closed platform where these few libraries are chosen to be the supported (and maintained) ones without giving others a clear path how to embrace the web platform.

I don't think that's accurate at all! First, again, some already do. Second, the documented path is to use the emscripten_* APIs, as SDL2 and those others do (or, you can also use EM_ASM/EM_JS).

Btw. that last link 'greetings' is 3.7 MB 😉

No, I think @juj is right: JS is 6K, wasm is 24K. You may be measuring it wrong if you see MBs there.

@schellingb
Copy link

schellingb commented Apr 7, 2020

That's why I added the wink, I'm aware of the size of just the .wasm file. The original sentence was "with music, WebGL sprites and text, and audio." which is the size number I quoted.
Though I think that's important to consider. Why really bother making the WASM as small as 24 KB when you have two PNGs at 97 KB that could be ran through zopflipng and get 56 KB out (lossless recompression with zero impact in image quality). Also together with the 3.6 MB MP3 song that could use a more modern compression this makes a rather bad "hey look at how small this is" example.

@kripken
Copy link
Author

kripken commented Apr 7, 2020

Obviously the other things could be compressed better, but I think you are interpreting @juj's example in the least charitable way. The context here is of the size of JS+wasm.

But yes, sure, in some cases art assets are much bigger than the JS+wasm, and their size hardly matters. But in others it's the reverse. In emscripten we focus so much on code size because sometimes it really, really matters, even if not always.

@floooh
Copy link

floooh commented Apr 9, 2020

Just a little thought because I've been also thinking a lot about an "emscripten free" mode for my cross-platform headers:

Such a "freestanding mode" can be useful for integration with other languages. For instance, Zig has the ability to directly include C headers, and directly compile C code though the same Zig compiler (also to WASM), and it also has the entire build system integrated right into the compiler, no cmake or similar.

In such an environment, mixing C and Zig feels completely natural, but this convenience falls apart when an external build tool must be integrated.

This sort of problem isn't limited to the emscripten SDK, it's more like a general cross-platform problem. For instance macOS/iOS APIs are mostly Objective-C, which creates a similar set of problems like the Web-API and Javascript interop, or on Android where C <=> Java interop is needed (and TBH, emscripten provides the most convenient solution of those, EM_JS() is simply brilliant). But there are valid scenarios where not depending on the emscripten SDK makes a lot of sense.

The one small critique I have with the current emscripten SDK is that it is such a "hodge podge" of tooling technologies. There's python, node.js, Java and native tools. Thankfully that's all hidden under "emcc", but I wonder how much overhead is required to keep it all running. It "feels" a bit brittle :)

@kripken
Copy link
Author

kripken commented Apr 9, 2020

But there are valid scenarios where not depending on the emscripten SDK makes a lot of sense.

I definitely agree!

Specifically, if you need little Web API integration, then emscripten is probably not necessary - but may still be useful as it gives good default optimizations, otherwise you need to integrate with lld and binaryen manually, and it's easy to miss important optimizations (like --low-memory-unused). But if you do need significant JS or Web API stuff, or you need stuff like files, pthreads, exceptions, asyncify, etc. etc., then that's what emscripten is for.

The one small critique I have with the current emscripten SDK is that it is such a "hodge podge" of tooling technologies. There's python, node.js, Java and native tools. Thankfully that's all hidden under "emcc", but I wonder how much overhead is required to keep it all running. It "feels" a bit brittle :)

I would prefer to have fewer as well. However, it doesn't take much overhead, and isn't brittle (at least I see no signs of that? the one annoyance I can think of is python issues with certificates, but not sure another language would avoid those...). The one thing we could in theory remove is the python code, and rewrite that for node or for native code. But I'm not sure that would be worth it as the benefit would mainly be just that things feel simpler. If someone is interested though, it's worth talking about.

Java, btw, is no longer necessary. The one thing we used Java for was the closure compiler. As we use that from node now, Java isn't normally needed (on most platforms the closure package will install a native executable, which I believe is AOT compiled Java).

Otherwise, we need those things:

  • Node.js is necessary for JS parsing and optimization. We use acorn and other things. It would be very hard to do JS integration without those!
  • Native tools are necessary since without LLVM, clang, lld, and binaryen we would be completely lost!

@floooh
Copy link

floooh commented Apr 9, 2020

Java, btw, is no longer necessary.

That is most excellent news :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment