Back around the time the first iPhone was released (feeling old! 😬) I was taking a course where we built a CPU-based ray tracer in C.
If you’re not familiar with the ray tracing technique, check it out here.
What I mostly remember from the course was math, pointers, and segfaults. Oh the segfaults. By the end of the course, however, I had a decent grasp on C and that’s been a valuable skill so many times in my career and hobbies.
How the original project functioned
- You wrote an input file that describes various shapes, light sources, and attributes of the environment.
- You fed the file into the C program.
- You waited a bit (remember, we’re doing everything on the CPU in ~2007).
- You got a fancy PPM file. (PPM was a great image format due to its simplicity – we were dealing with enough!)
Wasm motivation
Recently I decided that I wanted to learn more about the inner workings of WebAssembly (Wasm) and figured this would be a great candidate project. It’s fully contained without any external dependencies, *I wrote all the code so it shouldn’t be too mysterious, and if I got it to work there would be a visual payoff.
*Feel free to judge some of the rough spots in the code – it was a long time ago!
Process
The first thing I made sure of was that I could compile the project locally the non-Wasm way. There were no hiccups there – it worked on the first try using the Makefile
. ✅
I then started reading this tutorial on converting a C project to Wasm. After installing emscripten on macOS (I used Homebrew) I decided to add a new C source file to the project and added a function that looked something like:
#include "emscripten.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return 42;
}
If I could get this to work I could at least get information from C -> JS for starters. All that it took to make this work was:
- Substituting
gcc
withemcc
in theMakefile
- Making sure I added the
EXPORTED_RUNTIME_METHODS='["cwrap"]'
compiler flag - Calling
Module.cwrap
from JS to use the function
That was pretty much it. I’m not going to go super in-depth with this blog post because I think most of it can be figured out from the source.
Next challenges
I had a bit more to go but was surprised at how easy it was to send a value from C to JS. The next items to figure out were:
- The ray tracer expected an input file and an output file, how would this work with a browser?
- We can pass integers easily, but what about the big array of pixel data when we ultimately generate an image?
- Where would our C calls to
fprintf
and its siblings go when trying to debug tostdout
andstderr
? - What about
main
– does it run?
I’ll go ahead and spoil these really quickly in their respective order:
- fmemopen saved the day by taking the input string (which is a
char *
) and providing aFILE
type in return which is an in-memory buffer stream. In other words, no massive overhauling needed although we aren’t using “real” files anymore. In addition a slight refactoring was done to the project to return an array of pixels rather than write out an image file. - From what I understand, Wasm and C can share a heap and both just need to know where to find the data via pointers. Here’s an example of sending a pointer to C, and here’s an example of how JS grabs a pointer from C. In the latter, C sets a global (gross, I know, but they used it in their examples as well) and JS calls a function to get that
int
. It then is able to initialize an array ofUInt8
s. - They automagically show up in the browser console! This is a really nice feature, and
stderr
calls are even properly displayed as errors. - Yes! In my case I got rid of it because it was prompting for the CLI input, but it was interesting to see that it automatically ran. There may be a compiler setting to disable this.
A summary of what it took to convert the ray tracer to Wasm
- Tweaked the
Makefile
to useemcc
. - Removed
main()
because I didn’t need it. - Used fmemopen to substitute a real file with a
char
array. - Refactored the project to not try to write to a file, but instead return a big array of pixels that ultimately get passed to JS to write to a Canvas.
- Expanded the
pixel
struct to include an alpha channel for the expected RGBA format. Yay for properly usingsizeof
throughout the code. - Wrote a C source file with everything we needed to interface with JS.
- Created an HTML page that calls our compiled JS and gives us access to the exposed functions.
- Created a big string using a JS template literal for our input.
That’s mostly it! Check out the GitHub repo here. 🚀