Skip to main content

Swift + JavaScriptCore

JavaScriptCore (JSC) is the JavaScript engine powering the Safari web browser.

SheetJS 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.

This demo was tested in the following environments:

Swift Built-in

Swift on MacOS supports JavaScriptCore without additional dependencies.

ArchitectureSwiftDate
darwin-x646.0.32025-03-31
darwin-arm6.0.32025-03-30

C / C++ Compiled from Source

JavaScriptCore can be built from source and linked in C / C++ programs.

ArchitectureVersionDate
darwin-x647618.2.12.11.72025-01-10
darwin-arm7620.2.4.111.72025-02-13
linux-x647618.2.12.11.72024-06-22
linux-arm7618.2.12.11.72024-06-22

Swift Compiled from Source

Swift compiler can link against libraries built from the JavaScriptCore source.

ArchitectureVersionDate
linux-x647618.2.12.11.72024-06-22
linux-arm7618.2.12.11.72024-06-22

Integration Details

The SheetJS Standalone scripts can be parsed and evaluated in a JSC context.

Binary strings can be passed back and forth using String.Encoding.isoLatin1.

The SheetJS read method1, with the "binary" type, can parse binary strings.

The write method2, with the "binary" type, can create binary strings.

Initialize JSC

A JSC context can be created with the JSContext function:

var context: JSContext!
do {
context = JSContext();
context.exceptionHandler = { _, X in if let e = X { print(e.toString()!); }; };
} catch { print(error.localizedDescription); }

JSC does not provide a global variable. It can be created in one line:

do {
context.evaluateScript("var global = (function(){ return this; }).call(null);");
} catch { print(error.localizedDescription); }

Load SheetJS Scripts

The main library can be loaded by reading the scripts from the file system and evaluating in the JSC context:

let src = try String(contentsOfFile: "xlsx.full.min.js");
context.evaluateScript(src);

To confirm the library is loaded, XLSX.version can be inspected:

let XLSX: JSValue! = context.objectForKeyedSubscript("XLSX");
if let ver = XLSX.objectForKeyedSubscript("version") { print(ver.toString()); }

Reading Files

String(contentsOf:encoding:) reads from a path and returns an encoded string:

/* 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:

/* 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:

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
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.

Writing Files

When writing to binary string in JavaScriptCore, the result should be stored in a variable and converted to string in 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:

/* write to sheetjsw.xlsx */
let out_path = shared_dir.appendingPathComponent("sheetjsw.xlsx");
try? out.write(to: out_path, atomically: false, encoding: String.Encoding.isoLatin1);

Complete Example

Swift

The demo includes a sample SheetJSCore Wrapper class to simplify operations.

This demo only runs on MacOS

This example requires MacOS + Swift and will not work on Windows or Linux!

The "Swift C" section covers integration in other platforms.

  1. Ensure Swift is installed by running the following command in the terminal:
swiftc --version

If the command is not found, install Xcode.

  1. Create a folder for the project:
mkdir sheetjswift
cd sheetjswift
  1. Download the SheetJS Standalone script and the test file. Save both files in the project directory:
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
curl -LO https://docs.sheetjs.com/pres.numbers
  1. Download the Swift scripts for the demo
curl -LO https://docs.sheetjs.com/swift/SheetJSCore.swift
curl -LO https://docs.sheetjs.com/swift/main.swift
  1. Build the SheetJSwift program:
swiftc SheetJSCore.swift main.swift -o SheetJSwift
  1. Test the program:
./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++

  1. Install dependencies
Installation Notes (click to show)

The build requires CMake and Ruby.

On macOS, dependencies should be installed with brew:

brew install cmake ruby

On the Steam Deck, dependencies should be installed with pacman:

sudo pacman -Syu base-devel cmake ruby icu glibc linux-api-headers

On Debian and Ubuntu, dependencies should be installed with apt:

sudo apt-get install build-essential cmake ruby
  1. Create a project folder:
mkdir sheetjs-jsc
cd sheetjs-jsc
  1. Download and extract the WebKit snapshot:
curl -LO https://codeload.github.com/WebKit/WebKit/zip/refs/tags/WebKit-7620.2.4.111.7
mv WebKit-7620.2.4.111.7 WebKit.zip
unzip WebKit.zip
  1. Build JavaScriptCore:
