--- title: Swift + JavaScriptCore pagination_prev: demos/bigdata/index pagination_next: solutions/input --- import current from '/version.js'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; export const r = {style: {color:"red"}}; export const B = {style: {fontWeight:"bold"}}; [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. Binary strings can be passed back and forth using `String.Encoding.isoLatin1`. The SheetJS `read` method[^1], with the `"binary"` type, can parse binary strings. 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); } ``` ```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 /* read sheetjs.xls as Base64 string */ let file_path = shared_dir.appendingPathComponent("sheetjs.xls"); let data: String! = try String(contentsOf: file_path, encoding: String.Encoding.isoLatin1); ``` This string can be loaded into the JS engine and processed: ```swift /* load data in JSC */ context.setObject(data, forKeyedSubscript: "payload" as (NSCopying & NSObjectProtocol)); /* `payload` (the "forKeyedSubscript" parameter) is a binary string */ context.evaluateScript("var wb = XLSX.read(payload, {type:'binary'});"); ```
Direct Read (click to show) `Uint8Array` data can be passed directly, skipping string encoding and decoding: ```swift let url = URL(fileURLWithPath: file) var data: Data! = try Data(contentsOf: url); let count = data.count; /* Note: the operations must be performed in the closure! */ let wb: JSValue! = data.withUnsafeMutableBytes { (dataPtr: UnsafeMutableRawBufferPointer) in // highlight-next-line let ab: JSValue! = JSValue(jsValueRef: JSObjectMakeTypedArrayWithBytesNoCopy(context.jsGlobalContextRef, kJSTypedArrayTypeUint8Array, dataPtr.baseAddress, count, nil, nil, nil), in: context) /* prepare options argument */ context.evaluateScript(String(format: "var readopts = {type:'array', dense:true}")); let readopts: JSValue = context.objectForKeyedSubscript("readopts"); /* call XLSX.read */ let XLSX: JSValue! = context.objectForKeyedSubscript("XLSX"); let readfunc: JSValue = XLSX.objectForKeyedSubscript("read"); return readfunc.call(withArguments: [ab, readopts]); } ``` 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: ```swift /* write to binary string */ context.evaluateScript("var out = XLSX.write(wb, {type:'binary', bookType:'xlsx'})"); /* `out` from the script is a binary string that can be stringified in Swift */ let outvalue: JSValue! = context.objectForKeyedSubscript("out"); var out: String! = outvalue.toString(); ``` `String#write(to:atomically:encoding)` writes the string to the specified path: ```swift /* write to sheetjsw.xlsx */ 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: | Architecture | Swift | Date | |:-------------|:--------|:-----------| | `darwin-x64` | `5.10` | 2024-04-04 | | `darwin-arm` | `5.9.2` | 2024-02-21 | ::: The demo includes a sample `SheetJSCore` Wrapper class to simplify operations. :::caution This demo only runs on MacOS This example requires MacOS + Swift and will not work on Windows or Linux! ::: 0) Ensure Swift is installed by running the following command in the terminal: ```bash swiftc --version ``` If the command is not found, install Xcode. 1) Create a folder for the project: ```bash mkdir sheetjswift cd sheetjswift ``` 2) 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://docs.sheetjs.com/pres.numbers`} 3) Download the Swift scripts for the demo - [`SheetJSCore.swift`](pathname:///swift/SheetJSCore.swift) Wrapper library - [`main.swift`](pathname:///swift/main.swift) Command-line script ```bash curl -LO https://docs.sheetjs.com/swift/SheetJSCore.swift curl -LO https://docs.sheetjs.com/swift/main.swift ``` 4) Build the `SheetJSwift` program: ```bash swiftc SheetJSCore.swift main.swift -o SheetJSwift ``` 5) Test the program: ```bash ./SheetJSwift pres.numbers ``` 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 | | `darwin-arm` | `7618.2.12.11.7` | 2024-05-24 | | `linux-x64` | `7618.1.15.14.7` | 2024-04-24 | | `linux-arm` | `7618.2.12.11.7` | 2024-05-25 | ::: 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.2.12.11.7 mv WebKit-7618.2.12.11.7 WebKit.zip unzip WebKit.zip ``` 3) Build JavaScriptCore: ```bash cd WebKit-WebKit-7618.2.12.11.7 Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON" cd .. ``` :::danger pass When this demo was last tested on ARM64 macOS, JIT elicited runtime errors and WebAssembly elicited compile-time errors. WebAssembly and JIT must be disabled: ```bash cd WebKit-WebKit-7618.2.12.11.7 Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON" --no-jit --no-webassembly 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 { ``` ::: :::caution pass When this demo was tested on ARM64 macOS, the build failed with the error message ``` Source/JavaScriptCore/runtime/JSCBytecodeCacheVersion.cpp:37:10: fatal error: 'wtf/spi/darwin/dyldSPI.h' file not found #include ^~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error generated. ``` The `#include` should be changed to a relative directive: ```cpp title="Source/JavaScriptCore/runtime/JSCBytecodeCacheVersion.cpp (edit highlighted lines)" #include // highlight-start #include "../../WTF/wtf/spi/darwin/dyldSPI.h" // highlight-end #endif ``` ::: ```bash cd WebKit-WebKit-7618.2.12.11.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 .. ``` :::caution pass When this was last tested on the Steam Deck, the build ran for 24 minutes! ::: :::danger pass When this demo was last tested on ARM64, there was a dangling pointer error:
WebKitBuild/JSCOnly/Release/WTF/Headers/wtf/SentinelLinkedList.h:61:55: error: storing the address of local variable ‘toBeRemoved’ in ‘{"*"}MEM[(struct BasicRawSentinelNode {"*"} const &)this_4(D) + 96].WTF::BasicRawSentinelNode<JSC::CallLinkInfoBase>::m_next’ [-Werror=dangling-pointer=]
{"   61 |"}     void setNext(BasicRawSentinelNode* next) {"{"} m_next = next; {"}"}
{"      |"}                                                ~~~~~~~^~~~~~
The error can be suppressed with a preprocessor pragma: ```cpp title="WebKitBuild/JSCOnly/Release/WTF/Headers/wtf/SentinelLinkedList.h (add highlighted lines)" BasicRawSentinelNode() = default; // highlight-start #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdangling-pointer" // highlight-end void setPrev(BasicRawSentinelNode* prev) { m_prev = prev; } void setNext(BasicRawSentinelNode* next) { m_next = next; } // highlight-next-line #pragma GCC diagnostic pop T* prev() const { return static_cast(PtrTraits::unwrap(m_prev)); } ``` After patching the header, JSC must be built without WebAssembly or JIT support: ```bash cd WebKit-WebKit-7618.2.12.11.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 --no-jit --no-webassembly cd .. ``` :::
4) Create a symbolic link to the `Release` folder in the source tree: ```bash ln -s WebKit-WebKit-7618.2.12.11.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://docs.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)