Building JerryScript Arduino Library for ESP32

Building JerryScript Arduino Library for ESP32

This tutorial explains how to cross-compile JerryScript v3.0.0 for ESP32 and package it as an Arduino-compatible library.

Overview

JerryScript is a lightweight JavaScript engine designed for microcontrollers. This guide produces a ready-to-use Arduino library that enables ECMAScript support for SCXML state machines on ESP32 devices.

What you'll create:

  • Pre-compiled JerryScript static libraries for ESP32 (Xtensa architecture)
  • Arduino library with proper structure and metadata
  • Port implementation for Arduino/ESP32 platform

Requirements:

  • Linux, macOS, or Windows with WSL2
  • CMake 3.16+
  • Git
  • ESP-IDF toolchain (for xtensa-esp32-elf-gcc)

Step 1: Install ESP-IDF Toolchain

The ESP32 uses the Xtensa LX6 processor, which requires a specific cross-compiler.

bash
# Clone ESP-IDF
mkdir -p ~/esp
cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf

# Install tools for ESP32
./install.sh esp32

# Activate the environment (run this in each new terminal)
source export.sh

# Verify installation
xtensa-esp32-elf-gcc --version

Option B: Standalone Toolchain

Download from Espressif's toolchain releases:

bash
# Linux example
wget https://github.com/espressif/crosstool-NG/releases/download/esp-12.2.0_20230208/xtensa-esp32-elf-12.2.0_20230208-x86_64-linux-gnu.tar.xz
tar -xf xtensa-esp32-elf-*.tar.xz
export PATH=$PATH:$(pwd)/xtensa-esp32-elf/bin

Step 2: Clone and Configure JerryScript

bash
# Create working directory
mkdir -p ~/jerryscript-arduino
cd ~/jerryscript-arduino

# Clone JerryScript v3.0.0
git clone https://github.com/jerryscript-project/jerryscript.git
cd jerryscript
git checkout v3.0.0

Create ESP32 Arduino Toolchain File

bash
cat > cmake/toolchain-esp32-arduino.cmake << 'EOF'
# CMake toolchain file for ESP32 Arduino
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR xtensa)

# Compilers from ESP-IDF toolchain
set(CMAKE_C_COMPILER xtensa-esp32-elf-gcc)
set(CMAKE_CXX_COMPILER xtensa-esp32-elf-g++)
set(CMAKE_ASM_COMPILER xtensa-esp32-elf-gcc)

# Compiler flags for ESP32
# Added -Wno-unterminated-string-initialization to bypass GCC 15 strictness
set(CMAKE_C_FLAGS_INIT "-mlongcalls -Wno-frame-address -ffunction-sections -fdata-sections -fno-exceptions -Wno-unterminated-string-initialization")
set(CMAKE_CXX_FLAGS_INIT "-mlongcalls -Wno-frame-address -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -Wno-unterminated-string-initialization")

# Don't try to run test executables on host
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# Search paths
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
EOF

Step 3: Build JerryScript for ESP32

Configure Build Options

Choose heap size based on your needs:

Heap Size Use Case RAM Usage
32 KB Simple expressions only ~35 KB
64 KB Moderate complexity (recommended) ~67 KB
128 KB Complex JS with objects/arrays ~131 KB

Build Commands

bash
# Create build directory
mkdir -p build-esp32-arduino
cd build-esp32-arduino

