docs.sheetjs.com/docz/docs/03-demos/42-engines/09_hermes.md

494 lines
14 KiB
Markdown
Raw Normal View History

2023-05-30 06:41:09 +00:00
---
2023-08-03 02:49:32 +00:00
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.
2023-05-30 06:41:09 +00:00
pagination_prev: demos/bigdata/index
pagination_next: solutions/input
---
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
2023-08-03 02:49:32 +00:00
[Hermes](https://hermesengine.dev/) is an embeddable JS engine written in C++.
2023-05-30 06:41:09 +00:00
2023-08-03 02:49:32 +00:00
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
data from spreadsheets.
2023-05-30 06:41:09 +00:00
2023-08-03 02:49:32 +00:00
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
2023-05-30 06:41:09 +00:00
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.
:::
2023-08-03 02:49:32 +00:00
### Initialize Hermes
2023-05-30 06:41:09 +00:00
2023-08-03 02:49:32 +00:00
A Hermes engine instance is created with `facebook::hermes::makeHermesRuntime`:
2023-05-30 06:41:09 +00:00
```cpp
std::unique_ptr<facebook::jsi::Runtime> rt(facebook::hermes::makeHermesRuntime());
```
2023-08-03 02:49:32 +00:00
_Essential Objects_
2023-06-20 01:21:34 +00:00
2023-08-03 02:49:32 +00:00
Hermes does not expose a `console` or `global` variable, but they can be
synthesized from JS code in the runtime:
2023-06-20 01:21:34 +00:00
2023-08-03 02:49:32 +00:00
- `global` can be obtained from a reference to `this` in an unbound function:
2023-06-20 01:21:34 +00:00
```js
/* create global object */
var global = (function(){ return this; }).call(null);
2023-08-03 02:49:32 +00:00
```
- `console.log` can be constructed from the builtin `print` function:
```js
2023-06-20 01:21:34 +00:00
/* create a fake `console` from the hermes `print` builtin */
var console = { log: function(x) { print(x); } };
```
2023-08-03 02:49:32 +00:00
The code can be stored in a C string and evaluated using `prepareJavascript` to
prepare code and `evaluatePreparedJavascript` to evaluate:
2023-05-30 06:41:09 +00:00
```cpp
2023-08-03 02:49:32 +00:00
const char *init_code =
2023-05-30 06:41:09 +00:00
/* 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); } };"
2023-08-03 02:49:32 +00:00
;
auto src = std::make_shared<facebook::jsi::StringBuffer>(init_code);
2023-05-30 06:41:09 +00:00
auto js = rt->prepareJavaScript(src, std::string("<eval>"));
rt->evaluatePreparedJavaScript(js);
```
2023-08-03 02:49:32 +00:00
:::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<facebook::jsi::StringBuffer>(init_code);
auto js = rt->prepareJavaScript(src, std::string("<eval>"));
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.
2023-05-30 06:41:09 +00:00
The main library can be loaded by reading the script from the file system and
2023-08-03 02:49:32 +00:00
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 `<stdio.h>` methods are used:
2023-05-30 06:41:09 +00:00
```cpp
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);
fclose(f);
return buf;
}
2023-08-03 02:49:32 +00:00
// ...
/* 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
2023-05-30 06:41:09 +00:00
/* Unfortunately the library provides no C-friendly Buffer classes */
class CBuffer : public facebook::jsi::Buffer {
public:
CBuffer(const uint8_t *data, size_t size) : buf(data), sz(size) {}
size_t size() const override { return sz; }
const uint8_t *data() const override { return buf; }
private:
const uint8_t *buf;
size_t sz;
};
// ...
/* load SheetJS library */
auto src = std::make_shared<CBuffer>(CBuffer((uint8_t *)xlsx_full_min_js, sz));
2023-08-03 02:49:32 +00:00
```
_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
2023-05-30 06:41:09 +00:00
auto js = rt->prepareJavaScript(src, std::string("xlsx.full.min.js"));
rt->evaluatePreparedJavaScript(js);
```
2023-08-03 02:49:32 +00:00
_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:
2023-05-30 06:41:09 +00:00
```cpp
2023-08-03 02:49:32 +00:00
/* evaluate XLSX.version and capture the result */
auto src = std::make_shared<facebook::jsi::StringBuffer>("XLSX.version");
2023-05-30 06:41:09 +00:00
auto js = rt->prepareJavaScript(src, std::string("<eval>"));
2023-08-03 02:49:32 +00:00
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;
2023-05-30 06:41:09 +00:00
```
### Reading Files
2023-08-03 02:49:32 +00:00
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_
2023-05-30 06:41:09 +00:00
Hermes supports `ArrayBuffer` but has no simple helper to read raw memory.
Libraries are expected to implement `MutableBuffer`:
```cpp
2023-06-20 01:21:34 +00:00
/* ArrayBuffer constructor expects MutableBuffer */
2023-05-30 06:41:09 +00:00
class CMutableBuffer : public facebook::jsi::MutableBuffer {
public:
CMutableBuffer(uint8_t *data, size_t size) : buf(data), sz(size) {}
size_t size() const override { return sz; }
uint8_t *data() override { return buf; }
private:
uint8_t *buf;
size_t sz;
};
```
2023-08-03 02:49:32 +00:00
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>(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_
2023-05-30 06:41:09 +00:00
2023-06-20 01:21:34 +00:00
:::note pass
The stub function will be passed an `ArrayBuffer` object:
2023-05-30 06:41:09 +00:00
```js
function(buf) {
/* `buf` will be an ArrayBuffer */
var wb = XLSX.read(buf);
return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);
}
```
2023-06-20 01:21:34 +00:00
:::
2023-08-03 02:49:32 +00:00
The result after evaluating the stub is a `facebook::jsi::Value` object:
2023-05-30 06:41:09 +00:00
```cpp
2023-08-03 02:49:32 +00:00
/* define stub function to read and convert first sheet to CSV */
auto src = std::make_shared<facebook::jsi::StringBuffer>(
"(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("<eval>"));
facebook::jsi::Value funcval = rt->evaluatePreparedJavaScript(js);
```
To call this function, the opaque `Value` must be converted to a `Function`:
```cpp
facebook::jsi::Function func = func.asObject(*rt).asFunction(*rt);
```
2023-05-30 06:41:09 +00:00
2023-08-03 02:49:32 +00:00
The `Function` exposes a `call` method to perform the function invocation. The
stub accepts an `ArrayBuffer` argument:
2023-05-30 06:41:09 +00:00
2023-08-03 02:49:32 +00:00
```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;
2023-05-30 06:41:09 +00:00
```
## Complete Example
The "Integration Example" covers a traditional integration in a C++ application,
while the "CLI Test" demonstrates other concepts using the `hermes` CLI tool.
### Integration Example
:::note
2023-06-05 20:12:53 +00:00
This demo was tested in the following deployments:
2023-05-30 06:41:09 +00:00
2023-06-05 20:12:53 +00:00
| Architecture | Git Commit | Date |
|:-------------|:-----------|:-----------|
2023-08-28 22:40:53 +00:00
| `darwin-x64` | `70af78b` | 2023-08-27 |
2023-06-05 20:12:53 +00:00
| `darwin-arm` | `869312f` | 2023-06-05 |
2023-08-28 22:40:53 +00:00
| `linux-x64` | `70af78b` | 2023-08-27 |
2023-08-31 22:09:08 +00:00
| `linux-arm` | `70af78b` | 2023-08-27 |
2023-05-30 06:41:09 +00:00
:::
2023-08-28 22:40:53 +00:00
0) Install [dependencies](https://hermesengine.dev/docs/building-and-running/#dependencies)
<details><summary><b>Installation Notes</b> (click to show)</summary>
The official guidance[^5] has been verified in macOS and HoloOS (Linux).
On macOS:
2023-06-05 20:12:53 +00:00
```bash
brew install icu4c cmake ninja
```
2023-08-28 22:40:53 +00:00
On HoloOS (and other Arch Linux distros):
```bash
sudo pacman -Syu cmake git ninja icu python zip readline
```
2023-08-31 22:09:08 +00:00
On Debian and Ubuntu:
```bash
sudo apt install cmake git ninja-build libicu-dev python zip libreadline-dev
```
2023-08-28 22:40:53 +00:00
</details>
2023-06-05 20:12:53 +00:00
1) Make a project directory:
2023-05-30 06:41:09 +00:00
```bash
mkdir sheetjs-hermes
cd sheetjs-hermes
```
2023-06-05 20:12:53 +00:00
2) Download the [`Makefile`](pathname:///hermes/Makefile):
2023-05-30 06:41:09 +00:00
```bash
curl -LO https://docs.sheetjs.com/hermes/Makefile
```
2023-06-05 20:12:53 +00:00
3) Download [`sheetjs-hermes.cpp`](pathname:///hermes/sheetjs-hermes.cpp):
2023-05-30 06:41:09 +00:00
```bash
curl -LO https://docs.sheetjs.com/hermes/sheetjs-hermes.cpp
```
2023-06-05 20:12:53 +00:00
4) Build the library (this is the `init` target):
2023-05-30 06:41:09 +00:00
```bash
make init
```
2023-06-05 20:12:53 +00:00
5) Build the application:
2023-05-30 06:41:09 +00:00
```bash
make sheetjs-hermes
```
2023-06-05 20:12:53 +00:00
6) Download the standalone script and test file:
2023-05-30 06:41:09 +00:00
<ul>
<li><a href={`https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js`}>xlsx.full.min.js</a></li>
<li><a href="https://sheetjs.com/pres.numbers">pres.numbers</a></li>
</ul>
<CodeBlock language="bash">{`\
curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js
curl -LO https://sheetjs.com/pres.numbers`}
</CodeBlock>
2023-06-05 20:12:53 +00:00
7) Run the application:
2023-05-30 06:41:09 +00:00
```bash
./sheetjs-hermes pres.numbers
```
If successful, the program will print the library version number and the
contents of the first sheet as CSV rows.
### CLI Test
:::note
2023-08-28 22:40:53 +00:00
This demo was last tested on 2023 August 27 against Hermes version `0.11.0`.
2023-05-30 06:41:09 +00:00
:::
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.
0) Install the `hermes` command line tool
1) Download the standalone script and test file:
<ul>
<li><a href={`https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js`}>xlsx.full.min.js</a></li>
<li><a href="https://sheetjs.com/pres.numbers">pres.numbers</a></li>
</ul>
<CodeBlock language="bash">{`\
curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js
curl -LO https://sheetjs.com/pres.numbers`}
</CodeBlock>
2) Bundle the test file and create `payload.js`:
```bash
node -e "fs.writeFileSync('payload.js', 'var payload = \"' + fs.readFileSync('pres.numbers').toString('base64') + '\";')"
```
3) Create support scripts:
- `global.js` creates a `global` variable and defines a fake `console`:
```js title="global.js"
var global = (function(){ return this; }).call(null);
var console = { log: function(x) { print(x); } };
```
- `hermes.js` will call `XLSX.read` and `XLSX.utils.sheet_to_csv`:
```js title="hermes.js"
var wb = XLSX.read(payload, {type:'base64'});
console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]));
```
4) Create the amalgamation `xlsx.hermes.js`:
```bash
cat global.js xlsx.full.min.js payload.js hermes.js > xlsx.hermes.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.
5) Run the script using the Hermes standalone binary:
```bash
hermes xlsx.hermes.js
```
2023-08-03 02:49:32 +00:00
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)
2023-08-28 22:40:53 +00:00
[^5]: See ["Dependencies" in "Building and Running"](https://hermesengine.dev/docs/building-and-running/#dependencies) in the Hermes Documentation