From fecf228fedb4beacd6aeaeac450cb9453577296c Mon Sep 17 00:00:00 2001 From: SheetJS Date: Wed, 2 Aug 2023 22:49:32 -0400 Subject: [PATCH] hermes --- .../02-examples/04-import.md | 2 +- docz/docs/03-demos/09-cloud/02-netsuite.md | 7 +- docz/docs/03-demos/12-engines/08_quickjs.md | 4 +- docz/docs/03-demos/12-engines/09_hermes.md | 264 ++++++++++++++---- docz/docs/03-demos/12-engines/index.md | 6 +- docz/static/hermes/Makefile | 8 +- 6 files changed, 228 insertions(+), 63 deletions(-) diff --git a/docz/docs/02-getting-started/02-examples/04-import.md b/docz/docs/02-getting-started/02-examples/04-import.md index 14aad26..08f2bc1 100644 --- a/docz/docs/02-getting-started/02-examples/04-import.md +++ b/docz/docs/02-getting-started/02-examples/04-import.md @@ -1006,7 +1006,7 @@ When the app is loaded, the data will be displayed in rows. [^4]: See ["Workbook Object"](/docs/csf/book) [^5]: See ["Workbook Object"](/docs/csf/book) [^6]: See ["Sheet Objects"](/docs/csf/sheet) -[^7]: See [`sheet_to_csv` in "Utilities"](/docs/api/utilities/csv#csv-output) +[^7]: See [`sheet_to_html` in "Utilities"](/docs/api/utilities/html#html-table-output) [^8]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output) [^9]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output) [^10]: See [`!merges` in "Sheet Objects"](/docs/csf/sheet#worksheet-object) diff --git a/docz/docs/03-demos/09-cloud/02-netsuite.md b/docz/docs/03-demos/09-cloud/02-netsuite.md index a3bd8bc..0e46be1 100644 --- a/docz/docs/03-demos/09-cloud/02-netsuite.md +++ b/docz/docs/03-demos/09-cloud/02-netsuite.md @@ -1,13 +1,11 @@ --- -title: NetSuite -sidebar_title: NetSuite +title: Spreadsheets in NetSuite SuiteScripts +sidebar_label: NetSuite description: Automate the NetSuite ERP platform with SuiteScripts. Effortlessly read and write spreadsheets using SheetJS. Modernize Excel-powered business processes with confidence. pagination_prev: demos/local/index pagination_next: demos/extensions/index --- -# Spreadsheets in NetSuite SuiteScripts - import current from '/version.js'; import CodeBlock from '@theme/CodeBlock'; @@ -29,6 +27,7 @@ This demo was verified by NetSuite consultants in the following deployments: | ScheduledScript | 2.1 | 2023-03-09 | | Restlet | 2.1 | 2023-04-20 | | Suitelet | 2.1 | 2023-07-21 | +| MapReduceScript | 2.1 | 2023-07-31 | ::: diff --git a/docz/docs/03-demos/12-engines/08_quickjs.md b/docz/docs/03-demos/12-engines/08_quickjs.md index 94d4c96..0edbdce 100644 --- a/docz/docs/03-demos/12-engines/08_quickjs.md +++ b/docz/docs/03-demos/12-engines/08_quickjs.md @@ -1,13 +1,11 @@ --- -title: C + QuickJS +title: Data Processing with QuickJS sidebar_label: C + QuickJS description: Process structured data in C programs. Seamlessly integrate spreadsheets into your program by pairing QuickJS and SheetJS. Supercharge programs with modern data tools. pagination_prev: demos/bigdata/index pagination_next: solutions/input --- -# Data Processing with QuickJS - import current from '/version.js'; import CodeBlock from '@theme/CodeBlock'; diff --git a/docz/docs/03-demos/12-engines/09_hermes.md b/docz/docs/03-demos/12-engines/09_hermes.md index cdeb7f6..26d4fab 100644 --- a/docz/docs/03-demos/12-engines/09_hermes.md +++ b/docz/docs/03-demos/12-engines/09_hermes.md @@ -1,5 +1,7 @@ --- -title: C++ + Hermes +title: Sharing Sheets with Hermes +sidebar_label: C++ + Hermes +description: Process structured data in C++ programs. Seamlessly integrate spreadsheets into your program by pairing Hermes and SheetJS. Handle the most complex files without breaking a sweat. pagination_prev: demos/bigdata/index pagination_next: solutions/input --- @@ -7,62 +9,115 @@ pagination_next: solutions/input import current from '/version.js'; import CodeBlock from '@theme/CodeBlock'; -Hermes is an embeddable JS engine written in C++. With some light shims, it can -run the standalone browser scripts. +[Hermes](https://hermesengine.dev/) is an embeddable JS engine written in C++. -The [Standalone scripts](/docs/getting-started/installation/standalone) can be -parsed and evaluated in a Hermes context. +[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing +data from spreadsheets. -:::caution Here be Dragons +This demo uses Hermes and SheetJS to pull data from a spreadsheet and print CSV +rows. We'll explore how to load SheetJS in a Hermes context and process +spreadsheets from a C++ program. + +The ["Integration Example"](#integration-example) section includes a complete +command-line tool for reading data from files. + +## Integration Details + +:::info pass + +Many Hermes functions are not documented. The explanation was verified against +commit `70af78b`. + +::: + +:::warning pass The main target for Hermes is React Native. At the time of writing, there was no official documentation for embedding the Hermes engine in C++ programs. ::: -## Integration Details +### Initialize Hermes -_Initialize Hermes_ - -The runtime can be initialized in one line: +A Hermes engine instance is created with `facebook::hermes::makeHermesRuntime`: ```cpp std::unique_ptr rt(facebook::hermes::makeHermesRuntime()); ``` -Hermes does not expose a `console` or `global` variable, but those can be -synthesized from JS code in the runtime. +_Essential Objects_ -:::note pass +Hermes does not expose a `console` or `global` variable, but they can be +synthesized from JS code in the runtime: -The JavaScript code is shown below: +- `global` can be obtained from a reference to `this` in an unbound function: ```js /* create global object */ var global = (function(){ return this; }).call(null); +``` + +- `console.log` can be constructed from the builtin `print` function: + +```js /* create a fake `console` from the hermes `print` builtin */ var console = { log: function(x) { print(x); } }; ``` -::: - -The code can be stored in a C string and evaluated after creating a runtime: +The code can be stored in a C string and evaluated using `prepareJavascript` to +prepare code and `evaluatePreparedJavascript` to evaluate: ```cpp -auto src = std::make_shared( +const char *init_code = /* create global object */ "var global = (function(){ return this; }).call(null);" /* create a fake `console` from the hermes `print` builtin */ "var console = { log: function(x) { print(x); } };" -); + ; +auto src = std::make_shared(init_code); auto js = rt->prepareJavaScript(src, std::string("")); rt->evaluatePreparedJavaScript(js); ``` -_Load SheetJS Scripts_ +:::info Exception handling + +Standard C++ exception handling patterns are used in Hermes integration code. +The base class for Hermes exceptions is `facebook::jsi::JSIException`: + +```cpp +try { + const char *init_code = "..."; + auto src = std::make_shared(init_code); + auto js = rt->prepareJavaScript(src, std::string("")); + rt->evaluatePreparedJavaScript(js); +} catch (const facebook::jsi::JSIException &e) { + std::cerr << "JavaScript exception: " << e.what() << std::endl; + return 1; +} +``` + +::: + +### Load SheetJS Scripts + +[SheetJS Standalone scripts](/docs/getting-started/installation/standalone) can +be parsed and evaluated in a Hermes context. The main library can be loaded by reading the script from the file system and -evaluating in the Hermes context: +evaluating in the Hermes context. + +:::tip pass + +There are nonstandard tricks to embed the entire script in the binary. There are +language proposals such as `#embed` (mirroring the same feature in C23). + +For simplicity, the examples read the script file from the filesystem. + +::: + +_Reading scripts from the filesystem_ + +For the purposes of this demo, the standard C `` methods are used: ```cpp static char *read_file(const char *filename, size_t *sz) { @@ -75,6 +130,17 @@ static char *read_file(const char *filename, size_t *sz) { return buf; } +// ... + /* read SheetJS library from filesystem */ + size_t sz; char *xlsx_full_min_js = read_file("xlsx.full.min.js", &sz); +``` + +_Hermes Wrapper_ + +Hermes does not provide a friendly way to prepare JavaScript code stored in a +standard heap-allocated C string. Fortunately a wrapper can be created: + +```cpp /* Unfortunately the library provides no C-friendly Buffer classes */ class CBuffer : public facebook::jsi::Buffer { public: @@ -89,24 +155,67 @@ class CBuffer : public facebook::jsi::Buffer { // ... /* load SheetJS library */ - size_t sz; char *xlsx_full_min_js = read_file("xlsx.full.min.js", &sz); auto src = std::make_shared(CBuffer((uint8_t *)xlsx_full_min_js, sz)); +``` + +_Evaluating SheetJS Library Code_ + +The code wrapper can be "prepared" with `prepareJavascript` and "evaluated" with +`evaluatePreparedJavascript`. + +The second argument to `preparedJavascript` is a C++ `std::string` that holds +the source URL. Typically a name like `xlsx.full.min.js` helps distinguish +SheetJS library exceptions from other parts of the application. + +```cpp auto js = rt->prepareJavaScript(src, std::string("xlsx.full.min.js")); rt->evaluatePreparedJavaScript(js); ``` -To confirm the library is loaded, `XLSX.version` can be printed to the console: +_Testing_ + +If the library is loaded, `XLSX.version` will be a string. This string can be +pulled into the main C++ program. + +The `evaluatePreparedJavascript` method returns a `facebook::jsi::Value` object +that represents the result: ```cpp -auto src = std::make_shared( - "console.log('SheetJS Library Version: ' + XLSX.version)" -); +/* evaluate XLSX.version and capture the result */ +auto src = std::make_shared("XLSX.version"); auto js = rt->prepareJavaScript(src, std::string("")); -rt->evaluatePreparedJavaScript(js); +facebook::jsi::Value jsver = rt->evaluatePreparedJavaScript(js); +``` + +The `getString` method extracts the string value and returns an internal string +object (`facebook::jsi::String`). Given that string object, the `utf8` method +returns a proper C++ `std::string` that can be printed: + +```cpp +/* pull the version string into C++ code and print */ +facebook::jsi::String jsstr = jsver.getString(*rt); +std::string cppver = jsstr.utf8(*rt); +std::cout << "SheetJS version " << cppver << std::endl; ``` ### Reading Files +Typically C++ code will read files and Hermes will project the data in the JS +engine as an `ArrayBuffer`. SheetJS libraries can parse `ArrayBuffer` data. + +Standard SheetJS operations can pick the first worksheet and generate CSV string +data from the worksheet. Hermes provides methods to convert the JS strings back +to `std::string` objects for further processing in C++. + +:::note + +It is strongly recommended to create a stub function to perform the entire +workflow in JS code and pass the final result back to C++. + +::: + +_Hermes Wrapper_ + Hermes supports `ArrayBuffer` but has no simple helper to read raw memory. Libraries are expected to implement `MutableBuffer`: @@ -122,15 +231,48 @@ class CMutableBuffer : public facebook::jsi::MutableBuffer { uint8_t *buf; size_t sz; }; -// ... - /* load payload as ArrayBuffer */ - size_t sz; char *data = read_file(argv[1], &sz); - auto payload = std::make_shared(CMutableBuffer((uint8_t *)data, sz)); - auto ab = facebook::jsi::ArrayBuffer(*rt, payload); ``` -It is strongly recommended to create a stub function to perform the entire -workflow in JS code and pass the final result back to C++. +A `facebook::jsi::ArrayBuffer` object can be created using the wrapper: + +```cpp +/* load payload as ArrayBuffer */ +size_t sz; char *data = read_file("pres.xlsx", &sz); +auto payload = std::make_shared(CMutableBuffer((uint8_t *)data, sz)); +auto ab = facebook::jsi::ArrayBuffer(*rt, payload); +``` + +_SheetJS Operations_ + +In this example, the goal is to pull the first worksheet and generate CSV rows. + +`XLSX.read`[^1] parses the `ArrayBuffer` and returns a SheetJS workbook object: + +```js +var wb = XLSX.read(buf); +``` + +The `SheetNames` property[^2] is an array of the sheet names in the workbook. +The first sheet name can be obtained with the following JS snippet: + +```js +var first_sheet_name = wb.SheetNames[0]; +``` + +The `Sheets` property[^3] is an object whose keys are sheet names and whose +corresponding values are worksheet objects. + +```js +var first_sheet = wb.Sheets[first_sheet_name]; +``` + +The `sheet_to_csv` utility function[^4] generates a CSV string from the sheet: + +```js +var csv = XLSX.utils.sheet_to_csv(first_sheet); +``` + +_C++ integration code_ :::note pass @@ -146,24 +288,41 @@ function(buf) { ::: -_C++ integration code_ +The result after evaluating the stub is a `facebook::jsi::Value` object: ```cpp - /* define stub function to read and convert first sheet to CSV */ - auto src = std::make_shared( - "(function(buf) {" - "var wb = XLSX.read(buf);" - "return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);" - "})" - ); - auto js = rt->prepareJavaScript(src, std::string("")); - auto func = rt->evaluatePreparedJavaScript(js); +/* define stub function to read and convert first sheet to CSV */ +auto src = std::make_shared( + "(function(buf) {" + "var wb = XLSX.read(buf);" + "return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);" + "})" +); +auto js = rt->prepareJavaScript(src, std::string("")); +facebook::jsi::Value funcval = rt->evaluatePreparedJavaScript(js); +``` - /* call stub function and capture result */ - auto csv = func.asObject(*rt).asFunction(*rt).call(*rt, ab); +To call this function, the opaque `Value` must be converted to a `Function`: - /* interpret as utf8 and print to stdout */ - std::string str = csv.getString(*rt).utf8(*rt); +```cpp +facebook::jsi::Function func = func.asObject(*rt).asFunction(*rt); +``` + +The `Function` exposes a `call` method to perform the function invocation. The +stub accepts an `ArrayBuffer` argument: + +```cpp +/* call stub function and capture result */ +facebook::jsi::Value csv = func.call(*rt, ab); +``` + +In the same way the library version string was pulled into C++ code, the CSV +data can be captured using `getString` and `utf8` methods: + +```cpp +/* interpret as utf8 */ +std::string str = csv.getString(*rt).utf8(*rt); +std::cout << str << std::endl; ``` ## Complete Example @@ -179,7 +338,7 @@ This demo was tested in the following deployments: | Architecture | Git Commit | Date | |:-------------|:-----------|:-----------| -| `darwin-x64` | `9f8603b` | 2023-07-24 | +| `darwin-x64` | `70af78b` | 2023-08-02 | | `darwin-arm` | `869312f` | 2023-06-05 | ::: @@ -304,4 +463,9 @@ ready, it will read the bundled test data and print the contents as CSV. hermes xlsx.hermes.js ``` -If successful, the script will print CSV data from the test file \ No newline at end of file +If successful, the script will print CSV data from the test file. + +[^1]: See [`read` in "Reading Files"](/docs/api/parse-options) +[^2]: See ["Workbook Object"](/docs/csf/book) +[^3]: See ["Workbook Object"](/docs/csf/book) +[^4]: See [`sheet_to_csv` in "Utilities"](/docs/api/utilities/csv#csv-output) diff --git a/docz/docs/03-demos/12-engines/index.md b/docz/docs/03-demos/12-engines/index.md index c3695ab..23fab23 100644 --- a/docz/docs/03-demos/12-engines/index.md +++ b/docz/docs/03-demos/12-engines/index.md @@ -37,7 +37,7 @@ var global = (function(){ return this; }).call(null); **Console** Some engines do not provide a `console` object. `console.log` can be shimmed -using the engine functionality. For example, `hermes` provides `print()`: +using the engine functionality. For example, `hermes`[^1] provides `print()`: ```js var console = { log: function(x) { print(x); } }; @@ -235,3 +235,7 @@ NodeJS and Deno, Adobe UXP and other platforms. This demo has been moved [to a dedicated page](/docs/demos/engines/v8). The demo includes examples in C++ and Rust. + +The ["Python + Pandas" demo](/docs/demos/engines/pandas) uses V8 with Python. + +[^1]: See ["Initialize Hermes"](/docs/demos/engines/hermes#initialize-hermes) in the Hermes demo. \ No newline at end of file diff --git a/docz/static/hermes/Makefile b/docz/static/hermes/Makefile index 3aa711f..acfe45c 100644 --- a/docz/static/hermes/Makefile +++ b/docz/static/hermes/Makefile @@ -1,10 +1,10 @@ # Note: The official Hermes documentation includes zero guidance on embedding. -# Tested against commit 9f8603b9886c957e0ccead61fe4380616188bbb4 +# Tested against commit 70af78ba69391645749b40a3674d7321c4d6177a .PHONY: doit doit: sheetjs-hermes - curl -LO https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js - curl -LO https://sheetjs.com/pres.numbers + if [ ! -e xlsx.full.min.js ]; then curl -LO https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js; fi + if [ ! -e pres.numbers ]; then curl -LO https://sheetjs.com/pres.numbers; fi ./sheetjs-hermes pres.numbers .PHONY: clean @@ -53,5 +53,5 @@ sheetjs-hermes: sheetjs-hermes.cpp init .PHONY: init init: - if [ ! -e hermes ]; then git clone https://github.com/facebook/hermes.git; cd hermes; git checkout 9f8603b9886c957e0ccead61fe4380616188bbb4; cd ..; fi + if [ ! -e hermes ]; then git clone https://github.com/facebook/hermes.git; cd hermes; git checkout 70af78ba69391645749b40a3674d7321c4d6177a; cd ..; fi if [ ! -e build_release ]; then cmake -S hermes -B build_release -G Ninja -DCMAKE_BUILD_TYPE=Release; cmake --build ./build_release; fi