Skip to main content

C + JerryScript

JerryScript is a lightweight JavaScript engine. It is designed for microcontrollers and similar environments.

SheetJS is a JavaScript library for reading and writing data from spreadsheets.

This demo uses JerryScript and SheetJS to pull data from a spreadsheet and print CSV rows. We'll explore how to load SheetJS in a JerryScript realm and process spreadsheets from C programs.

The "Integration Example" section includes a complete command-line tool for reading data from files.

This demo requires a much larger heap size than is normally used in JerryScript deployments! In local testing, the following sizes were needed:

Tested Environments

This demo was tested in the following environments:

ArchitectureCommitDate
darwin-x6435465ed2024-05-25
darwin-arm35465ed2024-05-25
win10-x6447bd5d42024-04-14
win11-arm35465ed2024-05-25
linux-x64cefd3912024-03-21
linux-arm35465ed2024-05-25

The Windows tests were run in WSL.

Integration Details

The official JerryScript documentation and examples are out of date. This explanation was verified against the latest release (commit 514fa67).

Initialize JerryScript

The global engine instance can be initialized with jerry_init and cleaned up with jerry_cleanup:

#include "jerryscript.h"

int main (int argc, char **argv) {
/* Initialize engine */
jerry_init(JERRY_INIT_EMPTY);

// ... use engine methods ...

/* cleanup before exiting */
jerry_cleanup();
return 0;
}

API methods use jerry_value_t values to represent JS values and miscellany. Values representing errors can be distinguished using jerry_value_is_error. jerry_value_t values can be freed with jerry_value_free.

Evaluate Code

Evaluating code involves two steps:

  • jerry_parse will parse the script
  • jerry_run will run the parsed script object

The return value of jerry_parse is a jerry_value_t value that can be safely freed after jerry_run.

The following eval_str function parses and executes scripts. If parsing fails, the function will return the parsing error. If parsing succeeds, the function will return the result of executing the code.

jerry_value_t eval_str(const char *code, size_t sz) {
/* try to parse code */
jerry_value_t parsed = jerry_parse(code, sz, NULL);
/* return the parse error if parsing failed */
if(jerry_value_is_error(parsed)) return parsed;

/* run the code */
jerry_value_t out = jerry_run(parsed);
/* free the parsed representation */
jerry_value_free(parsed);

/* return the result */
return out;
}

Load SheetJS Scripts

SheetJS Standalone scripts can be parsed and run in JerryScript.

Scripts can be read from the filesystem using standard C functions:

static char *read_file(const char *filename, size_t *sz) {
FILE *f = fopen(filename, "rb");
if(!f) return NULL;
long fsize; { fseek(f, 0, SEEK_END); fsize = ftell(f); fseek(f, 0, SEEK_SET); }
char *buf = (char *)malloc(fsize * sizeof(char));
*sz = fread((void *) buf, 1, fsize, f) - 1;
fclose(f);
return buf;
}

The shim script must be evaluated before the main library. In both cases, after reading the script file, the previous eval_str function can run the code:

  /* evaluate shim.min.js */
{
size_t sz; const jerry_char_t *script = (jerry_char_t *)read_file("shim.min.js", &sz);
jerry_value_t result = eval_str(script, sz);
if(jerry_value_is_error(result)) { // failed to parse / execute
fprintf(stderr, "Failed to evaluate shim.min.js"); return 1;
}
jerry_value_free(result);
}

/* evaluate xlsx.full.min.js */
{
size_t sz; const jerry_char_t *script = (jerry_char_t *)read_file("xlsx.full.min.js", &sz);
jerry_value_t result = eval_str(script, sz);
if(jerry_value_is_error(result)) { // failed to parse / execute
fprintf(stderr, "Failed to evaluate xlsx.full.min.js"); return 1;
}
jerry_value_free(result);
}

Reading Files

Binary file data can be passed from C to JerryScript with ArrayBuffer objects.

Creating ArrayBuffers