# Configure with CMake
cmake .. \
    -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-esp32-arduino.cmake \
    -DCMAKE_BUILD_TYPE=MinSizeRel \
    -DJERRY_CMDLINE=OFF \
    -DJERRY_PORT=ON \
    -DJERRY_EXT=OFF \
    -DJERRY_LIBM=OFF \
    -DJERRY_MATH=ON \
    -DJERRY_GLOBAL_HEAP_SIZE=64 \
    -DJERRY_GC_LIMIT=0 \
    -DJERRY_STACK_LIMIT=4 \
    -DJERRY_CPOINTER_32_BIT=ON \
    -DJERRY_SYSTEM_ALLOCATOR=OFF \
    -DJERRY_BUILTIN_ANNEXB=OFF \
    -DJERRY_BUILTIN_BIGINT=OFF \
    -DJERRY_BUILTIN_CONTAINER=OFF \
    -DJERRY_BUILTIN_DATAVIEW=OFF \
    -DJERRY_BUILTIN_PROXY=OFF \
    -DJERRY_BUILTIN_REALMS=OFF \
    -DJERRY_BUILTIN_REFLECT=OFF \
    -DJERRY_BUILTIN_TYPEDARRAY=OFF \
    -DJERRY_BUILTIN_WEAKREF=OFF \
    -DJERRY_MODULE_SYSTEM=OFF \
    -DJERRY_SNAPSHOT_EXEC=OFF \
    -DJERRY_SNAPSHOT_SAVE=OFF \
    -DJERRY_PARSER=ON \
    -DJERRY_LINE_INFO=OFF \
    -DJERRY_LOGGING=OFF \
    -DJERRY_ERROR_MESSAGES=ON \
    -DJERRY_VALGRIND=OFF \
    -DJERRY_MEM_STATS=OFF \
    -DJERRY_DEBUGGER=OFF

# Build
cmake --build . --target jerry-core jerry-port-default -- -j$(nproc)

# Verify output
ls -la lib/
# Should show:
#   libjerry-core.a      (~120-180 KB)
#   libjerry-port-default.a (~3-5 KB)

Step 4: Create Arduino Library Structure

bash
# Go back to working directory
cd ~/jerryscript-arduino

# Create library structure
mkdir -p JerryScript/src/esp32
mkdir -p JerryScript/examples/basic

# Copy compiled libraries
cp jerryscript/build-esp32-arduino/lib/libjerry-core.a JerryScript/src/esp32/
cp jerryscript/build-esp32-arduino/lib/libjerry-port-default.a JerryScript/src/esp32/

# Copy headers
cp jerryscript/jerry-core/include/*.h JerryScript/src/
cp jerryscript/jerry-port/include/*.h JerryScript/src/

Create library.properties

bash
cat > JerryScript/library.properties << 'EOF'
name=JerryScript
version=3.0.0
author=JerryScript Project
maintainer=VSCXML Project
sentence=JerryScript JavaScript engine for ESP32
paragraph=Lightweight JavaScript engine (<64KB RAM) for running ECMAScript on microcontrollers. Supports ES5.1 with selected ES2015+ features.
category=Data Processing
url=https://jerryscript.net/
architectures=esp32
includes=jerryscript.h
precompiled=true
ldflags=-L{build.library_discovery_phase_flag_path}/JerryScript/src/esp32 -ljerry-core -ljerry-port-default
EOF

Create Arduino Port Implementation

bash
cat > JerryScript/src/jerry_port_arduino.cpp << 'EOF'
/**
 * JerryScript Port Implementation for Arduino/ESP32
 *
 * This file provides the platform-specific functions required by JerryScript
 * to run on Arduino/ESP32 hardware.
 */

#include <Arduino.h>
#include <stdlib.h>
#include <string.h>

extern "C" {
#include "jerryscript-port.h"
}

/* ============================================================================
 * Fatal Error Handler
 * ============================================================================ */

extern "C" void jerry_port_fatal(jerry_fatal_code_t code) {
    Serial.print(F("[JERRY FATAL] Code: "));
    Serial.println((int)code);
    Serial.flush();

    // Blink LED rapidly to indicate fatal error
    pinMode(LED_BUILTIN, OUTPUT);
    while (1) {
        digitalWrite(LED_BUILTIN, HIGH);
        delay(100);
        digitalWrite(LED_BUILTIN, LOW);
        delay(100);
    }
}

/* ============================================================================
 * Logging
 * ============================================================================ */