cd WebKit-WebKit-7620.2.4.111.7
Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON"
cd ..

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:

cd WebKit-WebKit-7620.2.4.111.7
env CFLAGS="-Wno-error -Wno-deprecated-declarations" CXXFLAGS="-Wno-error -Wno-deprecated-declarations" LDFLAGS="-framework Foundation" Tools/Scripts/build-webkit --jsc-only --cmakeargs="-Wno-error -DENABLE_STATIC_JSC=ON -DCMAKE_C_FLAGS=\"-Wno-error -Wno-deprecated-declarations\" -DCMAKE_CXX_FLAGS=\"-Wno-error -Wno-deprecated-declarations\"" --no-jit --no-webassembly --make-args="-Wno-error -Wno-deprecated-declarations"
cd ..

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:

Source/WTF/wtf/text/ASCIILiteral.h (add highlighted lines)
#include <wtf/text/SuperFastHash.h>

#ifdef __OBJC__
@class NSString;
#endif

namespace WTF {

When this demo was tested, 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 <wtf/spi/darwin/dyldSPI.h>
^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

The #include should be changed to a relative directive:

Source/JavaScriptCore/runtime/JSCBytecodeCacheVersion.cpp (edit highlighted lines)
#include <wtf/NeverDestroyed.h>
#include "../../WTF/wtf/spi/darwin/dyldSPI.h"
#endif
  1. Create a symbolic link to the Release folder in the source tree:
ln -s WebKit-WebKit-7620.2.4.111.7/WebKitBuild/JSCOnly/Release/ .
  1. Download sheetjs-jsc.c:
curl -LO https://docs.sheetjs.com/jsc/sheetjs-jsc.c
  1. Compile the program:
g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lbmalloc -licucore -lWTF -lJavaScriptCore -IRelease/JavaScriptCore/Headers -framework Foundation
  1. Download the SheetJS Standalone script and the test file. Save both files in the project directory:
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
curl -LO https://docs.sheetjs.com/pres.numbers
  1. Run the program:
./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.

Swift C

For macOS and iOS deployments, it is strongly encouraged to use the official JavaScriptCore bindings. This demo is suited for Linux Swift applications.

  1. 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

The linux-arm test was run on Debian 12 "bookworm" using Swift 5.10.1

  1. Follow the entire "C" demo. 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:

mkdir sheetjswift
cd sheetjswift
  1. Download the SheetJS Standalone script and the test file. Save both files in the project directory:
curl -LO https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
curl -LO https://docs.sheetjs.com/pres.numbers
  1. Copy all generated headers to the current directory:
find ../WebKit-WebKit*/WebKitBuild/JSCOnly/Release/JavaScriptCore/Headers/  -name \*.h | xargs -I '%' cp '%' .
  1. Edit each header file and replace all instances of <JavaScriptCore/ with <. For example, JavaScript.h includes <JavaScriptCore/JSBase.h>:
JavaScript.h (original include)
#include <JavaScriptCore/JSBase.h>

This must be changed to <JSBase.h>:

JavaScript.h (modified include)
#include <JSBase.h>
  1. Print the current working directory. It will be the path to sheetjswift:
pwd
  1. Create a new header named JavaScriptCore-Bridging-Header.h :
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

JavaScriptCore-Bridging-Header.h
#import "/home/sheetjs/sheetjs-jsc/JavaScript.h"
  1. Create the default module map module.modulemap:
module.modulemap
module JavaScriptCore {
header "./JavaScript.h"
link "JavaScriptCore"
}
  1. Download SheetJSCRaw.swift:
curl -LO https://docs.sheetjs.com/swift/SheetJSCRaw.swift
  1. Build SheetJSwift:
swiftc -Xcc -I$(pwd) -Xlinker -L../WebKit-WebKit-7620.2.4.111.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
  1. Run the command:
./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.

Bindings

It is straightforward to load the platform-native (macOS) or compiled libraries in programs built in other programming languages.

The JavaScriptCore C interface does not use "blingos" (function-like macros), so it is possible to reference each method in an external binding.

Rust

Writing bindings is fairly mechanical. There are 4 parts to the process:

  1. Link to the external library.

  2. Generate Rust representations of the original C data types.

  3. Translate the function declaration.

