diff --git a/.gitignore b/.gitignore index ad84161..d3ab5af 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ *.bak package-lock.json pnpm-lock.yaml -docs +/docs diff --git a/Makefile b/Makefile index 6d8e843..bfae0f0 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ init: .PHONY: dev dev: - cd docz; npm run start; cd .. + cd docz; npm run start -- --host=0.0.0.0; cd .. .PHONY: serve serve: diff --git a/docz/docs/03-demos/12-engines/02_v8.md b/docz/docs/03-demos/12-engines/02_v8.md new file mode 100644 index 0000000..84799cf --- /dev/null +++ b/docz/docs/03-demos/12-engines/02_v8.md @@ -0,0 +1,261 @@ +--- +title: C++ + V8 +pagination_prev: demos/bigdata/index +pagination_next: solutions/input +--- + +import current from '/version.js'; +import CodeBlock from '@theme/CodeBlock'; + +V8 is an embeddable JS engine written in C++. It powers Chromium and Chrome, +NodeJS and Deno, Adobe UXP and other platforms. + +The [Standalone scripts](/docs/getting-started/installation/standalone) can be +parsed and evaluated in a V8 context. + +## Integration Details + +_Initialize V8_ + +The official V8 `hello-world` example covers initialization and cleanup. For the +purposes of this demo, the key variables are noted below: + +```cpp +v8::Isolate* isolate = v8::Isolate::New(create_params); +v8::Local context = v8::Context::New(isolate); +``` + +The following helper function evaluates C strings as JS code: + +```cpp +v8::Local eval_code(v8::Isolate *i, v8::Local c, char* code) { + v8::Local source = v8::String::NewFromUtf8(i, code).ToLocalChecked(); + v8::Local script = v8::Script::Compile(i, source).ToLocalChecked(); + return script->Run(c).ToLocalChecked(); +} +``` + +_Load SheetJS Scripts_ + +The main library can be loaded by reading the scripts from the file system and +evaluating in the V8 context: + +```cpp +/* simple wrapper to read the entire script file */ +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; +} + +// ... + size_t sz; char *file = read_file("xlsx.full.min.js", &sz); + v8::Local result = eval_code(isolate, context, file); +``` + +To confirm the library is loaded, `XLSX.version` can be inspected: + +```cpp + /* get version string */ + v8::Local result = eval_code(isolate, context, "XLSX.version"); + v8::String::Utf8Value vers(isolate, result); + printf("SheetJS library version %s\n", *vers); +``` + +### Reading Files + +V8 supports `ArrayBuffer` natively. Assuming `buf` is a C byte array, with +length `len`, this snippet stores the data as an `ArrayBuffer` in global scope: + +```cpp +/* load C char array and save to an ArrayBuffer */ +std::unique_ptr back = v8::ArrayBuffer::NewBackingStore(isolate, len); +memcpy(back->Data(), buf, len); +v8::Local ab = v8::ArrayBuffer::New(isolate, std::move(back)); +v8::Maybe res = context->Global()->Set(context, v8::String::NewFromUtf8Literal(isolate, "buf"), ab); + +/* parse with SheetJS */ +v8::Local result = eval_code(isolate, context, "globalThis.wb = XLSX.read(buf)"); +``` + +`wb` will be a variable in the JS environment that can be inspected using the +various SheetJS API functions. + +### Writing Files + +The underlying memory from an `ArrayBuffer` can be recovered: + +```c +/* write with SheetJS using type: "array" */ +v8::Local result = eval_code(isolate, context, "XLSX.write(wb, {type:'array', bookType:'xlsb'})"); + +/* pull result back to C++ */ +v8::Local ab = v8::Local::Cast(result); +size_t sz = ab->ByteLength(); +char *buf = ab->Data(); +``` + +The resulting `buf` can be written to file with `fwrite`. + +## Complete Example + +:::note + +This demo was tested in the following deployments: + +| V8 Version | Platform | OS Version | Compiler | Date | +|:--------------|:-------------|:-----------|:---------------|:-----------| +| `11.3.244.11` | `darwin-x64` | macOS 13.2 | `clang 14.0.3` | 2023-05-20 | + +::: + +This program parses a file and prints CSV data from the first worksheet. It also +generates an XLSB file and writes to the filesystem. + +:::caution + +At the time of writing, there were errors in the official V8 embed guide for the +macOS platform. The correct instructions are included below. + +::: + +:::caution + +The build process is long and will test your patience. + +::: + +### Preparation + +1) Download and install `depot_tools`: + +```bash +mkdir -p /usr/local/lib +cd /usr/local/lib +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +``` + +2) Add the path to the `PATH` environment variable: + +```bash +export PATH="/usr/local/lib/depot_tools:$PATH" +``` + +At this point, it is strongly recommended to add the line to a shell startup +script such as `.bashrc` or `.zshrc` + +3) Run `gclient` once to update `depot_tools`: + +```bash +gclient +``` + +### Clone V8 + +4) Create a base directory: + +```bash +mkdir -p ~/dev/v8 +cd ~/dev/v8 +fetch v8 +cd v8 +``` + +Note that the actual repo will be placed in `~/dev/v8/v8`. + +5) Checkout the desired version. The following command pulls `11.3.244.11`: + +```bash +git checkout refs/tags/11.3.244.11 -b sample -t +``` + +### Build V8 + +6) Build the static library. + +```bash +tools/dev/v8gen.py x64.release.sample +ninja -C out.gn/x64.release.sample v8_monolith +``` + +7) Ensure the sample `hello-world` compiles and runs: + +```bash +g++ -I. -Iinclude samples/hello-world.cc -o hello_world -fno-rtti -lv8_monolith \ + -lv8_libbase -lv8_libplatform -ldl -Lout.gn/x64.release.sample/obj/ -pthread \ + -std=c++17 -DV8_COMPRESS_POINTERS=1 -DV8_ENABLE_SANDBOX +./hello_world +``` + +### Prepare Project + +8) Make a new project folder: + +```bash +cd ~/dev +mkdir sheetjs-v8 +cd sheetjs-v8 +``` + +9) Copy the sample source: + +```bash +cp ~/dev/v8/v8/samples/hello-world.cc . +``` + +10) Create symbolic links to the `include` headers and `obj` library folders: + +```bash +ln -s ~/dev/v8/v8/include +ln -s ~/dev/v8/v8/out.gn/x64.release.sample/obj +``` + +11) Build and run the `hello-world` example from this folder: + +```bash +g++ -I. -Iinclude hello-world.cc -o hello_world -fno-rtti -lv8_monolith \ + -lv8_libbase -lv8_libplatform -ldl -Lobj/ -pthread -std=c++17 \ + -DV8_COMPRESS_POINTERS=1 -DV8_ENABLE_SANDBOX +./hello_world +``` + +### Add SheetJS + +12) 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`} + + +13) Download [`sheetjs.v8.cc`](pathname:///v8/sheetjs.v8.cc): + +```bash +curl -LO https://docs.sheetjs.com/v8/sheetjs.v8.cc +``` + +14) Compile standalone `sheetjs.v8` binary + +```bash +g++ -I. -Iinclude sheetjs.v8.cc -o sheetjs.v8 -fno-rtti -lv8_monolith \ + -lv8_libbase -lv8_libplatform -ldl -Lobj/ -pthread -std=c++17 \ + -DV8_COMPRESS_POINTERS=1 -DV8_ENABLE_SANDBOX +``` + +15) Run the demo: + +```bash +./sheetjs.v8 pres.numbers +``` + +If the program succeeded, the CSV contents will be printed to console and the +file `sheetjsw.xlsb` will be created. That file can be opened with Excel. diff --git a/docz/docs/03-demos/12-engines/02_jsc.md b/docz/docs/03-demos/12-engines/04_jsc.md similarity index 100% rename from docz/docs/03-demos/12-engines/02_jsc.md rename to docz/docs/03-demos/12-engines/04_jsc.md diff --git a/docz/docs/03-demos/12-engines/05_goja.md b/docz/docs/03-demos/12-engines/06_goja.md similarity index 100% rename from docz/docs/03-demos/12-engines/05_goja.md rename to docz/docs/03-demos/12-engines/06_goja.md diff --git a/docz/docs/03-demos/12-engines/04_nashorn.md b/docz/docs/03-demos/12-engines/07_nashorn.md similarity index 100% rename from docz/docs/03-demos/12-engines/04_nashorn.md rename to docz/docs/03-demos/12-engines/07_nashorn.md diff --git a/docz/docs/03-demos/12-engines/07_chakra.md b/docz/docs/03-demos/12-engines/10_chakra.md similarity index 100% rename from docz/docs/03-demos/12-engines/07_chakra.md rename to docz/docs/03-demos/12-engines/10_chakra.md diff --git a/docz/static/v8/sheetjs.v8.cc b/docz/static/v8/sheetjs.v8.cc new file mode 100644 index 0000000..a6eb324 --- /dev/null +++ b/docz/static/v8/sheetjs.v8.cc @@ -0,0 +1,106 @@ +/* sheetjs.v8 (C) SheetJS -- https://sheetjs.com */ +/* based on the official V8 samples ("BSD-3-Clause") */ +#include +#include +#include +#include "include/libplatform/libplatform.h" +#include "include/v8.h" + +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; +} + +v8::Local eval_code(v8::Isolate *isolate, v8::Local context, char* code) { + v8::Local source = v8::String::NewFromUtf8(isolate, code).ToLocalChecked(); + v8::Local script = v8::Script::Compile(context, source).ToLocalChecked(); + return script->Run(context).ToLocalChecked(); +} +#define EVAL_CODE(x) eval_code(isolate, context, (char *)x) + +int main(int argc, char* argv[]) { + /* initialize -- this part is from the hello world example */ + v8::V8::InitializeICUDefaultLocation(argv[0]); + v8::V8::InitializeExternalStartupData(argv[0]); + std::unique_ptr platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + v8::Isolate* isolate = v8::Isolate::New(create_params); + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Local context = v8::Context::New(isolate); + v8::Context::Scope context_scope(context); + + /* load library */ + { + /* read file */ + size_t sz; char *file = read_file("xlsx.full.min.js", &sz); + if(!file) { perror("Error reading xlsx.full.min.js"); return 1; } + + /* evaluate */ + v8::Local result = EVAL_CODE(file); + + /* free */ + free(file); + } + + /* get version string */ + { + v8::Local result = EVAL_CODE("XLSX.version"); + v8::String::Utf8Value vers(isolate, result); + printf("SheetJS library version %s\n", *vers); + } + + /* read file */ + { + /* read bytes */ + size_t sz; char *file = read_file(argv[1], &sz); + if(!file) { perror("Error reading file"); return 1; } + + /* copy into array buffer and assign to `buf` in the global scope */ + { + std::unique_ptr back = v8::ArrayBuffer::NewBackingStore(isolate, sz); + memcpy(back->Data(), file, sz); + v8::Local ab = v8::ArrayBuffer::New(isolate, std::move(back)); + v8::Maybe res = context->Global()->Set(context, v8::String::NewFromUtf8Literal(isolate, "buf"), ab); + } + + printf("Loaded file %s\n", argv[1]); + } + + /* parse workbook and assign to global `wb` property */ + { + v8::Local result = EVAL_CODE("globalThis.wb = XLSX.read(buf)"); + } + + /* print CSV of first worksheet */ + { + v8::Local result = EVAL_CODE("XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])"); + v8::String::Utf8Value csv(isolate, result); + printf("%s\n", *csv); + } + + // write sheetjsw.xlsb + { + v8::Local result = EVAL_CODE("XLSX.write(wb, {type:'array', bookType:'xlsb'})"); + v8::Local ab = v8::Local::Cast(result); + FILE *f = fopen("sheetjsw.xlsb", "wb"); fwrite((char *)ab->Data(), 1, ab->ByteLength(), f); fclose(f); + } + } + + /* cleanup -- this part is from the hello world example */ + isolate->Dispose(); + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + delete create_params.array_buffer_allocator; + return 0; +}