Expanding Programming Language Support in JupyterLite

QuantStack Logo

Introduction to Emscripten-Forge

What is emscripten?

  • Toolchain to compile C/C++/Fortran to WebAssembly
  • Based on LLVM and Clang
  • Runs code in browsers and Node.js
Emscripten logo

Use Cases

Emscripten can compile a wide variety of applications to WebAssembly.

DOOM

DOOM game screenshot

doom-wasm

Python

Python logo

pyodide

Use Cases

Emscripten can compile a wide variety of applications to WebAssembly.

LLVM / Clang

LLVM / Clang logo

llvm.wasm

OS Emulation

copy.sh screenshot

copy.sh

What is conda-forge?

  • Distribution for conda packages
  • Community driven
  • Language agnostic
  • Multi platform
    • Linux
    • macOS
    • Windows
    • wasm is missing!
Conda forge logo

What is emscripten-forge?

  • Distribution for conda packages
  • Specifically targets the emscripten-wasm32 platform
  • Community-driven recipes
  • Enables running C, C++, Fortran, Python, R, Lua and Octave in the browser
    and in jupyterlite
QuantStack Logo

Xeus Kernels in JupyterLite

Jupyter Architecture

                  %%{init: {'look':'handDrawn','theme': 'default', 'themeVariables': { 'darkMode': true }}}%%
                  graph LR
                    subgraph MyBox["Jupyter"]
                      
                      subgraph KernelProcess["Kernel Process"]
                          kernel[Kernel]
                      end

                      subgraph ServerProcess["Server Process"]
                          server[Server]
                      end

                      subgraph FrontendProcess["Browser"]
                          frontend[Frontend]
                      end

                      kernel -- zmq  --> server -- websockets --> frontend

                    end
                  
  • Multiple processes
  • Kernel and Server communicate via zmq
  • Server and Frontend communicate via websockets

Jupyterlite Architecture

                    %%{init: {'look':'handDrawn', 'theme': 'default', 'themeVariables': { 'darkMode': false}}}%%
                  graph LR

                  
                    subgraph MyBox["Jupyterlite"]
          
              
                      subgraph WebWorker["WebWorker"]
                          kernel2[Kernel]
                      end

                      subgraph MainThread["MainThread"]
                          server2[Server] --> frontend2[Frontend]
                      end

                      kernel2 -- postMessage --> server2
                    end

                  
  • Everything runs in the browser
  • Kernel runs in separate WebWorker
  • Communication between Kernel and Server via postMessage

Architecture Comparison

              %%{init: {'look':'handDrawn','theme': 'default', 'themeVariables': { 'darkMode': true }}}%%
              graph LR
                subgraph MyBox["Jupyter"]
                  
                  subgraph KernelProcess["Kernel Process"]
                      kernel[Kernel]
                  end

                  subgraph ServerProcess["Server Process"]
                      server[Server]
                  end

                  subgraph FrontendProcess["Browser"]
                      frontend[Frontend]
                  end

                  kernel -- zmq  --> server -- websockets --> frontend

                end
              
              %%{init: {'look':'handDrawn', 'theme': 'default', 'themeVariables': { 'darkMode': false }}}%%
              graph LR

              
                subgraph MyBox["Jupyterlite"]
       
           
                  subgraph WebWorker["WebWorker"]
                      kernel2[Kernel]
                  end

                  subgraph MainThread["MainThread"]
                      server2[Server] -- js --> frontend2[Frontend]
                  end

                  kernel2 --> server2
                end

              

The kernel:

  • most important part of Jupyter
  • executes the code
  • multiple kernels available
  • can be implemented ontop of xeus:
    • c++ library to author kernels
    • handles the Jupyter messaging protocol
    • provides abstractions for common tasks

xeus based kernels:

  • xeus-python
  • xeus-lua
  • xeus-sqlite
  • xeus-ocaml
  • xeus-javascript
  • xeus-r
  • xeus-octave
  • xeus-cpp
QuantStack Logo

