docs.sheetjs.com/docz/docs/03-demos/12-engines/09_hermes.md
2023-06-19 21:21:34 -04:00

8.0 KiB

title pagination_prev pagination_next
C++ + Hermes demos/bigdata/index 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.

The Standalone scripts can be parsed and evaluated in a Hermes context.

:::caution Here be Dragons

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

The runtime can be initialized in one line:

std::unique_ptr<facebook::jsi::Runtime> rt(facebook::hermes::makeHermesRuntime());

Hermes does not expose a console or global variable, but those can be synthesized from JS code in the runtime.

:::note pass

The JavaScript code is shown below:

/* 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); } };

:::

The code can be stored in a C string and evaluated after creating a runtime:

auto src = std::make_shared<facebook::jsi::StringBuffer>(
  /* 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 js = rt->prepareJavaScript(src, std::string("<eval>"));
rt->evaluatePreparedJavaScript(js);

Load SheetJS Scripts

The main library can be loaded by reading the script from the file system and evaluating in the Hermes context:

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

/* 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 */
  size_t sz; char *xlsx_full_min_js = read_file("xlsx.full.min.js", &sz);
  auto src = std::make_shared<CBuffer>(CBuffer((uint8_t *)xlsx_full_min_js, sz));
  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:

auto src = std::make_shared<facebook::jsi::StringBuffer>(
  "console.log('SheetJS Library Version: ' + XLSX.version)"
);
auto js = rt->prepareJavaScript(src, std::string("<eval>"));
rt->evaluatePreparedJavaScript(js);

Reading Files

Hermes supports ArrayBuffer but has no simple helper to read raw memory. Libraries are expected to implement MutableBuffer:

/* ArrayBuffer constructor expects MutableBuffer */
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;
};
// ...
  /* load payload as ArrayBuffer */
  size_t sz; char *data = read_file(argv[1], &sz);
  auto payload = std::make_shared<CMutableBuffer>(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++.

:::note pass

The stub function will be passed an ArrayBuffer object:

function(buf) {
  /* `buf` will be an ArrayBuffer */
  var wb = XLSX.read(buf);
  return XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]);
}

:::

C++ integration code

  /* 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>"));
  auto func = rt->evaluatePreparedJavaScript(js);

  /* call stub function and capture result */
  auto csv = func.asObject(*rt).asFunction(*rt).call(*rt, ab);

  /* interpret as utf8 and print to stdout */
  std::string str = csv.getString(*rt).utf8(*rt);

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

This demo was tested in the following deployments:

Architecture Git Commit Date
darwin-x64 869312f 2023-05-30
darwin-arm 869312f 2023-06-05

:::

  1. Install build dependencies:
brew install icu4c cmake ninja
  1. Make a project directory:
mkdir sheetjs-hermes
cd sheetjs-hermes
  1. Download the Makefile:
curl -LO https://docs.sheetjs.com/hermes/Makefile
  1. Download sheetjs-hermes.cpp:
curl -LO https://docs.sheetjs.com/hermes/sheetjs-hermes.cpp
  1. Build the library (this is the init target):
make init
  1. Build the application:
make sheetjs-hermes
  1. Download the standalone script and test file:

{\ curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js curl -LO https://sheetjs.com/pres.numbers}

  1. Run the application:
./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

This demo was last tested on 2023 May 30 against Hermes version 0.11.0.

:::

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. Install the hermes command line tool

  2. Download the standalone script and test file:

{\ curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js curl -LO https://sheetjs.com/pres.numbers}

  1. Bundle the test file and create payload.js:
node -e "fs.writeFileSync('payload.js', 'var payload = \"' + fs.readFileSync('pres.numbers').toString('base64') + '\";')"
  1. Create support scripts:
  • global.js creates a global variable and defines a fake console:
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:
var wb = XLSX.read(payload, {type:'base64'});
console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]));
  1. Create the amalgamation xlsx.hermes.js:
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.

  1. Run the script using the Hermes standalone binary:
hermes xlsx.hermes.js

If successful, the script will print CSV data from the test file