  4. Write a wrapper to convert between Rust concepts and C concepts.

For example, the following C code creates a string within the engine from a UTF8 string:

Sample use of JSStringCreateWithUTF8CString
const char *code = "'Sheet' + 'JS'";
JSStringRef script = JSStringCreateWithUTF8CString(code);

An ergonomic wrapper function would take a Rust string literal and handle unsafe data operations:

Theoretical equivalent in Rust
let code: &str = "'Sheet' + 'JS'";
let script: JSStringRef = JSC::JSStringCreateWithUTF8CString(code);

Rust Linkage

Custom directives are typically added to build.rs. The cargo::rustc-link-lib directive instructs the Rust compiler to link against an external library.

The following snippet will instruct the toolchain to link against the system JavaScriptCore.framework framework on macOS:

build.rs (link to JavaScriptCore.framework on macOS)
#[cfg(target_os = "macos")]
fn main() {
println!("cargo::rustc-link-lib=framework=JavaScriptCore");
}

Rust Types

JSStringRef is a pointer to an opaque data type. The spiritual equivalent according to the Rustonomicon is a pointer to an opaque struct9:

JSStringRef opaque type in Rust
#[repr(C)]
pub struct JSString {
_data: [u8; 0],
_marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
type JSStringRef = *mut JSContext;

Function Declaration

The JSStringCreateWithUTF8CString function is declared in C as follows:

JSStringRef JSStringCreateWithUTF8CString(const char * string);

The equivalent Rust declaration must be defined in an extern "C" block:

JSStringCreateWithUTF8CString Rust declaration
unsafe extern "C" {
// JSStringRef JSStringCreateWithUTF8CString(const char * string);
pub unsafe fn JSStringCreateWithUTF8CString(string: *const u8) -> JSStringRef;
}

Rust Interchange

The std::ffi module includes a number of helpers for passing data between Rust code and C libraries.

Rust string literals are commonly represented as &str types:

let script: &str = "'Sheet' + 'JS'"; // 'Sheet' + 'JS'

The following unsafe waltz creates a compatible pointer:

A) Convert the &str to a byte slice

B) Create a std::ffi::CString instance from the bytes

C) Use the as_ptr method to generate a pointer

Generate a JSString from a Rust string literal
/* start with a string literal */
let script: &str = "'Sheet' + 'JS'";

/* generate CString */
let cstr: std::ffi::CString = std::ffi::CString::new(script.as_bytes()).unwrap();

/* call JSStringCreateWithUTF8CString */
let ref: JSStringRef = JSStringCreateWithUTF8CString(cstr.as_ptr() as *const u8);

The demo makes a safe wrapper to perform the unsafe waltz in one line:

Ergonomic wrapper for JSStringCreateWithUTF8CString
pub struct JSC;
impl JSC {
pub fn JSStringCreateWithUTF8CString(str: &str) -> JSStringRef { unsafe {
JSStringCreateWithUTF8CString(std::ffi::CString::new(str.as_bytes()).unwrap().as_ptr() as *const u8)
} }
}

Rust Example

Tested Deployments

This demo was last tested in the following deployments:

ArchitectureDate
darwin-x642025-03-31
darwin-arm2025-03-30
  1. Create a new project:
cargo new sheetjs-jsc
cd sheetjs-jsc
cargo run
  1. Download the SheetJS Standalone script to the src folder in the project:
curl -L -o src/xlsx.full.min.js https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
  1. Download the test file to the project directory:
curl -LO https://docs.sheetjs.com/pres.numbers
  1. Download main.rs and replace src/main.rs:
curl -L -o src/main.rs https://docs.sheetjs.com/jsc/main.rs
  1. Download build.rs to the project folder:
curl -LO https://docs.sheetjs.com/jsc/build.rs
  1. Build and run the app:
cargo run 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 a spreadsheet editor that supports XLSB spreadsheets.

Footnotes

  1. See read in "Reading Files"

  2. See writeFile in "Writing Files"

  3. See JSObjectMakeTypedArrayWithBytesNoCopy in the JavaScriptCore documentation.

  4. See JSObjectGetTypedArrayLength in the JavaScriptCore documentation.

  5. See JSObjectGetTypedArrayBytesPtr in the JavaScriptCore documentation.

  6. See read in "Reading Files"

  7. See writeFile in "Writing Files"

  8. See "Install Swift" in the Swift website.

  9. See "Representing opaque structs" in the Rustonomicon