Packaging R for WebAssembly

WebR

  • Emscripten
  • Fortran compiler
    • Custom development version of LLVM Flang (v18)
    • GCC and Dragonegg
webr logo - purple webassembly square with R-wa rwasm logo - truck with packages

Core Dependencies of R

C language logo
  • Required
    • libiconv
    • zlib
    • bzip2
    • lzma
    • pcre2
    • libcurl
  • Optional
    • libpng
    • cairo
    • libtiff
    • glib
Fortran language logo
  • Required
    • BLAS / LAPACK
  • R packages
    • nlme

Fortran to WebAssembly

LLVM Flang

  • Modern LLVM Integration
  • Active Development
  • LLVM backend to target WebAssembly
LLVM logo of wyvern dragon

LLVM Flang

LLVM Flang (v20)

Screenshot of flang feedstock repo on conda-forge

github.com/conda-forge/flang-feedstock/tree/emscripten


            micromamba install conda-forge/label/emscripten::flang --no-channel-priority
            

Assembling the Toolchain

Diagram of Fortran and C/C++ to WebAssembly toolchain with Emscripten and Flang

Cross-Compilation of R

  • Phase 1: Host
    • Build a minimal native version of R for Linux with GCC and Flang
    • This generates the tools required for Phase 2 (R and Rscript executables)
  • Phase 2: Target
    • Cross-compile to WebAssembly with Emscripten and Flang

Modifications to R Source Code

  • Add wasm32-unknown-emscripten as a recognized target platform
  • Use R and Rscript executables from Phase 1
  • Load the shared libraries from Phase 1
    • Required to create the R Data Base (.rdb) and R Data Index (.rdx) files
    • Package cross-r-base_emscripten-wasm32
  • Remove dependency on libcurl
    • Disable internet package
  • Set default graphics device to cairo instead of X11

[full list of patches]

r-base

recipe.yaml


                context:
                  name: r-base
                  version: 4.5.1

                package:
                  name: ${{ name }}
                  version: ${{ version }}

                source:
                  url: https://cran.r-project.org/src/base/R-4/R-${{ version }}.tar.gz
                  sha256: b42a7921400386645b10105b91c68728787db5c4c83c9f6c30acdce632e1bb70
                  patches:
                  - patches/0001-Add-emscripten-platform-to-configure-script.patch
                  - patches/0002-Disable-libcurl.patch
                  - patches/0003-Disable-internet-module.patch
                  - patches/0004-Fix-iconv-checks-when-using-internal-lapack-blas.patch
                  - patches/0005-Add-sigsuspend-stub-for-Emscripten-compatibility.patch
                  - patches/0006-Disable-which-for-emscripten.patch
                  - patches/0007-Set-cairo-as-default-bitmap-type.patch
                  - patches/0008-Use-source-files-directly.patch
                  - patches/0009-Install-.wasm-files.patch
                  - patches/0010-Fix-extern-C-error-expected-an-identifier.patch
                  - patches/0011-Remove-png-lib-from-bitmap-libs.patch
                  - patches/0012-Use-linux-executables.patch
                  - patches/0013-Use-source-files-when-installing.patch

                build:
                  number: 0

                requirements:
                  build:
                  - ${{ compiler('c') }}
                  - ${{ compiler('cxx') }}
                  - flang_emscripten-wasm32
                  - libtool
                  - pkg-config
                  - cmake
                  - make
                  # Dependencies to build R in the build environment
                  - gcc
                  - libiconv
                  - zlib
                  - bzip2
                  - xz
                  - pcre2
                  - libpng
                  - libtiff
                  - cairo
                  host:
                  - libflang
                  - libiconv
                  - zlib
                  - bzip2
                  - xz
                  - pcre2
                  - libpng
                  - libtiff
                  - cairo
                  - fontconfig
                  - pixman
                  - expat
                  - freetype

                tests:
                - package_contents:
                    lib:
                    - R/lib/libR.a
                    - R/lib/libRblas.so
                    - R/lib/libRlapack.so
                    - R/library/grDevices/libs/cairo.so
                - script:
                  - node ${PREFIX}/lib/R/bin/Rscript --version
                  requirements:
                    build:
                    - nodejs

                about:
                  homepage: http://www.r-project.org/
                  license: GPL-2.0-or-later
                  license_family: GPL
                  license_file: COPYING
                  summary: |
                    R is a free software environment for statistical computing and graphics.
              

