From 1d99c0f18188ab5907b7f4b26a87c6cf1619cb6e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Jun 2025 12:30:21 +0900 Subject: [PATCH 1/2] Add `JavaScriptFoundationCompat` module to provide utilities to interact Foundation types --- Package.swift | 13 +++++ .../Data+JSValue.swift | 42 ++++++++++++++ .../Data+JSValueTests.swift | 55 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 Sources/JavaScriptFoundationCompat/Data+JSValue.swift create mode 100644 Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift diff --git a/Package.swift b/Package.swift index 3657bfa9..4f4ecd06 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]), .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), .plugin(name: "PackageToJS", targets: ["PackageToJS"]), .plugin(name: "BridgeJS", targets: ["BridgeJS"]), @@ -106,6 +107,18 @@ let package = Package( "JavaScriptEventLoopTestSupport", ] ), + .target( + name: "JavaScriptFoundationCompat", + dependencies: [ + "JavaScriptKit" + ] + ), + .testTarget( + name: "JavaScriptFoundationCompatTests", + dependencies: [ + "JavaScriptFoundationCompat" + ] + ), .plugin( name: "PackageToJS", capability: .command( diff --git a/Sources/JavaScriptFoundationCompat/Data+JSValue.swift b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift new file mode 100644 index 00000000..ac8e773b --- /dev/null +++ b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift @@ -0,0 +1,42 @@ +import Foundation +import JavaScriptKit + +/// Data <-> Uint8Array conversion. The conversion is lossless and copies the bytes at most once per conversion +extension Data: ConvertibleToJSValue, ConstructibleFromJSValue { + /// Convert a Data to a JSTypedArray. + /// + /// - Returns: A Uint8Array that contains the bytes of the Data. + public var jsTypedArray: JSTypedArray { + self.withUnsafeBytes { buffer in + return JSTypedArray(buffer: buffer.bindMemory(to: UInt8.self)) + } + } + + /// Convert a Data to a JSValue. + /// + /// - Returns: A JSValue that contains the bytes of the Data as a Uint8Array. + public var jsValue: JSValue { jsTypedArray.jsValue } + + /// Construct a Data from a JSTypedArray. + public static func construct(from uint8Array: JSTypedArray) -> Data? { + // First, allocate the data storage + var data = Data(count: uint8Array.lengthInBytes) + // Then, copy the byte contents into the Data buffer + data.withUnsafeMutableBytes { destinationBuffer in + uint8Array.copyMemory(to: destinationBuffer.bindMemory(to: UInt8.self)) + } + return data + } + + /// Construct a Data from a JSValue. + /// + /// - Parameter jsValue: The JSValue to construct a Data from. + /// - Returns: A Data, if the JSValue is a Uint8Array. + public static func construct(from jsValue: JSValue) -> Data? { + guard let uint8Array = JSTypedArray(from: jsValue) else { + // If the JSValue is not a Uint8Array, fail. + return nil + } + return construct(from: uint8Array) + } +} diff --git a/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift new file mode 100644 index 00000000..8c0d6162 --- /dev/null +++ b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift @@ -0,0 +1,55 @@ +import XCTest +import Foundation +import JavaScriptFoundationCompat +import JavaScriptKit + +final class DataJSValueTests: XCTestCase { + func testDataToJSValue() { + let data = Data([0x00, 0x01, 0x02, 0x03]) + let jsValue = data.jsValue + + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 4) + XCTAssertEqual(uint8Array?[0], 0x00) + XCTAssertEqual(uint8Array?[1], 0x01) + XCTAssertEqual(uint8Array?[2], 0x02) + XCTAssertEqual(uint8Array?[3], 0x03) + } + + func testJSValueToData() { + let jsValue = JSTypedArray([0x00, 0x01, 0x02, 0x03]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data([0x00, 0x01, 0x02, 0x03])) + } + + func testDataToJSValue_withLargeData() { + let data = Data(repeating: 0x00, count: 1024 * 1024) + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 1024 * 1024) + } + + func testJSValueToData_withLargeData() { + let jsValue = JSTypedArray(Array(repeating: 0x00, count: 1024 * 1024)).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data?.count, 1024 * 1024) + } + + func testDataToJSValue_withEmptyData() { + let data = Data() + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 0) + } + + func testJSValueToData_withEmptyData() { + let jsValue = JSTypedArray([]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data()) + } + + func testJSValueToData_withInvalidJSValue() { + let data = Data.construct(from: JSObject().jsValue) + XCTAssertNil(data) + } +} From da1675f9dba0d05331a43074caeb21f222c8df99 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Jun 2025 12:40:34 +0900 Subject: [PATCH 2/2] Add benchmarks for Data to JSValue and vice versa --- Benchmarks/Package.swift | 5 ++++- Benchmarks/Sources/Benchmarks.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 8e11282e..a41a86e8 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -10,7 +10,10 @@ let package = Package( targets: [ .executableTarget( name: "Benchmarks", - dependencies: ["JavaScriptKit"], + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit"), + ], exclude: ["Generated/JavaScript", "bridge-js.d.ts"], swiftSettings: [ .enableExperimentalFeature("Extern") diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift index 602aa843..155acae1 100644 --- a/Benchmarks/Sources/Benchmarks.swift +++ b/Benchmarks/Sources/Benchmarks.swift @@ -1,4 +1,6 @@ import JavaScriptKit +import JavaScriptFoundationCompat +import Foundation class Benchmark { init(_ title: String) { @@ -75,4 +77,22 @@ class Benchmark { } } } + + do { + let conversion = Benchmark("Conversion") + let data = Data(repeating: 0, count: 10_000) + conversion.testSuite("Data to JSTypedArray") { + for _ in 0..<1_000_000 { + _ = data.jsTypedArray + } + } + + let uint8Array = data.jsTypedArray + + conversion.testSuite("JSTypedArray to Data") { + for _ in 0..<1_000_000 { + _ = Data.construct(from: uint8Array) + } + } + } }