Exam website that ships with an editor and a terminal that can run C or Python code completely offline using WebAssembly.
- Examide
- Table of Contents
- Getting Started
- Structure
- Adding custom header files to C
- Create custom wasm32-wasi library
- Packaging Python files in stdlib
- Acknowledgements
Simply clone the project:
git clone https://github.com/uvapl/examide && cd examide
You need some kind of tool that serves static files in order for everything to
work, as it can't be run in file:https:///path/to/index.html
.
One example is to use Python's http module: python3 -m http.server
, then open
localhost:8000
in your browser and enjoy.
.
├── index.html
└── static/
├── css/
│ ├── main.css # Includes custom css from include/
│ ├── include/ # Custom app CSS
│ └── vendor/ # Third-party CSS
├── img/ # App images, i.e. icons
├── js/
│ ├── constants.js # Global app constants
│ ├── helpers.js # Global app helper functions
│ ├── layout-components.js # Main layout and component classes
│ ├── main.js # Bootstraps the app, contains most logic
│ ├── vendor/ # Third-party javascript files
│ ├── worker-api.js # Bridge between app and other workers
│ └── workers/ # Language specific workers that compiles and runs the code
└── wasm # WASM files grouped per lang, loaded by corresponding worker
The static/wasm/sysroot.tar
contains C++ standard headers and libraries.
Inspecting which header files are included can be done through
tar -tf static/wasm/sysroot.tar
Including another header .h
file can be done by extracting the tar, adding the
header and making a new tar. For example:
cd ./static/wasm/c_cpp/
make sure you're in this foldertar -xvf sysroot.tar
extract tarcp my-header-file.h ./include/
add custommy-header-file.h
to theinclude/
foldertar --format ustar -cvf sysroot.tar include lib share
make a new tarrm -rf include lib share
remove previously extracted folders
This project contains a custom wasm32-wasi
cs50 build. This project currently uses
wasi-sdk-5,
which provides all the necessary functionality, despite being quite an outdated
version.
To create a custom library archive .a
file yourself, download the
corresponding wasi-sdk-5.0-<PLATFORM>.tar.gz
from
here for your
own platform. Then, extract this, for example, in your ~/Downloads
folder.
In order to compile to wasm32-wasi
, we need to use
~/Downloads/wasi-sdk-5.0/opt/wasi-sdk/bin/clang
. Consecutively we need to use
~/Downloads/wasi-sdk-5.0/opt/wasi-sdk/bin/llvm-ar
to create the archive .a
file.
Start off with cd ./static/wasm/c_cpp/
and extract the sysroot.tar
through
tar -xvf sysroot.tar
. This will extract 3 folder, namely include
, lib
and
share
.
Inside ./static/wasm/c_cpp/
, create a temporary folder for your library. For
the sake of this example, we'll call our library libfoo
which we can later
link through -lfoo
. We'll create it through: mkdir libfoo
.
Next, inside our libfoo/
folder we have 3 files: foo.c
, foo.h
and
Makefile
, with the following contents:
foo.c
#include <stdio.h>
#include "foo.h"
void say_hello() {
printf("Hello from foo!\n");
}
foo.h
#ifndef FOO_H
#define FOO_H
void say_hello();
#endif /* FOO_H */
Makefile
CC = /Users/<USER>/Downloads/wasi-sdk-5.0/opt/wasi-sdk/bin/clang --sysroot=/Users/<USER>/Downloads/wasi-sdk-5.0/opt/wasi-sdk/share/sysroot
AR = /Users/<USER>/Downloads/wasi-sdk-5.0/opt/wasi-sdk/bin/llvm-ar
CFLAGS = -Wall -Wextra -I/Users/<USER>/Downloads/wasi-sdk-5.0/opt/wasi-sdk/share/sysroot/include
all: libfoo.a
libfoo.a: foo.o
$(AR) rcs $@ $^
foo.o: foo.c foo.h
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f libfoo.a foo.o
The CFLAGS
contains
-I/Users/<USER>/Downloads/wasi-sdk-5.0/opt/wasi-sdk/share/sysroot/include
which is needed because foo.c
includes <stdio.h>
. Since we compile to
wasm32-wasi
, we need to include the wasm32-wasi
header files from the
wasi-sdk.
When running make
, you should have the libfoo.a
.
Next, we go one folder up through cd ..
and then copy the libfoo.a
and
foo.h
to the corresponding folder of the sysroot.tar
we extracted earlier:
$ cp ./libfoo/foo.h ./include
$ cp ./libfoo/libfoo.a ./lib/wasm32-wasi/
Next, we create a new tar:
rm sysroot.tar && tar -cvf sysroot.tar include lib share
Go into your browser settings, clear the cached web content and then finally you
can go into the ./static/js/workers/clang.worker.js
and add -lfoo
to the
this.ldflags
inside the constructor:
class API extends BaseAPI {
constructor(options) {
// ...
this.ldflags = ['-lc', '-lcs50', '-lfoo'];
}
}
The ./static/wasm/py/python_stdlib.zip
contains all the default python modules
that pyodide ships. If you want to import other modules from pypi then it is
recommended to use the ./static/wasm/py/custom_stdlib.zip
.
Let's say you want to import mypy
, then you should do the following:
- Locally
cd
into./static/wasm/py/
- Run
unzip custom_stdlib.zip -d stdlib
to extract the files into astdlib
directory - Run
pip3 install -t . mypy
to installmypy
and all its dependencies in the current directory - Run
rm -rf *.so __pycache__ **/__pycache__ bin
to remove unnecessary files - Run
rm ../custom_stdlib.zip && zip -vr ../custom_stdlib.zip .
to create a new zip - Run
cd .. && rm -rf stdlib
to remove the folder we just created
It might be that the contents are cached. In that case, clear your browser cache through the settings.
When refreshing the page, you should be able to import the module directly inside the front-end editor. However, there might be dependencies. If you have this error:
PythonError: Traceback (most recent call last):
File "/lib/python311.zip/_pyodide/_base.py", line 499, in eval_code
.run(globals, locals)
^^^^^^^^^^^^^^^^^^^^
File "/lib/python311.zip/_pyodide/_base.py", line 340, in run
coroutine = eval(self.code, globals, locals)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<exec>", line 2, in <module>
ModuleNotFoundError: The module '<MODULE_NAME_HERE>' is included in the Pyodide distribution, but it is not installed.
You can install it by calling:
await micropip.install("<MODULE_NAME_HERE>") in Python, or
await pyodide.loadPackage("<MODULE_NAME_HERE>") in JavaScript
See https://pyodide.org/en/stable/usage/loading-packages.html for more details.
Then check what <MODULE_NAME_HERE>
is and repeat the packaging steps described
above for that package as well, until all dependencies are resolved.
Thanks to wasm-clang for the amazing C/C++ in WASM implementation.