build.sh


              #-------------------------------------------------------------------------------
              # PHASE 1
              #-------------------------------------------------------------------------------
              # Building R for Linux so that we can use the R and Rscript binaries to build
              # the R internal modules for WebAssembly.

              mkdir -p _build_linux
              pushd _build_linux
              (
                  export PKG_CONFIG_PATH=$BUILD_PREFIX/lib/pkgconfig
                  export PREFIX=$BUILD_PREFIX
                  export CC=gcc
                  export CXX=g++
                  export FC=flang-new
                  export CPPFLAGS="-I$BUILD_PREFIX/include"
                  export LDFLAGS="-L$BUILD_PREFIX/lib"
                  export FC_LEN_T=size_t
                  export LINUX_BUILD_DIR=$(pwd)

                  ../configure \
                      --prefix=$BUILD_PREFIX \
                      $CONFIG_ARGS

                  make -j${CPU_COUNT}
                  # No need to install, we just need the R binary
              )
              popd

              #-------------------------------------------------------------------------------
              # PHASE 2
              #-------------------------------------------------------------------------------
              # Building R for WebAssembly using the R binary from the Linux build.

              mkdir -p _build_wasm
              pushd _build_wasm
              (
                  cp $RECIPE_DIR/config.site .

                  export CROSS_COMPILING="true"
                  export R_EXECUTABLE=$(realpath ..)/_build_linux/bin/exec/R # binary not shell wrapper
                  export R_SCRIPT_EXECUTABLE=$(realpath ..)/_build_linux/bin/Rscript
                  export LINUX_BUILD_DIR=$(realpath ..)/_build_linux
                  export WASM_BUILD_DIR=$(pwd)

                  emconfigure ../configure \
                      --prefix=$PREFIX    \
                      --build="x86_64-conda-linux-gnu" \
                      --host="wasm32-unknown-emscripten" \
                      $CONFIG_ARGS

                  emmake make -j${CPU_COUNT}

                  $RECIPE_DIR/cross_libraries.sh --restore $(pwd)

                  emmake make install

              )
              popd
              
Cardboard box with R language logo

Expanding the R Ecosystem

  • Pure R packages, "noarch" packages, from conda-forge work instantly
  • Packages that need to be compiled require new recipes on emscripten-forge
    • r-askpass
    • r-base64enc
    • r-cachem
    • r-cli
    • r-nlme
    • r-rlang
    • ... and many more

              package:
                name: r-bit
                version: 4.6.0

              source:
                url: https://cran.r-project.org/src/contrib/bit_4.6.0.tar.gz
                sha256: 48fe21c5d04c7b724d695eeb60074395c0c631a7fb234e2075de92471445de08

              build:
                number: 0
                script: $R CMD INSTALL $R_ARGS .

              requirements:
                build:
                  - cross-r-base_emscripten-wasm32
                  - ${{ compiler('c') }}
                  - r-base
                host:
                  - r-base
                run:
                  - r-base
              

Xeus-R

Xeus-R logo
Example Jupyter notebook for Xeus-R kernel

Xeus-R

