Skip to main content

Swift + JavaScriptCore

JavaScript 核心 (JSC) 是为 Safari Web 浏览器提供支持的 JavaScript 引擎。

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

SheetJS 是一个用于从电子表格读取和写入数据的 JavaScript 库。

¥SheetJS is a JavaScript library for reading and writing data from spreadsheets.

该演示使用 JSC 和 SheetJS 来读取和写入电子表格。我们将探讨如何在 JSC 上下文中加载 SheetJS,并处理来自 C++ 和 Swift 程序的电子表格和结构化数据。

¥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

SheetJS 独立脚本 可以在 JSC 上下文中进行解析和评估。

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

可以使用 String.Encoding.isoLatin1 来回传递二进制字符串。

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

SheetJS read 方法 [^1],具有 "binary" 类型,可以解析二进制字符串。

¥The SheetJS read method[^1], with the "binary" type, can parse binary strings.

write 方法 [^2],具有 "binary" 类型,可以创建二进制字符串。

¥The write method[^2], with the "binary" type, can create binary strings.

初始化 JSC

¥Initialize JSC

可以使用 JSContext 函数创建 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 不提供 global 变量。它可以在一行中创建:

¥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); }

加载 SheetJS 脚本

¥Load SheetJS Scripts

可以通过从文件系统读取脚本并在 JSC 上下文中进行评估来加载主库:

¥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);

要确认库已加载,可以检查 XLSX.version

¥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:) 从路径读取并返回编码字符串:

¥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);

该字符串可以加载到 JS 引擎中并进行处理:

¥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

在 JavaScriptCore 中写入二进制字符串时,结果应存储在变量中并在 Swift 中转换为字符串:

¥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) 将字符串写入指定路径:

¥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

本 demo 在以下环境下进行了测试:

¥This demo was tested in the following environments:

内置

¥Built-in

MacOS 上的 Swift 支持 JavaScriptCore,无需额外依赖。

¥Swift on MacOS supports JavaScriptCore without additional dependencies.

架构迅速日期
darwin-x645.102024-04-04
darwin-arm5.102024-06-30

已编译

¥Compiled

"Swift C" 部分从 "C++" 部分中构建的静态库开始,并构建 Swift 绑定。​​

¥The "Swift C" section starts from the static libraries built in the "C++" section and builds Swift bindings.

架构版本日期
linux-x647618.2.12.11.72024-06-22
linux-arm7618.2.12.11.72024-06-22

该演示包含一个示例 SheetJSCore Wrapper 类来简化操作。

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

该演示仅在 MacOS 上运行

此示例需要 MacOS + Swift,不适用于 Windows 或 Linux!

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

"Swift C" 部分介绍了在其他平台中的集成。

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

  1. 通过在终端中运行以下命令确保已安装 Swift:

    ¥Ensure Swift is installed by running the following command in the terminal:

swiftc --version

如果未找到该命令,请安装 Xcode。

¥If the command is not found, install Xcode.

  1. 为项目创建一个文件夹:

    ¥Create a folder for the project:

mkdir sheetjswift
cd sheetjswift
  1. 下载 SheetJS Standalone 脚本和测试文件。将这两个文件保存在项目目录中:

    ¥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://xlsx.nodejs.cn/pres.numbers
  1. 下载演示的 Swift 脚本

    ¥Download the Swift scripts for the demo

curl -LO https://xlsx.nodejs.cn/swift/SheetJSCore.swift
curl -LO https://xlsx.nodejs.cn/swift/main.swift
  1. 构建 SheetJSwift 程序:

    ¥Build the SheetJSwift program:

swiftc SheetJSCore.swift main.swift -o SheetJSwift
  1. 测试程序:

    ¥Test the program:

./SheetJSwift pres.numbers

如果成功,CSV 将打印到控制台。该脚本还尝试写入 SheetJSwift.xlsx。该文件可以通过在 Excel/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++

本 demo 在以下环境下进行了测试:

¥This demo was tested in the following environments:

架构版本日期
darwin-x647618.1.15.14.72024-04-24
darwin-arm7618.2.12.11.72024-05-24
linux-x647618.2.12.11.72024-06-22
linux-arm7618.2.12.11.72024-06-22
  1. 安装依赖

    ¥Install dependencies

Installation Notes (click to show)

The build requires CMake and 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. 下载并解压 WebKit 快照:

    ¥Download and extract the WebKit snapshot:

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
  1. 构建 JavaScriptCore:

    ¥Build JavaScriptCore:

cd WebKit-WebKit-7618.2.12.11.7
Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON"
cd ..

上次在 ARM64 macOS 上测试此演示时,JIT 引发运行时错误,WebAssembly 引发编译时错误。必须禁用 WebAssembly 和 JIT:

¥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-7618.2.12.11.7
Tools/Scripts/build-webkit --jsc-only --cmakeargs="-DENABLE_STATIC_JSC=ON" --no-jit --no-webassembly
cd ..

当在 macOS 上测试此演示时,构建失败并显示错误消息

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

