Skip to main content

C + JerryScript

JerryScript 是一个轻量级的 JavaScript 引擎。它专为微控制器和类似环境而设计。

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

SheetJS 是一个用于从电子表格读取和写入数据的 JavaScript 库。

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

该演示使用 JerryScript 和 SheetJS 从电子表格中提取数据并打印 CSV 行。我们将探讨如何在 JerryScript 字段中加载 SheetJS 并处理来自 C 程序的电子表格。

¥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.

该演示需要比 JerryScript 部署中通常使用的堆大小大得多的堆大小!在本地测试中,需要以下尺寸:

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

测试环境

本 demo 在以下环境下进行了测试:

¥This demo was tested in the following environments:

架构提交日期
darwin-x6435465ed2024-05-25
darwin-arm35465ed2024-05-25
win10-x6447bd5d42024-04-14
win11-arm35465ed2024-05-25
linux-x64cefd3912024-03-21
linux-arm35465ed2024-05-25

Windows 测试在 WSL 中运行。

¥The Windows tests were run in WSL.

集成详情

¥Integration Details

官方 JerryScript 文档和示例已过时。此解释已针对最新版本(提交 514fa67)进行了验证。

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

初始化 JerryScript

¥Initialize JerryScript

全局引擎实例可以使用 jerry_init 进行初始化并使用 jerry_cleanup 进行清理:

¥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 方法使用 jerry_value_t 值来表示 JS 值和杂项。可以使用 jerry_value_is_error 来区分代表错误的值。jerry_value_t 值可以通过 jerry_value_free 释放。

¥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 将解析脚本

    ¥jerry_parse will parse the script

  • jerry_run 将运行解析后的脚本对象

    ¥jerry_run will run the parsed script object

jerry_parse 的返回值是 jerry_value_t 值,可以在 jerry_run 之后安全释放。

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

以下 eval_str 函数解析并执行脚本。如果解析失败,该函数将返回解析错误。如果解析成功,该函数将返回执行代码的结果。

¥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;
}

加载 SheetJS 脚本

¥Load SheetJS Scripts

SheetJS 独立脚本 可以在 JerryScript 中解析并运行。

¥SheetJS Standalone scripts can be parsed and run in JerryScript.

可以使用标准 C 函数从文件系统读取脚本:

¥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;
}

必须在主库之前评估垫片脚本。这两种情况,读取脚本文件后,前面的 eval_str 函数都可以运行代码:

¥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

二进制文件数据可以使用 ArrayBuffer 对象从 C 传递到 JerryScript。

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

创建 ArrayBuffer

¥Creating ArrayBuffers

jerry_arraybuffer 将生成指定长度的 ArrayBuffer 对象。创建数组后,jerry_arraybuffer_write 将复制数据。

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

以下 load_file 函数从文件系统读取文件并将数据加载到 ArrayBuffer 中:

¥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;
}

该过程可能会失败。结果应使用 jerry_value_is_error 进行测试:

¥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

ArrayBuffer 对象必须先绑定到变量才能使用。

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

目标是将 ArrayBuffer 绑定到全局范围内的 buf 属性。

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

  1. 获取全局 this 变量(使用 jerry_current_realm):

    ¥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. 为该属性创建 JerryScript 字符串 ("buf"):

    ¥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. 使用 jerry_object_set 分配属性:

    ¥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

目标是运行与以下 JavaScript 代码等效的内容:

¥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);

上一步中的 ArrayBuffer 可在 buf 变量中使用。该 ArrayBuffer 可以传递给 SheetJS read 方法 [^1],该方法将解析原始数据并返回 SheetJS 工作簿对象 [^2]。

¥The ArrayBuffer from the previous step is available in the buf variable. That ArrayBuffer can be passed to the SheetJS read method[^1], which will parse the raw data and return a SheetJS workbook object[^2].

var wb = XLSX.read(buf) 可以存储在字节数组中并直接求值:

¥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);
}

生成 CSV

¥Generating CSV

目标是运行与以下 JavaScript 代码等效的内容:

¥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]])

一个 SheetJS 工作簿对象可以包含多个工作表对象 [^3]。Sheets 属性是一个对象,其键是工作表名称,其值是工作表对象。SheetNames 属性是工作表名称的数组。

¥A SheetJS workbook object can contain multiple sheet objects[^3]. 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.

第一个工作表名称可以在 wb.SheetNames[0] 处找到。第一个工作表对象可以在 wb.Sheets[wb.SheetNames[0]] 处找到。

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

SheetJS sheet_to_csv 实用函数 [^4] 接受一个工作表对象并生成一个 JS 字符串。

¥The SheetJS sheet_to_csv utility function[^4] accepts a sheet object and generates a JS string.

结合所有内容,XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]) 基于工作簿 wb 中的第一个工作表生成 CSV 字符串:

¥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 公开了编码感知方法,将 JS 字符串提取到 C 中。JERRY_ENCODING_UTF8 编码强制使用 UTF8 解释。

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

jerry_string_size 函数返回存储字符串所需的字节数。分配内存后,jerry_string_to_buffer 将复制数据。以下 pull_str 函数使用 malloc

¥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;
}

此函数可用于从上一节中提取 csv 值:

¥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

"集成示例" 涵盖了 C 应用中的传统集成,而 "CLI 测试" 使用 jerry CLI 工具演示了其他概念。

¥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. 下载 SheetJS Standalone 脚本、shim 脚本和测试文件。将所有三个文件移动到 SheetJSJerry 目录:

    ¥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://xlsx.nodejs.cn/pres.xlsx
  1. sheetjs.jerry.c 下载到同一文件夹中:

    ¥Download sheetjs.jerry.c into the same folder:

curl -LO https://xlsx.nodejs.cn/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

如果成功,程序会将第一张表的内容打印为 CSV 行。

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

CLI 测试

¥CLI Test

由于独立二进制文件的限制,该演示将测试文件编码为 Base64 字符串,并将其直接添加到合并脚本中。

¥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

如果测试了 "集成示例",请进入 jerryscript 文件夹:

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

cd jerryscript
  1. 下载 SheetJS Standalone 脚本、shim 脚本和测试文件。将所有三个文件移至 jerryscript 克隆的存储库目录:

    ¥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://xlsx.nodejs.cn/pres.xlsx
  1. 打包测试文件并创建 payload.js

    ¥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 创建一个 global 变量并定义一个假 console

    ¥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 将调用 XLSX.readXLSX.utils.sheet_to_csv

    ¥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. 创建合并 xlsx.jerry.js

    ¥Create the amalgamation xlsx.jerry.js:

cat global.js xlsx.full.min.js payload.js jerry.js > xlsx.jerry.js

最终脚本在加载独立库之前定义了 global。准备就绪后,它将读取打包的测试数据并将内容打印为 CSV。

¥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. 使用 jerry 独立二进制文件运行脚本:

    ¥Run the script using the jerry standalone binary:

build/bin/jerry xlsx.jerry.js; echo $?

如果成功,测试文件的内容将显示在 CSV 行中。状态代码 0 将打印在行之后。

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

[^1]: 见 read 于 "读取文件"

¥See read in "Reading Files"

[^2]: 见 "工作簿对象" 于 "SheetJS 数据模型"

¥See "Workbook Object" in "SheetJS Data Model"

[^3]: 见 "Sheet 对象"

¥See "Sheet Objects"

[^4]: 见 sheet_to_csv 于 "CSV 和文本"

¥See sheet_to_csv in "CSV and Text"