extern "C" void jerry_port_log(const char *message_p) {
    if (message_p) {
        Serial.print(message_p);
    }
}

/* ============================================================================
 * Date/Time Support
 * ============================================================================ */

extern "C" double jerry_port_current_time(void) {
    // Return milliseconds since boot
    // For real date/time, integrate with NTP or RTC
    return (double)millis();
}

extern "C" int32_t jerry_port_local_tza(double unix_ms) {
    (void)unix_ms;
    // Return timezone offset in milliseconds (0 = UTC)
    // Adjust for your timezone if needed
    return 0;
}

/* ============================================================================
 * Sleep (for async operations)
 * ============================================================================ */

extern "C" void jerry_port_sleep(uint32_t sleep_time_ms) {
    delay(sleep_time_ms);
}

/* ============================================================================
 * Path Operations (stubs - not used on Arduino)
 * ============================================================================ */

extern "C" jerry_size_t jerry_port_path_normalize(const jerry_char_t *in_path_p,
                                                   jerry_char_t *out_buf_p,
                                                   jerry_size_t out_buf_size) {
    (void)in_path_p;
    (void)out_buf_p;
    (void)out_buf_size;
    return 0;
}

extern "C" jerry_size_t jerry_port_path_base(const jerry_char_t *path_p) {
    (void)path_p;
    return 0;
}

/* ============================================================================
 * Context Allocation
 * ============================================================================ */

extern "C" void *jerry_port_context_alloc(size_t size) {
    return malloc(size);
}

extern "C" void jerry_port_context_free(void *context_p) {
    free(context_p);
}
EOF

Create Basic Example

bash
cat > JerryScript/examples/basic/basic.ino << 'EOF'
/**
 * JerryScript Basic Example
 *
 * Demonstrates running JavaScript code on ESP32 using JerryScript.
 */

#include <jerryscript.h>

void setup() {
    Serial.begin(115200);
    while (!Serial) delay(10);

    Serial.println("\n=== JerryScript Basic Example ===");

    // Initialize JerryScript engine
    jerry_init(JERRY_INIT_EMPTY);

    // Print version
    Serial.print("JerryScript version: ");
    Serial.print(JERRY_API_MAJOR_VERSION);
    Serial.print(".");
    Serial.print(JERRY_API_MINOR_VERSION);
    Serial.print(".");
    Serial.println(JERRY_API_PATCH_VERSION);

    // Run simple JavaScript
    const char* script = "var x = 40 + 2; x;";

    jerry_value_t parsed = jerry_parse(
        (const jerry_char_t*)script,
        strlen(script),
        NULL
    );

    if (jerry_value_is_exception(parsed)) {
        Serial.println("Parse error!");
    } else {
        jerry_value_t result = jerry_run(parsed);

        if (jerry_value_is_exception(result)) {
            Serial.println("Runtime error!");
        } else {
            double num = jerry_value_as_number(result);
            Serial.print("Result of '40 + 2': ");
            Serial.println(num);
        }

        jerry_value_free(result);
    }

    jerry_value_free(parsed);

    // Test string operations
    const char* str_script = "'Hello ' + 'ESP32!'";
    parsed = jerry_parse(
        (const jerry_char_t*)str_script,
        strlen(str_script),
        NULL
    );

    if (!jerry_value_is_exception(parsed)) {
        jerry_value_t result = jerry_run(parsed);

        if (!jerry_value_is_exception(result)) {
            jerry_value_t str_val = jerry_value_to_string(result);
            jerry_size_t str_size = jerry_string_size(str_val, JERRY_ENCODING_UTF8);

            char* buffer = (char*)malloc(str_size + 1);
            jerry_string_to_buffer(str_val, JERRY_ENCODING_UTF8,
                                   (jerry_char_t*)buffer, str_size);
            buffer[str_size] = '\0';

            Serial.print("String result: ");
            Serial.println(buffer);

            free(buffer);
            jerry_value_free(str_val);
        }
        jerry_value_free(result);
    }
    jerry_value_free(parsed);

    // Cleanup
    jerry_cleanup();

    Serial.println("\nJerryScript test complete!");
}