必须修补引用的头文件以声明 NSString

¥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 {

在 ARM64 macOS 上测试此演示时,构建失败并显示错误消息

¥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 <wtf/spi/darwin/dyldSPI.h>
^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

#include 应更改为相对指令:

¥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. 创建指向源树中 Release 文件夹的符号链接:

    ¥Create a symbolic link to the Release folder in the source tree:

ln -s WebKit-WebKit-7618.2.12.11.7/WebKitBuild/JSCOnly/Release/ .
  1. 下载 sheetjs-jsc.c

    ¥Download sheetjs-jsc.c:

curl -LO https://xlsx.nodejs.cn/jsc/sheetjs-jsc.c
  1. 编译程序 x:

    ¥Compile the program:

g++ -o sheetjs-jsc sheetjs-jsc.c -IRelease/JavaScriptCore/Headers -LRelease/lib -lbmalloc -licucore -lWTF -lJavaScriptCore -IRelease/JavaScriptCore/Headers
  1. 下载 SheetJS Standalone 脚本和测试文件。将这两个文件保存在项目目录中:

    ¥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://xlsx.nodejs.cn/pres.numbers
  1. 运行程序:

    ¥Run the program:

./sheetjs-jsc pres.numbers

如果成功,CSV 将打印到控制台。该脚本还尝试写入 sheetjsw.xlsb,该文件可以在电子表格编辑器中打开。

¥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

对于 macOS 和 iOS 部署,强烈建议使用官方 JavaScriptCore 绑定。此演示适用于 Linux Swift 应用。

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

  1. 安装 Swift 工具链.[^8]

    ¥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. 遵循整个 "C" 演示。共享库将在 Swift 中使用。

    ¥Follow the entire "C" demo. The shared library will be used in Swift.

  2. 输入上一步中的 sheetjs-jsc 文件夹。

    ¥Enter the sheetjs-jsc folder from the previous step.

  3. 创建文件夹 sheetjswift。它应该在 sheetjs-jsc 文件夹中:

    ¥Create a folder sheetjswift. It should be in the sheetjs-jsc folder:

mkdir sheetjswift
cd sheetjswift
  1. 下载 SheetJS Standalone 脚本和测试文件。将这两个文件保存在项目目录中:

    ¥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://xlsx.nodejs.cn/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. 编辑每个头文件并将 <JavaScriptCore/ 的所有实例替换为 <。例如,JavaScript.h 包括 <JavaScriptCore/JSBase.h>

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

必须将其更改为 <JSBase.h>

¥This must be changed to <JSBase.h>:

JavaScript.h (modified include)
#include <JSBase.h>
  1. 打印当前工作目录。它将是 sheetjswift 的路径:

    ¥Print the current working directory. It will be the path to sheetjswift:

pwd
  1. 创建一个名为 JavaScriptCore-Bridging-Header.h 的新标题:

    ¥Create a new header named JavaScriptCore-Bridging-Header.h :

JavaScriptCore-Bridging-Header.h
#import "/tmp/sheetjs-jsc/sheetjswift/JavaScript.h"

替换步骤 7 中工作目录的导入路径。例如,如果路径是 /home/sheetjs/sheetjs-jsc/sheetjswift/,则导入应该是

¥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. 创建默认模块映射 module.modulemap

    ¥Create the default module map module.modulemap:

module.modulemap
module JavaScriptCore {
header "./JavaScript.h"
link "JavaScriptCore"
}
  1. 下载 SheetJSCRaw.swift

    ¥Download SheetJSCRaw.swift:

curl -LO https://xlsx.nodejs.cn/swift/SheetJSCRaw.swift
  1. 构建 SheetJSwift

    ¥Build SheetJSwift:

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
  1. 运行命令:

    ¥Run the command:

./SheetJSwift pres.numbers

如果成功,CSV 将打印到控制台。程序还尝试写入 SheetJSwift.xlsx,可在电子表格编辑器中打开。

¥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]: 见 read 于 "读取文件"

¥See read in "Reading Files"

[^2]: 见 writeFile 于 "写入文件"

¥See writeFile in "Writing Files"

[^3]: 请参阅 JavaScriptCore 文档中的 JSObjectMakeTypedArrayWithBytesNoCopy

¥See JSObjectMakeTypedArrayWithBytesNoCopy in the JavaScriptCore documentation.

[^4]: 请参阅 JavaScriptCore 文档中的 JSObjectGetTypedArrayLength

¥See JSObjectGetTypedArrayLength in the JavaScriptCore documentation.

[^5]: 请参阅 JavaScriptCore 文档中的 JSObjectGetTypedArrayBytesPtr

¥See JSObjectGetTypedArrayBytesPtr in the JavaScriptCore documentation.

[^6]: 见 read 于 "读取文件"

¥See read in "Reading Files"

[^7]: 见 writeFile 于 "写入文件"

¥See writeFile in "Writing Files"

[^8]: 有关 Swift 网站中的 "安装 Swift"

¥See "Install Swift" in the Swift website.