Cardboard box of xeus-r package

              package:
                name: xeus-r
                version: 0.8.1

              source:
                url: https://github.com/jupyter-xeus/xeus-r/archive/refs/tags/${{ version }}.tar.gz
                sha256: 032fe1e337b67bb35b965a8d7535a3a1cd1c0e69e74d03af12acde06b4b83490

              build:
                number: 0

              requirements:
                build:
                - ${{ compiler("cxx") }}
                - cmake
                - make
                # Dependencies to build hera
                - cross-r-base_${{ target_platform }}
                - r-base == ${{ r_base_version }}
                - r-cli
                - r-evaluate
                - r-glue
                - r-IRdisplay
                - r-R6
                - r-repr
                host:
                - nlohmann_json
                - nlohmann_json-abi
                - xeus
                - xeus-lite
                - pcre2
                - zlib
                - libflang
                - xz
                - bzip2
                - libiconv
                - r-base
                run:
                - r-base
                - r-cli
                - r-evaluate
                - r-glue
                - r-IRdisplay
                - r-jsonlite
                - r-R6
                - r-repr
                - r-rlang

              tests:
              - package_contents:
                  files:
                  - bin/xr.js
                  - bin/xr.wasm

              about:
                license: GPL-3.0-only
                license_family: GPL
                license_file: LICENSE
                summary: Jupyter kernel for the R programming language
                homepage: https://github.com/jupyter-xeus/xeus-r
                documentation: https://xeus-r.readthedocs.io
              

recipe.yaml

Xeus-R-Lite

[open in new tab]

QuantStack Logo

Packaging GNU Octave for WebAssembly

What is GNU Octave?

GNU Octave logo
  • Scientific programming language
  • Mathematics-oriented syntax
  • Drop-in compatible with MATLAB
  • Free and open-source

              b = [4; 9; 2]
              A = [ 3 4 5;
                    1 3 1;
                    3 5 9 ]
              x = A \ b
              

Cross-Compilation of GNU Octave

  • C/C++ compiler: Emscripten
  • Fortran compiler: LLVM Flang

Dependencies:

  • Fortran runtime library
  • BLAS/LAPACK
    • Netlib LAPACK
    • OpenBLAS
  • pcre2
  • freetype
Cardboad box of GNU Octave package

Modifications:

  • Add wasm32-unknown-emscripten support
  • Disable GUI functionality

Fortran Challenges


              C-----------------------------------------------------------------------
              C The following internal Common block contains
              C (a) variables which are local to any subroutine but whose values must
              C     be preserved between calls to the routine ("own" variables), and
              C (b) variables which are communicated between subroutines.
              C The block SLS001 is declared in subroutines SLSODE, SINTDY, SSTODE,
              C SPREPJ, and SSOLSY.
              C Groups of variables are replaced by dummy arrays in the Common
              C declarations in routines where those variables are not used.
              C-----------------------------------------------------------------------
                    COMMON /SLS001/ CONIT, CRATE, EL(13), ELCO(13,12),
                  1   HOLD, RMAX, TESCO(3,12),
                  1   CCMAX, EL0, H, HMIN, HMXI, HU, RC, TN, UROUND,
                  2   INIT, MXSTEP, MXHNIL, NHNIL, NSLAST, NYH,
                  3   IALTH, IPUP, LMAX, MEO, NQNYH, NSLP,
                  3   ICF, IERPJ, IERSL, JCUR, JSTART, KFLAG, L,
                  4   LYH, LEWT, LACOR, LSAVF, LWM, LIWM, METH, MITER,
                  5   MAXORD, MAXCOR, MSBP, MXNCF, N, NQ, NST, NFE, NJE, NQU
              C
                    DATA  MORD(1),MORD(2)/12,5/, MXSTP0/500/, MXHNL0/10/
              

liboctave/external/odepack/slsode.f


              void MCWasmStreamer::emitCommonSymbol(MCSymbol *S, uint64_t Size,
                                                    Align ByteAlignment) {
                llvm_unreachable("Common symbols are not yet implemented for Wasm");
              }
              

llvm/lib/MC/MCWasmStreamer.cpp

