From 671729b289e3e876c5ed3f6ad06cf942eccbec0f Mon Sep 17 00:00:00 2001 From: SheetJS Date: Thu, 25 Apr 2024 04:39:55 -0400 Subject: [PATCH] JavaScriptCore C demo --- docz/data/engines.xls | 12 +- .../docs/03-demos/03-net/08-headless/index.md | 2 +- docz/docs/03-demos/42-engines/04-jsc.md | 366 +++++++++++++++++- docz/docs/12-constellation/02-frac.md | 2 +- docz/static/jsc/sheetjs-jsc.c | 112 ++++++ 5 files changed, 476 insertions(+), 18 deletions(-) create mode 100644 docz/static/jsc/sheetjs-jsc.c diff --git a/docz/data/engines.xls b/docz/data/engines.xls index cf2ddd4..a1fefa5 100644 --- a/docz/data/engines.xls +++ b/docz/data/engines.xls @@ -37,7 +37,7 @@ - @@ -91,6 +91,16 @@ + + JSC + C++ + + + + + + + Jint C# diff --git a/docz/docs/03-demos/03-net/08-headless/index.md b/docz/docs/03-demos/03-net/08-headless/index.md index d6c3460..9e62d28 100644 --- a/docz/docs/03-demos/03-net/08-headless/index.md +++ b/docz/docs/03-demos/03-net/08-headless/index.md @@ -409,7 +409,7 @@ This demo was tested in the following environments: |:-------------|:----------|:-----------| | `darwin-x64` | `2.1.1` | 2024-03-15 | | `win10-x64` | `2.1.1` | 2024-03-24 | -| `linux-x64` | `2.1.1` | 2024-03-29 | +| `linux-x64` | `2.1.1` | 2024-04-25 | ::: 1) [Download and extract PhantomJS](https://phantomjs.org/download.html) diff --git a/docz/docs/03-demos/42-engines/04-jsc.md b/docz/docs/03-demos/42-engines/04-jsc.md index e2b59cf..d90f136 100644 --- a/docz/docs/03-demos/42-engines/04-jsc.md +++ b/docz/docs/03-demos/42-engines/04-jsc.md @@ -5,60 +5,171 @@ pagination_next: solutions/input --- import current from '/version.js'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; -iOS and MacOS ship with the JavaScriptCore framework for running JS code from -Swift and Objective-C. Hybrid function invocation is tricky, but explicit data -passing is straightforward. The demo shows a standalone Swift sample for MacOS. +[JavaScriptCore](https://developer.apple.com/documentation/javascriptcore) (JSC) +is the JavaScript engine powering the Safari web browser. + +[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing +data from spreadsheets. + +This demo uses JSC and SheetJS to read and write spreadsheets. We'll explore how +to load SheetJS in a JSC context and process spreadsheets and structured data +from C++ and Swift programs. + +## Integration Details The [SheetJS Standalone scripts](/docs/getting-started/installation/standalone) can be parsed and evaluated in a JSC context. -:::danger Platform Limitations - -JavaScriptCore is primarily deployed in MacOS and iOS applications. There is -some experimental support through the Bun runtime, but apps intending to support -Windows / Linux / Android should try to embed [V8](/docs/demos/engines/v8). - -::: - -## Integration Details + + Binary strings can be passed back and forth using `String.Encoding.isoLatin1`. -_Initialize JavaScriptCore_ +The SheetJS `read` method[^1], with the `"binary"` type, can parse binary strings. -JSC does not provide a `global` variable. It can be created in one line: +The `write` method[^2], with the `"binary"` type, can create binary strings. + + + + +JSC provides a few special methods for working with `Uint8Array` objects: + +- `JSObjectMakeTypedArrayWithBytesNoCopy`[^3] creates a typed array from a +pointer and size. It uses the memory address directly (no copy). + +- `JSObjectGetTypedArrayLength`[^4] and `JSObjectGetTypedArrayBytesPtr`[^5] can +return a pointer and size pair from a `Uint8Array` in the JSC engine. + +The SheetJS `read` method[^6] can process `Uint8Array` objects. + +The `write` method[^7], with the `"buffer"` type, creates `Uint8Array` data. + + + + +### Initialize JSC + + + + +A JSC context can be created with the `JSContext` function: ```swift var context: JSContext! do { context = JSContext(); context.exceptionHandler = { _, X in if let e = X { print(e.toString()!); }; }; +} catch { print(error.localizedDescription); } +``` + + + + +A JSC context can be created with the `JSGlobalContextCreate` function: + +```cpp +JSGlobalContextRef ctx = JSGlobalContextCreate(NULL); +``` + + + + +JSC does not provide a `global` variable. It can be created in one line: + + + + +```swift +do { // highlight-next-line context.evaluateScript("var global = (function(){ return this; }).call(null);"); } catch { print(error.localizedDescription); } ``` -_Load SheetJS Scripts_ + + + +```cpp +#define DOIT(cmd) \ + JSStringRef script = JSStringCreateWithUTF8CString(cmd); \ + JSValueRef result = JSEvaluateScript(ctx, script, NULL, NULL, 0, NULL); \ + JSStringRelease(script); + +{ DOIT("var global = (function(){ return this; }).call(null);") } +``` + + + + +### Load SheetJS Scripts The main library can be loaded by reading the scripts from the file system and evaluating in the JSC context: + + + ```swift let src = try String(contentsOfFile: "xlsx.full.min.js"); context.evaluateScript(src); ``` + + + +```cpp +/* load library */ +{ + size_t sz = 0; char *file = read_file("xlsx.full.min.js", &sz); + DOIT(file); +} +``` + + + + To confirm the library is loaded, `XLSX.version` can be inspected: + + + ```swift let XLSX: JSValue! = context.objectForKeyedSubscript("XLSX"); if let ver = XLSX.objectForKeyedSubscript("version") { print(ver.toString()); } ``` + + + +```cpp +#define JS_STR_TO_C \ + JSStringRef str = JSValueToStringCopy(ctx, result, NULL); \ + size_t sz = JSStringGetMaximumUTF8CStringSize(str); \ + char *buf = (char *)malloc(sz); \ + JSStringGetUTF8CString(str, buf, sz); \ + +/* get version string */ +{ + DOIT("XLSX.version") + + JS_STR_TO_C + + printf("SheetJS library version %s\n", buf); +} +``` + + + + ### Reading Files + + + `String(contentsOf:encoding:)` reads from a path and returns an encoded string: ```swift @@ -104,8 +215,41 @@ For broad compatibility with Swift versions, the demo uses the String method. + + + +There are a few steps for loading data into the JSC engine: + +A) The file must be read into a `char*` buffer (using standard C methods) + +```cpp +size_t sz; char *file = read_file(argv[1], &sz); +``` + +B) The typed array must be created with `JSObjectMakeTypedArrayWithBytesNoCopy` + +```cpp +JSValueRef u8 = JSObjectMakeTypedArrayWithBytesNoCopy(ctx, kJSTypedArrayTypeUint8Array, file, sz, NULL, NULL, NULL); +``` + +C) The typed array must be bound to a variable in the global scope: + +```cpp +/* assign to `global.buf` */ +JSObjectRef global = JSContextGetGlobalObject(ctx); +JSStringRef key = JSStringCreateWithUTF8CString("buf"); +JSObjectSetProperty(ctx, global, key, u8, 0, NULL); +JSStringRelease(key); +``` + + + + ### Writing Files + + + When writing to binary string in JavaScriptCore, the result should be stored in a variable and converted to string in Swift: @@ -126,8 +270,41 @@ let out_path = shared_dir.appendingPathComponent("sheetjsw.xlsx"); try? out.write(to: out_path, atomically: false, encoding: String.Encoding.isoLatin1); ``` + + + +The SheetJS `write` method with type `"buffer"` will return a `Uint8Array` object: + +```cpp +DOIT("XLSX.write(wb, {type:'buffer', bookType:'xlsb'});") +JSObjectRef u8 = JSValueToObject(ctx, result, NULL); +``` + +Given the result object, `JSObjectGetTypedArrayLength` pulls the length into C: + +```cpp +size_t sz = JSObjectGetTypedArrayLength(ctx, u8, NULL); +``` + +`JSObjectGetTypedArrayBytesPtr` returns a pointer to the result buffer: + +```cpp +char *buf = (char *)JSObjectGetTypedArrayBytesPtr(ctx, u8, NULL); +``` + +The data can be written to file using standard C methods: + +```cpp +FILE *f = fopen("sheetjsw.xlsb", "wb"); fwrite(buf, 1, sz, f); fclose(f); +``` + + + + ## Complete Example +### Swift + :::note pass This demo was tested in the following environments: @@ -200,3 +377,162 @@ swiftc SheetJSCore.swift main.swift -o SheetJSwift If successful, a CSV will be printed to console. The script also tries to write to `SheetJSwift.xlsx`. That file can be verified by opening in Excel / Numbers. + +### C++ + +:::note pass + +This demo was tested in the following environments: + +| Architecture | Version | Date | +|:-------------|:-----------------|:-----------| +| `darwin-x64` | `7618.1.15.14.7` | 2024-04-24 | +| `linux-x64` | `7618.1.15.14.7` | 2024-04-24 | + +::: + +0) Install dependencies + +
+ Installation Notes (click to show) + +On the Steam Deck, a few dependencies must be installed before building JSC: + +```bash +sudo pacman -Syu base-devel cmake ruby icu glibc linux-api-headers +``` + +
+ +1) Create a project folder: + +```bash +mkdir sheetjs-jsc +cd sheetjs-jsc +``` + +2) Download and extract the WebKit snapshot: + +```bash +curl -LO https://codeload.github.com/WebKit/WebKit/zip/refs/tags/WebKit-7618.1.15.14.7 +mv WebKit-7618.1.15.14.7 WebKit.zip +unzip WebKit.zip +``` + +3) Build JavaScriptCore: + + + + +```bash +cd WebKit-WebKit-7618.1.15.14.7 +Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON" +cd .. +``` + +:::caution pass + +When this demo was tested on macOS, the build failed with the error message + +``` +Source/WTF/wtf/text/ASCIILiteral.h:65:34: error: use of undeclared identifier 'NSString' + WTF_EXPORT_PRIVATE RetainPtr createNSString() const; + ^ +1 error generated. +``` + +The referenced header file must be patched to declare `NSString`: + +```objc title="Source/WTF/wtf/text/ASCIILiteral.h (add highlighted lines)" +#include + +// highlight-start +#ifdef __OBJC__ +@class NSString; +#endif +// highlight-end + +namespace WTF { +``` + +::: + + + + +```bash +cd WebKit-WebKit-7618.1.15.14.7 +env CFLAGS="-Wno-error=dangling-reference -Wno-dangling-reference" CXXFLAGS="-Wno-error=dangling-reference -Wno-dangling-reference" Tools/Scripts/build-webkit --jsc-only --cmakeargs="-Wno-error -DENABLE_STATIC_JSC=ON -DUSE_THIN_ARCHIVES=OFF -DCMAKE_C_FLAGS="-Wno-error -Wno-dangling-reference" -DCMAKE_CXX_FLAGS=-Wno-error -Wno-dangling-reference" --make-args="-j1 -Wno-error -Wno-error=dangling-reference" -j1 +cd .. +``` + +:::danger pass + +When this was last tested on the Steam Deck, the build ran for 24 minutes! + +::: + + + + +4) Create a symbolic link to the `Release` folder in the source tree: + +```bash +ln -s WebKit-WebKit-7618.1.15.14.7/WebKitBuild/JSCOnly/Release/ . +``` + +5) Download [`sheetjs-jsc.c`](pathname:///jsc/sheetjs-jsc.c): + +```bash +curl -LO https://docs.sheetjs.com/jsc/sheetjs-jsc.c +``` + +6) Compile the program: + + + + +```bash +g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lbmalloc -licucore -lWTF -lJavaScriptCore -IRelease/JavaScriptCore/Headers +``` + + + + +```bash +g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lJavaScriptCore -lWTF -lbmalloc -licui18n -licuuc -latomic -IRelease/JavaScriptCore/Headers +``` + + + + +7) Download the SheetJS Standalone script and the test file. Save both files in +the project directory: + + + +{`\ +curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js +curl -LO https://sheetjs.com/pres.numbers`} + + +8) Run the program: + +```bash +./sheetjs-jsc pres.numbers +``` + +If successful, a CSV will be printed to console. The script also tries to write +to `sheetjsw.xlsb`, which can be opened in a spreadsheet editor. + +[^1]: See [`read` in "Reading Files"](/docs/api/parse-options) +[^2]: See [`writeFile` in "Writing Files"](/docs/api/write-options) +[^3]: See [`JSObjectMakeTypedArrayWithBytesNoCopy`](https://developer.apple.com/documentation/javascriptcore/jsobjectmaketypedarraywithbytesnocopy(_:_:_:_:_:_:_:)/) in the JavaScriptCore documentation. +[^4]: See [`JSObjectGetTypedArrayLength`](https://developer.apple.com/documentation/javascriptcore/jsobjectgettypedarraylength(_:_:_:)/) in the JavaScriptCore documentation. +[^5]: See [`JSObjectGetTypedArrayBytesPtr`]( +https://developer.apple.com/documentation/javascriptcore/jsobjectgettypedarraybytesptr(_:_:_:)/) in the JavaScriptCore documentation. +[^6]: See [`read` in "Reading Files"](/docs/api/parse-options) +[^7]: See [`writeFile` in "Writing Files"](/docs/api/write-options) \ No newline at end of file diff --git a/docz/docs/12-constellation/02-frac.md b/docz/docs/12-constellation/02-frac.md index 705b1e1..7fe0a18 100644 --- a/docz/docs/12-constellation/02-frac.md +++ b/docz/docs/12-constellation/02-frac.md @@ -117,7 +117,7 @@ The estimate can be recovered from the array: var estimate = int + num / den; ``` -If `mixed` is `false`, then `int = 0` and `0` ≤ `|num|` < `den` ≤ `D` +If `mixed` is `false`, then `int = 0` and `0` < `den` ≤ `D` If `mixed` is `true`, then `0` ≤ `num` < `den` ≤ `D` diff --git a/docz/static/jsc/sheetjs-jsc.c b/docz/static/jsc/sheetjs-jsc.c new file mode 100644 index 0000000..cdf6ea1 --- /dev/null +++ b/docz/static/jsc/sheetjs-jsc.c @@ -0,0 +1,112 @@ +#include +#include +#include + +/* 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(buf, 1, fsize, f); + fclose(f); + return buf; +} + +#define JS_STR_TO_C \ + JSStringRef str = JSValueToStringCopy(ctx, result, NULL); \ + size_t sz = JSStringGetMaximumUTF8CStringSize(str); \ + char *buf = (char *)malloc(sz); \ + JSStringGetUTF8CString(str, buf, sz); \ + +#define DOIT(cmd) \ + JSStringRef script = JSStringCreateWithUTF8CString(cmd); \ + JSValueRef result = JSEvaluateScript(ctx, script, NULL, NULL, 0, NULL); \ + JSStringRelease(script); + +int main(int argc, char **argv) { + int res = 0; + size_t sz = 0; + char *file = NULL; + + /* initialize */ + JSGlobalContextRef ctx = JSGlobalContextCreate(NULL); + /* JSC does not expose a standard "global" by default */ + { DOIT("var global = (function(){ return this; }).call(null);") } + + /* load library */ + { + file = read_file("xlsx.full.min.js", &sz); + DOIT(file); + free(file); + } + + /* get version string */ + { + DOIT("XLSX.version") + + if(!JSValueIsString(ctx, result)) { + printf("Could not get SheetJS version.\n"); + res = 1; goto cleanup; + } + + JS_STR_TO_C + + printf("SheetJS library version %s\n", buf); + free(buf); + JSStringRelease(str); + } + + /* read file */ + file = read_file(argv[1], &sz); + { + /* push data to JSC */ + JSValueRef u8 = JSObjectMakeTypedArrayWithBytesNoCopy(ctx, kJSTypedArrayTypeUint8Array, file, sz, NULL, NULL, NULL); + + /* assign to `global.buf` */ + JSObjectRef global = JSContextGetGlobalObject(ctx); + JSStringRef key = JSStringCreateWithUTF8CString("buf"); + JSObjectSetProperty(ctx, global, key, u8, 0, NULL); + JSStringRelease(key); + } + + /* parse workbook and print CSV */ + { + DOIT( + "var wb = XLSX.read(global.buf);" + "var ws = wb.Sheets[wb.SheetNames[0]];" + "XLSX.utils.sheet_to_csv(ws)" + ) + + if(!JSValueIsString(ctx, result)) { + printf("Could not generate CSV.\n"); + res = 2; goto cleanup; + } + + JS_STR_TO_C + + printf("%s\n", buf); + free(buf); + JSStringRelease(str); + } + + /* write file */ + { + DOIT("XLSX.write(wb, {type:'buffer', bookType:'xlsb'});") + + /* pull Uint8Array data back to C */ + JSObjectRef u8 = JSValueToObject(ctx, result, NULL); + size_t sz = JSObjectGetTypedArrayLength(ctx, u8, NULL); + char *buf = (char *)JSObjectGetTypedArrayBytesPtr(ctx, u8, NULL); + + /* save file */ + FILE *f = fopen("sheetjsw.xlsb", "wb"); fwrite(buf, 1, sz, f); fclose(f); + } + +cleanup: + // Release the JavaScript context + JSGlobalContextRelease(ctx); + if(file) free(file); + + return res; +} \ No newline at end of file