jerry_arraybuffer will generate an ArrayBuffer object of specified length. After creating the array, jerry_arraybuffer_write will copy data.

The following load_file function reads a file from the filesystem and loads the data into an ArrayBuffer:

static jerry_value_t load_file(const char *filename) {
/* read file */
size_t len; char *buf = read_file(filename, &len);
if(!buf) return 0;

/* create ArrayBuffer */
jerry_value_t out = jerry_arraybuffer(len);
/* copy file data into ArrayBuffer */
jerry_arraybuffer_write(out, 0, (const uint8_t*)buf, len);
return out;
}

The process may fail. The result should be tested with jerry_value_is_error:

  jerry_value_t ab = load_file("pres.xlsx");
if(!ab || jerry_value_is_error(ab)) { // failed to create ArrayBuffer
fprintf(stderr, "Failed to read pres.xlsx"); return 1;
}

Creating Global Variable

The ArrayBuffer object must be bound to a variable before it can be used.

The goal is to bind the ArrayBuffer to the buf property in global scope.

  1. Get the global this variable (using jerry_current_realm):
  /* get the global variable */
jerry_value_t this = jerry_current_realm();
if(jerry_value_is_error(this)) { // failed to get global object
fprintf(stderr, "Failed to get global object"); return 1;
}
  1. Create a JerryScript string ("buf") for the property:
  /* create a string "buf" for the property access */
jerry_value_t prop = jerry_string_sz("buf");
if(jerry_value_is_error(this)) { // failed to create "buf"
fprintf(stderr, "Failed to create string"); return 1;
}
  1. Assign the property using jerry_object_set:
  /* set global["buf"] to the ArrayBuffer */
jerry_value_t set = jerry_object_set(this, prop, ab);
if(jerry_value_is_error(set)) { // failed to set property
fprintf(stderr, "Failed to assign ArrayBuffer"); return 1;
}

Parsing Data

The goal is to run the equivalent of the following JavaScript code:

/* `buf` is the `ArrayBuffer` from the previous step */
var wb = XLSX.read(buf);

The ArrayBuffer from the previous step is available in the buf variable. That ArrayBuffer can be passed to the SheetJS read method1, which will parse the raw data and return a SheetJS workbook object2.

var wb = XLSX.read(buf) can be stored in a byte array and evaluated directly:

  /* run `var wb = XLSX.read(buf)` */
{
const jerry_char_t code[] = "var wb = XLSX.read(buf);";
jerry_value_t result = eval_str(code, sizeof(code) - 1);
if(jerry_value_is_error(result)) {
fprintf(stderr, "Failed to parse file"); return 1;
}
jerry_value_free(result);
}

Generating CSV

The goal is to run the equivalent of the following JavaScript code:

/* `wb` is the workbook from the previous step */
XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])

A SheetJS workbook object can contain multiple sheet objects3. The Sheets property is an object whose keys are sheet names and whose values are sheet objects. The SheetNames property is an array of worksheet names.

The first sheet name can be found at wb.SheetNames[0]. The first sheet object can be found at wb.Sheets[wb.SheetNames[0]].

The SheetJS sheet_to_csv utility function4 accepts a sheet object and generates a JS string.

Combining everything, XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]) generates a CSV string based on the first worksheet in the workbook wb:

  const jerry_char_t code[] = "XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])";
jerry_value_t csv = eval_str(code, sizeof(code) - 1);
if(jerry_value_is_error(result)) { // CSV generation failed
fprintf(stderr, "Failed to generate csv"); return 1;
}

Pulling Strings

JerryScript exposes encoding-aware methods to pull JS strings into C. The JERRY_ENCODING_UTF8 encoding forces UTF8 interpretations.

The jerry_string_size function returns the number of bytes required to store the string. After allocating memory, jerry_string_to_buffer will copy data. The following pull_str function uses malloc:

char *pull_str(jerry_value_t str, size_t *sz) {
/* determine string size in bytes */
jerry_size_t str_sz = jerry_string_size(str, JERRY_ENCODING_UTF8);

/* allocate memory */
jerry_char_t *buf = (jerry_char_t *)malloc(str_sz + 1);

/* copy from JS string to C byte array */
jerry_string_to_buffer(str, JERRY_ENCODING_UTF8, buf, str_sz + 1);

/* pass back size and return the pointer */
*sz = str_sz;
return (char *)buf;
}

This function can be used to pull the csv value from the previous section:

  size_t sz; char *buf = pull_str(result, &sz);
printf("%s\n", buf);

Complete Example

The "Integration Example" covers a traditional integration in a C application, while the "CLI Test" demonstrates other concepts using the jerry CLI tool.

Integration Example

Build Dependencies (click to show)

The JerryScript build system requires cmake.

Debian and WSL additionally require python3 and python-is-python3 packages.

  1. Create a project folder:
mkdir SheetJSJerry
cd SheetJSJerry
  1. Clone the repository and build the library with required options:
git clone --depth=1 https://github.com/jerryscript-project/jerryscript.git
cd jerryscript
python3 tools/build.py --error-messages=ON --logging=ON --mem-heap=8192 --cpointer-32bit=ON
cd ..
  1. Download the SheetJS Standalone script, shim script and test file. Move all three files to the SheetJSJerry directory:
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/shim.min.js
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
curl -LO https://docs.sheetjs.com/pres.xlsx
  1. Download sheetjs.jerry.c into the same folder:
curl -LO https://docs.sheetjs.com/jerryscript/sheetjs.jerry.c
  1. Build the sample application:
gcc -o sheetjs.jerry -Ijerryscript/jerry-ext/include -Ijerryscript/jerry-math/include -Ijerryscript/jerry-core/include sheetjs.jerry.c -ljerry-core -ljerry-ext -ljerry-port -lm -Ljerryscript/build/lib -Wno-pointer-sign
  1. Run the test program:
./sheetjs.jerry pres.xlsx

If successful, the program will print contents of the first sheet as CSV rows.

CLI Test

Due to limitations of the standalone binary, this demo will encode a test file as a Base64 string and directly add it to an amalgamated script.

  1. Build the library and command line tool with required options.

If the "Integration Example" was not tested, run the following commands:

git clone --depth=1 https://github.com/jerryscript-project/jerryscript.git
cd jerryscript
python3 tools/build.py --error-messages=ON --logging=ON --mem-heap=8192 --cpointer-32bit=ON

If the "Integration Example" was tested, enter the jerryscript folder:

cd jerryscript
  1. Download the SheetJS Standalone script, shim script and test file. Move all three files to the jerryscript cloned repo directory:
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/shim.min.js
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
curl -LO https://docs.sheetjs.com/pres.xlsx
  1. Bundle the test file and create payload.js:
node -e "fs.writeFileSync('payload.js', 'var payload = \"' + fs.readFileSync('pres.xlsx').toString('base64') + '\";')"
  1. Create support scripts:
  • global.js creates a global variable and defines a fake console:
global.js
var global = (function(){ return this; }).call(null);
var console = { log: function(x) { print(x); } };
  • jerry.js will call XLSX.read and XLSX.utils.sheet_to_csv:
jerry.js
var wb = XLSX.read(payload, {type:'base64'});
console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]));
  1. Create the amalgamation xlsx.jerry.js:
cat global.js xlsx.full.min.js payload.js jerry.js > xlsx.jerry.js

The final script defines global before loading the standalone library. Once ready, it will read the bundled test data and print the contents as CSV.

  1. Run the script using the jerry standalone binary:
build/bin/jerry xlsx.jerry.js; echo $?

If successful, the contents of the test file will be displayed in CSV rows. The status code 0 will be printed after the rows.

Footnotes

  1. See read in "Reading Files"

  2. See "Workbook Object" in "SheetJS Data Model"

  3. See "Sheet Objects"

  4. See sheet_to_csv in "CSV and Text"