Patching LLVM


          From 703b5fa8bd61289c8664ef603da8c502cb595ac7 Mon Sep 17 00:00:00 2001
          From: serge-sans-paille 
          Date: Tue, 8 Jul 2025 17:02:04 +0200
          Subject: Simulate common symbols as weak

          ---
          llvm/lib/MC/MCWasmStreamer.cpp | 12 ++++++++++--
          1 file changed, 10 insertions(+), 2 deletions(-)

          diff --git a/llvm/lib/MC/MCWasmStreamer.cpp b/llvm/lib/MC/MCWasmStreamer.cpp
          index 8560f0ebe..f8a83e01a 100644
          --- a/llvm/lib/MC/MCWasmStreamer.cpp
          +++ b/llvm/lib/MC/MCWasmStreamer.cpp
          @@ -150,7 +150,11 @@ bool MCWasmStreamer::emitSymbolAttribute(MCSymbol *S, MCSymbolAttr Attribute) {

          void MCWasmStreamer::emitCommonSymbol(MCSymbol *S, uint64_t Size,
                                                Align ByteAlignment) {
          -  llvm_unreachable("Common symbols are not yet implemented for Wasm");
          +  // llvm_unreachable("Common symbols are not yet implemented for Wasm");
          +  auto *Symbol = cast(S);
          +  getAssembler().registerSymbol(*Symbol);
          +  Symbol->setWeak(true);
          +  Symbol->setExternal(true);
          }

          void MCWasmStreamer::emitELFSize(MCSymbol *Symbol, const MCExpr *Value) {
          @@ -159,7 +163,11 @@ void MCWasmStreamer::emitELFSize(MCSymbol *Symbol, const MCExpr *Value) {

          void MCWasmStreamer::emitLocalCommonSymbol(MCSymbol *S, uint64_t Size,
                                                      Align ByteAlignment) {
          -  llvm_unreachable("Local common symbols are not yet implemented for Wasm");
          +  // llvm_unreachable("Local common symbols are not yet implemented for Wasm");
          +  auto *Symbol = cast(S);
          +  getAssembler().registerSymbol(*Symbol);
          +  Symbol->setWeak(true);
          +  Symbol->setExternal(true);
          }

          void MCWasmStreamer::emitIdent(StringRef IdentString) {
          

Partial support for common linkage for WebAssembly (llvm-project/pull/151478)

Xeus-Octave

Xeus-Octave logo
Example Jupyter notebook for Xeus-Octave kernel

Xeus-Octave-Lite

[open in new tab]

Expanding the GNU Octave Ecosystem

Future plans

  • Add pure Octave packages to conda-forge as noarch packages
  • Add Octave packages to emscripten-forge, those which require compilation
Four cardboard boxes as packages
QuantStack Logo

JupyterLite Terminal

Terminal

In JupyterLab

  • Terminal connects to a real shell running on the server

In JupyterLite

  • Terminal connects to a shell emulator running in the browser
  • Mixture of TypeScript and WebAssembly
  • Two projects
    • cockle: standalone shell without any JupyterLite dependencies
    • terminal: JupyterLite extension using cockle

Demo of JupyterLite Terminal

[open in new tab]

Demo of JupyterLite Terminal 2

  • Access the shared JupyterLite file system
  • List commands available
  • Editors vim and nano
  • uname -a shows it is an Emscripten build
  • cockle-config package shows installed Emscripten-Forge packages

vim Emscripten-Forge package

recipe.yaml


                context:
                  version: 9.1.0917

                package:
                  name: vim
                  version: ${{ version }}

                source:
                  url: https://github.com/vim/vim/archive/refs/tags/v${{ version }}.tar.gz
                  sha256: a5cdcfcfeb13dc4deddcba461d40234dbf47e61941cb7170c9ebe147357bb62d
                  patches:
                    - patches/0001-const-char-args.patch

                build:
                  number: 4

                requirements:
                  build:
                    - ${{ compiler("c") }}
                  host:
                    - ncurses <6.5

                tests:
                  - script:
                    - test -f $PREFIX/bin/vim.data
                    - test -f $PREFIX/bin/vim.js
                    - test -f $PREFIX/bin/vim.wasm
                  - script: |
                      # Limit output to exclude compilation date and time.
                      OUTPUT=$(run_modularized $PREFIX/bin/vim.js --version | head -1 | cut -c -22)
                      if [[ "$OUTPUT" != "VIM - Vi IMproved 9.1 " ]]; then
                        echo "Unexpected output: $OUTPUT"
                        exit 1
                      fi
                    requirements:
                      build:
                        - run_modularized >= 0.1.2

                about:
                  license: Vim
                  license_file: LICENSE

                extra:
                  recipe-maintainers:
                    - ianthomas23
              

  • Source from official vim repo
  • Small patch
  • Depends on ncurses

build.sh


                export NCURSES_CFLAGS=$($PREFIX/bin/ncurses6-config --cflags)
                export NCURSES_LDFLAGS=$($PREFIX/bin/ncurses6-config --libs-only-L)
                export XTERM_256COLOR=$($PREFIX/bin/ncurses6-config --terminfo)/x/xterm-256color
                export VIMRC=$PWD/vimrc

                echo $NCURSES_CFLAGS
                echo $NCURSES_LDFLAGS
                ls -l $XTERM_256COLOR

                export CONFIG_CFLAGS="\
                    $NCURSES_CFLAGS \
                    -Os \
                    "
                export CONFIG_LDFLAGS="\
                    $NCURSES_LDFLAGS \
                    -Os \
                    --minify=0 \
                    -sALLOW_MEMORY_GROWTH=1 \
                    -sEXIT_RUNTIME=1 \
                    -sEXPORTED_RUNTIME_METHODS=FS,ENV,getEnvStrings,TTY \
                    -sFORCE_FILESYSTEM=1 \
                    -sMODULARIZE=1 \
                    -sLZ4 \
                    "

                emconfigure ./configure \
                    --disable-nls \
                    --disable-xattr \
                    --host=wasm32-unknown-emscripten \
                    --with-compiledby=emscripten-forge \
                    --with-features=normal \
                    --with-tlib=tinfo \
                    CFLAGS="$CFLAGS $CONFIG_CFLAGS" \
                    LDFLAGS="$LDFLAGS $CONFIG_LDFLAGS" \
                    ac_cv_sizeof_int=4 \
                    ac_cv_sizeof_long=8 \
                    ac_cv_sizeof_off_t=4 \
                    ac_cv_sizeof_time_t=4 \
                    ac_cv_func_ftruncate=no

                # Use /etc/vimrc for system vimrc file and turn some features on and off.
                sed -ri "s/.*(#define SYS_VIMRC_FILE\s.*)/\1/" src/feature.h
                echo "syntax on" > $VIMRC
                echo "set termguicolors" >> $VIMRC
                echo "set nobackup" >> $VIMRC
                echo "set noswapfile" >> $VIMRC
                echo "set nowritebackup" >> $VIMRC

                # Installs runtime config files under $PWD/usr/local/share/vim
                DESTDIR=$PWD make -C src installruntime

                emmake make EXEEXT=.js -j4 LDFLAGS=" \
                    $LDFLAGS \
                    $CONFIG_LDFLAGS \
                    --preload-file $PWD/usr/local/share/vim/vim91@/usr/local/share/vim/vim91 \
                    --exclude-file $PWD/usr/local/share/vim/vim91/lang \
                    --exclude-file $PWD/usr/local/share/vim/vim91/doc \
                    --exclude-file $PWD/usr/local/share/vim/vim91/tutor \
                    --preload-file $XTERM_256COLOR@/usr/local/share/terminfo/x/xterm-256color \
                    --preload-file $VIMRC@/etc/vimrc \
                    "

                mkdir -p $PREFIX/bin
                cp src/vim.{data,js,wasm} $PREFIX/bin/
              

  • configure and make steps
  • Creates vim.js, vim.wasm and vim.data

Looking ahead: git

  • git2cpp (C++) wraps libgit2 (C) to provide command-line tools
  • Emscripten Forge builds patched to communicate with remote hosts
  • Basic functionality almost ready ...
  • Incrementally add new functionality
git2cpp running in cockle

Questions?