Have you ever experienced the pain of porting legacy code in C/C++ to JavaScript? Have you ever wished you could reuse your legacy application or have near-native performance in your web app?
If your answer to any of the questions above is yes, WebAssembly can help!
According to the documentation:
It is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++ and Rust with a compilation target so that they can run on the web.
What this basically means is, WebAssembly provides a way to convert our C/C++ code to a particular format which can then be easily consumed.
WebAssembly is like a new language but you, as a developer, don’t have to write it because it acts as a compilation target for your low-level languages like C/C++, or Rust.
So, when you have your C or C++ application, you can compile it to a .wasm file which then can be imported into your web application.
WebAssembly even allows you to pass and access data/variables to and from your JS app.
This article here explains in detail how WebAssembly works.
So now that we have seen what WebAssembly can do, let us create our first game to see how it works.
We will be building a 3Ă—3 tic-tac-toe game. Our game logic includes:
The entire game logic is written in C++.
As I mentioned before, we need to convert our C++ application in an intermediate format — which is .wasm. To do this, we need a tool that compiles our C++ file to this format.
There are a lot of options to doing this, however, for this post, I will be using Emscripten. The reasons for that are because:
Make sure you install Emscripten before you start coding. You can learn more about Emscripten here.
Before we start, let us first break down our game into manageable chunks:
The final game code can be found here.
The logic for the game is written in the tic_tac_toe.cpp file.
To begin with, we first import all the necessary Emscripten libraries:
#include <emscripten/bind.h> #include <emscripten/val.h>
The code that is shown above imports Embind which is used to bind our C++ code with JavaScript (i.e we can now call and manipulate our JavaScript variables within our C++ code).
Next, we move on to our tic_tac_toe() which includes the main logic for our game:
val tic_tac_toe() { val board_values = val::global("BoardValues"); val moves_pending_label = val::global("movesPendingLabel"); val no_winner_label = val::global("noWinnerLabel"); val empty_block = val::global("emptyBlock"); bool moves_pending = false; val solutions[8][3]= { { board_values[0][0], board_values[0][1], board_values[0][2]}, { board_values[1][0], board_values[1][1], board_values[1][2]}, { board_values[2][0], board_values[2][1], board_values[2][2]}, { board_values[0][0], board_values[1][0], board_values[2][0]}, { board_values[0][1], board_values[1][1], board_values[2][1]}, { board_values[0][2], board_values[1][2], board_values[2][2]}, { board_values[0][0], board_values[1][1], board_values[2][2]}, { board_values[0][2], board_values[1][1], board_values[2][0]}, }; for ( int i = 0; i < 8; i++ ){ if((solutions[i][0] != empty_block) && (solutions[i][1] != empty_block) && (solutions[i][2] != empty_block)&& (solutions[i][0] == solutions[i][1]) && ( solutions[i][1] == solutions[i][2] )) { return solutions[i][1]; } else if((solutions[i][0] == empty_block) || (solutions[i][1] == empty_block) || (solutions[i][2] == empty_block)){ moves_pending = true; } } if (moves_pending) { return moves_pending_label; } return no_winner_label; }
Our function signature val tic_tac_toe()
states that the value returned by our function is a JavaScript variable.
All the initial values are declared within our JavaScript, now we need a way to access these initial values and manipulate them.
Lucky for us, Embind provides a way to do this using:
val cpp_var_name = val::global("js_var_name");
Using this we will import all our necessary labels, game board values, and game state variables:
val board_values = val::global("BoardValues"); // Object representing our 3*3 board val moves_pending_label = val::global("movesPendingLabel"); // Label returned if there are any pending moves/blocks remaining val no_winner_label = val::global("noWinnerLabel"); // Label indicating who won val empty_block = val::global("emptyBlock"); // Indicator for an empty cell on board bool moves_pending = false; // State variable indicating if any blocks are pending
Now our next step is to create a solutions matrix indicating all our possible solutions. val solutions[8][3]
, is a 8*3 array representing all of our possible solutions combinations.
Note: There might be other ways to implement the solution for a tic-tac-toe game, however, for simplicity, we will go ahead with a list of all possible solutions and cross-validate that with our current board values to check if any single player has won.
Now, once we have all our possible solutions in place, we will compare our current board values with these solutions to see if any one player has won.
If our current board value matches any of our solutions then we return that cell value, which represents one of the players. However, if no matches are found and if there are any empty cells on the board then the moves_pending_label
is returned otherwise no_winner_label
is returned.
Now, let’s export this function so that we can call it in our web app:
EMSCRIPTEN_BINDINGS(my_module) { function("tic_tac_toe", &tic_tac_toe); }
The above code allows us to now call the function using tic_tac_toe()
. This block runs when our glue code .js file is initially loaded.
Once our .cpp file is ready, the next step is to transpile the file to .wasm file and .js file which can be used in our web app:
emcc --bind -o tic_tac_toe.js tic_tac_toe.cpp
The above command transpiles our tic_tac_toe.cpp file to two new files namely tic_tac_toe.wasm and tic_tac_toe.js. The tic_tac_toe.js file includes all the glue code necessary for loading our C++ function and exposing it as a module that can then be imported.
All the code discussed below can be found here. Now that we have completely transpiled our .cpp file, it’s time for us to prepare our JavaScript code which will call our tic_tac_toe().
The first step is to declare the variables which hold the initial values of the board as discussed earlier:
var currentBoardValues = [] var BoardValues = {0:[],1:[],2:[]} var movesPendingLabel = 'Moves still pending!'; var emptyBlock = ''; var noWinnerLabel = 'Oops! Looks like no one won :('
Next,
var Module = { onRuntimeInitialized: function() { Module.tic_tac_toe() } };
You remember our tic_tac_toe() C++ function which was exposed by Embind, that is now available on the Emscripten module object.
However, we can only call our tic_tac_toe() once it’s fully loaded i.e, it’s runtime(.js glue code and .wasm file) is initialized. For this, we use onRuntimeInitialized
callback which will run when the runtime is ready.
Now whenever any player clicks on any cell we call our C++ function as Module.tic_tac_toe()
which will return the appropriate results.
And voila, our first game in WebAssembly is ready! You can check out the final game code here.
WebAssembly is truly remarkable and has indeed allowed us to do things that were previously impossible. I hope this post helps you take the first step on your WebAssembly journey.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.