From 066741f1851b68a3ea9e5ba78a2ee055bf224ca1 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Sun, 23 Jun 2024 01:30:41 -0400 Subject: [PATCH] JSC Swift Linux demo --- docz/data/engines.xls | 4 +- .../02-examples/06-loader.md | 2 +- docz/docs/03-demos/42-engines/04-jsc.md | 147 +++++++++++++- docz/static/swift/SheetJSCRaw.swift | 189 ++++++++++++++++++ 4 files changed, 334 insertions(+), 8 deletions(-) create mode 100644 docz/static/swift/SheetJSCRaw.swift diff --git a/docz/data/engines.xls b/docz/data/engines.xls index 248cb36..ac6df1a 100644 --- a/docz/data/engines.xls +++ b/docz/data/engines.xls @@ -334,8 +334,8 @@ - - + + ExecJS diff --git a/docz/docs/02-getting-started/02-examples/06-loader.md b/docz/docs/02-getting-started/02-examples/06-loader.md index 056d89a..660eb80 100644 --- a/docz/docs/02-getting-started/02-examples/06-loader.md +++ b/docz/docs/02-getting-started/02-examples/06-loader.md @@ -24,7 +24,7 @@ generate CSV files for the LangChain CSV loader. These conversions can be run in a preprocessing step without disrupting existing CSV workflows. In ["SheetJS Loader"](#sheetjs-loader), we will use SheetJS libraries in a -custom loader to directly generate documents and metadata. +custom `LoadOfSheet` data loader to directly generate documents and metadata. ["SheetJS Loader Demo"](#sheetjs-loader-demo) is a complete demo that uses the SheetJS Loader to answer questions based on data from a XLS workbook. diff --git a/docz/docs/03-demos/42-engines/04-jsc.md b/docz/docs/03-demos/42-engines/04-jsc.md index 6680bbc..7c98eff 100644 --- a/docz/docs/03-demos/42-engines/04-jsc.md +++ b/docz/docs/03-demos/42-engines/04-jsc.md @@ -312,11 +312,25 @@ FILE *f = fopen("sheetjsw.xlsb", "wb"); fwrite(buf, 1, sz, f); fclose(f); This demo was tested in the following environments: +**Built-in** + +Swift on MacOS supports JavaScriptCore without additional dependencies. + | Architecture | Swift | Date | |:-------------|:--------|:-----------| | `darwin-x64` | `5.10` | 2024-04-04 | | `darwin-arm` | `5.9.2` | 2024-02-21 | +**Compiled** + +The ["Swift C"](#swift-c) section starts from the static libraries built in the +["C++"](#c) section and build Swift bindings. + +| Architecture | Version | Date | +|:-------------|:-----------------|:-----------| +| `linux-x64` | `7618.2.12.11.7` | 2024-06-22 | +| `linux-arm` | `7618.2.12.11.7` | 2024-06-22 | + ::: The demo includes a sample `SheetJSCore` Wrapper class to simplify operations. @@ -325,6 +339,8 @@ The demo includes a sample `SheetJSCore` Wrapper class to simplify operations. This example requires MacOS + Swift and will not work on Windows or Linux! +The ["Swift C"](#swift-c) section covers integration in other platforms. + ::: 0) Ensure Swift is installed by running the following command in the terminal: @@ -391,8 +407,8 @@ This demo was tested in the following environments: |:-------------|:-----------------|:-----------| | `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 | +| `linux-x64` | `7618.2.12.11.7` | 2024-06-22 | +| `linux-arm` | `7618.2.12.11.7` | 2024-06-22 | ::: @@ -401,12 +417,20 @@ This demo was tested in the following environments:
Installation Notes (click to show) -On the Steam Deck, a few dependencies must be installed before building JSC: +The build requires CMake and Ruby. + +On the Steam Deck, dependencies should be installed with `pacman`: ```bash sudo pacman -Syu base-devel cmake ruby icu glibc linux-api-headers ``` +On Debian and Ubuntu, dependencies should be installed with `apt`: + +```bash +sudo apt-get install build-essential cmake ruby +``` +
1) Create a project folder: @@ -523,7 +547,7 @@ When this demo was last tested on ARM64, there was a dangling pointer error: {" |"} ~~~~~~~^~~~~~ -The error can be suppressed with a preprocessor pragma: +The error can be suppressed with preprocessor directives around the definition: ```cpp title="WebKitBuild/JSCOnly/Release/WTF/Headers/wtf/SentinelLinkedList.h (add highlighted lines)" BasicRawSentinelNode() = default; @@ -609,6 +633,118 @@ curl -LO https://docs.sheetjs.com/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. +### Swift C + +:::note pass + +For macOS and iOS deployments, it is strongly encouraged to use the official +`JavaScriptCore` bindings. This demo is suited for Linux Swift applications. + +::: + +0) Install the Swift toolchain.[^8] + +
+ Installation Notes (click to show) + +The `linux-x64` test was run on [Ubuntu 22.04 using Swift 5.10.1](https://download.swift.org/swift-5.10.1-release/ubuntu2204/swift-5.10.1-RELEASE/swift-5.10.1-RELEASE-ubuntu22.04.tar.gz) + +The `linux-arm` test was run on [Debian 12 "bookworm" using Swift 5.10.1](https://download.swift.org/swift-5.10.1-release/debian12-aarch64/swift-5.10.1-RELEASE/swift-5.10.1-RELEASE-debian12-aarch64.tar.gz) + +
+ +1) Follow the entire ["C" demo](#c). The shared library will be used in Swift. + +2) Enter the `sheetjs-jsc` folder from the previous step. + +3) Create a folder `sheetjswift`. It should be in the `sheetjs-jsc` folder: + +```bash +mkdir sheetjswift +cd sheetjswift +``` + +4) 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`} + + +5) Copy all generated headers to the current directory: + +```bash +find ../WebKit-WebKit*/WebKitBuild/JSCOnly/Release/JavaScriptCore/Headers/ -name \*.h | xargs -I '%' cp '%' . +``` + +6) Edit each header file and replace all instances of ``: + +```c title="JavaScript.h (original include)" +#include +``` + +This must be changed to ``: + +```c title="JavaScript.h (modified include)" +#include +``` + +7) Print the current working directory. It will be the path to `sheetjswift`: + +```bash +pwd +``` + +8) Create a new header named `JavaScriptCore-Bridging-Header.h` : + +```c title="JavaScriptCore-Bridging-Header.h" +#import "/tmp/sheetjs-jsc/sheetjswift/JavaScript.h" +``` + +Replace the import path to the working directory from step 7. For example, if +the path was `/home/sheetjs/sheetjs-jsc/sheetjswift/`, the import should be + +```c title="JavaScriptCore-Bridging-Header.h" +#import "/home/sheetjs/sheetjs-jsc/JavaScript.h" +``` + +9) Create the default module map `module.modulemap`: + +```text title="module.modulemap" +module JavaScriptCore { + header "./JavaScript.h" + link "JavaScriptCore" +} +``` + +10) Download [`SheetJSCRaw.swift`](pathname:///swift/SheetJSCRaw.swift): + +```bash +curl -LO https://docs.sheetjs.com/swift/SheetJSCRaw.swift +``` + +11) Build `SheetJSwift`: + +```bash +swiftc -Xcc -I$(pwd) -Xlinker -L../WebKit-WebKit-7618.2.12.11.7/WebKitBuild/JSCOnly/Release/lib/ -Xlinker -lJavaScriptCore -Xlinker -lWTF -Xlinker -lbmalloc -Xlinker -lstdc++ -Xlinker -latomic -Xlinker -licuuc -Xlinker -licui18n -import-objc-header JavaScriptCore-Bridging-Header.h SheetJSCRaw.swift -o SheetJSwift +``` + +12) Run the command: + +```bash +./SheetJSwift pres.numbers +``` + +If successful, a CSV will be printed to console. The program also tries to write +to `SheetJSwift.xlsx`, 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. @@ -616,4 +752,5 @@ to `sheetjsw.xlsb`, which can be opened in a spreadsheet editor. [^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 +[^7]: See [`writeFile` in "Writing Files"](/docs/api/write-options) +[^8]: See ["Install Swift"](https://www.swift.org/install) in the Swift website. diff --git a/docz/static/swift/SheetJSCRaw.swift b/docz/static/swift/SheetJSCRaw.swift new file mode 100644 index 0000000..558f3cb --- /dev/null +++ b/docz/static/swift/SheetJSCRaw.swift @@ -0,0 +1,189 @@ +/* SheetJSCore (C) 2024-present SheetJS LLC -- https://sheetjs.com */ +import Foundation; +import JavaScriptCore; + +enum JSCError: Error { + case badJSContext; + case badJSWorkbook; + case badJSWorksheet; + case badJSValue; +}; + +func DOIT(code: String, ctx: JSContextRef) -> JSValueRef { + let script: JSStringRef = JSStringCreateWithUTF8CString(code); + let result: JSValueRef = JSEvaluateScript(ctx, script, nil, nil, 0, nil); + JSStringRelease(script); + return result; +} + +func JS_STR_TO_C(val: JSValueRef, ctx: JSContextRef) -> String { + let str: JSStringRef = JSValueToStringCopy(ctx, val, nil); + let sz = JSStringGetMaximumUTF8CStringSize(str); + let buf = malloc(sz); + JSStringGetUTF8CString(str, buf, sz); + let ptr = buf!.bindMemory(to: CChar.self, capacity: 1); + let result = String.init(cString: ptr); + free(buf); + JSStringRelease(str); + return result; +} + +func GET_NAMED_PROP(val: JSValueRef, key: String, ctx: JSContextRef) throws -> JSValueRef { + if(!JSValueIsObject(ctx, val)) { throw JSCError.badJSValue; } + let o: JSObjectRef = JSValueToObject(ctx, val, nil); + let k: JSStringRef = JSStringCreateWithUTF8CString(key); + let result: JSValueRef = JSObjectGetProperty(ctx, o, k, nil); + JSStringRelease(k); + return result; +} + +func SET_NAMED_PROP(obj: JSValueRef, key: String, val: JSValueRef, ctx: JSContextRef) throws { + if(!JSValueIsObject(ctx, obj)) { throw JSCError.badJSValue; } + let k: JSStringRef = JSStringCreateWithUTF8CString(key); + JSObjectSetProperty(ctx, obj, k, val, 0, nil); + JSStringRelease(k); +} + +class SJSWorksheet { + var context: JSContextRef!; + var wb: JSValueRef; + var ws: JSValueRef; + var idx: UInt32; + + func toCSV() throws -> String { + let global = JSContextGetGlobalObject(self.context); + let XLSX = try GET_NAMED_PROP(val: global!, key: "XLSX", ctx: self.context); + let utils: JSValueRef = try GET_NAMED_PROP(val: XLSX, key: "utils", ctx: self.context); + let sheet_to_csv: JSValueRef = try GET_NAMED_PROP(val: utils, key: "sheet_to_csv", ctx: self.context); + var exc: JSValueRef?; + let result = JSObjectCallAsFunction(self.context, JSValueToObject(self.context, sheet_to_csv, nil), JSValueToObject(self.context, utils, nil), 1, [ws], &exc); + if(exc != nil && JSValueIsObject(exc, self.context)) { + let e = JS_STR_TO_C(val: exc!, ctx: self.context); + print(e) + throw JSCError.badJSValue; + } + return JS_STR_TO_C(val: result!, ctx: self.context); + } + + init(ctx: JSContextRef, wb: JSValueRef, ws: JSValueRef, idx: UInt32) throws { + self.context = ctx; + self.wb = wb; + self.ws = ws; + self.idx = idx; + } +} + +class SJSWorkbook { + var context: JSContextRef!; + var wb: JSValueRef; + var SheetNames: JSValueRef; + var Sheets: JSValueRef; + + func getSheetAtIndex(idx: UInt32) throws -> SJSWorksheet { + let SheetNameRef = try GET_NAMED_PROP(val: self.SheetNames, key: String(idx), ctx: self.context); + let SheetName: String = JS_STR_TO_C(val: SheetNameRef, ctx: self.context) + let ws: JSValueRef! = try GET_NAMED_PROP(val: self.Sheets, key: SheetName, ctx: self.context); + return try SJSWorksheet(ctx: self.context, wb: self.wb, ws: ws, idx: idx); + } + + func writeData(bookType: String = "xlsx") throws -> Data { + let global = JSContextGetGlobalObject(self.context)!; + let XLSX = try GET_NAMED_PROP(val: global, key: "XLSX", ctx: self.context); + let write: JSValueRef = try GET_NAMED_PROP(val: XLSX, key: "write", ctx: self.context); + let opts = DOIT(code: String(format: "({type:'buffer', bookType:'%@', WTF:1})", bookType), ctx: self.context); + + var exc: JSValueRef?; + let result = JSObjectCallAsFunction(self.context, JSValueToObject(self.context, write, nil), JSValueToObject(self.context, XLSX, nil), 2, [self.wb, opts], &exc); + if(exc != nil && JSValueIsObject(exc, self.context)) { + let e = JS_STR_TO_C(val: exc!, ctx: self.context); + print(e) + throw JSCError.badJSValue; + } + let u8: JSObjectRef = JSValueToObject(self.context, result, nil); + let sz = JSObjectGetTypedArrayLength(self.context, result, nil); + let buf = JSObjectGetTypedArrayBytesPtr(self.context, u8, nil); + let data = Data(bytes: buf!, count: sz); + return data; + } + + init(ctx: JSContextRef, wb: JSValueRef) throws { + self.context = ctx; + self.wb = wb; + self.SheetNames = try GET_NAMED_PROP(val: self.wb, key: "SheetNames", ctx: self.context); + self.Sheets = try GET_NAMED_PROP(val: self.wb, key: "Sheets", ctx: self.context); + } +} + +class SheetJSCore { + var context: JSContextRef!; + var XLSX: JSValueRef!; + + func init_context() throws -> JSContextRef { + let context = JSGlobalContextCreate(nil); + if (context == nil) { throw JSCError.badJSContext; } + do { + _ = DOIT(code: "var global = (function(){ return this; }).call(null);", ctx: context!); + _ = DOIT(code: "if(typeof wbs == 'undefined') wbs = [];", ctx: context!); + let src = try String(contentsOfFile: "xlsx.full.min.js"); + _ = DOIT(code: src, ctx: context!); + return context!; + } catch { print(error.localizedDescription); } + throw JSCError.badJSContext; + } + + func version() throws -> String { + if(self.context == nil) { throw JSCError.badJSContext; } + let res: JSValueRef = try GET_NAMED_PROP(val: self.XLSX, key: "version", ctx: self.context); + if(!JSValueIsString(self.context!, res)) { + print("Could not get SheetJS version."); + throw JSCError.badJSValue; + } + return JS_STR_TO_C(val: res, ctx: self.context); + } + + func readData(data: inout Data) throws -> SJSWorkbook { + let wb: JSValueRef = try data.withUnsafeMutableBytes{ (ptr: UnsafeMutableRawBufferPointer) throws in + let u8: JSValueRef = JSObjectMakeTypedArrayWithBytesNoCopy(self.context, kJSTypedArrayTypeUint8Array, ptr.baseAddress, ptr.count, nil, nil, nil); + try SET_NAMED_PROP(obj: JSContextGetGlobalObject(self.context), key: "payload", val: u8, ctx: self.context); + let wb: JSValueRef = DOIT(code: "XLSX.read(payload);", ctx: self.context); + if !JSValueIsObject(wb, self.context) { throw JSCError.badJSWorkbook; } + return wb; + } + return try SJSWorkbook(ctx: context, wb: wb); + } + func readFile(file: String) throws -> SJSWorkbook { + var data: Data! = try NSData(contentsOfFile: file) as Data; + return try readData(data: &data); + } + + init() throws { + self.context = try init_context(); + do { + let global = JSContextGetGlobalObject(self.context); + self.XLSX = try GET_NAMED_PROP(val: global!, key: "XLSX", ctx: self.context); + if self.XLSX == nil { throw JSCError.badJSContext; } + } catch { print(error.localizedDescription); } + } +} + +// --- + +let sheetjs = try SheetJSCore(); + + +/* Print the SheetJS library version */ +try print(sheetjs.version()); + +/* Read file */ +let wb: SJSWorkbook = try sheetjs.readFile(file: CommandLine.arguments[1]); + +/* Convert the first worksheet to CSV and print */ +let ws: SJSWorksheet = try wb.getSheetAtIndex(idx: 0); +let csv: String = try ws.toCSV(); +print(csv); + +/* write an XLSX file to SheetJSwift.xlsx */ +var wbout: Data = try wb.writeData(bookType: "xlsx"); +let cwd = FileManager.default.currentDirectoryPath; +let uri = URL(fileURLWithPath: cwd).appendingPathComponent("SheetJSwift.xlsx"); +try wbout.write(to: uri);