void loop() {
    delay(1000);
}
EOF

Step 5: Create Distribution ZIP

bash
cd ~/jerryscript-arduino

# Create ZIP for distribution
zip -r JerryScript-ESP32-v3.0.0.zip JerryScript/

# Verify contents
unzip -l JerryScript-ESP32-v3.0.0.zip

Expected output:

Archive:  JerryScript-ESP32-v3.0.0.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2024-XX-XX XX:XX   JerryScript/
      XXX  2024-XX-XX XX:XX   JerryScript/library.properties
        0  2024-XX-XX XX:XX   JerryScript/src/
   XXXXXX  2024-XX-XX XX:XX   JerryScript/src/esp32/libjerry-core.a
     XXXX  2024-XX-XX XX:XX   JerryScript/src/esp32/libjerry-port-default.a
     XXXX  2024-XX-XX XX:XX   JerryScript/src/jerryscript.h
     XXXX  2024-XX-XX XX:XX   JerryScript/src/jerryscript-core.h
     ...

Step 6: Install in Arduino IDE

  1. Open Arduino IDE
  2. Go to Sketch → Include Library → Add .ZIP Library...
  3. Select JerryScript-ESP32-v3.0.0.zip
  4. Restart Arduino IDE

Method 2: Manual Installation

Copy the JerryScript folder to your Arduino libraries directory:

Platform Location
Windows C:\Users\<username>\Documents\Arduino\libraries\
macOS ~/Documents/Arduino/libraries/
Linux ~/Arduino/libraries/

Verification

  1. Open Arduino IDE
  2. Go to File → Examples → JerryScript → basic
  3. Select your ESP32 board
  4. Upload and open Serial Monitor (115200 baud)

Expected output:

=== JerryScript Basic Example ===
JerryScript version: 3.0.0
Result of '40 + 2': 42.00
String result: Hello ESP32!

JerryScript test complete!

Build Configuration Reference

Heap Size Options

CMake Flag Effect
-DJERRY_GLOBAL_HEAP_SIZE=32 32 KB heap, minimal JS
-DJERRY_GLOBAL_HEAP_SIZE=64 64 KB heap, moderate JS
-DJERRY_GLOBAL_HEAP_SIZE=128 128 KB heap, complex JS

Feature Flags

Flag Default Description
JERRY_BUILTIN_ARRAY ON Array methods
JERRY_BUILTIN_DATE ON Date object
JERRY_BUILTIN_JSON ON JSON.parse/stringify
JERRY_BUILTIN_MATH ON Math functions
JERRY_BUILTIN_REGEXP ON Regular expressions
JERRY_ERROR_MESSAGES ON Detailed error messages
JERRY_PARSER ON Runtime JS parsing

To reduce binary size, disable unused features:

bash
-DJERRY_BUILTIN_REGEXP=OFF   # Saves ~20KB if not using regex
-DJERRY_ERROR_MESSAGES=OFF   # Saves ~10KB

Troubleshooting

"xtensa-esp32-elf-gcc: command not found"

Ensure ESP-IDF environment is activated:

bash
source ~/esp/esp-idf/export.sh

Build fails with "cannot find -lgcc"

The toolchain path is incorrect. Verify with:

bash
which xtensa-esp32-elf-gcc
xtensa-esp32-elf-gcc -print-libgcc-file-name

Arduino IDE doesn't find the library

  1. Check library.properties exists and has correct syntax
  2. Ensure architectures=esp32 is set
  3. Restart Arduino IDE completely

"undefined reference to jerry_init"

The precompiled library isn't being linked. Check:

  1. .a files are in src/esp32/ folder
  2. precompiled=true in library.properties
  3. ldflags path is correct

Next Steps


References