JavaScriptCore C demo

This commit is contained in:
SheetJS 2024-04-25 04:39:55 -04:00
parent 922b84e1e3
commit 671729b289
5 changed files with 476 additions and 18 deletions

@ -37,7 +37,7 @@
</Style>
</Styles>
<Worksheet ss:Name="Engines">
<Table ss:ExpandedColumnCount="8" ss:ExpandedRowCount="16" x:FullColumns="1"
<Table ss:ExpandedColumnCount="8" ss:ExpandedRowCount="17" x:FullColumns="1"
x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="16">
<Column ss:Index="3" ss:Width="24"/>
<Column ss:Width="31"/>
@ -91,6 +91,16 @@
<Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
<Cell ss:StyleID="s16"/>
</Row>
<Row>
<Cell><Data ss:Type="String">JSC</Data></Cell>
<Cell><Data ss:Type="String">C++</Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String"></Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String"></Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String"></Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
<Cell ss:StyleID="s16"><Data ss:Type="String"></Data></Cell>
</Row>
<Row>
<Cell><Data ss:Type="String">Jint</Data></Cell>
<Cell><Data ss:Type="String">C#</Data></Cell>

@ -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)

@ -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
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
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.
</TabItem>
<TabItem value="cpp" label="C++">
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.
</TabItem>
</Tabs>
### Initialize JSC
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
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); }
```
</TabItem>
<TabItem value="cpp" label="C++">
A JSC context can be created with the `JSGlobalContextCreate` function:
```cpp
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
```
</TabItem>
</Tabs>
JSC does not provide a `global` variable. It can be created in one line:
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
```swift
do {
// highlight-next-line
context.evaluateScript("var global = (function(){ return this; }).call(null);");
} catch { print(error.localizedDescription); }
```
_Load SheetJS Scripts_
</TabItem>
<TabItem value="cpp" label="C++">
```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);") }
```
</TabItem>
</Tabs>
### Load SheetJS Scripts
The main library can be loaded by reading the scripts from the file system and
evaluating in the JSC context:
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
```swift
let src = try String(contentsOfFile: "xlsx.full.min.js");
context.evaluateScript(src);
```
</TabItem>
<TabItem value="cpp" label="C++">
```cpp
/* load library */
{
size_t sz = 0; char *file = read_file("xlsx.full.min.js", &sz);
DOIT(file);
}
```
</TabItem>
</Tabs>
To confirm the library is loaded, `XLSX.version` can be inspected:
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
```swift
let XLSX: JSValue! = context.objectForKeyedSubscript("XLSX");
if let ver = XLSX.objectForKeyedSubscript("version") { print(ver.toString()); }
```
</TabItem>
<TabItem value="cpp" label="C++">
```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);
}
```
</TabItem>
</Tabs>
### Reading Files
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
`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.
</details>
</TabItem>
<TabItem value="cpp" label="C++">
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);
```
</TabItem>
</Tabs>
### Writing Files
<Tabs groupId="jsclang">
<TabItem value="swift" label="Swift">
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);
```
</TabItem>
<TabItem value="cpp" label="C++">
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);
```
</TabItem>
</Tabs>
## 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
<details>
<summary><b>Installation Notes</b> (click to show)</summary>
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
```
</details>
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:
<Tabs groupId="triple">
<TabItem value="darwin-x64" label="MacOS">
```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<NSString> 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 <wtf/text/SuperFastHash.h>
// highlight-start
#ifdef __OBJC__
@class NSString;
#endif
// highlight-end
namespace WTF {
```
:::
</TabItem>
<TabItem value="linux-x64" label="Linux">
```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!
:::
</TabItem>
</Tabs>
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:
<Tabs groupId="triple">
<TabItem value="darwin-x64" label="MacOS">
```bash
g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lbmalloc -licucore -lWTF -lJavaScriptCore -IRelease/JavaScriptCore/Headers
```
</TabItem>
<TabItem value="linux-x64" label="Linux">
```bash
g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lJavaScriptCore -lWTF -lbmalloc -licui18n -licuuc -latomic -IRelease/JavaScriptCore/Headers
```
</TabItem>
</Tabs>
7) Download the SheetJS Standalone script and the test file. Save both files in
the project directory:
<ul>
<li><a href={`https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js`}>xlsx.full.min.js</a></li>
<li><a href="https://sheetjs.com/pres.numbers">pres.numbers</a></li>
</ul>
<CodeBlock language="bash">{`\
curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js
curl -LO https://sheetjs.com/pres.numbers`}
</CodeBlock>
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)

@ -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` &leq; `|num|` &lt; `den` &leq; `D`
If `mixed` is `false`, then `int = 0` and `0` &lt; `den` &leq; `D`
If `mixed` is `true`, then `0` &leq; `num` &lt; `den` &leq; `D`

@ -0,0 +1,112 @@
#include <stdio.h>
#include <stdlib.h>
#include <JavaScriptCore/JavaScript.h>
/* 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;
}